Animated Architecture Patterns

SAML 2.0 Single Sign-On

SSO ในองค์กร: identity provider POST assertion ที่ sign แล้วไปให้แอปผ่าน browser

หรือที่เรียกกันว่า SAML SSO, SAML 2.0 Web Browser SSO

Metadata exchanged once, in advance at sign-in the two servers never call each other stale: the IdP now signs with cert B Expense → IdP entity ID https://expense.example ACS URL https://expense.example/saml/acs IdP → Expense entity ID https://idp.example SSO URL https://idp.example/saml/sso signing cert A Ana Browser expense.example idp.example travel.example Expenses /reports not signed in redirecting to your company sign-in… Second factor approve on your phone signing you in… no password asked <form method="post" action=".../acs"> SAMLResponse=PHNh… RelayState=r9x </form> submits itself Hi, Ana Expenses · /reports role: approver Travel redirecting… Hi, Ana Travel My apps · idp.example E Expense T Travel opening Expense… Hi, Ana Expenses via the IdP portal cookies idp expense travel Expense app service provider (SP) entity ID https://expense.example ACS URL /saml/acs no session sent AuthnRequest id-7f3a session · Ana · approver Travel app second service provider https://travel.example no session sent AuthnRequest id-2b8d session · Ana SSO URL /saml/sso Identity provider IdP · company sign-in https://idp.example no SSO session SSO session · Ana SP known · ACS URL listed password ✓ password ✓ · MFA ✓ session found · no password unsolicited: no request signing key A · private signing key B · new user directory anaFinance-Leads boSales apps, from their metadata Expense Travel front channel: every message goes through the browser HTTP-Redirect binding HTTP-POST binding Travel: the same two bindings unsolicited HTTP-POST AuthnRequest XML built by Expense ID="id-7f3a" Expense remembers it <Issuer>https://expense.example AssertionConsumerServiceURL= "https://expense.example/saml/acs" ProtocolBinding="…:HTTP-POST" 302 to the IdP’s SSO URL GET /saml/sso?SAMLRequest=fZJN… &RelayState=r9x the XML deflated, base64- and URL-encoded SAMLResponse posted to the ACS URL <Response> status=Success <Assertion ID="_c9e1"> <Issuer>https://idp.example <Signature> signs #_c9e1 <Subject> <NameID>7Gq9Xf2 InResponseTo="id-7f3a" Recipient="…/saml/acs" <Conditions> NotBefore="09:00:00Z" NotOnOrAfter="09:05:00Z" <Audience> https://expense.example <AuthnStatement> pwd + MFA <AttributeStatement> email: ana@example.com groups: Finance-Leads IdP key A persistent, opaque not seen before known IdP with cert A = its request = its ACS URL now 09:00:04in window = its entity ID → user Ana → role approver SAMLResponse unsolicited, from the portal InResponseTo (none) nothing to match ✓ signature, issuer, audience, recipient, time No request to match: one replay defence fewer, so some apps refuse or restrict it. Signature wrapping A forged assertion slipped in beside the signed one: act only on what the signature covers, through a maintained library. Certificate rollover The IdP switches to signing cert B. An SP that hasn’t loaded the new metadata still trusts only A and rejects every sign-in. GET 302 POST SAMLRequest SAMLResponse

ขั้นที่ 1 · SP ส่ง AuthnRequest Ana เปิด Expense app ที่ยังไม่มี session ของเธอ แอปสร้าง <AuthnRequest> ที่มี ID ใหม่ (id-7f3a แอปจำค่านี้ไว้) ใส่ entity ID ของตัวเองเป็น <Issuer> แล้วตอบกลับเป็น redirect ไปที่ SSO URL ของ IdP ที่ได้จาก metadata แล้ว HTTP-Redirect binding ก็ใส่ request ที่ deflate แล้ว encode เป็น base64 ไว้ใน query parameter SAMLRequest คู่กับ RelayState สั้น ๆ ฝั่ง IdP ก็ตรวจว่า request มาจาก service provider ที่รู้จัก และระบุ ACS URL ที่ลงทะเบียนไว้ให้ SP นั้นจริง

