20. Cognito federation to external CIAM — PKCE and client type
Date: 2026-03-24
Status
Accepted
Context
HDC Auth Proxy uses AWS Cognito as the identity broker. Cognito User Pool is configured with an external OIDC identity provider (CIAM, provider name ciam-dev) for federated authentication of external users.
The PKCE gap
The browser-based login flow uses PKCE S256 (ADR-0015) to protect the authorization code exchange between the proxy and Cognito. However, when Cognito federates to the external CIAM, the OIDC flow has two legs:
Leg 1 (proxy ↔ Cognito): PKCE ✓ — controlled by us
Leg 2 (Cognito → CIAM): PKCE ✗ — controlled by Cognito
When identity_provider=ciam-dev is present in the authorize URL, Cognito redirects the browser to the CIAM's /authorize endpoint without code_challenge or code_challenge_method.
This limitation is confirmed by AWS Premium Support (case opened 2026-03-25):
PKCE is not supported between Cognito User Pools and a Federated IdP at this time. When using federated OIDC, the authentication request is sent from the SP server (Cognito) to the IdP server. When PKCE is used, the communication is between the client (app) and the server (Cognito), which is why this is not supported on the Cognito-to-Federated-IdP side.
AWS confirms there is an active feature request to enable this, but no ETA is available. AWS also references a sample proxy implementation for PKCE flows with external IdPs.
The full flow today:
Browser → Proxy → Cognito /oauth2/authorize?...&code_challenge=X&identity_provider=ciam-dev
Browser ← 302 → CIAM /authorize?response_type=code&client_id=...
↑ NO code_challenge, NO code_challenge_method
Browser → CIAM login page
Browser ← 302 → Cognito /oauth2/idpresponse?code=ABC
Cognito backend → CIAM /token (code=ABC + client_secret)
Browser ← 302 → Proxy /auth/callback?code=DEF&state=...
Proxy → Cognito /token (code=DEF + code_verifier) ✓
The authorization code from the CIAM (ABC) transits through the browser without PKCE protection. Cognito exchanges it server-side using client_secret (confidential client), which mitigates code interception.
CIAM client types
The CIAM supports two recognized client patterns:
- Public client — requires PKCE (mandatory). No client_secret.
- Confidential client — authenticates at the token endpoint with
client_secret. PKCE is not required because the code exchange is protected by the secret, which never leaves the server.
Cognito acts as a confidential client toward the CIAM: it holds the client_secret configured in the User Pool's external IdP settings, and sends it server-side during the token exchange. This is a standard, supported pattern — not a policy exception.
Options considered
Option A: Confidential client (Cognito as-is)
Register Cognito as a confidential client on the CIAM. The CIAM recognizes this as a supported pattern and does not require PKCE for confidential clients.
Security model for the Cognito → CIAM leg:
| Protection | Mechanism |
|---|---|
| Code interception at token exchange | client_secret (server-side, never exposed to browser) |
| Code redirect to attacker | Fixed redirect_uri (https://{cognito-domain}/oauth2/idpresponse) |
| Scope escalation | CIAM enforces scopes per client registration |
The authorization code transits through the browser, but is unusable without the client_secret. This matches the OAuth 2.0 security model for confidential clients (RFC 6749 §2.1).
Pros: - Zero development effort — Cognito already works this way - No additional infrastructure - Standard pattern recognized by the CIAM - The proxy ↔ Cognito leg retains full PKCE protection (ADR-0015)
Cons:
- No PKCE on the Cognito → CIAM leg (defense-in-depth trade-off)
- Relies on client_secret being correctly configured and rotated
Option B: Bypass Cognito — proxy talks directly to the CIAM
The proxy acts as a direct OIDC+PKCE client to the CIAM, bypassing Cognito federation entirely.
Pros: - Full PKCE end-to-end - No additional infrastructure components - The proxy already supports OIDC+PKCE (ADR-0015)
Cons: - Loses Cognito as unified user pool — two separate token issuers to validate - No automatic claim mapping from CIAM to Cognito user attributes - Authorization (groups, roles) must be managed separately for CIAM users - Breaks the Cognito-centric architecture established in ADR-0010/0017
Option C: SAML 2.0 federation between Cognito and the CIAM
Replace the OIDC federation with SAML 2.0. Cognito natively supports SAML 2.0 identity providers, and the CIAM likely exposes a SAML IdP endpoint. SAML uses POST bindings for assertion delivery — the security token (SAML assertion) is sent via an HTML form POST, not as a query parameter in a redirect.
Browser → Proxy → Cognito /oauth2/authorize?identity_provider=ciam-saml
→ CIAM SAML SSO endpoint (HTTP-Redirect binding)
← CIAM authenticates user
→ Cognito /saml2/idpresponse (HTTP-POST binding, signed assertion)
← Cognito maps SAML attributes to user pool claims, generates Cognito JWT
→ Proxy /auth/callback → standard Cognito JWT
Pros: - Eliminates the PKCE gap entirely — SAML does not use authorization codes - SAML assertions are digitally signed by the CIAM and verified by Cognito (integrity + authenticity) - Assertions travel via POST binding, not URL query params (less exposure in browser history/logs) - Cognito has mature SAML support with attribute mapping - Cognito remains the single user pool and token issuer - No additional components to develop
Cons: - Requires the CIAM to expose a SAML IdP endpoint (must be verified) - SAML attribute mapping differs from OIDC claim mapping — requires reconfiguration - SAML metadata exchange and certificate rotation add operational overhead - SAML is a more complex protocol than OIDC — harder to debug - Mixed federation (OIDC for some IdPs, SAML for others) adds configuration complexity
Option D: OIDC PKCE bridge between Cognito and the CIAM
Deploy a lightweight OIDC bridge service between Cognito and the CIAM. Cognito federates to the bridge (as its "external IdP"), and the bridge federates to the real CIAM with PKCE. AWS provides a reference implementation for this pattern (cognito-external-idp-proxy).
Browser → Proxy → Cognito /oauth2/authorize?identity_provider=ciam-bridge
→ Bridge /authorize
→ CIAM /authorize + code_challenge + code_challenge_method=S256 ✓
← CIAM callback (code)
→ CIAM /token (code + code_verifier) ✓
← Bridge → Cognito /oauth2/idpresponse (bridge-issued code or proxied tokens)
← Cognito maps claims, generates Cognito JWT
→ Proxy /auth/callback → standard Cognito JWT
The bridge is a minimal OIDC provider that exposes:
| Endpoint | Purpose |
|---|---|
/.well-known/openid-configuration |
Discovery document pointing to bridge endpoints |
/authorize |
Generates PKCE challenge, stores verifier, redirects to CIAM |
/token |
Receives code from Cognito, exchanges with CIAM using code_verifier |
/jwks |
Publishes keys for token verification (or proxies CIAM JWKS) |
Pros: - Full PKCE end-to-end, satisfying even public-client requirements - Cognito remains the single user pool and token issuer - Unified claim mapping and authorization through Cognito
Cons: - Additional component to develop, deploy, and operate - The bridge must manage PKCE state (code_verifier storage, correlation with sessions) - Adds latency (one extra hop in the redirect chain) - The bridge becomes a critical component in the authentication path - Over-engineered if the CIAM supports confidential client (Option A) or SAML (Option C)
Decision
Option C: SAML 2.0 federation between Cognito and the CIAM.
After alignment with the TEC division, SAML 2.0 was selected as the federation protocol. The integration has been validated end-to-end (2026-03-25): browser login via CIAM, SAML assertion delivery to Cognito, claim mapping, Cognito JWT issuance, and session cookie set by the proxy.
| Leg | Protection |
|---|---|
| Proxy ↔ Cognito | PKCE S256 (ADR-0015) |
| Cognito ↔ CIAM | SAML 2.0 — signed assertions via POST binding (no authorization codes in browser) |
This eliminates the PKCE gap entirely: SAML does not use authorization codes, so the Cognito limitation with OIDC PKCE toward external IdPs is irrelevant. The SAML assertion is digitally signed by the CIAM and verified by Cognito, providing integrity and authenticity guarantees.
Why not the other options
- Option A (OIDC confidential client): technically viable today, but TEC/CIAM considers OIDC confidential client a pattern that may be deprecated in favor of SAML 2.0, which is the established federation standard within the organization. Remains available as a short-term fallback if SAML is temporarily unavailable for a specific IdP.
- Option B (bypass Cognito): breaks the unified user pool architecture.
- Option D (OIDC PKCE bridge): not needed given SAML support. Remains documented above as a fallback if a future IdP supports neither SAML nor OIDC confidential client. AWS provides a reference implementation (
cognito-external-idp-proxy).
Consequences
- No additional components to develop or operate
- Cognito remains the single user pool and token issuer for all users (internal and external)
- SAML metadata and signing certificates must be exchanged between Cognito (SP) and the CIAM (IdP), and certificates must be rotated periodically
- SAML attribute mapping in Cognito must be configured to map CIAM attributes to Cognito user pool claims
- ADR-0019 (federated IdP selection) works unchanged —
identity_provider=ciam-devdirects users to the CIAM via SAML - ADR-0015 (PKCE S256) continues to protect the proxy ↔ Cognito leg
- The proxy itself requires no changes for federation support
- If a future IdP does not support SAML, Option A (OIDC confidential client) or Option D (OIDC PKCE bridge) can be evaluated without affecting the proxy
References
- RFC 6749 §2.1 — Client Types — Confidential vs public clients
- RFC 7636 — PKCE — Proof Key for Code Exchange
- ADR-0015 PKCE S256 and Session Cookie — PKCE design for proxy ↔ Cognito
- ADR-0019 Federated Identity Provider Selection —
identity_providerparameter handling - ADR-0017 Cognito Adapter Specialization — Cognito-specific adapter pattern
- AWS Cognito Federation Docs — External IdP configuration
- [AWS Support confirmation (2026-03-25)] — PKCE not supported between Cognito and federated IdP; active feature request, no ETA
- cognito-external-idp-proxy — AWS sample: OIDC proxy between Cognito and external IdP (reference for Option D)