This project demonstrates OAuth2 Token Exchange (RFC 8693) for secure delegation in a multi-service architecture. It solves a common enterprise problem: how can a web application query a backend service (like a database) on behalf of an authenticated user while preserving the user's identity and permissions?
In a typical setup where a web application needs to access a database:
- Option A: Use a shared service account - loses user identity, no per-user audit trail, overly broad permissions
- Option B: Pass the user's token directly - the token may not be authorized for the target service, violates audience restrictions
Token Exchange (On-Behalf-Of flow) allows the web application to exchange the user's token for a new token specifically scoped for the target service. The new token:
- Preserves the original user's identity (email, groups)
- Has the correct audience claim for the target service
- Can have restricted scopes appropriate for the specific use case
- User Authentication - OAuth2 Authorization Code flow with Keycloak
- Token Exchange - RFC 8693 token exchange to obtain Trino-specific tokens
- Group-Based Access Control - Trino enforces permissions based on Keycloak group membership
- End-to-End Security - Full audit trail from user login through database query
- docker compose
- mkcert
- uv
First of all you need certificates and keys to start keycloak and trinodb with https.
To do so head to certs/README.md and follow instructions.
Then you will start Keycloak and configure it. Docker compose takes care of it. Just run:
docker compose up --build
This will start at https://keycloak:8443 with administrator credentials admin/admin.
It will also run pyscripts/src/main.py to setup clients, scopes and users.
Our human users will be:
antonio.murgia@agilelab.it/StrongP@ssword123(in trino-admins group)andrea.fonti@agilelab.it/StrongP@ssword123
The generated client secrets for webapp and trino will be written to env files inside .env_files folder.
Such files will be sourced by docker-compose in appropriate containers.
Next step is to start our webapp and trinodb, we need to start them after initializing keycloak so that they can be configured with the needed client secrets to connect to keycloak.
docker compose up --build webapp trinodb
In order to test that RBAC works correctly, we need to create a view that can be accessed by any
andrea.fonti@agilelab.it and one that can be accessed only by trino-admins.
Access control rules are controlled by trino/rules.json and trino/catalog/hive-rules.json
N.B. For access control based on groups to work we need to add a custom plugin (not production ready) that is in keycloak-groups-plugin and it is packaged with the custom trino image that we are using see trino/container-image/Dockerfile
cd pyscripts
uv run src/create_view.py
This will open a browser where you will need to login as antonio.murgia@agilelab.it (because it's a trino-admin), andrea fonti will not work, it will return a permission denied error.
Now the cool part:
- we're going to head to http://webapp:5555
- Click on Login with Keycloak
- log in with either antonio or andrea credentials
- Then click on "Protected Route"
If we logged as antonio the 6th element of the bullet point will show a list of nations
If we logged as andrea the 6th element of the bullet point will show a permission denied error (Query failed)
This section explains the inner workings of the delegation demo, covering the OAuth2/OIDC flows, token exchange mechanism, and group-based access control.
The demo consists of three main components:
- Keycloak (Identity Provider) - Handles user authentication, issues tokens, and performs token exchange
- FastAPI Web App (Intermediary) - Manages user sessions and orchestrates token exchange for backend services
- Trino (Resource Server) - Validates JWT tokens and enforces group-based access control
When a user clicks "Login with Keycloak", the standard OAuth2 Authorization Code flow is initiated:
- Authorization Request: The webapp redirects the user to Keycloak's authorization endpoint with the required parameters (
client_id,redirect_uri,scope,state) - User Authentication: The user authenticates with Keycloak (username/password)
- Authorization Code: Keycloak redirects back to the webapp's
/callbackendpoint with an authorization code - Token Exchange: The webapp exchanges the authorization code for tokens (access token, refresh token, ID token)
- Session Storage: The access token is stored in the user's session for subsequent requests
The core innovation in this demo is the Token Exchange mechanism defined in RFC 8693. This allows the webapp to obtain a new token with a different audience (Trino) while preserving the original user's identity.
Without token exchange, the webapp would need to:
- Use its own service account credentials to query Trino (losing user identity)
- Pass the user's original token directly (which may not be authorized for Trino)
Token exchange solves this by allowing the webapp to request a new token on behalf of the authenticated user, specifically scoped for Trino.
The webapp sends a POST request to Keycloak's token endpoint with:
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
subject_token=<user's access token>
subject_token_type=urn:ietf:params:oauth:token-type:access_token
requested_token_type=urn:ietf:params:oauth:token-type:access_token
audience=trinodb
Keycloak validates:
- The webapp client is authorized to perform token exchange
- The subject token is valid
- The target audience (trinodb) allows token exchange from the webapp client
If successful, Keycloak returns a new access token with:
- The original user's identity (email, groups, etc.)
- The audience claim set to
trinodb - Scopes appropriate for Trino access
sequenceDiagram
autonumber
participant User as User Browser
participant WebApp as FastAPI WebApp
participant KC as Keycloak
participant Trino as Trino DB
rect rgb(240, 248, 255)
Note over User,KC: Phase 1: User Authentication (OAuth2 Authorization Code Flow)
User->>WebApp: GET /login
WebApp->>User: Redirect to Keycloak /auth
User->>KC: Authorization Request (client_id, redirect_uri, scope, state)
KC->>User: Login Page
User->>KC: Submit Credentials
KC->>User: Redirect to /callback with auth code
User->>WebApp: GET /callback?code=xxx&state=yyy
WebApp->>KC: POST /token (exchange code for tokens)
KC->>WebApp: Access Token + Refresh Token + ID Token
WebApp->>WebApp: Store access_token in session
WebApp->>User: Redirect to home (authenticated)
end
rect rgb(255, 248, 240)
Note over User,Trino: Phase 2: Token Exchange (RFC 8693 On-Behalf-Of)
User->>WebApp: GET /protected
WebApp->>WebApp: Retrieve user's access_token from session
WebApp->>KC: POST /token (token exchange request)
Note right of WebApp: grant_type=token-exchange<br/>subject_token=user_token<br/>audience=trinodb
KC->>KC: Validate subject_token
KC->>KC: Check exchange permissions
KC->>KC: Generate new token for trinodb audience
KC->>WebApp: New Access Token (audience=trinodb)
end
rect rgb(240, 255, 240)
Note over WebApp,Trino: Phase 3: Resource Access with Exchanged Token
WebApp->>Trino: SQL Query with JWT Bearer Token
Trino->>KC: Fetch JWKS (public keys)
Trino->>Trino: Validate JWT signature
Trino->>Trino: Verify audience=trinodb
Trino->>KC: GET /groups/{user} (via custom plugin)
KC->>Trino: User's group memberships
Trino->>Trino: Apply access rules based on groups
Trino->>WebApp: Query Results (or Access Denied)
WebApp->>User: Display Results
end
Trino uses a custom plugin (keycloak-groups-plugin) to fetch user group memberships from Keycloak at runtime. This enables fine-grained access control:
- JWT Validation: Trino validates the incoming JWT token using Keycloak's JWKS endpoint
- Identity Extraction: The user's email is extracted from the token's
emailclaim - Group Lookup: The custom plugin queries Keycloak's Admin API for the user's groups
- Rule Evaluation: Access rules in
trino/rules.jsonare evaluated based on the user's groups
Example access rule:
- Users in
trino-adminsgroup can accessprivate.nation_view - Other users receive "Permission Denied"
| Component | Configuration | Purpose |
|---|---|---|
| Keycloak | Token Exchange enabled on webapp client | Allows webapp to exchange tokens |
| Keycloak | scope-token-exchange scope |
Required scope for token exchange |
| Trino | jwt.required-audience=trinodb |
Validates audience claim in JWT |
| Trino | jwt.principal-field=email |
Uses email as user identity |
| WebApp | audience=trinodb in exchange request |
Requests token for Trino |
- Token Isolation: The original user token is never sent directly to Trino; only the exchanged token with proper audience is used
- Audience Restriction: Trino only accepts tokens specifically issued for it (
audience=trinodb) - Principle of Least Privilege: The exchanged token can have reduced scopes compared to the original
- Audit Trail: Keycloak logs both the original authentication and the token exchange, enabling full traceability
- Remove all verify=False