ขั้นที่ 2 · IdP ส่ง assertion กลับมา IdP ยืนยันตัวตน Ana (password แล้วตามด้วย second factor) และเริ่ม SSO session ของตัวเอง จากนั้นตอบกลับเป็นหน้า HTML เล็ก ๆ ที่มี form ให้ browser submit เอง: HTTP-POST binding พา SAMLResponse ที่ encode เป็น base64 กับ RelayState ไปที่ ACS URL ข้างในคือ assertion ที่ sign ด้วย key ของ IdP บอกว่าใครเข้าสู่ระบบ (NameID) สำหรับแอปไหน (Audience, Recipient) เมื่อไร (NotBefore, NotOnOrAfter) ตอบ request ไหน (InResponseTo) และมี attribute อะไรบ้าง assertion วิ่งผ่าน browser ส่วน server สองฝั่งไม่เคยเรียกหากันเลย

ขั้นที่ 3 · ตรวจ map แล้วก็ SSO Expense ตรวจ signature ด้วย certificate จาก metadata ของ IdP แล้วตรวจ Issuer ดูว่า Audience เป็น entity ID ของตัวเอง และ Recipient เป็น ACS URL ของตัวเอง ดูช่วงเวลา ดูว่า InResponseTo คือ request ที่ตัวเองส่งไป และไม่เคยเห็น assertion ที่มี ID นี้มาก่อน จากนั้นก็ map NameID เข้ากับผู้ใช้ของตัวเอง map attribute groups เป็น role แล้วตั้ง session cookie ของตัวเอง พอ Ana เปิด Travel ฝั่ง IdP ก็เจอ SSO session ของเธอ เลยส่ง assertion ชุดใหม่สำหรับ Travel ไปให้ โดยไม่ต้องถาม password: นี่คือ single sign-on

ขั้นที่ 4 · IdP-initiated กับความเสี่ยง อีกวันหนึ่ง session ของแอปหมดอายุไปแล้ว Ana เริ่มจาก portal ของ IdP แล้วคลิก tile ของ Expense จากนั้นฝั่ง IdP ก็ส่ง assertion ที่ไม่มีใครขอมา: ไม่มี InResponseTo Expense เลยขาดการตรวจที่ผูก response เข้ากับ request จาก browser นี้ และต้องพึ่ง replay cache กับอายุที่สั้นแทน บางแอปถึงไม่รับหรือจำกัดการเข้าสู่ระบบแบบ IdP-initiated ก็เพราะเรื่องนี้ ยังมีอีกสองเรื่องที่ต้องระวัง: signature wrapping (ใช้เฉพาะ assertion ที่ signature ครอบคลุม และทำผ่าน library ที่ยังมีคนดูแล) และ certificate rollover (SP ที่ยังไม่ได้โหลด signing certificate ใหม่ของ IdP จะปฏิเสธการเข้าสู่ระบบทุกครั้ง)

ปัญหา

บริษัทหนึ่งมี web application อยู่หลายสิบตัว หลายตัวเป็น SaaS: ค่าใช้จ่าย การเดินทาง HR และ CRM ถ้าทุกตัวเก็บ password ของตัวเอง แต่ละตัวก็ต้องมีระบบ multi-factor และการ reset password ของตัวเอง คนทำ phishing ก็มีหน้า login ให้เลียนแบบหลายสิบหน้า และไม่มีใครจำได้ว่าต้องปิดบัญชีของคนที่ลาออกไปให้ครบทุกที่ บริษัทอยากมีที่เดียวให้คนเข้าสู่ระบบ ภายใต้ policy ของบริษัทเอง แล้วให้ทุกแอปเชื่อการเข้าสู่ระบบนั้นโดยไม่เคยเห็น password เลย

ตัวแอปกับ identity provider มักเป็นของคนละองค์กรและอยู่คนละ domain เลยใช้ cookie หรือ database ร่วมกันไม่ได้ สิ่งที่ต้องมีคือ message มาตรฐานที่ฝั่งหนึ่งสร้างได้ อีกฝั่งตรวจได้ และ browser ของผู้ใช้พาไปมาระหว่างสองฝั่งได้ SAML 2.0 ที่เป็น OASIS Standard ตั้งแต่มีนาคม 2005 กำหนด message นั้นกับ protocol รอบ ๆ มันไว้ และทุกวันนี้ก็ยังใช้กันกว้างขวางสำหรับ web single sign-on ในองค์กร

ทำงานยัง⁠ไง

