ให้พนักงานในองค์กร sign in เข้า Amazon Quick Desktop ด้วย identity ที่มีอยู่แล้ว โดยใช้ Amazon Cognito, CloudFront Function ราว 60 บรรทัด และไม่มี server สักตัว พร้อม CloudFormation แบบ ready-to-deploy


Architecture: Quick Desktop -> CloudFront Function -> Cognito -> IAM Identity Center
Architecture overview

ปัญหา

Amazon Quick Desktop รองรับ Enterprise sign-in ผ่าน identity provider ใดก็ได้ ที่พูด OIDC ได้: เรากรอก Authorization Endpoint, Token Endpoint, JWKS URI, Issuer และ Client ID ไว้ใน Extension access แล้วตัว desktop app จะทำ Authorization Code + PKCE flow มาตรฐานกับ endpoint เหล่านั้นเอง

แต่ถ้า identity ขององค์กรอยู่ใน AWS IAM Identity Center จะติดอยู่หนึ่งเรื่อง: Identity Center ไม่ใช่ OIDC authorization server ที่จะเอา Quick Desktop ไปชี้ใส่ ตรง ๆ ได้ ต้องมีชั้น OIDC มาคั่นกลาง ซึ่งตัวเลือกธรรมชาติที่สุดคือ Amazon Cognito — ด้านหน้าพูด OIDC ด้านหลัง federate ไป Identity Center ผ่าน SAML

ฟังดูเหมือนจะจบ แต่ในทางปฏิบัติมีสองจุดที่ทำให้ flow พัง:

  1. offline_access — Quick Desktop ใส่ scope offline_access มาใน authorize request เสมอ แต่ Cognito app client แบบ public (ไม่มี client secret ซึ่งเป็น สิ่งเดียวที่ desktop app ใช้ได้) ไม่รองรับ scope นี้ Cognito จึงปฏิเสธ request ทันที
  2. หน้า sign-in ส่วนเกิน — ถ้าไม่บอกอะไรเพิ่ม Cognito จะแสดงหน้า hosted sign-in ของตัวเองก่อนค่อย federate ทั้งที่ผู้ใช้ควรเด้งไปหน้า Identity Center (หรือ IdP ต้นทาง) ทันที

สังเกตว่าปัญหาทั้งสองอยู่ใน HTTP request เดียวกัน คือ GET /oauth2/authorize ที่ browser ยิงออกไป — แปลว่าแก้ได้ด้วย proxy เล็ก ๆ ที่คั่นหน้า endpoint เดียวนี้

ทางแก้: authorize-proxy ด้วย CloudFront Function

เราวาง Amazon CloudFront distribution ไว้หน้า authorize endpoint ของ Cognito พร้อม CloudFront Function ที่ event viewer-request ทำหน้าที่:

Request ไม่ได้วิ่งไปถึง origin ด้วยซ้ำ — function สร้าง response เองที่ edge ส่วนที่เหลือของ flow ทั้งหมด (token exchange, JWKS, issuer) คุยกับ Cognito ตรง ไม่ผ่าน proxy ไม่มี Lambda ไม่มี server ไม่มี state

Quick Desktop
   │ GET /oauth2/authorize?...scope=openid+email+profile+offline_access...
   ▼
CloudFront Function ── 302 ──> Cognito Hosted UI (authorize)
                                   │ SAML AuthnRequest
                                   ▼
                          IAM Identity Center ──> ผู้ใช้ sign in
                                   │ SAML assertion
                                   ▼
                               Cognito ── code ──> Quick Desktop (localhost:18080)
                                   ▲
                                   └── POST /oauth2/token (PKCE) — ตรง ไม่ผ่าน proxy

แล้ว refresh ล่ะ ในเมื่อ offline_access หายไป? Cognito ออก refresh token ตาม refresh token validity ของ app client (เราตั้งไว้ 30 วัน) ไม่ได้ขึ้นกับ scope offline_access — session จึง refresh เงียบ ๆ ได้ตามปกติ ผู้ใช้ไม่โดน บังคับ login ใหม่ทุกวัน

โบนัสของสถาปัตยกรรมนี้: เพราะเราลงทะเบียน Cognito เป็น Trusted Token Issuer ใน Identity Center ตัว token จึงรองรับ trusted identity propagation — ฝั่ง Quick รู้ว่า มนุษย์คนไหน กำลังใช้งาน ไม่ใช่แค่รู้ว่ามี token ที่ valid ส่งมา

สิ่งที่ต้อง deploy

ทั้งหมดอยู่ใน repository ที่แนบกับบทความนี้:

