A small OIDC callback broker for Keycloak that supports:
- Shared redirect callback endpoint (
/callback) - DB-backed allowed app configuration
- CRUD APIs for app management (for GUI use)
- One-time code exchange API for apps (
/v1/auth/exchange) - Dedicated audience token exchange API for MFEs (
/v1/auth/token-exchange)
GET /healthzGET /start?app=<slug>&return_to=<path-or-url>GET /callbackPOST /v1/auth/exchangePOST /v1/auth/token-exchangeGET /v1/appsPOST /v1/appsGET /v1/apps/{slug}PUT /v1/apps/{slug}DELETE /v1/apps/{slug}
Set ADMIN_API_TOKEN (or ADMIN_API_TOKEN_FILE) to require:
Authorization: Bearer <token>
for /v1/apps* endpoints.
curl -X POST http://localhost:8080/v1/apps \
-H 'Content-Type: application/json' \
-d '{
"slug": "shell",
"display_name": "Shell App",
"base_urls": [
"https://shell.suncoast.systems",
"http://localhost:4173"
],
"enabled": true
}'base_url is still accepted for backward compatibility, but base_urls is preferred.
curl -i 'http://localhost:8080/start?app=shell&return_to=/auth/callback'curl -X POST http://localhost:8080/v1/auth/exchange \
-H 'Content-Type: application/json' \
-d '{"code":"<gateway_code>","app_slug":"shell"}'To keep default behavior unchanged, token shaping is opt-in at exchange time.
Request a token exchange for a specific audience:
curl -X POST http://localhost:8080/v1/auth/exchange \
-H 'Content-Type: application/json' \
-d '{
"code":"<gateway_code>",
"app_slug":"shell",
"requested_audience":"graphql-api-7603d234"
}'Request Hasura claims using the configured Hasura audience lookup:
curl -X POST http://localhost:8080/v1/auth/exchange \
-H 'Content-Type: application/json' \
-d '{
"code":"<gateway_code>",
"app_slug":"shell",
"request_hasura_claims": true
}'When request_hasura_claims=true and requested_audience is not provided, the audience is resolved in this order:
- Vault lookup (
HASURA_TOKEN_AUDIENCE_VAULT_PATH, if configured) - Fallback static value (
HASURA_TOKEN_AUDIENCEorHASURA_TOKEN_AUDIENCE_FILE)
Vault lookup supports {app_slug} in the path template. Example:
HASURA_TOKEN_AUDIENCE_VAULT_PATH=kv/data/keycloak/hasura/{app_slug}HASURA_TOKEN_AUDIENCE_VAULT_KEY=audience
Use this when an MFE already has a bearer token and needs a different audience (or multiple audiences).
Request:
curl -X POST http://localhost:8080/v1/auth/token-exchange \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <user_access_token>' \
-d '{
"app_slug":"shell",
"requested_audience":"graphql-api-7603d234"
}'Multiple audiences (single exchanged token with multiple aud values):
curl -X POST http://localhost:8080/v1/auth/token-exchange \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <user_access_token>' \
-d '{
"app_slug":"shell",
"requested_audiences":["graphql-api-7603d234","openwebui"]
}'Request body fields:
app_slug(required)subject_token(optional if passed asAuthorization: Bearer ...)requested_audience(optional)requested_audiences(optional array)requested_scope(optional)request_hasura_claims(optional; resolves audience using Hasura audience config if no audience is provided)
EXTERNAL_BASE_URL(for examplehttps://login.suncoast.systems)KEYCLOAK_ISSUER(for examplehttps://auth.suncoast.systems/realms/external)OIDC_CLIENT_IDOIDC_EXCHANGE_CLIENT_ID(optional; defaults toOIDC_CLIENT_ID)- Database config via
DATABASE_URLorDB_*vars
OIDC_CLIENT_SECRETorOIDC_CLIENT_SECRET_FILEOIDC_EXCHANGE_CLIENT_SECRETorOIDC_EXCHANGE_CLIENT_SECRET_FILE(optional; defaults toOIDC_CLIENT_SECRET)ADMIN_API_TOKENorADMIN_API_TOKEN_FILECORS_ALLOW_ORIGINS(comma-separated list,*, wildcard subdomain patterns like*.suncoast.systems, andlocalhost)HASURA_TOKEN_AUDIENCEorHASURA_TOKEN_AUDIENCE_FILE(fallback whenrequest_hasura_claims=true)VAULT_ADDR(required whenHASURA_TOKEN_AUDIENCE_VAULT_PATHis set)VAULT_TOKENorVAULT_TOKEN_FILE(required whenHASURA_TOKEN_AUDIENCE_VAULT_PATHis set)HASURA_TOKEN_AUDIENCE_VAULT_PATH(Vault API path; can include{app_slug})HASURA_TOKEN_AUDIENCE_VAULT_KEY(field name in the Vault secret, defaultaudience)STATE_TTL(default10m)EXCHANGE_CODE_TTL(default2m)APP_CODE_PARAM(defaultgateway_code)CALLBACK_PATH(default/callback)
If not using DATABASE_URL:
DB_HOST(defaultyb-tserver-service.yugabyte.svc.cluster.local)DB_PORT(default5433)DB_NAME(defaultkeycloak)DB_SEARCH_PATH(defaultkeycloak)DB_SSLMODE(defaultdisable)DB_USERorDB_USER_FILEDB_PASSWORDorDB_PASSWORD_FILE
docker build -t keycloak-auth-gateway:local .This repo includes a GitHub Actions workflow at .github/workflows/publish-image.yml.
On pushes to the default branch (and on tags), it builds and publishes:
ghcr.io/<repo-owner>/keycloak-auth-gateway:latest(default branch)ghcr.io/<repo-owner>/keycloak-auth-gateway:sha-<commit>
You can also run the workflow manually with workflow_dispatch.