ผู้เกี่ยวข้อง identity provider (IdP) ยืนยันตัวตนผู้ใช้และรับรองให้ ส่วน service provider (SP) คือแอปที่เชื่อคำรับรองนั้น แต่ละฝั่งมี entity ID เป็น URI (มักเป็น URL) ที่ใช้เป็นชื่อเฉพาะของมัน และไปโผล่เป็น Issuer กับ Audience ใน message ก่อนจะมีใครเข้าสู่ระบบ ทั้งสองฝั่งต้องแลก metadata กันก่อน: ฝั่ง SP ให้ entity ID ของตัวเองกับ Assertion Consumer Service (ACS) URL ที่เป็น endpoint รับ response ส่วนฝั่ง IdP ให้ entity ID ของตัวเอง single sign-on (SSO) URL และ certificate ของ key ที่ใช้ sign assertion

Assertion คือคำรับรองเกี่ยวกับผู้ใช้ที่ IdP เขียนเป็น XML แล้ว sign ไว้:

  • Issuer: entity ID ของ IdP
  • Signature: enveloped XML signature ที่มี reference ตัวเดียว ชี้ไปที่ ID ของ assertion เอง (SAML Core §5.4)
  • Subject: NameID ที่ระบุตัวผู้ใช้ กับ SubjectConfirmation แบบ bearer ที่มีข้อมูลบอกว่าส่ง assertion ไปที่ไหนได้ (Recipient คือ ACS URL) ใช้ได้ถึงเมื่อไร (NotOnOrAfter) และเป็นคำตอบของ request ไหน (InResponseTo)
  • Conditions: ช่วงเวลาที่ใช้ได้ (NotBefore, NotOnOrAfter) และ AudienceRestriction ที่ระบุ entity ID ของ SP
  • AuthnStatement: ผู้ใช้ยืนยันตัวตนเมื่อไรและด้วยวิธีไหน กับ SessionIndex ที่ตอน logout จะอ้างถึง
  • AttributeStatement: attribute อย่างอีเมล ชื่อที่แสดง และ group ที่เป็นสมาชิก

NameID format (SAML Core §8.3) บอกว่า subject เป็น identifier แบบไหน identifier แบบ persistent เป็นค่าสุ่มเทียมที่อ่านความหมายไม่ออก โดย IdP สร้างให้ service provider ตัวเดียวหรือกลุ่มเดียว (เป็น pairwise pseudonym ยาวไม่เกิน 256 ตัวอักษร) สองแอปเลยใช้มันเชื่อมโยงผู้ใช้คนเดียวกันไม่ได้ ส่วน identifier แบบ transient เป็นของชั่วคราว ไม่ใช้ซ้ำข้ามการเข้าสู่ระบบ แบบ emailAddress สะดวกดี แต่อีเมลเปลี่ยนได้และถูกเอาไปให้คนอื่นใช้ต่อได้ แล้ว unspecified ก็ให้ IdP เลือกเอง นอกจากนี้ยังมีการกำหนด X.509 subject name, Windows domain name, Kerberos principal และ entity identifier ไว้ด้วย อย่าง Microsoft Entra ID ก็ ออกค่าแบบ pairwise ให้ persistent (และให้ unspecified ด้วย) และออกค่าสุ่มใหม่ทุกครั้งที่เข้าสู่ระบบให้ transient

Binding บอกว่า SAML message เดินทางผ่าน HTTP ยัง⁠ไง (SAML Bindings):

  • HTTP-Redirect เอา XML ไป deflate แล้ว encode เป็น base64 และ URL-encode จากนั้นใส่ไว้ใน query parameter SAMLRequest (หรือ SAMLResponse) ตัว signature อยู่ใน XML ต่อไม่ได้ ผู้ส่งเลย sign query string แทน แล้วเพิ่ม parameter SigAlg กับ Signature เข้าไป มันเหมาะกับ AuthnRequest ที่มีขนาดเล็ก
  • HTTP-POST ใส่ XML ที่เป็น base64 ไว้ใน hidden field ของ form ที่ submit ตัวเอง ส่วน response ใช้วิธีนี้: Web Browser SSO profile ห้ามใช้ HTTP-Redirect กับ response เพราะ response มักยาวเกินกว่าจะใส่ใน URL ได้
  • HTTP-Artifact ส่งแค่ reference สั้น ๆ ผ่าน browser แล้วฝั่งรับก็ไปดึง message มาเองผ่าน back channel (มักเป็น SOAP) ที่ต้องยืนยันตัวตนกันทั้งสองฝั่ง วิธีนี้ทำให้ assertion ไม่ต้องผ่าน browser แต่ต้องแลกกับการเรียกระหว่าง server กับ server

Profile เอา message กับ binding มารวมกันสำหรับ use case หนึ่ง ๆ Web Browser SSO profile (SAML Profiles §4.1) คือ flow ใน diagram ส่วน profile อื่นก็ครอบคลุม single logout, client และ proxy แบบ enhanced ที่คุยกับ IdP เอง (ECP) และ artifact resolution

