OIDC & FAPI 2.0 Flows - TRusTY

Last update: 2025-11-30 - Version 0.8.1 Specification compliance: OpenID Connect Core 1.0 + FAPI 2.0 Security Profile

This document describes the complete authentication flows implemented in TRusTY, including both standard OIDC and FAPI 2.0 (Financial-grade API) flows.


Table of Contents

  1. Supported Flows
  2. Standard OIDC Authorization Code Flow
  3. FAPI 2.0 Flow with PAR
  4. Logout Flow (RP-Initiated)
  5. Security Features
  6. API Endpoints

Supported Flows

TRusTY implements multiple authentication flows to support different security requirements:

FlowSecurity LevelUse CaseStandards
Standard OIDCBasicWeb applicationsOIDC Core 1.0 + PKCE
FAPI 2.0 with PARFinancial-gradeBanking, high-security APIsFAPI 2.0 + PAR + DPoP + private_key_jwt
RP-Initiated Logout-Session terminationOIDC RP-Initiated Logout 1.0

Standard OIDC Authorization Code Flow

Flow Diagram

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
sequenceDiagram
    participant User as User (Browser)
    participant Client as Client App
    participant Server as TRusTY Server

    User->>Client: 1. Access protected resource
    Client->>User: 2. Redirect to /auth

    Note over Client,Server: Authorization Request
    User->>Server: 3. GET /auth?response_type=code&client_id=...&redirect_uri=...&scope=...&state=...&nonce=...
    Server->>Server: 4. Validate client and params
    Server->>User: 5. Display login page

    Note over User,Server: User Authentication
    User->>Server: 6. POST /login (username + password)
    Server->>Server: 7. Validate credentials
    Server->>Server: 8. Create authorization code
    Server->>User: 9. Redirect to redirect_uri?code=...&state=...

    Note over Client,Server: Token Exchange
    User->>Client: 10. GET /callback?code=...&state=...
    Client->>Server: 11. POST /token (code + client_secret/PKCE)
    Server->>Server: 12. Validate code
    Server->>Client: 13. Return access_token + id_token + refresh_token

    Note over Client,Server: Resource Access
    Client->>Server: 14. GET /userinfo (Authorization: Bearer <access_token>)
    Server->>Client: 15. Return user profile (sub, email, name...)
    Client->>User: 16. Display protected resource

Detailed Steps

1-2. Initial Request

  • User accesses a protected resource
  • Client redirects to authorization endpoint

3-4. Authorization Request

Endpoint: GET /auth

Required parameters:

  • response_type=code (only Authorization Code Flow supported)
  • client_id - Client application identifier
  • redirect_uri - Callback URL (must be pre-registered)
  • scope - Requested scopes (e.g., openid email profile)

Optional parameters:

  • state - CSRF protection token (recommended)
  • nonce - Replay protection for ID token (recommended)
  • code_challenge - PKCE challenge (S256 method)
  • code_challenge_method - Always S256
  • prompt - UI behavior (none, login, consent)
  • max_age - Maximum authentication age
  • ui_locales - Preferred language (en, fr)

5-6. User Authentication

  • Server displays login page (localized in EN/FR)
  • User submits credentials via POST /login

7-9. Authorization Code Generation

  • Server validates credentials
  • Creates short-lived authorization code (90 seconds TTL)
  • Redirects to client’s redirect_uri with code

10-13. Token Exchange

Endpoint: POST /token

Request (form-encoded):

1
2
3
4
5
6
7
8
9
POST /token HTTP/1.1
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=<authorization_code>
&redirect_uri=<same_redirect_uri>
&client_id=<client_id>
&client_secret=<client_secret>  // or PKCE code_verifier
&code_verifier=<pkce_verifier>   // if PKCE was used

Response (JSON):

1
2
3
4
5
6
7
8
{
  "access_token": "eyJhbGc...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "id_token": "eyJhbGc...",
  "refresh_token": "refresh_xyz...",
  "scope": "openid email profile"
}

ID Token Claims (JWT):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "iss": "https://trusty.example.com",
  "sub": "a1b2c3d4-5678-90ab-cdef-1234567890ab",  // UUID (stable)
  "aud": "demo_client",
  "exp": 1701388800,
  "iat": 1701385200,
  "nonce": "xyz123...",
  "email": "alice@example.com",
  "email_verified": true,
  "name": "Alice Smith"
}

14-15. UserInfo Access

Endpoint: GET /userinfo

Request:

1
2
GET /userinfo HTTP/1.1
Authorization: Bearer <access_token>

Response (JSON):

1
2
3
4
5
6
7
{
  "sub": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
  "email": "alice@example.com",
  "email_verified": true,
  "name": "Alice Smith",
  "preferred_username": "alice"
}

FAPI 2.0 Flow with PAR