ส่วน เครื่องมือ วางไว้ที่ account ไหนได้
Cognito user pool, hosted UI, public PKCE client, SAML IdP templates/identity.yaml identity account
CloudFront Function + distribution (ตัว proxy) templates/edge.yaml network account
Trusted Token Issuer scripts/create-trusted-token-issuer.sh identity account
SAML app ใน Identity Center, Extension access ใน Quick console (ไม่มี API รองรับ) identity / workload account

การออกแบบนี้ เป็นมิตรกับ multi-account โดยไม่ต้องตั้ง cross-account IAM policy เลยแม้แต่ข้อเดียว: ทุกจุดเชื่อม trust กันผ่าน URL (SAML metadata URL, OIDC issuer URL, public HTTPS origin) ข้อจำกัดแข็งข้อเดียวเป็นระดับองค์กร คือ account ที่รัน Amazon Quick ต้องอยู่ AWS Organization เดียวกัน กับ Identity Center และทุกอย่างต้องอยู่ us-east-1

ลงมือทำ

ขั้นที่ 1 — Cognito (phase 1)

aws cloudformation deploy \
  --region us-east-1 \
  --stack-name quick-desktop-identity \
  --template-file templates/identity.yaml \
  --parameter-overrides DomainPrefix=quick-desktop-<ACCOUNT_ID>

Stack จะ output ทุกค่าที่ขั้นถัด ๆ ไปต้องใช้: UserPoolId, ClientId, HostedUiDomain, TokenEndpoint, Issuer, JwksUri — รวมถึงสองค่าที่ SAML application จะถามหา: SamlAcsUrl และ SamlAudience

ทำไมต้องแบ่งสอง phase? Cognito ต้องการ SAML metadata จาก Identity Center ส่วน Identity Center ก็ต้องการ ACS URL กับ audience จาก Cognito — chicken-egg คลาสสิก Phase 1 สร้าง Cognito ให้ identifier เกิดก่อน จากนั้นไปสร้าง SAML app แล้วค่อยป้อน metadata URL กลับมาใน phase 2

ขั้นที่ 2 — สร้าง customer-managed SAML application ใน Identity Center (console)

นี่คือหนึ่งในสามขั้นที่ CloudFormation ทำไม่ได้ — API ของ Identity Center รองรับเฉพาะ customer-managed application แบบ OAuth 2.0 ไม่รองรับ SAML

IAM Identity Center console → Applications → Customer managed → Add application → SAML 2.0 แล้วกรอก:

ขั้นที่ 3 — Cognito (phase 2: ต่อ IdP)

aws cloudformation deploy \
  --region us-east-1 \
  --stack-name quick-desktop-identity \
  --template-file templates/identity.yaml \
  --parameter-overrides \
    DomainPrefix=quick-desktop-<ACCOUNT_ID> \
    IdcSamlMetadataUrl='<metadata URL จากขั้นที่ 2>'

Stack เดิมจะเพิ่ม SAML IdP ชื่อ IdentityCenter เข้า Cognito และสลับ supported identity provider ของ app client จาก COGNITO เป็น IdentityCenter ให้อัตโนมัติ

ขั้นที่ 4 — Trusted Token Issuer

ไม่มี CloudFormation resource สำหรับขั้นนี้ จึงเป็น script สั้น ๆ ครอบ aws sso-admin create-trusted-token-issuer (idempotent — รันซ้ำได้ปลอดภัย):

./scripts/create-trusted-token-issuer.sh <UserPoolId> <profile> us-east-1