การเข้าสู่ระบบแบบ SP-initiated (step 1 ถึง 3) Expense สร้าง <AuthnRequest> ที่มี ID ใหม่ ใส่ entity ID ของตัวเองเป็น Issuer ใส่ ACS URL ของตัวเองและ binding ที่อยากให้ใช้ส่งคำตอบ เก็บ ID ไว้ใน session ของ Ana แล้ว redirect browser ของเธอไปที่ SSO URL ของ IdP ฝั่ง IdP ต้องตรวจว่า ACS URL นั้นเป็นของ SP ตัวนั้นจริง (Profiles §4.1.4.1) ไม่อย่างนั้น attacker ก็สั่งให้ส่ง assertion ไปที่ URL ไหนก็ได้ตามใจ หลังยืนยันตัวตน Ana แล้ว IdP ก็ตอบกลับเป็นหน้าที่ POST SAMLResponse ไปที่ ACS URL จากนั้น Expense ก็ตรวจมัน map ผู้ใช้ แล้วตั้ง session cookie ของตัวเอง พอทีหลัง Travel ส่ง Ana ไปที่ IdP ตัว session cookie ของ IdP เองก็ทำให้เธอไม่ต้อง login รอบสอง: นี่แหละคือ single sign-on

การเข้าสู่ระบบแบบ IdP-initiated (step 4) profile นี้ยังให้ IdP ส่ง response ที่ ไม่มีใครขอ มาได้ด้วย เช่น ตอนผู้ใช้คลิก tile ของแอปบน portal ของ IdP (Profiles §4.1.5) response แบบนี้ต้องไม่มี InResponseTo และปกติจะส่งไปที่ ACS endpoint ตัว default ของ SP แล้ว IdP ก็อาจใส่ RelayState ที่บอกว่าจะเปิดหน้าไหน ตามที่ตกลงกับ SP ไว้ก่อน

การตรวจ response SAML Profiles §4.1.4.3 วางกฎไว้ และ OWASP SAML Security Cheat Sheet ก็เพิ่มเรื่อง hardening ส่วนตัว library ของคุณควรทำทั้งหมดนี้ให้ แต่คุณก็ควรรู้ว่ามันทำอะไรบ้าง:

  1. Parse แบบระวังตัว ปฏิเสธ DTD และการประกาศ DOCTYPE (แนวทางป้องกัน XXE ของ OWASP) และ validate document กับสำเนา SAML schema ที่เก็บไว้ในเครื่องและเชื่อถือได้
  2. ตรวจ response: status เป็น Success มี Destination ตรงกับ ACS URL ที่มันมาถึง (บังคับเมื่อ response ถูก sign) และมี InResponseTo ตรงกับ request ที่ browser session นี้ยังรอคำตอบอยู่ แล้วก็ mark ว่า request นั้นใช้ไปแล้ว
  3. ตรวจ signature ด้วย key จาก metadata ของ IdP ห้ามใช้ certificate ที่ message แนบมาเองใน KeyInfo รับแค่ algorithm ที่คาดไว้ (OWASP ขอให้ใช้ RSA-SHA256 หรือที่แข็งแรงกว่า และไม่ใช้ SHA-1) และรับแค่ transform แบบ enveloped-signature กับ exclusive canonicalization (Core §5.4.4) ดูให้แน่ใจด้วยว่า reference ชี้ไปที่ element ตัวเดียวกับที่คุณจะอ่านต่อ
  4. Issuer ของ assertion ตรงกับ entity ID ของ IdP
  5. Subject confirmation: มี SubjectConfirmationData แบบ bearer ที่ Recipient เป็น ACS URL ของคุณ NotOnOrAfter ยังไม่ผ่านไป และ InResponseTo เป็น request ID ของคุณ (ไม่มีได้ก็ต่อเมื่อคุณรับ response ที่ไม่มีใครขอ)
  6. Conditions: NotBefore ≤ ตอนนี้ < NotOnOrAfter โดยเผื่อเรื่องนาฬิกาคลาดกันไว้นิดหน่อย มี Audience ตรงกับ entity ID ของคุณ และปฏิเสธ condition ไหนก็ตามที่คุณไม่เข้าใจ
  7. Replay: จำ ID ของทุก assertion ที่รับไปแล้วไว้จนกว่า NotOnOrAfter ของมันจะผ่านไป แล้วปฏิเสธตัวที่ซ้ำ (Profiles §4.1.4.5 บังคับเรื่องนี้สำหรับ POST binding)
  8. อ่านแค่ส่วนที่ถูก sign: เอา NameID กับ attribute มาจาก element ที่ signature ครอบคลุม โดยหาจากตำแหน่งของมัน ไม่ใช่ค้นหา <Assertion> หรือ NameID ตัวแรกใน document
  9. ข้อจำกัดของ session: ถ้า AuthnStatement มี SessionNotOnOrAfter ให้จบ session ของคุณภายในเวลานั้น และเช็ก authentication context ด้วยถ้าแอปต้องใช้ MFA

