Wiring your identity provider
GuideOperationsSince version 0.3.0
Operations guide: configure, deploy and observe the gateway. What the capability does, product side: product guide; calling it: developer guide.
Goal: require a token exactly where the contract requires one, without writing code.
Command - the configuration says how to verify an identity:
auth:
tenant_header: X-Tenant # where the tenant is written toward the upstream
issuers:
- name: staff
discovery: https://id.example.com/realms/staff/.well-known/openid-configuration
audience: api
roles_claim: realm_access.roles # Keycloak; `roles` for Entra ID
- name: customers
discovery: https://login.example.net/.well-known/openid-configuration
audience: api
tenant_claim: orgThe contract says what is required, operation by operation - never the configuration:
components:
securitySchemes:
customers: { type: openIdConnect, openIdConnectUrl: https://login.example.net/.well-known/openid-configuration }
staff: { type: http, scheme: bearer }
paths:
/me/orders:
get:
operationId: listMyOrders
security: [ { customers: [orders:read] } ] # a scope
/admin/report:
get:
operationId: getAdminReport
security: [ { staff: [admin] } ] # a role (http bearer scheme)A scheme's name does not pick the issuer: a token from any declared issuer satisfies the
operation when it carries the scopes or roles required. Name schemes after your issuers for the
reader's sake; trust is decided by auth.issuers, access by the scopes and roles.
Check:
| Call | Answer |
|---|---|
| No token | 401, WWW-Authenticate: Bearer |
| Invalid, expired, wrong-issuer or wrong-audience token | 401, error="invalid_token" - never 500, and never the exact reason |
| Valid token without the scope or role | 403, error="insufficient_scope", scope="..." - the scopes required; empty when only a role is missing, since a role is not an OAuth scope |
| Valid token with what is required | relayed; the upstream receives Authorization and X-Tenant from the claim |
Split horizon (
0.1.0): a provider reached under an internal name that announces its public one - Keycloak with a publicKC_HOSTNAMEand dynamic internal URLs - is declared withissuerbesidediscovery. Discovery and every token must state exactly thatissuer; the local stack uses it (config/dev/flexway.yaml).- name: staff discovery: http://keycloak:8080/realms/staff/.well-known/openid-configuration # reached issuer: https://login.example.com/realms/staff # statedSeveral audiences (
1.0.0): while an audience is renamed,audiencesaccepts a token issued for any one of them - the old name and the new, until the old tokens expire. Exclusive withaudience;/configlists the accepted audiences.- name: customers discovery: https://login.example.net/.well-known/openid-configuration audiences: [orders-api, api]Several issuers, from different vendors: declare as many as you need. A customer often has one provider for employees and another for end users. The start waits for no provider: until its keys are loaded, its tokens get
401, and the log says so (issuer not reachable yet).Closed by default: a header the contract does not declare does not reach the upstream, an undeclared query parameter is refused (
400, naming it), a browser origin not allowed is refused (403). To allow one, acorspolicy:
policies:
definitions:
browser: { type: cors, config: { allow_origins: ["https://app.example.com"], allow_credentials: true } }
chains:
global: [browser]