ข้ามไปยังเนื้อหา
กลับไปหน้าบทความ
การพัฒนาซอฟต์แวร์

OAuth 2.0 กับ OpenID Connect ต่างกันอย่างไร

OAuth 2.0 ให้แอปได้ access token ไปเรียก API แทนผู้ใช้ ส่วน OpenID Connect เพิ่ม ID token ที่บอกว่าใคร sign in ไล่ authorization code flow กับ PKCE ทีละ request

ปานกลาง7 นาทีในการอ่าน

ในบทความนี้

OAuth 2.0 เป็นเรื่อง authorization คือให้แอปได้ access token ไปเรียก API แทนผู้ใช้ โดยแอปไม่ต้องรู้รหัสผ่านของผู้ใช้ ส่วน OpenID Connect (OIDC) เป็นเรื่อง authentication คือชั้นที่วางบน OAuth 2.0 เพื่อให้แอปรู้ว่าใครเป็นคน sign in ผ่าน token อีกตัวที่ชื่อ ID token

ทั้งสองใช้ flow เดียวกัน ต่างกันที่ request ของ OIDC ใส่ openid ไว้ใน scope และ token response มี id_token เพิ่มมา บทความนี้ไล่ authorization code flow แบบมี PKCE ทีละ request แล้วดูว่า token แต่ละตัวมีไว้ให้ใครอ่านครับ

authorization code flow แบบมี PKCE หกขั้น ตั้งแต่ /authorize จนถึงการเรียก API ด้วย access token

OAuth 2.0 ใช้ทำอะไร

OAuth 2.0 ใช้ให้แอปของบุคคลที่สามได้สิทธิ์เข้าถึง HTTP service แบบจำกัด โดยเจ้าของข้อมูลเป็นคนอนุมัติ RFC 6749 แบ่งบทบาทไว้สี่บทบาท

บทบาท ในตัวอย่างนี้
resource owner ผู้ใช้ที่เป็นเจ้าของข้อมูล
client แอปที่ต้องการเรียก API
authorization server server ที่ยืนยันตัวผู้ใช้และออก token
resource server API ที่รับ access token

ผลลัพธ์ของ OAuth คือ access token RFC 6749 นิยามว่าเป็นข้อมูลที่แทนสิทธิ์ที่ผู้ใช้ให้กับ client และปกติ client อ่านความหมายข้างในไม่ได้ (opaque) (หัวข้อ 1.4) token แทน scope และระยะเวลาที่ได้รับอนุญาต และ resource server เป็นคนบังคับใช้ ข้อนี้คือจุดที่ทำให้ OAuth อย่างเดียวไม่พอสำหรับการ sign in เพราะ token ถูกออกแบบมาให้ API อ่าน ไม่ได้ออกแบบมาให้แอปใช้รู้ว่าผู้ใช้เป็นใคร

OpenID Connect เพิ่มอะไรเข้ามา

OIDC เพิ่ม ID token OpenID Connect Core 1.0 เรียกตัวเองว่าชั้น identity แบบเรียบง่ายที่วางบน OAuth 2.0 ซึ่งทำให้ client ยืนยันตัวตนของผู้ใช้ได้จากการ authenticate ที่ authorization server ทำ และสิ่งที่เพิ่มเข้ามาหลักคือ ID token ซึ่งอยู่ในรูป JWT และต้องมีลายเซ็นด้วย JWS

claim ที่ ID token ต้องมีทุกครั้งได้แก่ (OIDC Core หัวข้อ 2)

ส่วน nonce ใช้ผูก session ของ client เข้ากับ ID token และกัน replay ถ้าแอปส่ง nonce ไปใน request ค่าเดียวกันจะกลับมาใน ID token

นอกจาก ID token แล้ว OIDC ยังกำหนด UserInfo endpoint ซึ่งเป็น OAuth 2.0 protected resource ที่คืน claim ของผู้ใช้ แอปเรียกด้วย access token ที่ได้จาก flow เดียวกัน และ request ของ OIDC ต้องมี scope openid ทุกครั้ง

Authorization code flow กับ PKCE ทำงานอย่างไร

flow นี้มีหกขั้น แอปส่ง browser ไปที่ authorization server ผู้ใช้ sign in แอปได้ code กลับมา แล้วเอา code ไปแลก token ผ่านช่องทางตรง PKCE (RFC 7636) เพิ่มค่าลับหนึ่งค่าที่ผูก request แรกกับ request แลก token เข้าด้วยกัน ค่าในตัวอย่างข้างล่างมาจากตัวอย่างใน RFC 6749, RFC 7636 และ OIDC Core

ขั้นที่ 1 แอปสร้าง code_verifier และ code_challenge