มันคือวินัยแบบเดียวกับ JWT validation บวกกับทางพังแบบอื่น ๆ ที่ XML แถมมาให้

Metadata และ certificate rollover SAML metadata (หนึ่ง EntityDescriptor ต่อฝั่ง) บอก endpoint และ binding ของแต่ละตัว, key ที่ mark ไว้สำหรับ signing หรือ encryption และ flag อย่าง WantAuthnRequestsSigned ของ IdP กับ AuthnRequestsSigned และ WantAssertionsSigned ของ SP ส่วน validUntil กับ cacheDuration กำหนดว่าสำเนาหนึ่งใช้ได้นานแค่ไหน signing certificate มีวันหมดอายุ อย่าง Microsoft Entra ID ก็ สร้าง self-signed certificate อายุสามปีให้ SAML application แต่ละตัว และส่งอีเมลเตือนก่อนหมดอายุ 60, 30 และ 7 วัน การ rollover ที่ไม่ทำให้ระบบล่มจะเผยแพร่ certificate ใหม่ก่อน ให้ SP แต่ละตัวเพิ่มมันจาก metadata เป็น key ที่เชื่อถือตัวที่สอง แล้วค่อยเปลี่ยนไป sign ด้วยตัวใหม่ และค่อยปลดตัวเก่าหลังจากนั้น Microsoft ขอให้ SaaS vendor อ่าน metadata URL แยกตามแอปของลูกค้าแต่ละราย เช็กอย่างน้อยทุก 24 ชั่วโมง และเก็บ signing certificate ไว้ทั้งตัวหลักและตัวสำรอง ส่วน SP ที่ก็อป certificate มาแปะเองด้วยมือจะปฏิเสธการเข้าสู่ระบบทุกครั้งตั้งแต่วินาทีที่ IdP เปลี่ยน

ใช้ตอนไหนดี

  • Web application และ SaaS ในองค์กร ที่ผู้ใช้เข้าสู่ระบบผ่าน IdP ของบริษัท ส่วน IdP สำหรับพนักงานอย่าง Microsoft Entra ID และ Okta ก็มีเอกสารเรื่องการเชื่อม SAML สำหรับงานนี้
  • แอปที่พูด SAML อยู่แล้ว หรือลูกค้าที่ IdP ไม่มีทางเลือกอื่นให้ แนวทางการวางแผน ของ Microsoft คือให้เลือก OpenID Connect ถ้าแอปรองรับ และใช้ SAML กับแอปเดิมที่ไม่ได้ใช้ OpenID Connect หรือ OAuth
  • ไม่ใช่สำหรับ native mobile app, single-page app หรือ API Web Browser SSO profile ส่งคำรับรองที่ sign แล้วให้ web app ฝั่ง server ผ่านการ POST ของ browser และไม่ได้ให้อะไรไว้เรียก API เลย OpenID Connect ที่อยู่บน OAuth 2.0 Authorization Code + PKCE ครอบคลุมกรณีพวกนี้: ID token แบบ JSON สำหรับแอป access token สำหรับ API และ authorization code ที่เอาไปแลกผ่าน back channel ทำให้ token ไม่เคยไปอยู่ใน URL หรือ form ส่วน IdP หลายตัวก็พูดได้ทั้งสองแบบ และ federation broker ก็พูด SAML กับ IdP ของลูกค้า แล้วพูด OpenID Connect กับแอปของคุณได้ (ดู Federated Identity)