Enhanced Security Features

FAPI 2.0 (Financial-grade API) adds advanced security mechanisms:

FeatureSpecificationPurpose
PAR (Pushed Authorization Request)RFC 9126Pre-register authorization parameters server-side
PKCE (S256)RFC 7636Mandatory - Prevent code interception
private_key_jwtRFC 7523Client authentication with asymmetric keys
DPoPRFC 9449Token binding - Prevent token theft/replay

Complete FAPI 2.0 Flow Diagram

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
sequenceDiagram
    participant User as User (Browser)
    participant Client as FAPI Client (Python)
    participant ParSrv as /par (TRusTY)
    participant Auth as /auth (TRusTY)
    participant Login as /login (TRusTY)
    participant Token as /token (TRusTY)
    participant Resource as /userinfo (TRusTY)

    Note over Client: 1. KEY GENERATION
    Client->>Client: Generate PKCE code_verifier (43-128 chars)
    Client->>Client: Calculate code_challenge = SHA256(verifier)
    Client->>Client: Generate DPoP key pair (RSA ephemeral)
    Client->>Client: Load client RSA key (persistent)

    Note over Client,ParSrv: 2. PUSHED AUTHORIZATION REQUEST (PAR)
    Client->>Client: Create JWT client_assertion
    Note right of Client: {iss, sub, aud, exp, iat, jti}
    Client->>ParSrv: POST /par (client_id, client_assertion,<br/>response_type, redirect_uri, scope,<br/>state, nonce, code_challenge)

    ParSrv->>ParSrv: Validate client_assertion JWT signature
    ParSrv->>ParSrv: Store PAR request (90s TTL, in-memory)
    ParSrv->>ParSrv: Generate request_uri UUID
    ParSrv-->>Client: 201 Created<br/>{request_uri, expires_in}

    Note over Client,Auth: 3. AUTHORIZATION REQUEST
    Client->>User: Redirect browser to /auth
    User->>Auth: GET /auth?client_id=...&request_uri=urn:ietf:...

    Auth->>Auth: Validate request_uri format
    Auth->>Auth: Retrieve PAR request from storage
    Auth->>Auth: Validate client and parameters
    Auth->>User: Display login page

    Note over User,Login: 4. USER AUTHENTICATION
    User->>Login: POST /login (username, password)
    Login->>Login: Validate credentials
    Login->>Login: Create session + authorization code
    Login->>User: Redirect to redirect_uri?code=...&state=...

    Note over Client,Token: 5. TOKEN EXCHANGE WITH DPoP
    User->>Client: GET /callback?code=...&state=...
    Client->>Client: Validate state
    Client->>Client: Create DPoP proof JWT
    Note right of Client: {htm: POST, htu: /token,<br/>jti, iat, nonce}
    Client->>Token: POST /token<br/>Header: DPoP: <proof_jwt><br/>Body: grant_type, code, client_assertion,<br/>code_verifier

    Token->>Token: Validate client_assertion
    Token->>Token: Validate DPoP proof
    Token->>Token: Validate PKCE code_verifier
    Token->>Token: Validate authorization code
    Token->>Token: Generate DPoP-bound tokens
    Token-->>Client: 200 OK<br/>{access_token, id_token, token_type: DPoP}

    Note over Client,Resource: 6. RESOURCE ACCESS WITH DPoP
    Client->>Client: Create DPoP proof for /userinfo
    Client->>Resource: GET /userinfo<br/>Authorization: DPoP <access_token><br/>DPoP: <proof_jwt>

    Resource->>Resource: Validate DPoP proof
    Resource->>Resource: Verify token binding (jkt)
    Resource->>Resource: Validate access token
    Resource-->>Client: 200 OK<br/>{sub, email, name...}

    Client->>User: Display user info

Detailed FAPI 2.0 Steps

1. Key Generation

PKCE (Proof Key for Code Exchange):

1
2
3
4
5
6
7
8
import base64, hashlib, secrets

# Generate verifier (43-128 random characters)
code_verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode('utf-8').rstrip('=')

# Calculate challenge (SHA-256 hash)
challenge = hashlib.sha256(code_verifier.encode('utf-8')).digest()
code_challenge = base64.urlsafe_b64encode(challenge).decode('utf-8').rstrip('=')

DPoP Keys (ephemeral for this session):

1
2
3
4
5
6
from cryptography.hazmat.primitives.asymmetric import rsa

dpop_private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048
)

Client Authentication Keys (persistent, pre-registered):

  • Private key stored securely
  • Public key (JWKS) registered with TRusTY

2. Pushed Authorization Request (PAR)

Endpoint: POST /par

Client Assertion JWT (signed with client private key):