ก่อนเริ่ม แอปสุ่มค่า code_verifier ความยาวระหว่าง 43 ถึง 128 ตัวอักษร แล้วคำนวณ code_challenge จากสูตร

code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))

code_verifier  = dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
code_challenge = E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM

คู่ค่านี้มาจากภาคผนวก B ของ RFC 7636 วิธีนี้เรียกว่า S256 และ client ที่ทำได้ต้องใช้ S256 ส่วน code_verifier เก็บไว้ในแอป ไม่ส่งออกไปในขั้นนี้

ขั้นที่ 2 แอปส่ง browser ไปที่ /authorize

GET /authorize?response_type=code
  &client_id=s6BhdRkqt3
  &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
  &scope=openid%20profile
  &state=af0ifjsldkj
  &nonce=n-0S6_WzA2Mj
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256 HTTP/1.1
Host: server.example.com

response_type=code บังคับต้องมี และบอกว่าขอ authorization code ส่วน state คือค่าที่แอปตั้งเองซึ่ง authorization server จะส่งกลับมาตอน redirect และควรใช้กัน CSRF scope=openid ทำให้ request นี้เป็น OIDC ถ้าไม่มี openid ก็เป็น OAuth ธรรมดา

ขั้นที่ 3 ผู้ใช้ sign in แล้ว browser ได้ code กลับมา

authorization server ยืนยันตัวผู้ใช้และถามว่าอนุญาตหรือไม่ จากนั้น redirect browser กลับไปที่ redirect_uri

HTTP/1.1 302 Found
Location: https://client.example.org/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=af0ifjsldkj

แอปต้องตรวจว่า state ตรงกับที่ส่งไป code นี้มีอายุสั้น RFC 6749 แนะนำให้อายุไม่เกิน 10 นาที และใช้ได้ครั้งเดียว ถ้ามีคนเอา code เดิมมาใช้ซ้ำ server ต้องปฏิเสธ

code ผ่าน browser และ URL มาแล้ว จึงอาจรั่วได้ ปัญหาที่ PKCE ถูกออกแบบมาแก้คือการดักเอา authorization code ไป เช่นแอปอันตรายบนเครื่องเดียวกันที่ลงทะเบียน custom URI scheme เดียวกับแอปจริงไว้

ขั้นที่ 4 แอปแลก code เป็น token

แอปส่ง request ตรงไปที่ token endpoint โดยไม่ผ่าน browser

POST /token HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
&client_id=s6BhdRkqt3
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

server คำนวณ code challenge จาก code_verifier ที่ได้รับแล้วเทียบกับ code_challenge ที่เก็บไว้ ตอนออก code ถ้าไม่ตรงก็ไม่ออก token ผู้ที่ดักได้แค่ code จึงแลก token ไม่ได้ เพราะไม่มี code_verifier ซึ่งไม่เคยผ่าน browser

ขั้นที่ 5 แอปได้ access token และ ID token

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store

{
  "access_token": "SlAV32hkKG",
  "token_type": "Bearer",
  "refresh_token": "8xLOxBtZp8",
  "expires_in": 3600,
  "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ...."
}

ตัวอย่างนี้มาจากtoken response ใน OIDC Core ถ้า decode ส่วน payload ของ id_token ในตัวอย่างนั้นจะได้ claim ประมาณนี้

{
  "iss": "http://server.example.com",
  "sub": "248289761001",
  "aud": "s6BhdRkqt3",
  "nonce": "n-0S6_WzA2Mj",
  "exp": 1311281970,
  "iat": 1311280970
}

ก่อนเชื่อ ID token แอปต้องตรวจตามที่ OIDC Core กำหนด ได้แก่ issuer ต้องตรงกับค่า iss ทุกตัวอักษร aud ต้องมี client_id ของแอป และ token ต้องไม่หมดอายุ (iss ในตัวอย่างของ spec ขึ้นต้นด้วย http แต่ spec กำหนดให้ค่าจริงเป็น URL แบบ https) ถ้าได้ ID token มาทางช่องทางตรงกับ token endpoint แบบในขั้นนี้ OIDC Core ให้ใช้การตรวจ TLS server แทนการตรวจลายเซ็นได้ แต่ ID token ที่ได้มาทางอื่นต้องตรวจลายเซ็นด้วย key ของ issuer

ขั้นที่ 6 แอปเรียก API ด้วย access token

GET /api/orders HTTP/1.1
Host: api.example.com
Authorization: Bearer SlAV32hkKG

รูปแบบ Authorization: Bearer มาจาก RFC 6750 ซึ่งนิยาม bearer token ว่าใครก็ตามที่ถือ token ใช้มันได้แบบเดียวกับคนอื่นที่ถือ โดยไม่ต้องพิสูจน์ว่ามี key ลับ access token จึงต้องเก็บให้ดีเท่ากับรหัสผ่านครับ