ได้อะไร เสียอะไร

  • XML signature ทำผิดได้ง่าย canonicalization, ID และ reference เปิดช่องให้ parser อ่านอะไรบางอย่างที่ไม่ใช่สิ่งที่ถูก sign และประวัติการโจมตีด้านล่างก็ยาวมาก ให้ใช้ library ที่ยังมีคนดูแล และ patch ให้ทันเสมอ
  • assertion เป็น bearer credential ที่วิ่งใน front channel ใครถือมันไว้ก็ใช้มันได้จนกว่าจะหมดอายุ และมันวิ่งผ่าน browser, extension ของ browser และอะไรก็ตามที่ log traffic ไว้ อายุที่สั้น การตรวจ Recipient, Audience และ replay และการเข้ารหัส (ถ้าเลือกใช้) ช่วยจำกัดความเสียหาย
  • ต้อง config แยกทุกแอป SP กับ IdP ทุกคู่ต้องแลก metadata กัน ตกลง NameID format กับชื่อ attribute และต้องรอดผ่าน certificate rollover ให้ได้ ถ้ามีแอปหลายร้อยตัวก็เป็นงาน operation จริงจัง
  • Logout ไม่แข็งแรง การจบ session หนึ่งแทบไม่เคยจบ session อื่นได้อย่างเชื่อถือได้ (ดูด้านล่าง)
  • attribute เป็นภาพ ณ ตอนนั้น group membership คือค่าตอนเข้าสู่ระบบ ส่วน group ที่ถูกเอาออกหรือบัญชีที่ถูกปิดจะเห็นผลก็ตอนเข้าสู่ระบบครั้งถัดไป ยกเว้นคุณจะ provision ผ่าน SCIM ด้วย หรือให้ session สั้น

