Skip to content

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-dev directs 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