1
2
3
4
5
6
7
8
{
  "iss": "demo_client",
  "sub": "demo_client",
  "aud": "https://trusty.example.com/par",
  "exp": 1701385260,
  "iat": 1701385200,
  "jti": "unique-jwt-id-123"
}

PAR Request (form-encoded):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
POST /par HTTP/1.1
Content-Type: application/x-www-form-urlencoded

client_id=demo_client
&client_assertion=eyJhbGc...  # JWT signed with client private key
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&response_type=code
&redirect_uri=https://client.example.com/callback
&scope=openid+email+profile
&state=xyz123
&nonce=abc456
&code_challenge=E9Melhoa...
&code_challenge_method=S256

PAR Response:

1
2
3
4
{
  "request_uri": "urn:ietf:params:oauth:request_uri:a1b2c3d4-...",
  "expires_in": 90
}

3. Authorization Request with request_uri

Simplified authorization request:

1
GET /auth?client_id=demo_client&request_uri=urn:ietf:params:oauth:request_uri:a1b2c3d4-... HTTP/1.1

All other parameters (redirect_uri, scope, PKCE, etc.) are retrieved from the PAR request stored server-side.

5. Token Exchange with DPoP

DPoP Proof JWT (signed with DPoP private key):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{
  "typ": "dpop+jwt",
  "alg": "RS256",
  "jwk": {  // DPoP public key
    "kty": "RSA",
    "n": "0vx7agoebGc...",
    "e": "AQAB"
  }
}
// Payload:
{
  "jti": "dpop-proof-123",
  "htm": "POST",
  "htu": "https://trusty.example.com/token",
  "iat": 1701385200,
  "nonce": "server-provided-nonce"  // if server requires
}

Token Request:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
POST /token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
DPoP: eyJhbGc...  # DPoP proof JWT

grant_type=authorization_code
&code=authorization_code_123
&redirect_uri=https://client.example.com/callback
&client_id=demo_client
&client_assertion=eyJhbGc...  # New JWT for /token endpoint
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&code_verifier=dBjftJeZ...  # PKCE verifier

Token Response:

1
2
3
4
5
6
7
{
  "access_token": "eyJhbGc...",
  "token_type": "DPoP",  // Not "Bearer"!
  "expires_in": 3600,
  "id_token": "eyJhbGc...",
  "refresh_token": "refresh_xyz..."
}

Access Token contains cnf.jkt (JWK Thumbprint):

1
2
3
4
5
6
7
8
9
{
  "iss": "https://trusty.example.com",
  "sub": "a1b2c3d4-...",
  "aud": "demo_client",
  "exp": 1701388800,
  "cnf": {
    "jkt": "0ZcOCORZNYy-DWpqq30j..."  // SHA-256 thumbprint of DPoP public key
  }
}

6. UserInfo Access with DPoP

New DPoP Proof for GET /userinfo:

1
2
3
4
5
6
7
{
  "jti": "dpop-proof-456",
  "htm": "GET",  // Changed from POST
  "htu": "https://trusty.example.com/userinfo",  // Changed endpoint
  "iat": 1701385300,
  "ath": "fUHyO2T1..."  // Hash of access token (optional)
}

UserInfo Request:

1
2
3
GET /userinfo HTTP/1.1
Authorization: DPoP eyJhbGc...  # access_token (not "Bearer"!)
DPoP: eyJhbGc...  # New proof for this request

Logout Flow (RP-Initiated)

RP-Initiated Logout Diagram

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
sequenceDiagram
    participant User as User (Browser)
    participant Client as Client App
    participant Server as TRusTY Server

    User->>Client: 1. Click "Logout"
    Client->>User: 2. Redirect to /logout

    Note over Client,Server: Logout Request
    User->>Server: 3. GET /logout?id_token_hint=...&post_logout_redirect_uri=...&state=...&client_id=...
    Server->>Server: 4. Decode id_token_hint
    Server->>Server: 5. Extract sub, aud, session_id
    Server->>Server: 6. Invalidate session
    Server->>Server: 7. Revoke active tokens
    Server->>Server: 8. Validate post_logout_redirect_uri
    Server->>User: 9. Clear session cookie
    Server->>User: 10. Redirect to post_logout_redirect_uri?state=...

    User->>Client: 11. GET /logout/callback?state=...
    Client->>User: 12. Display logout confirmation

Logout Request Parameters

Endpoint: GET /logout

Parameters:

  • id_token_hint - ID token received during login (recommended)
  • post_logout_redirect_uri - Where to redirect after logout (must be pre-registered)
  • state - State to maintain across logout flow
  • client_id - Client identifier (optional if in id_token_hint)
  • ui_locales - Logout page language

Example:

1
GET /logout?id_token_hint=eyJhbGc...&post_logout_redirect_uri=https://client.example.com/&state=xyz HTTP/1.1

Security Features