ข้อควรรู้ตอนลงมือทำ

  • ใช้ library ที่ยังมีคนดูแล หรือใช้ broker อย่าเขียนการตรวจ XML signature เอง library ที่ยังออก release ใหม่อยู่ก็มี SAML 2.0 support ของ Spring Security (Java), Sustainsys.Saml2 (.NET), pysaml2 (Python), node-saml (Node.js), ruby-saml (Ruby) และ SimpleSAMLphp (PHP) อีกทางคือให้ identity broker พูด SAML กับ IdP ของลูกค้า แล้วให้ OpenID Connect กับแอปของคุณ
  • sign response หรือ sign assertion ถ้าใช้ POST binding ทุก assertion ต้องมี signature ปกป้อง จะ sign ที่ตัว assertion เองหรือที่ response ทั้งก้อนก็ได้ (SAML 2.0 Errata 05, E26) ตัว signature ของ response ครอบคลุม Destination, InResponseTo และ status ด้วย ส่วน signature ของ assertion ยังตรวจได้แม้ทีหลังจะดึง assertion ออกมาเก็บแยก Microsoft Entra ID sign ที่ assertion โดย default สำหรับ gallery application ส่วนใหญ่ เปลี่ยนไป sign ที่ response หรือทั้งสองอย่างได้ และ sign ด้วย SHA-256 ยกเว้น admin จะเปลี่ยนให้แอปที่รองรับแค่ SHA-1 ถ้าคุณต้องการ assertion ที่ถูก sign ให้ตั้ง WantAssertionsSigned ใน metadata ของคุณ และเช็กว่า signature ตัวที่คุณตรวจครอบคลุมข้อมูลที่คุณใช้จริง
  • เข้ารหัส assertion SAML Bindings แนะนำว่าไม่ควรใช้ Redirect และ POST binding ถ้าเนื้อหาต้องไม่ถูกเปิดเผยให้ browser เห็น ส่วน EncryptedAssertion (XML Encryption โดยใช้ key จาก metadata ของ SP ที่ mark ไว้สำหรับ encryption) ทำให้ attribute ไม่ไปถึง browser และ extension ของมัน ให้ decrypt ก่อน แล้วค่อยตรวจ signature แบบเดิมทุกอย่าง: การเข้ารหัสซ่อนเนื้อหาได้ แต่ไม่ได้พิสูจน์อะไรเลยว่าใครเป็นคนเขียน SAML token encryption ของ Entra ID ที่เป็น feature ของ P1 หรือ P2 ใช้ AES-256 กับ public key ของ certificate ที่คุณอัปโหลด นอกจากนี้ OWASP ยังแนะนำให้ตัด algorithm ของ XML Encryption ที่รู้กันว่าไม่ปลอดภัยออกด้วย
  • RelayState ใช้พาผู้ใช้กลับไปที่ที่เขาเริ่ม ส่วน binding ก็จำกัดมันไว้ที่ 80 byte บอกว่าควรมีการปกป้อง integrity และบังคับให้ IdP ส่งมันกลับมาแบบไม่แก้อะไร ให้เก็บ URL ปลายทางไว้ที่ server แล้วส่ง key สุ่มที่อ่านความหมายไม่ออกไปแทน ถ้า portal ส่ง URL มาใน RelayState สำหรับการเข้าสู่ระบบแบบ IdP-initiated ให้รับเฉพาะ URL ที่อยู่ใน allowlist ไม่อย่างนั้นมันจะกลายเป็น open redirect (OWASP)
  • การเข้าสู่ระบบแบบ IdP-initiated response ที่ไม่มีใครขอไม่มีอะไรให้จับคู่: ไม่มี request ID และไม่มีหลักฐานว่า browser นี้เป็นคนเริ่มการเข้าสู่ระบบ ทำให้เสียการป้องกัน assertion ที่ถูก replay หรือถูกฉีดเข้ามา และการป้องกัน login CSRF ที่ attacker พาเหยื่อเข้าสู่ระบบในบัญชีของ attacker เอง ส่วน OWASP ก็บอกว่า flow นี้ปลอดภัยน้อยกว่าโดยการออกแบบ และ Auth0 แนะนำว่าไม่ควรใช้ ถ้าใช้แบบ SP-initiated ได้ ส่วน NIST SP 800-63C-4 บังคับให้ป้องกัน assertion injection ตั้งแต่ FAL2 ขึ้นไป โดยให้ transaction เริ่มที่ relying party รูปแบบที่ปลอดภัยกว่าคือ tile บน portal ที่เปิด login URL ของแอป แล้วแอปก็เริ่มการเข้าสู่ระบบแบบ SP-initiated เอง: My Apps ของ Entra ID ทำแบบนี้ได้ถ้าแอปมี Sign on URL และ OpenID Connect ก็กำหนดไอเดียเดียวกันไว้เป็น login ที่ third party เป็นคนเริ่ม ถ้าต้องรับ response ที่ไม่มีใครขอจริง ๆ ให้รับเฉพาะจาก IdP ที่คุณระบุไว้ พร้อม replay cache และอายุที่สั้น
  • request ID ต้องรอดผ่าน cross-site POST response มาถึงเป็น POST ระดับ top-level จากเว็บของ IdP และ browser ไม่ส่ง cookie ที่เป็น SameSite=Lax หรือ Strict ไปกับ cross-site POST ถ้า ID ของ AuthnRequest ที่ยังรอคำตอบอยู่เก็บไว้ใน session ที่อิงกับ cookie ตัว cookie นั้นต้องเป็น SameSite=None; Secure ไม่อย่างนั้น InResponseTo จะไม่มีวันตรง
  • Attribute mapping และ provisioning ใช้ NameID จาก IdP ที่ออกมันเป็น key ของผู้ใช้ (ถ้าได้ให้เป็นแบบ persistent) ไม่ใช่ใช้อีเมล และ map group ไปเป็น role ของคุณเอง IdP แต่ละตัวตั้งชื่อ attribute ตามแบบของตัวเอง: Entra ID ใช้ URI อย่าง http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name และ ไม่ใส่ groups claim มาเลย ถ้าผู้ใช้อยู่ในมากกว่า 150 group ส่วน just-in-time provisioning สร้างผู้ใช้ในระบบตอนเข้าสู่ระบบครั้งแรก แล้ว SCIM (RFC 7643, RFC 7644) ก็สร้างผู้ใช้และ group ไว้ล่วงหน้า และลบคนที่ลาออกไปแล้ว Federated Identity อธิบายทั้งสองแบบ และวงจรชีวิตของบัญชีรอบ ๆ มัน
  • Single logout เชื่อถือไม่ได้ Single Logout profile ให้ IdP ส่ง LogoutRequest (ที่ต้อง sign ถ้าใช้ Redirect และ POST binding) ไปหาทุก SP ที่ผู้ใช้มี session อยู่ โดยระบุด้วย SessionIndex จะส่งผ่าน browser หรือผ่าน SOAP ก็ได้ ถ้าส่งผ่าน browser ตัว SP แค่ตัวเดียวที่ล่มหรือช้าก็ทำให้ทั้งสายพังพร้อมหน้า error และ SP ที่เริ่ม logout ก็รู้น้อยมากว่าอะไรพัง บันทึกเรื่อง SLO ของ Shibboleth สรุปว่าคำแนะนำที่สมเหตุสมผลหลังความล้มเหลวส่วนใหญ่ก็คือให้ปิด browser ถ้าส่งผ่าน SOAP ตัว SP จะไม่เคยเห็น cookie ของผู้ใช้ เลยต้องหาและจบ session ได้ด้วย NameID หรือ SessionIndex ให้ session ของแอปสั้นไว้ และมอง single logout ว่าเป็นแค่ best effort
  • ประเภทการโจมตีที่รู้จักกันดี แต่ละข้อคือเหตุผลที่ต้องใช้ library ที่คนใช้กันเยอะและยังมีคนดูแล อัปเดตให้ทัน และ test มันด้วย response ที่ถูกแก้
    • Signature wrapping: attacker ย้าย assertion ที่ถูก sign ไปไว้ข้าง ๆ แล้วใส่ตัวปลอมไว้ตรงที่แอปจะมองหา ทำให้ signature ตรวจผ่าน แต่แอปไปอ่านตัวปลอม มี paper ของ USENIX Security ปี 2012 ที่เจอช่องโหว่ wrapping ระดับ critical ใน 11 จาก 14 SAML framework หลัก ๆ ในนั้นก็มี Salesforce, Shibboleth และ IBM XS40
    • จุดอ่อนของ XML parser: DTD เปิดทางให้การโจมตีแบบ external entity (XXE) และ entity-expansion เลยต้องปฏิเสธมัน parser สองตัวที่อ่านไม่ตรงกันก็อันตรายพอกัน: ในเดือนมีนาคม 2025 GitHub Security Lab แสดงให้เห็นว่า ruby-saml ถึงเวอร์ชัน 1.17.0 (CVE-2025-25291 และ CVE-2025-25292) ใช้ XML parser สองตัวคือ REXML กับ Nokogiri ตอนตรวจ signature และ document ที่สองตัวนี้อ่านไม่เหมือนกันก็ทำให้ใครก็ตามที่ถือ signature ที่ถูกต้องจาก IdP อยู่หนึ่งตัว เข้าสู่ระบบเป็นผู้ใช้คนไหนก็ได้ GitLab โดนด้วย และ ruby-saml 1.18.0 ก็แก้แล้ว
    • Comment ใน NameID (2018): canonicalization มักไม่เอา comment ไปรวมในส่วนที่ถูก sign ทำให้ attacker ที่ลงทะเบียนเป็น user@example.com.evil.example แทรก <!----> หลัง user@example.com ได้โดยไม่ทำให้ signature เสีย แล้ว library ที่อ่านแค่ text node ตัวแรกก็จะเห็นเป็น user@example.com ส่วน CERT/CC VU#475445 (กุมภาพันธ์ 2018) ที่ Kelby Ludwig จาก Duo Security รายงาน ได้ระบุ implementation ที่โดนไว้ รวมถึง python-saml, ruby-saml, saml2-js, OmniAuth-SAML และ OpenSAML C++ ของ Shibboleth
  • สิ่งที่แอปยังต้องดูแลเอง SAML จบที่การเข้าสู่ระบบ แอปต้องดูแล session ของตัวเอง (cookie flag, idle timeout และ absolute timeout ที่ไม่เกิน SessionNotOnOrAfter) และตัดสินเรื่อง authorization เอง โดยมอง role ที่ map มาเป็น input ไม่ใช่ตัวคำตัดสิน ดู Sessions vs Tokens และ Policy-Based Authorization
  • Testing และ troubleshooting browser extension อย่าง SAML-tracer (Firefox และ Chrome) decode ทุก SAML message ที่วิ่งผ่าน ความล้มเหลวส่วนใหญ่มาจาก config ไม่ตรงกัน: entity ID, ACS URL, certificate, NameID format หรือชื่อ attribute ถ้ายกตัวอย่าง Microsoft Entra ID มันจะ รายงาน AADSTS50011 เมื่อ reply (ACS) URL ไม่ได้ลงทะเบียนไว้ และ AADSTS700016 เมื่อหา identifier (entity ID) ไม่เจอ เช็กนาฬิกาด้วย: Entra ID ตั้ง NotBefore เป็นเวลาที่ออก assertion และไม่เผื่อเวลาให้นาฬิกาที่คลาดกันเลย SP ที่นาฬิกาช้าไปนิดเดียวเลยจะเห็น assertion ที่ยังไม่ถึงเวลาใช้ได้ ให้ NTP ทำงานตลอด และเผื่อเรื่องนาฬิกาคลาดไว้น้อย ๆ เพิ่ม negative test ด้วย: signature ที่เสีย audience ที่ผิด assertion ที่หมดอายุหรือถูก replay และ response ที่ไม่มีใครขอ (ถ้าคุณไม่รับมัน) ต้องถูกปฏิเสธทั้งหมด