Script จะลงทะเบียน issuer URL ของ Cognito (https://cognito-idp.us-east-1.amazonaws.com/<pool>) เป็น OIDC JWT trusted token issuer โดย map claim email เข้ากับ emails.value ใน identity store

ขั้นที่ 5 — Deploy ตัว authorize-proxy

aws cloudformation deploy \
  --region us-east-1 \
  --stack-name quick-desktop-authz-proxy \
  --template-file templates/edge.yaml \
  --parameter-overrides CognitoAuthDomain=<output HostedUiDomain>

หัวใจของ edge.yaml คือตัว function ซึ่งมีรายละเอียดสามจุดที่เคยกินเวลา debug จริงมาแล้ว จึงขอชี้ให้เห็นชัด ๆ:

// 1. ค่าใน query string มาถึงแบบ URL-ENCODED ตัวคั่น scope อาจเป็น
//    "%20" หรือ "+" แล้วแต่ client — ต้องตรวจหาแล้ว "คงตัวคั่นเดิมไว้"
//    ห้าม decode/re-encode เด็ดขาด (จะทำ redirect_uri เพี้ยน)
var sep = raw.indexOf('%20') >= 0 ? '%20' : (raw.indexOf('+') >= 0 ? '+' : ' ');

// 2. ตัดเฉพาะ offline_access ที่เหลือคงเดิมทุก byte
if (toks[i] && toks[i] !== 'offline_access') { kept.push(toks[i]); }

// 3. ข้ามหน้า sign-in ของ Cognito
qs['identity_provider'] = { value: IDP };

แล้วตรวจสอบ:

./scripts/verify-authorize-endpoint.sh "$AUTH_ENDPOINT" "$CLIENT_ID"

Script จะ assert — กับตัวคั่น scope ทั้งสองแบบ — ว่าได้ HTTP/2 302 ทันที พร้อม x-cache: FunctionGeneratedResponse from cloudfront, offline_access หายไป, มี identity_provider เติมเข้ามา และ PKCE code_challenge อยู่ครบ ไม่ถูกแตะต้อง

ขั้นที่ 6 — ตั้งค่า Amazon Quick (console)

Manage Quick → Extension access:

ฟิลด์ ค่า
Authorization Endpoint output AuthorizeEndpoint (CloudFront)
Token Endpoint output TokenEndpoint (Cognito ตรง)
JWKS URI output JwksUri
Issuer output Issuer
Client ID output ClientId

คำเตือนสองข้อจากประสบการณ์จริง:

ขั้นที่ 7 — ทดสอบ sign in

เปิด Quick Desktop → Sign in with SSO → ควรเด้งตรงไปหน้า sign-in ของ Identity Center (ไม่มีหน้า Cognito คั่น) แล้วกลับเข้าแอปได้เลย ถ้า profile เก่าในเครื่อง กวน: rm ~/.quickwork/profiles.json แล้วลองใหม่

Hardening สำหรับ production

edge.yaml มีพารามิเตอร์เสริมสามตัว: CustomDomain, AcmCertificateArn (ต้องอยู่ us-east-1) และ WebAclArn (scope CLOUDFRONT)

มี WAF gotcha หนึ่งข้อที่คุ้มค่ากับการอ่านบทความนี้: ถ้าคุณติด AWSManagedRulesCommonRuleSet กฎ EC2MetaDataSSRF_QUERYARGUMENTS และ EC2MetaDataSSRF_BODY จะ false-positive กับ redirect URI ของ Quick Desktop (redirect_uri=http://localhost:18080 เข้า pattern ของการโจมตี SSRF ไปหา instance metadata) ทำให้ authorize request ทุกอันโดน 403 หมด ต้อง override สองกฎนี้เป็น Count ส่วนที่เหลือคง Block ไว้:

aws wafv2 update-web-acl ... \
  # RuleActionOverrides:
  #   EC2MetaDataSSRF_QUERYARGUMENTS -> Count
  #   EC2MetaDataSSRF_BODY           -> Count

ทริคช่วยวินิจฉัย: aws wafv2 get-sampled-requests --rule-metric-name AWSCommon จะบอกชัดว่า managed rule ตัวไหนเป็นคนยิง

ค่าใช้จ่าย

ไม่มี Lambda ไม่มี server ไม่มีอะไรต้อง patch

สรุป

โจทย์ วิธีตอบ
Quick Desktop ต้องการ OIDC Cognito user pool + public PKCE client
ผู้ใช้อยู่ใน Identity Center Cognito SAML federation + customer-managed app
Public client ไม่รองรับ offline_access CloudFront Function ตัดออกที่ edge
ข้ามหน้า sign-in ของ Cognito Function บังคับ identity_provider=IdentityCenter
Refresh เงียบ ๆ โดยไม่มี offline_access Refresh-token validity ของ app client (30 วัน)
Token ระบุตัวตนคนได้ Trusted Token Issuer (trusted identity propagation)
Multi-account Trust เป็น URL ทุกจุด; บังคับ Org เดียวกันเฉพาะ Quick↔︎Identity Center

หยิบ template ไป deploy สอง stack กดตาม console อีกสามขั้น แล้วเครื่อง Quick Desktop ทั้ง fleet ก็ sign in ด้วย identity ที่องค์กรของคุณจัดการอยู่แล้ว


Template ทั้งหมดในบทความนี้ผ่าน cfn-lint และ assertion ใน verification script ยืนยันแล้วกับ deployment จริงระดับ production ของสถาปัตยกรรมเดียวกันนี้