Skip to main content

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.

ScenarioWhen to useLogin page shown
A: Direct FederationThe 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 pageBroker'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 experiencePartner's own login page
C: Build an OIDC ServerThe partner has no OIDC provider and prefers to build and host one for full controlPartner'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:

ItemExampleNotes
Issuer URLhttps://auth.partner.com/.well-known/openid-configuration must be publicly accessible at this URL
Client IDvio-cognito-prodRegistered 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:

ItemExampleNotes
Redirect URIhttps://<cognito-domain>/oauth2/idpresponseThe 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:

EndpointMethodCalled byDescription
{issuer}/.well-known/openid-configurationGETCognito (on setup)Returns OIDC discovery metadata
{issuer}/.well-known/jwks.jsonGETCognitoPublic keys to verify id_token signatures
{issuer}/authorizeGETBrowser (redirect)Starts auth flow; returns code via redirect if session exists
{issuer}/oauth/tokenPOSTCognito (server-to-server)Exchanges authorization code for id_token + access_token
{cognito-domain}/oauth2/idpresponseGETBrowser (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:

  1. Migrate the login flow to go through the OIDC IdP (so the session cookie is created during normal login)
  2. 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:

ItemExampleNotes
Broker Issuer URLhttps://partner.eu.auth0.com/The broker's OIDC issuer (not the partner's internal auth system)
Client IDvio-cognito-prodRegistered on the broker for our Cognito pool
Client Secret<secret>Share via secure channel

What Vio provides to the partner:

ItemExampleNotes
Redirect URIhttps://<cognito-domain>/oauth2/idpresponseThe 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):

  1. Set up a managed OIDC service (e.g., Auth0, Okta, AWS Cognito) as an OIDC broker
  2. Configure the existing identity system as a custom database connection in the broker (e.g., Auth0 Custom DB with login and getUser scripts that call the partner's API)
  3. Route login traffic through the broker — this is critical: the broker must create its own session during normal login

How it works:

Endpoints involved:

EndpointMethodCalled byDescription
{broker}/.well-known/openid-configurationGETCognito (on setup)Broker's OIDC discovery metadata
{broker}/.well-known/jwks.jsonGETCognitoBroker's public keys to verify id_token signatures
{broker}/authorizeGETBrowser (redirect)Starts auth flow on the broker; shows broker login page or silently redirects if session exists
{broker}/oauth/tokenPOSTCognito (server-to-server)Exchanges authorization code for id_token + access_token
{cognito-domain}/oauth2/idpresponseGETBrowser (redirect)Cognito's callback URL — receives the authorization code
{partner-api}/auth/validatePOSTBroker (server-to-server)Partner's endpoint — validates email + password, returns user profile
{partner-api}/auth/get-userPOSTBroker (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:

ItemExampleNotes
Broker Issuer URLhttps://partner.eu.auth0.com/The broker's OIDC issuer (not the partner's internal IdP)
Client IDvio-cognito-prodRegistered on the broker for our Cognito pool
Client Secret<secret>Share via secure channel

What Vio provides to the partner:

ItemExampleNotes
Redirect URIhttps://<cognito-domain>/oauth2/idpresponseThe 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:

  1. Set up a managed OIDC service (e.g., Auth0, Okta) as an OIDC broker
  2. Register the partner's IdP as an Enterprise OIDC Connection in the broker (issuer URL, client ID, client secret)
  3. 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)
  4. 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:

EndpointMethodCalled byDescription
{broker}/.well-known/openid-configurationGETCognito (on setup)Broker's OIDC discovery metadata
{broker}/.well-known/jwks.jsonGETCognitoBroker's public keys to verify id_token signatures
{broker}/authorizeGETBrowser (redirect)Starts auth flow on the broker; redirects to partner IdP or silently redirects if session exists
{broker}/oauth/tokenPOSTCognito (server-to-server)Exchanges authorization code for id_token + access_token
{broker}/login/callbackGETBrowser (redirect)Broker's callback URL — receives the authorization code from the partner's IdP
{partner-idp}/authorizeGETBrowser (redirect)Partner's IdP authorization endpoint — shows partner's login page
{partner-idp}/oauth/tokenPOSTBroker (server-to-server)Partner's IdP token endpoint — broker exchanges code for tokens
{cognito-domain}/oauth2/idpresponseGETBrowser (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 pageBroker's login page (e.g., Auth0 Universal Login)Partner's own login page
Credential validationBroker calls partner API (server-to-server)User authenticates directly on partner's IdP
Partner requirementExpose two REST endpoints (login, getUser)Run an OIDC-compliant IdP (existing or built per Scenario C)
Broker configurationCustom Database ConnectionEnterprise OIDC Connection

For example, in Auth0 this is configured as an Enterprise Connection (OIDC type):

  1. Go to Authentication → Enterprise → OpenID Connect → Create Connection
  2. Provide the partner IdP's issuer URL, client ID, and client secret
  3. 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:

ItemExampleNotes
Issuer URLhttps://auth.partner.com/.well-known/openid-configuration must be publicly accessible at this URL
Client IDvio-cognito-prodRegistered on the partner's OIDC server for our Cognito pool
Client Secret<secret>Share via secure channel

What Vio provides to the partner:

ItemExampleNotes
Redirect URIhttps://<cognito-domain>/oauth2/idpresponseThe 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:

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_uri with ?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_uri is 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_id and redirect_uri used 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"
}
ClaimTypeDescription
issstringMust exactly match the issuer URL in the discovery document
substringUnique, stable, non-reassignable user identifier
audstring or arrayMust contain the client_id issued to our Cognito pool
expnumberExpiration time (Unix timestamp). Token must not be expired
iatnumberIssued-at time (Unix timestamp)
noncestringMust echo the nonce sent in the authorization request
emailstringUser's email address
given_namestringUser's first name
family_namestringUser's last name

Cognito Attribute Mapping

IdP ClaimCognito Attribute
subusername (federated identity, format: <provider_name>_<sub>)
emailemail
given_namegiven_name
family_namefamily_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:

ScenarioSession cookie ownerNotes
APartner's IdPPartner must ensure SameSite=None; Secure on their IdP's session cookie
BOIDC Broker (e.g., Auth0)Managed services like Auth0 handle this automatically
B2OIDC Broker (e.g., Auth0)Managed services handle this automatically; partner's IdP session cookie is also needed if the broker re-authenticates against it
CPartner's custom OIDC serverPartner 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

ComponentComplexityDescription
Discovery endpointLowStatic JSON response
JWKS endpointLowServe public keys, handle rotation
RSA key managementMediumGenerate, store, rotate RS256 key pairs
Authorization endpointMediumSession validation, authorization code issuance, redirect URI validation
Token endpointMediumCode exchange, JWT signing, client authentication
Session managementMediumCookie-based sessions shared between partner site and IdP
Client registryLowStore client_id/secret pairs and allowed redirect URIs
Authorization code storeLowShort-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:

OptionEffortNotes
Auth0 (free tier: 7,500 MAU)DaysConfigure your user DB as a custom connection. Handles all OIDC endpoints, key rotation, session management
Keycloak (self-hosted, open source)DaysDeploy via Docker. Full OIDC provider with admin UI. Can federate to existing user databases
AWS Cognito (if already on AWS)DaysCreate a User Pool, configure as OIDC provider. Native OIDC compliance
Authlib + FastAPI (custom)1–2 weeksFull control. Use the skeleton above as a starting point. Requires key management, session storage, monitoring
From scratch (no framework)WeeksNot 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-configuration returns valid JSON with all required fields
  • GET {jwks_uri} returns at least one RSA key with "alg": "RS256"
  • Authorization endpoint accepts response_type=code and scope=openid email profile
  • Authorization endpoint returns code and state via redirect to our callback URL
  • Authorization endpoint silently redirects (no login UI) when a valid session cookie exists
  • Token endpoint accepts grant_type=authorization_code with client_secret_post authentication
  • Token endpoint returns a valid JWT id_token signed with RS256
  • id_token contains required claims: iss, sub, aud, exp, iat, nonce, email, given_name, family_name
  • id_token signature validates against the JWKS public key
  • iss claim exactly matches the issuer URL in the discovery document
  • Session cookie has SameSite=None; Secure attributes

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

  1. Partner assesses current identity infrastructure and picks a scenario (A, B, B2, or C)
  2. Partner provides OIDC issuer URL + client credentials (staging first)
  3. Vio validates discovery endpoint and token flow using the checklist above
  4. Vio provisions Cognito User Pool and shares redirect URIs
  5. Joint testing — end-to-end SSO flow in staging
  6. Production rollout with production credentials

Glossary

TermDefinition
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 BrokerAn 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 AuthenticationAn 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 FlowAn 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 CookieA 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.
ClaimsKey-value pairs inside a JWT that carry information about the user (e.g., email, sub, given_name). Cognito maps these claims to user attributes.
IssuerThe 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 EndpointA 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.
FederationThe 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 ConnectionA 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 ConnectionA 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.