SSO Integration — OIDC Technical Specification
Overview
Vio's White Label (WL) infrastructure uses OIDC federation to enable SSO. When a user authenticated on the partner's site navigates to the WL site, Cognito performs a silent authorization code flow against the partner's IdP. If a valid session exists on the IdP, tokens are issued without user interaction.
This document specifies what a partner must expose for this integration to work, and provides implementation guidance if the partner needs to build an OIDC-compliant authorization server.
Each brand maintains its own User Pool, and each pool federates authentication to the partner's OIDC-compliant Identity Provider (IdP). The Cognito User Pools do not communicate with each other — SSO happens entirely at the IdP layer through session sharing.
Integration Scenarios
Depending on the partner's current identity infrastructure, there are four paths forward.
| Scenario | When to use | Login page shown |
|---|---|---|
| A: Direct Federation | The partner already has an OIDC-compliant IdP (Okta, Azure AD, Keycloak, etc.) | Partner's IdP login page |
| B: Identity Brokering (Custom DB) | The partner's identity system is not OIDC-compliant and the partner does not need to show their own login page | Broker's login page (e.g., Auth0 Universal Login) |
| B2: Identity Brokering (Enterprise OIDC) | The partner has an OIDC-compliant IdP but prefers not to expose it directly, while retaining their own login experience | Partner's own login page |
| C: Build an OIDC Server | The partner has no OIDC provider and prefers to build and host one for full control | Partner's own login page |
Scenario A: Direct Federation
Use this if the partner already has an OIDC-compliant IdP (self-hosted or managed: Okta, Azure AD, Keycloak, etc.).
This is the simplest path — register our Cognito pool as a relying party on the existing IdP and provide us with credentials.
What the partner provides to Vio:
| Item | Example | Notes |
|---|---|---|
| Issuer URL | https://auth.partner.com | /.well-known/openid-configuration must be publicly accessible at this URL |
| Client ID | vio-cognito-prod | Registered on the partner's IdP for our Cognito pool |
| Client Secret | <secret> | Share via secure channel (e.g., 1Password, Vault) |
What Vio provides to the partner:
| Item | Example | Notes |
|---|---|---|
| Redirect URI | https://<cognito-domain>/oauth2/idpresponse | The partner must whitelist this URI on their IdP. Provided after Cognito provisioning |
Separate credentials are needed for staging and production environments.
How it works:
Endpoints involved:
| Endpoint | Method | Called by | Description |
|---|---|---|---|
{issuer}/.well-known/openid-configuration | GET | Cognito (on setup) | Returns OIDC discovery metadata |
{issuer}/.well-known/jwks.json | GET | Cognito | Public keys to verify id_token signatures |
{issuer}/authorize | GET | Browser (redirect) | Starts auth flow; returns code via redirect if session exists |
{issuer}/oauth/token | POST | Cognito (server-to-server) | Exchanges authorization code for id_token + access_token |
{cognito-domain}/oauth2/idpresponse | GET | Browser (redirect) | Cognito's callback URL — receives the authorization code |
Session cookie requirement: The IdP's session cookie must be set with SameSite=None; Secure for the silent SSO flow to work. Without this, the browser won't send the cookie on the cross-site redirect from the WL domain to the IdP, and the user will see a login prompt. See Session Management for details.
Session federation requirement: The SSO depends on session sharing between the partner's site and our Cognito pool. This only works if both systems authenticate against the same IdP instance, so the browser has a valid session cookie when Cognito redirects to the IdP.
Concretely, this means the partner's login flow must go through the same OIDC authorization server that we register with Cognito. If the partner authenticates users through a different system (e.g., a proprietary backend) and only uses the OIDC IdP for third-party integrations, the user will not have a session on the IdP when they arrive at the WL site, and silent authentication will fail — they'll see a login prompt instead.
If the partner currently uses a different authentication system (custom backend, session tokens not managed by the OIDC IdP), there are two options:
- Migrate the login flow to go through the OIDC IdP (so the session cookie is created during normal login)
- Use Scenario B, B2, or C instead — put a broker or new OIDC server in front of the existing system
Scenario B: Identity Brokering via a Managed Service (Custom DB)
Use this if the partner's existing identity system is not OIDC-compliant and the partner does not need to show their own login page. In this case, the partner sets up a managed OIDC service (e.g., Auth0, Okta, AWS Cognito) as an intermediary that brokers authentication between the existing system and our Cognito pool.
Identity brokering is a standard pattern where an intermediary IdP receives auth requests from relying parties, delegates the actual credential validation to the partner's backend (via a custom database connection or API), and issues its own OIDC tokens.
Important: In this scenario, the user sees the broker's login page (e.g., Auth0's Universal Login), not the partner's own login page. The broker collects the credentials and validates them against the partner's backend behind the scenes. If the partner needs to show their own branded login experience, see Scenario B2 instead.
What the partner provides to Vio:
| Item | Example | Notes |
|---|---|---|
| Broker Issuer URL | https://partner.eu.auth0.com/ | The broker's OIDC issuer (not the partner's internal auth system) |
| Client ID | vio-cognito-prod | Registered on the broker for our Cognito pool |
| Client Secret | <secret> | Share via secure channel |
What Vio provides to the partner:
| Item | Example | Notes |
|---|---|---|
| Redirect URI | https://<cognito-domain>/oauth2/idpresponse | The partner must whitelist this URI on the broker. Provided after Cognito provisioning |
Separate credentials are needed for staging and production environments.
What the partner configures (on the broker):
- Set up a managed OIDC service (e.g., Auth0, Okta, AWS Cognito) as an OIDC broker
- Configure the existing identity system as a custom database connection in the broker (e.g., Auth0 Custom DB with
loginandgetUserscripts that call the partner's API) - Route login traffic through the broker — this is critical: the broker must create its own session during normal login
How it works:
Endpoints involved:
| Endpoint | Method | Called by | Description |
|---|---|---|---|
{broker}/.well-known/openid-configuration | GET | Cognito (on setup) | Broker's OIDC discovery metadata |
{broker}/.well-known/jwks.json | GET | Cognito | Broker's public keys to verify id_token signatures |
{broker}/authorize | GET | Browser (redirect) | Starts auth flow on the broker; shows broker login page or silently redirects if session exists |
{broker}/oauth/token | POST | Cognito (server-to-server) | Exchanges authorization code for id_token + access_token |
{cognito-domain}/oauth2/idpresponse | GET | Browser (redirect) | Cognito's callback URL — receives the authorization code |
{partner-api}/auth/validate | POST | Broker (server-to-server) | Partner's endpoint — validates email + password, returns user profile |
{partner-api}/auth/get-user | POST | Broker (server-to-server) | Partner's endpoint — looks up user by email, returns profile if found |
Partner Auth API — endpoint specification:
The partner exposes two HTTPS endpoints that the broker calls during login and user lookup. These are not OIDC endpoints — they are simple REST APIs secured with an API key.
POST /auth/validate — called when the user submits credentials on the broker's login page:
// Request
{
"email": "user@example.com",
"password": "user-password"
}
// Success response (200)
{
"user_id": "unique-user-id",
"email": "user@example.com",
"given_name": "John",
"family_name": "Doe"
}
// Failure response (401)
{ "error": "Invalid credentials" }
POST /auth/get-user — called to check if a user exists (e.g., during token refresh):
// Request
{
"email": "user@example.com"
}
// Found response (200)
{
"user_id": "unique-user-id",
"email": "user@example.com",
"given_name": "John",
"family_name": "Doe"
}
// Not found response (404)
{ "error": "User not found" }
Both endpoints must require authentication (e.g., Authorization: Bearer <shared-api-key> header).
How it connects — Auth0 Custom Database scripts:
The scripts below are configured on Auth0's side (Authentication → Database → Custom Database → Database Action Scripts). They run inside Auth0's infrastructure and call the partner's API. The partner does not need to implement these scripts — only the API endpoints above.
// Auth0 Custom DB — Login script
// Configured on Auth0, calls the partner's /auth/validate endpoint
async function login(email, password, callback) {
const axios = require("axios");
try {
const { data: user } = await axios.post(
"https://api.partner.com/auth/validate",
{ email, password },
{
headers: {
"Content-Type": "application/json",
Authorization: "Bearer <shared-api-key>",
},
},
);
return callback(null, {
user_id: user.user_id,
email: user.email,
given_name: user.given_name,
family_name: user.family_name,
});
} catch (e) {
if (e.response && e.response.status === 401) {
return callback(new WrongUsernameOrPasswordError(email));
}
return callback(new Error(e.message));
}
}
// Auth0 Custom DB — Get User script
// Configured on Auth0, calls the partner's /auth/get-user endpoint
async function getUser(email, callback) {
const axios = require("axios");
try {
const { data: user } = await axios.post(
"https://api.partner.com/auth/get-user",
{ email },
{
headers: {
"Content-Type": "application/json",
Authorization: "Bearer <shared-api-key>",
},
},
);
return callback(null, {
user_id: user.user_id,
email: user.email,
given_name: user.given_name,
family_name: user.family_name,
});
} catch (e) {
if (e.response && e.response.status === 404) {
return callback(null);
}
return callback(new Error(e.message));
}
}
The partner's API is the source of truth for credentials — the broker simply relays the validation. Communication between the broker and the partner API is secured via HTTPS and an API key or shared secret. This lets the partner keep their existing user database and password hashing unchanged.
Our Cognito pool only interacts with the broker, relying exclusively on the broker's session. The broker does not need to re-check with the partner's auth API for each SSO attempt — once the session exists, silent authentication works independently.
Session cookie requirement: The broker's session cookie must be set with SameSite=None; Secure. Managed services like Auth0 handle this automatically. See Session Management for details.
Key requirement: Both the partner site and our Cognito pool must use the same broker instance as the identity provider. The SSO works because the session cookie lives on the broker's domain and is shared across both relying parties.
Scenario B2: Identity Brokering with Partner's Login Page
Use this if the partner has an OIDC-compliant IdP (or is willing to build one — see Scenario C) but prefers not to expose it directly to Cognito. Instead, a managed OIDC broker (e.g., Auth0, Okta) sits between Cognito and the partner's IdP. The user sees the partner's own login page, not the broker's.
This is useful when:
- The partner wants to keep their IdP on a private network or behind a firewall
- The partner wants a single integration point (the broker) rather than registering multiple relying parties on their IdP
- The partner wants the broker to handle token issuance, key rotation, and session management while retaining control over the login experience
What the partner provides to Vio:
| Item | Example | Notes |
|---|---|---|
| Broker Issuer URL | https://partner.eu.auth0.com/ | The broker's OIDC issuer (not the partner's internal IdP) |
| Client ID | vio-cognito-prod | Registered on the broker for our Cognito pool |
| Client Secret | <secret> | Share via secure channel |
What Vio provides to the partner:
| Item | Example | Notes |
|---|---|---|
| Redirect URI | https://<cognito-domain>/oauth2/idpresponse | The partner must whitelist this URI on the broker. Provided after Cognito provisioning |
Separate credentials are needed for staging and production environments.
What the partner configures:
- Set up a managed OIDC service (e.g., Auth0, Okta) as an OIDC broker
- Register the partner's IdP as an Enterprise OIDC Connection in the broker (issuer URL, client ID, client secret)
- Register the broker as a relying party on the partner's IdP (whitelist the broker's callback URL, e.g.,
https://partner.eu.auth0.com/login/callback) - Route login traffic through the broker — the broker redirects to the partner's IdP, which shows the partner's login page
How it works:
Endpoints involved:
| Endpoint | Method | Called by | Description |
|---|---|---|---|
{broker}/.well-known/openid-configuration | GET | Cognito (on setup) | Broker's OIDC discovery metadata |
{broker}/.well-known/jwks.json | GET | Cognito | Broker's public keys to verify id_token signatures |
{broker}/authorize | GET | Browser (redirect) | Starts auth flow on the broker; redirects to partner IdP or silently redirects if session exists |
{broker}/oauth/token | POST | Cognito (server-to-server) | Exchanges authorization code for id_token + access_token |
{broker}/login/callback | GET | Browser (redirect) | Broker's callback URL — receives the authorization code from the partner's IdP |
{partner-idp}/authorize | GET | Browser (redirect) | Partner's IdP authorization endpoint — shows partner's login page |
{partner-idp}/oauth/token | POST | Broker (server-to-server) | Partner's IdP token endpoint — broker exchanges code for tokens |
{cognito-domain}/oauth2/idpresponse | GET | Browser (redirect) | Cognito's callback URL — receives the authorization code from the broker |
Key differences from Scenario B:
| Scenario B (Custom DB) | Scenario B2 (Enterprise OIDC) | |
|---|---|---|
| Login page | Broker's login page (e.g., Auth0 Universal Login) | Partner's own login page |
| Credential validation | Broker calls partner API (server-to-server) | User authenticates directly on partner's IdP |
| Partner requirement | Expose two REST endpoints (login, getUser) | Run an OIDC-compliant IdP (existing or built per Scenario C) |
| Broker configuration | Custom Database Connection | Enterprise OIDC Connection |
For example, in Auth0 this is configured as an Enterprise Connection (OIDC type):
- Go to Authentication → Enterprise → OpenID Connect → Create Connection
- Provide the partner IdP's issuer URL, client ID, and client secret
- Auth0 auto-discovers the partner's OIDC endpoints and handles the federation
Our Cognito pool only interacts with the broker — it never communicates with the partner's IdP directly. The SSO works because the broker session (Session 1) is created during login and reused when Cognito triggers silent authentication.
Session cookie requirement: The broker's session cookie must be set with SameSite=None; Secure. Managed services like Auth0 handle this automatically. The partner's own IdP session cookie also needs SameSite=None; Secure if the broker needs to perform silent auth against it during token refresh. See Session Management for details.
Key requirement: Both the partner site and our Cognito pool must use the same broker instance. The SSO relies on the broker's session cookie, not the partner IdP's session.
Scenario C: Build an OIDC Authorization Server
Use this if the partner has no existing OIDC provider and prefers to build and host one rather than adopt a managed service (Auth0, Okta, etc.). This gives full control but requires implementing the OIDC spec.
What the partner provides to Vio:
| Item | Example | Notes |
|---|---|---|
| Issuer URL | https://auth.partner.com | /.well-known/openid-configuration must be publicly accessible at this URL |
| Client ID | vio-cognito-prod | Registered on the partner's OIDC server for our Cognito pool |
| Client Secret | <secret> | Share via secure channel |
What Vio provides to the partner:
| Item | Example | Notes |
|---|---|---|
| Redirect URI | https://<cognito-domain>/oauth2/idpresponse | The partner must whitelist this URI on their OIDC server. Provided after Cognito provisioning |
Separate credentials are needed for staging and production environments.
The rest of this document serves as the implementation specification for this scenario. It covers:
- Required OIDC Endpoints — what endpoints to expose and their exact request/response contracts
- ID Token Specification — JWT structure, signing, and required claims
- Session Management — cookie requirements for silent SSO
- Implementation Guide — Python/FastAPI skeleton and managed alternatives
The same endpoint and token specifications also serve as a compliance reference for Scenarios A, B, and B2 — they describe exactly what Cognito expects from any IdP.
Required OIDC Endpoints
1. Discovery — GET /.well-known/openid-configuration
Must be publicly accessible (no authentication). Returns JSON metadata:
{
"issuer": "https://auth.partner.com",
"authorization_endpoint": "https://auth.partner.com/authorize",
"token_endpoint": "https://auth.partner.com/oauth/token",
"userinfo_endpoint": "https://auth.partner.com/userinfo",
"jwks_uri": "https://auth.partner.com/.well-known/jwks.json",
"response_types_supported": ["code"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["RS256"],
"scopes_supported": ["openid", "email", "profile"],
"token_endpoint_auth_methods_supported": [
"client_secret_post",
"client_secret_basic"
],
"claims_supported": ["sub", "email", "given_name", "family_name"]
}
Cognito fetches this endpoint to auto-configure the federation. If any URL is wrong or unreachable, the federation setup fails.
2. Authorization — GET /authorize
Cognito will redirect the user's browser to this endpoint with these query parameters:
GET /authorize?
response_type=code
&client_id=<client_id_we_receive_from_partner>
&redirect_uri=https://<cognito-domain>/oauth2/idpresponse
&scope=openid+email+profile
&state=<random_state>
&nonce=<random_nonce>
Expected behavior:
- If the user has an active session (cookie present) → redirect back to
redirect_uriwith?code=<authorization_code>&state=<state>— no login UI shown - If no session exists → show login form, authenticate user, set session cookie, then redirect with code
- If
redirect_uriis not in the registered allowlist → return an error, do NOT redirect
The authorization code must be:
- Single-use
- Short-lived (recommended: 30–60 seconds)
- Bound to the
client_idandredirect_uriused in the request
3. Token — POST /oauth/token
Cognito exchanges the authorization code for tokens server-to-server (not a browser redirect):
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=<authorization_code>
&redirect_uri=https://<cognito-domain>/oauth2/idpresponse
&client_id=<client_id>
&client_secret=<client_secret>
Client authentication: Cognito sends client_id and client_secret in the POST body (client_secret_post). If the server only supports client_secret_basic (HTTP Basic Auth header), let us know — Cognito supports both.
Expected response:
{
"access_token": "eyJhbG...",
"id_token": "eyJhbG...",
"token_type": "Bearer",
"expires_in": 3600
}
4. JWKS — GET /.well-known/jwks.json
Public endpoint serving the JSON Web Key Set used to verify id_token signatures:
{
"keys": [
{
"kty": "RSA",
"kid": "key-id-1",
"use": "sig",
"alg": "RS256",
"n": "<base64url-encoded modulus>",
"e": "AQAB"
}
]
}
Cognito fetches this to validate the id_token signature. Key rotation is supported — include both old and new keys during rotation periods.
5. UserInfo — GET /userinfo (Optional)
Only needed if id_token does not contain the required claims. If provided:
GET /userinfo
Authorization: Bearer <access_token>
{
"sub": "user-uuid-123",
"email": "john@example.com",
"given_name": "John",
"family_name": "Doe"
}
ID Token Specification
The id_token must be a signed JWT (JWS) using RS256 (RSA Signature with SHA-256). Cognito does not support HS256 (symmetric) for OIDC federation.
Required Claims
{
"iss": "https://auth.partner.com",
"sub": "user-uuid-123",
"aud": "<client_id>",
"exp": 1709654400,
"iat": 1709650800,
"nonce": "<nonce_from_authorize_request>",
"email": "john@example.com",
"given_name": "John",
"family_name": "Doe"
}
| Claim | Type | Description |
|---|---|---|
iss | string | Must exactly match the issuer URL in the discovery document |
sub | string | Unique, stable, non-reassignable user identifier |
aud | string or array | Must contain the client_id issued to our Cognito pool |
exp | number | Expiration time (Unix timestamp). Token must not be expired |
iat | number | Issued-at time (Unix timestamp) |
nonce | string | Must echo the nonce sent in the authorization request |
email | string | User's email address |
given_name | string | User's first name |
family_name | string | User's last name |
Cognito Attribute Mapping
| IdP Claim | Cognito Attribute |
|---|---|
sub | username (federated identity, format: <provider_name>_<sub>) |
email | email |
given_name | given_name |
family_name | family_name |
If the partner's system uses different claim names (e.g., first_name instead of given_name), we can configure custom attribute mapping on the Cognito side.
Session Management — The Critical Piece
The SSO flow depends entirely on session cookies. When Cognito redirects the user to /authorize, the browser sends cookies for the IdP domain. If a valid session cookie exists, the IdP issues a code silently (no login UI).
Requirements for the session cookie:
response.set_cookie(
key="partner_session",
value=session_id,
domain=".partner.com", # Accessible from auth.partner.com subdomain
httponly=True,
secure=True, # HTTPS only
samesite="none", # REQUIRED: Cognito redirect is cross-site
max_age=86400, # Session lifetime — align with your policy
)
SameSite=None is non-negotiable. Without it, browsers won't send the cookie on the cross-site redirect from the WL domain to the IdP domain, and silent auth will always fail.
This applies to all scenarios (A, B, B2, and C). The entity whose session cookie enables silent auth varies by scenario:
| Scenario | Session cookie owner | Notes |
|---|---|---|
| A | Partner's IdP | Partner must ensure SameSite=None; Secure on their IdP's session cookie |
| B | OIDC Broker (e.g., Auth0) | Managed services like Auth0 handle this automatically |
| B2 | OIDC Broker (e.g., Auth0) | Managed services handle this automatically; partner's IdP session cookie is also needed if the broker re-authenticates against it |
| C | Partner's custom OIDC server | Partner must set SameSite=None; Secure explicitly (see code example above) |
Implementation Guide
If the partner chooses Scenario C (building an OIDC server), here's the practical guidance.
Scope of Work
| Component | Complexity | Description |
|---|---|---|
| Discovery endpoint | Low | Static JSON response |
| JWKS endpoint | Low | Serve public keys, handle rotation |
| RSA key management | Medium | Generate, store, rotate RS256 key pairs |
| Authorization endpoint | Medium | Session validation, authorization code issuance, redirect URI validation |
| Token endpoint | Medium | Code exchange, JWT signing, client authentication |
| Session management | Medium | Cookie-based sessions shared between partner site and IdP |
| Client registry | Low | Store client_id/secret pairs and allowed redirect URIs |
| Authorization code store | Low | Short-lived, single-use codes (Redis or in-memory with TTL) |
Python + FastAPI Recommendation
For a Python-based implementation, we recommend FastAPI with Authlib as the OAuth2/OIDC server framework. Authlib implements the core RFC logic (code grants, token issuance, JWT signing) so you don't have to.
Key dependencies:
fastapi>=0.100.0
authlib>=1.3.0
python-jose[cryptography]>=3.3.0 # JWT handling
cryptography>=41.0.0 # RSA key generation
redis>=5.0.0 # Authorization code + session storage
uvicorn>=0.23.0 # ASGI server
Skeleton Implementation
# server.py — Minimal OIDC Provider with FastAPI + Authlib
from authlib.jose import JsonWebKey, jwt
from cryptography.hazmat.primitives.asymmetric import rsa
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import RedirectResponse
import time, secrets
app = FastAPI()
# --- Key Management ---
# In production: load from secure storage (AWS KMS, Vault, etc.)
# Rotate keys periodically; serve both old and new in JWKS during transition
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()
KID = "partner-oidc-key-1"
def get_jwks():
"""Export public key as JWK for the JWKS endpoint."""
jwk = JsonWebKey.import_key(
public_key, {"kty": "RSA", "kid": KID, "use": "sig", "alg": "RS256"}
)
return {"keys": [jwk.as_dict()]}
# --- In-memory stores (use Redis/DB in production) ---
CLIENTS = {
"vio-cognito-staging": {
"client_secret": "replace-with-secure-secret",
"redirect_uris": ["https://<cognito-domain>/oauth2/idpresponse"],
}
}
auth_codes = {} # code -> {client_id, redirect_uri, user_id, nonce, expires_at}
sessions = {} # session_id -> {user_id, created_at}
ISSUER = "https://auth.partner.com"
# --- Discovery ---
@app.get("/.well-known/openid-configuration")
async def discovery():
return {
"issuer": ISSUER,
"authorization_endpoint": f"{ISSUER}/authorize",
"token_endpoint": f"{ISSUER}/oauth/token",
"userinfo_endpoint": f"{ISSUER}/userinfo",
"jwks_uri": f"{ISSUER}/.well-known/jwks.json",
"response_types_supported": ["code"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["RS256"],
"scopes_supported": ["openid", "email", "profile"],
"token_endpoint_auth_methods_supported": [
"client_secret_post", "client_secret_basic"
],
"claims_supported": [
"sub", "email", "given_name", "family_name",
"iss", "aud", "exp", "iat"
],
}
@app.get("/.well-known/jwks.json")
async def jwks():
return get_jwks()
# --- Authorization ---
@app.get("/authorize")
async def authorize(
request: Request,
response_type: str,
client_id: str,
redirect_uri: str,
scope: str,
state: str,
nonce: str = "",
):
# Validate client and redirect_uri
client = CLIENTS.get(client_id)
if not client or redirect_uri not in client["redirect_uris"]:
raise HTTPException(400, "Invalid client_id or redirect_uri")
if response_type != "code":
raise HTTPException(400, "Only response_type=code is supported")
# Check for existing session (cookie-based)
session_id = request.cookies.get("partner_session")
session = sessions.get(session_id) if session_id else None
if not session:
# No session → redirect to login page (implement your login flow)
# After successful login, set session cookie and redirect back here
# This is where you integrate with your existing user authentication
raise HTTPException(401, "No active session — redirect to login")
# Session exists → issue authorization code silently
code = secrets.token_urlsafe(32)
auth_codes[code] = {
"client_id": client_id,
"redirect_uri": redirect_uri,
"user_id": session["user_id"],
"nonce": nonce,
"expires_at": time.time() + 60, # 60-second TTL
}
return RedirectResponse(f"{redirect_uri}?code={code}&state={state}")
# --- Token Exchange ---
@app.post("/oauth/token")
async def token(request: Request):
form = await request.form()
grant_type = form.get("grant_type")
code = form.get("code")
redirect_uri = form.get("redirect_uri")
client_id = form.get("client_id")
client_secret = form.get("client_secret")
if grant_type != "authorization_code":
raise HTTPException(400, "Unsupported grant_type")
# Validate client credentials
client = CLIENTS.get(client_id)
if not client or client["client_secret"] != client_secret:
raise HTTPException(401, "Invalid client credentials")
# Validate and consume authorization code (single-use)
code_data = auth_codes.pop(code, None)
if not code_data:
raise HTTPException(400, "Invalid or expired code")
if code_data["expires_at"] < time.time():
raise HTTPException(400, "Code expired")
if code_data["client_id"] != client_id or code_data["redirect_uri"] != redirect_uri:
raise HTTPException(400, "Code was issued for a different client/redirect_uri")
# Look up user claims (integrate with your user database)
user = get_user_claims(code_data["user_id"])
# Build and sign id_token
now = int(time.time())
id_token_payload = {
"iss": ISSUER,
"sub": user["sub"],
"aud": client_id,
"exp": now + 3600,
"iat": now,
"nonce": code_data["nonce"],
"email": user["email"],
"given_name": user["given_name"],
"family_name": user["family_name"],
}
header = {"alg": "RS256", "kid": KID}
id_token = jwt.encode(header, id_token_payload, private_key)
access_token = secrets.token_urlsafe(32)
return {
"access_token": access_token,
"id_token": (
id_token.decode("utf-8") if isinstance(id_token, bytes) else id_token
),
"token_type": "Bearer",
"expires_in": 3600,
}
def get_user_claims(user_id: str) -> dict:
"""Replace with actual user database lookup."""
return {
"sub": user_id,
"email": "user@example.com",
"given_name": "John",
"family_name": "Doe",
}
Alternatives to Building from Scratch
If building a full OIDC server feels heavyweight, consider these managed options in order of effort:
| Option | Effort | Notes |
|---|---|---|
| Auth0 (free tier: 7,500 MAU) | Days | Configure your user DB as a custom connection. Handles all OIDC endpoints, key rotation, session management |
| Keycloak (self-hosted, open source) | Days | Deploy via Docker. Full OIDC provider with admin UI. Can federate to existing user databases |
| AWS Cognito (if already on AWS) | Days | Create a User Pool, configure as OIDC provider. Native OIDC compliance |
| Authlib + FastAPI (custom) | 1–2 weeks | Full control. Use the skeleton above as a starting point. Requires key management, session storage, monitoring |
| From scratch (no framework) | Weeks | Not recommended. Too many RFC edge cases to get right securely |
Validation Checklist
Once the IdP is set up (any scenario), we'll validate the following before proceeding to Cognito registration:
-
GET {issuer}/.well-known/openid-configurationreturns valid JSON with all required fields -
GET {jwks_uri}returns at least one RSA key with"alg": "RS256" - Authorization endpoint accepts
response_type=codeandscope=openid email profile - Authorization endpoint returns
codeandstatevia redirect to our callback URL - Authorization endpoint silently redirects (no login UI) when a valid session cookie exists
- Token endpoint accepts
grant_type=authorization_codewithclient_secret_postauthentication - Token endpoint returns a valid JWT
id_tokensigned with RS256 -
id_tokencontains required claims:iss,sub,aud,exp,iat,nonce,email,given_name,family_name -
id_tokensignature validates against the JWKS public key -
issclaim exactly matches the issuer URL in the discovery document - Session cookie has
SameSite=None; Secureattributes
Quick Validation Commands
# 1. Fetch discovery document
curl -s https://auth.partner.com/.well-known/openid-configuration | jq .
# 2. Fetch JWKS
curl -s https://auth.partner.com/.well-known/jwks.json | jq .
# 3. Initiate auth flow (open in browser to test)
open "https://auth.partner.com/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=https://CALLBACK&scope=openid+email+profile&state=test123&nonce=nonce123"
# 4. Exchange code for tokens
curl -X POST https://auth.partner.com/oauth/token \
-d "grant_type=authorization_code" \
-d "code=AUTH_CODE_FROM_STEP_3" \
-d "redirect_uri=https://CALLBACK" \
-d "client_id=CLIENT_ID" \
-d "client_secret=CLIENT_SECRET"
# 5. Decode and inspect id_token (paste token from step 4)
echo "PASTE_ID_TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq .
Next Steps
- Partner assesses current identity infrastructure and picks a scenario (A, B, B2, or C)
- Partner provides OIDC issuer URL + client credentials (staging first)
- Vio validates discovery endpoint and token flow using the checklist above
- Vio provisions Cognito User Pool and shares redirect URIs
- Joint testing — end-to-end SSO flow in staging
- Production rollout with production credentials
Glossary
| Term | Definition |
|---|---|
| OIDC (OpenID Connect) | An identity layer built on top of OAuth 2.0. It allows a client application to verify a user's identity based on authentication performed by an authorization server and to obtain basic profile information about the user. |
| IdP (Identity Provider) | The server that authenticates users and issues identity tokens. In this integration, the partner's auth system (or a broker like Auth0) acts as the IdP. |
| Relying Party (RP) | An application that depends on an IdP to authenticate users. In this integration, Cognito is the relying party — it trusts the IdP to verify user identity. |
| Identity Broker | An intermediary service that sits between a relying party and an upstream identity system. It receives auth requests, delegates credential validation to the upstream system, and issues its own tokens. Auth0 acting as a broker in Scenarios B and B2 is an example. |
| SSO (Single Sign-On) | A mechanism where a user authenticates once and gains access to multiple applications without re-entering credentials. In this integration, the user logs in on the partner site and is automatically authenticated on the WL site. |
| Silent Authentication | An authentication flow where no user interaction is required. The IdP detects an existing session (via a cookie) and issues tokens automatically, redirecting back without showing a login page. |
| Authorization Code Flow | An OAuth 2.0 / OIDC flow where the IdP issues a short-lived authorization code via browser redirect, which the relying party then exchanges for tokens via a server-to-server call. This is the flow Cognito uses. |
| Session Cookie | A browser cookie set by the IdP after successful authentication. It allows the IdP to recognize the user on subsequent requests without requiring them to log in again. Must be set with SameSite=None; Secure for cross-site SSO to work. |
| JWKS (JSON Web Key Set) | A public endpoint that serves the cryptographic keys used to verify JWT signatures. Cognito fetches this to validate id_token signatures from the IdP. |
| JWT (JSON Web Token) | A compact, signed token format used to transmit claims (user identity, expiration, etc.) between parties. The id_token in OIDC is a JWT. |
| Claims | Key-value pairs inside a JWT that carry information about the user (e.g., email, sub, given_name). Cognito maps these claims to user attributes. |
| Issuer | The URL that uniquely identifies the IdP. It must match the iss claim in issued tokens and the base URL of the discovery endpoint ({issuer}/.well-known/openid-configuration). |
| Discovery Endpoint | A well-known URL (/.well-known/openid-configuration) that returns a JSON document describing the IdP's endpoints, supported features, and signing algorithms. Cognito uses this to auto-configure the federation. |
| PKCE (Proof Key for Code Exchange) | An extension to the authorization code flow that prevents authorization code interception attacks. The client generates a code_verifier and code_challenge; the IdP verifies them during token exchange. |
| Federation | The process of establishing trust between two identity systems so that users authenticated by one system are recognized by the other. In this integration, Cognito federates authentication to the partner's IdP. |
| Custom Database Connection | A feature in managed OIDC services (e.g., Auth0) that allows the broker to validate credentials against an external database or API instead of storing credentials locally. Used in Scenario B. |
| Enterprise OIDC Connection | A feature in managed OIDC services (e.g., Auth0) that allows the broker to federate to an external OIDC-compliant IdP. The broker redirects the user to the external IdP for authentication. Used in Scenario B2. |