ID token กับ access token ต่างกันอย่างไร

ID token มีไว้ให้แอปอ่านเพื่อรู้ว่าใคร sign in ส่วน access token มีไว้ให้ API อ่านเพื่อตัดสินว่าอนุญาต request นี้หรือไม่ สองตัวนี้สลับกันใช้ไม่ได้

ID token access token
มาจาก spec OpenID Connect Core OAuth 2.0 (RFC 6749)
ผู้รับ client (aud คือ client_id) resource server
รูปแบบ JWT ที่มีลายเซ็นเสมอ ไม่กำหนด อาจเป็นค่าสุ่มหรือ JWT
ใช้ทำอะไร บอกว่าใคร sign in เมื่อไร เรียก API ตาม scope
ส่งไปที่ API ไม่ควร ใช่ ใน Authorization: Bearer

ถ้า access token เป็นค่าสุ่ม API ถาม authorization server ได้ผ่าน token introspection (RFC 7662) ซึ่งส่ง request แบบ POST แล้วได้คำตอบว่า token ยัง active หรือไม่ ถ้า access token เป็น JWT ก็มีมาตรฐาน RFC 9068 ซึ่งกำหนดให้ใส่ typ เป็น at+jwt เหตุผลหนึ่งที่ RFC 9068 ระบุไว้คือกันไม่ให้ resource server รับ ID token ของ OIDC เป็น access token เพราะทั้งคู่เป็น JWT ที่มาจาก issuer เดียวกันได้

ความผิดพลาดที่เกิดจากการสลับสองตัวนี้มีสองแบบ แบบแรกคือ API รับ ID token แล้วถือว่าเป็นสิทธิ์เรียก API ทั้งที่ aud ของ ID token คือแอป ไม่ใช่ API แบบที่สองคือแอปเอา access token มาใช้เป็นหลักฐานว่าผู้ใช้ sign in แล้ว ทั้งที่ access token ไม่ได้บอกว่าผู้ใช้เป็นใคร และแอปก็ไม่ควรต้องอ่านมัน

RFC 9700 เปลี่ยนคำแนะนำอะไรบ้าง

RFC 9700 (มกราคม 2568) คือ Best Current Practice ด้านความปลอดภัยของ OAuth 2.0 ซึ่งปรับปรุงคำแนะนำของ RFC 6749, 6750 และ 6819 และเลิกใช้บางโหมดที่ไม่ปลอดภัย ข้อที่กระทบ flow ข้างบนโดยตรงมีดังนี้

  1. PKCE สำหรับทุก client public client ต้องใช้ PKCE ส่วน confidential client แนะนำให้ใช้ และ authorization server ต้องรองรับ PKCE RFC ยังเขียนไว้ว่าแม้ PKCE จะออกแบบมาสำหรับ native app แต่คำแนะนำนี้ใช้กับ OAuth client ทุกแบบ รวมถึง web application
  2. ใช้ S256 วิธีที่ไม่เปิดเผย verifier ใน request แรก ตอนนี้มีแค่ S256
  3. เลิกใช้ implicit grant client ไม่ควรใช้ response_type=token หรือ response type อื่นที่ส่ง access token กลับมาใน authorization response
  4. ห้ามใช้ password grant resource owner password credentials grant ต้องไม่ถูกใช้ เพราะทำให้ client เห็นรหัสผ่านของผู้ใช้
  5. เทียบ redirect URI แบบตรงทุกตัวอักษร authorization server ต้องใช้ exact string matching ยกเว้น port ของ localhost สำหรับ native app
  6. จำกัดผู้รับและผู้ใช้ token access token ควรจำกัด audience ให้ resource server ตัวเดียวหรือไม่กี่ตัว และ refresh token ของ public client ต้องผูกกับผู้ส่ง (sender-constrained) หรือใช้ refresh token rotation

สำหรับ OIDC RFC 9700 มีข้อยกเว้นหนึ่งข้อ คือ confidential client ของ OIDC อาจใช้ nonce แทน PKCE ได้ ถ้าทำตามข้อควรระวังเพิ่มเติมในหัวข้อ 4.5.3.2 แต่ถ้าเริ่มระบบใหม่ ผมใช้ทั้ง PKCE และ nonce ไปเลย เพราะทำเพิ่มแค่ไม่กี่บรรทัด และไม่ต้องมานั่งเช็กว่าเข้าเงื่อนไขข้อยกเว้นหรือไม่ครับ

แหล่งอ้างอิง

อ่านต่อ