How to let your workforce sign in to Amazon Quick Desktop with the identities they already have — using Amazon Cognito, a 60-line CloudFront Function, and zero servers. Ready-to-deploy CloudFormation included.

Amazon Quick Desktop supports Enterprise sign-in through any OIDC-compliant identity provider: you register an Authorization Endpoint, Token Endpoint, JWKS URI, Issuer, and Client ID under Extension access, and the desktop app runs a standard Authorization Code + PKCE flow against them.
If your organization's identities live in AWS IAM Identity Center, there is a catch: Identity Center is not an OIDC authorization server you can point Quick Desktop at directly. You need an OIDC layer in front of it. Amazon Cognito is the natural choice — it speaks OIDC on the front and federates to Identity Center over SAML on the back.
That almost works out of the box. Two details break it:
offline_access — Quick Desktop includes the offline_access scope in its authorize request. A Cognito public app client (no secret, which is what a desktop app must use) does not support that scope, and Cognito rejects the request outright.Both problems live in a single HTTP request: the browser's GET /oauth2/authorize. Which means both can be fixed by a tiny proxy in front of that one endpoint.
We put an Amazon CloudFront distribution in front of Cognito's authorize endpoint, with a CloudFront Function on the viewer-request event that:
offline_access from the scope parameter (preserving the original encoding and separator — no decode/re-encode),identity_provider=IdentityCenter so Cognito skips its own page and federates immediately,302 straight from the edge, pointing at the real Cognito authorize URL with the rewritten query string.The request never even reaches the origin — the function generates the response. Everything else in the flow (token exchange, JWKS, issuer) keeps talking to Cognito directly. No Lambda, no servers, no state.
Quick Desktop
│ GET /oauth2/authorize?...scope=openid+email+profile+offline_access...
▼
CloudFront Function ── 302 ──> Cognito Hosted UI (authorize)
│ SAML AuthnRequest
▼
IAM Identity Center ──> your users sign in
│ SAML assertion
▼
Cognito ── code ──> Quick Desktop (localhost:18080)
▲
└── POST /oauth2/token (PKCE) — direct, no proxy
And what about refresh, now that offline_access is gone? Cognito issues refresh tokens based on the app client's refresh token validity (we use 30 days), not the offline_access scope. Sessions refresh silently; users are not re-prompted daily.
A bonus of this architecture: because Identity Center is registered in Cognito as a Trusted Token Issuer, the tokens support trusted identity propagation — Quick knows who the human is, not just that a valid token arrived.
Everything is in the accompanying repository:
| Piece | Tool | Where it can live |
|---|---|---|
| Cognito user pool, hosted UI, public PKCE client, SAML IdP | templates/identity.yaml |
identity account |
| CloudFront Function + distribution (the proxy) | templates/edge.yaml |
network account |
| Trusted Token Issuer | scripts/create-trusted-token-issuer.sh |
identity account |
| Identity Center SAML app, Quick Extension access | console (no API support) | identity / workload account |
The design is multi-account friendly with zero cross-account IAM: every trust hop is URL-based (SAML metadata URL, OIDC issuer URL, public HTTPS origin). The only hard constraint is organizational — the account running Amazon Quick must be in the same AWS Organization as Identity Center, and everything is homed in 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>The stack outputs everything the later steps need: UserPoolId, ClientId, HostedUiDomain, TokenEndpoint, Issuer, JwksUri — plus the two values the SAML application will ask for: SamlAcsUrl and SamlAudience.
Why two phases? Cognito needs Identity Center's SAML metadata, and Identity Center needs Cognito's ACS URL and audience. Classic chicken-and-egg. Phase 1 creates Cognito so its identifiers exist; you then create the SAML app; phase 2 feeds the metadata URL back in.
This is one of three steps CloudFormation cannot do — the Identity Center API only supports OAuth 2.0 customer-managed applications, not SAML ones.
IAM Identity Center console → Applications → Customer managed → Add application → SAML 2.0, then:
SamlAcsUrl stack outputSamlAudience stack outputSubject → ${user:email} (emailAddress format) and 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 from step 2>'The same stack now adds the IdentityCenter SAML IdP and flips the app client's supported identity provider from COGNITO to IdentityCenter.
No CloudFormation resource exists for this, so it is a 10-line script around aws sso-admin create-trusted-token-issuer (idempotent — safe to re-run):
./scripts/create-trusted-token-issuer.sh <UserPoolId> <profile> us-east-1It registers the Cognito issuer URL (https://cognito-idp.us-east-1.amazonaws.com/<pool>) as an OIDC JWT trusted token issuer, mapping the email claim to emails.value in the identity store.
aws cloudformation deploy \
--region us-east-1 \
--stack-name quick-desktop-authz-proxy \
--template-file templates/edge.yaml \
--parameter-overrides CognitoAuthDomain=<HostedUiDomain output>The interesting part of edge.yaml is the function itself. Three details in it cost us real debugging time, so they deserve a callout:
// 1. Query string values arrive URL-ENCODED. The scope separator can be
// "%20" or "+" depending on the client. Detect it, and preserve it —
// never decode/re-encode (that corrupts redirect_uri).
var sep = raw.indexOf('%20') >= 0 ? '%20' : (raw.indexOf('+') >= 0 ? '+' : ' ');
// 2. Drop only offline_access; keep everything else byte-identical.
if (toks[i] && toks[i] !== 'offline_access') { kept.push(toks[i]); }
// 3. Skip Cognito's own sign-in page.
qs['identity_provider'] = { value: IDP };Then verify:
./scripts/verify-authorize-endpoint.sh "$AUTH_ENDPOINT" "$CLIENT_ID"The script asserts — for both scope separators — that you get an immediate HTTP/2 302 with x-cache: FunctionGeneratedResponse from cloudfront, that offline_access is gone, that identity_provider was added, and that the PKCE code_challenge survived untouched.
Manage Quick → Extension access:
| Field | Value |
|---|---|
| Authorization Endpoint | AuthorizeEndpoint output (CloudFront) |
| Token Endpoint | TokenEndpoint output (Cognito, direct) |
| JWKS URI | JwksUri output |
| Issuer | Issuer output |
| Client ID | ClientId output |
Two warnings from experience:
Open Quick Desktop → Sign in with SSO. You should be bounced straight to your Identity Center sign-in (no Cognito page in between) and land back in the app. If a stale local profile interferes: rm ~/.quickwork/profiles.json.
edge.yaml takes three optional parameters: CustomDomain, AcmCertificateArn (us-east-1), and WebAclArn (CLOUDFRONT scope).
One WAF gotcha worth the price of this article: if you attach AWSManagedRulesCommonRuleSet, the rules EC2MetaDataSSRF_QUERYARGUMENTS and EC2MetaDataSSRF_BODY false-positive on Quick Desktop's redirect URI (redirect_uri=http://localhost:18080 pattern-matches an SSRF-to-instance- metadata attempt) and block every single authorize request with a 403. Override those two rules to Count and keep the rest in Block:
aws wafv2 update-web-acl ... \
# RuleActionOverrides:
# EC2MetaDataSSRF_QUERYARGUMENTS -> Count
# EC2MetaDataSSRF_BODY -> CountDiagnosis tip: aws wafv2 get-sampled-requests --rule-metric-name AWSCommon shows exactly which managed rule fired.
No Lambda, no servers, nothing to patch.
| Requirement | How it is met |
|---|---|
| Quick Desktop needs OIDC | Cognito user pool + public PKCE client |
| Users live in Identity Center | Cognito SAML federation + customer-managed app |
offline_access unsupported on public clients |
CloudFront Function strips it at the edge |
| Skip the Cognito sign-in page | Function forces identity_provider=IdentityCenter |
Silent refresh without offline_access |
App client refresh-token validity (30 days) |
| Identity-aware tokens | Trusted Token Issuer (trusted identity propagation) |
| Multi-account | URL-based trust everywhere; same-Org only for Quick↔︎Identity Center |
Grab the templates, deploy the two stacks, click through the three console steps, and your Quick Desktop fleet signs in with the identities you already manage.
All templates in this post are validated with cfn-lint, and the verification script's assertions were confirmed against a production deployment of this exact architecture.