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

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 พัง:
offline_access — Quick Desktop ใส่ scope offline_access มาใน authorize request เสมอ แต่ Cognito app client แบบ public (ไม่มี client secret ซึ่งเป็น สิ่งเดียวที่ desktop app ใช้ได้) ไม่รองรับ scope นี้ Cognito จึงปฏิเสธ request ทันทีสังเกตว่าปัญหาทั้งสองอยู่ใน HTTP request เดียวกัน คือ GET /oauth2/authorize ที่ browser ยิงออกไป — แปลว่าแก้ได้ด้วย proxy เล็ก ๆ ที่คั่นหน้า endpoint เดียวนี้
เราวาง Amazon CloudFront distribution ไว้หน้า authorize endpoint ของ Cognito พร้อม CloudFront Function ที่ event viewer-request ทำหน้าที่:
offline_access ออกจากพารามิเตอร์ scope (โดยคง encoding และตัวคั่นเดิม ไว้ทุก byte — ไม่ decode/re-encode)identity_provider=IdentityCenter เพื่อให้ Cognito ข้ามหน้า sign-in ของตัวเองแล้ว federate ทันที302 กลับจาก edge เลย โดยชี้ location ไปที่ authorize URL จริงของ Cognito พร้อม query string ที่แก้แล้ว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 ส่งมา
ทั้งหมดอยู่ใน 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
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
นี่คือหนึ่งในสามขั้นที่ CloudFormation ทำไม่ได้ — API ของ Identity Center รองรับเฉพาะ customer-managed application แบบ OAuth 2.0 ไม่รองรับ SAML
IAM Identity Center console → Applications → Customer managed → Add application → SAML 2.0 แล้วกรอก:
SamlAcsUrlSamlAudienceSubject → ${user:email} (format emailAddress) และ email → ${user:email}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 ให้อัตโนมัติ
ไม่มี CloudFormation resource สำหรับขั้นนี้ จึงเป็น script สั้น ๆ ครอบ aws sso-admin create-trusted-token-issuer (idempotent — รันซ้ำได้ปลอดภัย):
./scripts/create-trusted-token-issuer.sh <UserPoolId> <profile> us-east-1Script จะลงทะเบียน issuer URL ของ Cognito (https://cognito-idp.us-east-1.amazonaws.com/<pool>) เป็น OIDC JWT trusted token issuer โดย map claim email เข้ากับ emails.value ใน identity store
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 อยู่ครบ ไม่ถูกแตะต้อง
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 |
คำเตือนสองข้อจากประสบการณ์จริง:
เปิด Quick Desktop → Sign in with SSO → ควรเด้งตรงไปหน้า sign-in ของ Identity Center (ไม่มีหน้า Cognito คั่น) แล้วกลับเข้าแอปได้เลย ถ้า profile เก่าในเครื่อง กวน: rm ~/.quickwork/profiles.json แล้วลองใหม่
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 ของสถาปัตยกรรมเดียวกันนี้