Implemented Security Mechanisms

FeatureStandardImplementation
PKCE (S256)RFC 7636Mandatory for all flows
State parameterRFC 6749CSRF protection
NonceOIDC CoreID token replay protection
private_key_jwtRFC 7523Asymmetric client authentication
DPoPRFC 9449Token binding (jkt verification)
PARRFC 9126Pre-register authorization params
JWT Request ObjectsRFC 9101Signed authorization requests
Stable sub claimOIDC CoreUUID (not email)
Token revocationRFC 7009Explicit token invalidation
Session management-Server-side session tracking

PKCE Flow Protection

1
2
3
4
5
6
7
1. Client generates code_verifier (random string)
2. Client calculates code_challenge = SHA256(code_verifier)
3. Client sends code_challenge in /auth request
4. Server stores code_challenge with authorization code
5. Client sends code_verifier in /token request
6. Server validates: SHA256(received_verifier) == stored_challenge
7. If match: issue tokens. If not: reject with invalid_grant

DPoP Token Binding

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
1. Client generates ephemeral DPoP key pair
2. Client creates DPoP proof (JWT signed with DPoP private key)
3. Server validates DPoP proof signature
4. Server calculates JWK thumbprint (jkt) of DPoP public key
5. Server embeds jkt in access_token (cnf.jkt claim)
6. For each resource request:
   a. Client sends new DPoP proof + access_token
   b. Server validates proof signature
   c. Server extracts jkt from access_token
   d. Server calculates jkt from proof's JWK
   e. If jkt values match: grant access. If not: reject 401

API Endpoints

Discovery & Configuration

EndpointMethodDescription
/.well-known/openid-configurationGETOIDC Discovery metadata
/.well-known/jwks.jsonGETServer public keys (JWKS)

Core OIDC Endpoints

EndpointMethodDescriptionAuth Required
/authGETAuthorization endpointNo
/loginGET/POSTUser authentication pageNo
/tokenPOSTToken exchangeClient auth
/userinfoGETUser profileAccess token
/logoutGETRP-initiated logoutNo
/revokePOSTToken revocationClient auth

FAPI 2.0 Endpoints

EndpointMethodDescriptionAuth Required
/parPOSTPushed Authorization RequestClient auth (private_key_jwt)
/introspectPOSTToken introspectionClient auth

Utility Endpoints

EndpointMethodDescription
/healthGETHealth check
/observability/sessionsGETActive sessions count
/observability/tokensGETToken statistics

Client Configuration Requirements

Standard OIDC Client

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
client:
  client_id: "demo_client"
  client_secret: "demo_secret"
  redirect_uris:
    - "http://localhost:5001/auth/callback"
  post_logout_redirect_uris:
    - "http://localhost:5001/"
  token_endpoint_auth_method: "client_secret_post"  # or client_secret_basic
  grant_types:
    - "authorization_code"
  response_types:
    - "code"
  scope: "openid email profile"

FAPI 2.0 Client

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
client:
  client_id: "fapi_client"
  # No client_secret! Uses private_key_jwt
  redirect_uris:
    - "http://localhost:5002/auth/callback"
  post_logout_redirect_uris:
    - "http://localhost:5002/"
  token_endpoint_auth_method: "private_key_jwt"
  jwks:  # Client public key for signature verification
    keys:
      - kty: "RSA"
        use: "sig"
        kid: "fapi-client-key-1"
        n: "0vx7agoebGc..."
        e: "AQAB"
  grant_types:
    - "authorization_code"
  response_types:
    - "code"
  scope: "openid email profile"
  require_pushed_authorization_requests: true  # PAR mandatory

Error Handling

Common Error Responses

Authorization Errors (redirect to redirect_uri):

1
2
HTTP/1.1 302 Found
Location: https://client.example.com/callback?error=invalid_request&error_description=Missing+nonce+parameter&state=xyz

Token Endpoint Errors (JSON response):

1
2
3
4
{
  "error": "invalid_grant",
  "error_description": "Authorization code has expired or already been used"
}

UserInfo Errors:

1
2
3
4
{
  "error": "invalid_token",
  "error_description": "Access token is expired or invalid"
}

Error Codes

Error CodeDescription
invalid_requestMissing or invalid parameters
unauthorized_clientClient not authorized for this operation
access_deniedUser denied authorization
unsupported_response_typeResponse type not supported
invalid_scopeRequested scope is invalid
server_errorInternal server error
invalid_clientClient authentication failed
invalid_grantAuthorization code/refresh token invalid
unsupported_grant_typeGrant type not supported
invalid_dpop_proofDPoP proof validation failed

References

OpenID Connect Specifications

FAPI 2.0 Specifications

OAuth 2.0 RFCs


Document Version: 2.0 Status: Production-ready Last Review: 2025-11-30