| title | Authentication |
|---|---|
| sidebar_position | 2 |
The gateway supports multiple authentication methods that can be combined per route. Authentication is configured globally (provider settings) and per route (which methods are required).
Validates requests against a list of known API keys, checked in a header or query parameter.
authentication:
api_key:
enabled: true
header: "X-API-Key" # or use query_param: "api_key"
keys:
- key: "${API_KEY_1}"
client_id: "client-1"
name: "Production Client"
expires_at: "2026-12-31T23:59:59Z" # optional RFC3339 expiry
roles: ["admin", "read"] # optional roles
- key: "${API_KEY_2}"
client_id: "client-2"
name: "Staging Client"API keys can be managed at runtime via the Admin API at /admin/keys (GET, POST, DELETE).
The gateway provides built-in API key lifecycle management: generation, rotation (with grace periods), revocation, and per-key rate limits. This eliminates the need for an external key management service.
authentication:
api_key:
enabled: true
header: "X-API-Key"
keys: [] # static keys still supported alongside managed keys
management:
enabled: true
key_length: 32 # bytes (default 32, range 16-128)
key_prefix: "gw_" # prefix for generated keys
store: memory # "memory" or "redis"
default_rate_limit: # optional default per-key rate limit
rate: 100
period: 1m
burst: 20Key operations (via Admin API):
- Generate:
POST /api-keys/generate— returns the raw key once (only time visible) - Rotate:
POST /api-keys/{prefix}/rotate— creates a new key with a grace period for the old key - Revoke:
POST /api-keys/{prefix}/revoke— marks key as revoked (returns 403, not 401) - Unrevoke:
POST /api-keys/{prefix}/unrevoke— restores a revoked key - Delete:
DELETE /api-keys/{prefix}/delete— permanently removes the key - Stats:
GET /api-keys/stats— management statistics
Managed keys are looked up by SHA-256 hash — the raw key is never stored. When both managed and static keys are configured, managed keys are checked first.
Validates JSON Web Tokens using HMAC shared secrets, RSA public keys, or remote JWKS endpoints.
authentication:
jwt:
enabled: true
algorithm: "RS256"
# Static key:
# secret: "${JWT_SECRET}" # for HS256
# public_key: "${JWT_PUB_KEY}" # for RS256
# Or dynamic JWKS:
jwks_url: "https://auth.example.com/.well-known/jwks.json"
jwks_refresh_interval: 1h
issuer: "https://auth.example.com"
audience: ["my-api"]JWT claims are accessible as variables ($jwt_claim_sub, $jwt_claim_role) and in the rules engine (auth.claims["sub"]).
When jwks_url is set, the gateway fetches and caches the JSON Web Key Set, automatically refreshing it on the configured interval. This supports key rotation without gateway restarts.
Validates bearer tokens via token introspection or JWKS, with scope enforcement.
authentication:
oauth:
enabled: true
introspection_url: "https://auth.example.com/oauth/introspect"
client_id: "${OAUTH_CLIENT_ID}"
client_secret: "${OAUTH_CLIENT_SECRET}"
# Or JWKS-based:
# jwks_url: "https://auth.example.com/.well-known/jwks.json"
issuer: "https://auth.example.com"
audience: "my-api"
scopes: ["read", "write"]
cache_ttl: 5mValidates requests using HTTP Basic Authentication against a local list of users with bcrypt-hashed passwords. This is the simplest auth method and is suitable for internal tools, staging environments, or small-scale APIs.
authentication:
basic:
enabled: true
realm: "My API" # optional, default "Restricted"
users:
- username: "admin"
password_hash: "$2a$10$..." # bcrypt hash
client_id: "admin-user"
roles: ["admin", "write"]
- username: "reader"
password_hash: "$2a$10$..."
client_id: "reader-user"
roles: ["read"]Use htpasswd (from Apache) or any bcrypt tool:
htpasswd -nbBC 10 admin mypassword | cut -d: -f2Or in Go:
hash, _ := bcrypt.GenerateFromPassword([]byte("mypassword"), bcrypt.DefaultCost)When an unknown username is submitted, the gateway still runs a bcrypt comparison against a dummy hash. This prevents timing-based user enumeration attacks.
Basic auth is not included in the default auth methods — it must be explicitly listed in auth.methods to avoid triggering browser credential dialogs on API routes:
routes:
- id: "internal-tool"
path: "/internal/*"
backends:
- url: "http://backend:9000"
auth:
required: true
methods: ["basic"]Validates requests using HTTP Basic Authentication against an LDAP or Active Directory server. The gateway uses a bind-search-bind flow: it binds as a service account, searches for the user, then binds as the found user to verify the password.
authentication:
ldap:
enabled: true
url: "ldap://ldap.example.com:389" # or ldaps:// for TLS
start_tls: false
bind_dn: "cn=gateway,ou=services,dc=example,dc=org"
bind_password: "${LDAP_BIND_PASSWORD}" # env var expansion
user_search_base: "ou=users,dc=example,dc=org"
user_search_filter: "(uid={{username}})"
user_search_scope: "sub" # sub (default), one, base
realm: "LDAP"
attribute_mapping:
client_id: "uid" # default "uid"
email: "mail"
display_name: "displayName"
tls:
skip_verify: false
ca_file: "/etc/certs/ldap-ca.pem"
cache_ttl: 5m # auth result cache TTL (default 5m)
conn_timeout: 10s # connection timeout (default 10s)
max_conn_lifetime: 5m # max lifetime for pooled connections (default 5m)
pool_size: 5 # connection pool size (default 5)- The gateway binds to LDAP as the service account (
bind_dn/bind_password) - It searches for the user using the
user_search_filterwith{{username}}replaced by the login name (LDAP-escaped) - If found, it binds as the user's DN with the provided password to verify credentials
- If
group_search_baseis set, it re-binds as the service account and searches for group membership
authentication:
ldap:
enabled: true
url: "ldaps://ad.corp.example.com:636"
bind_dn: "CN=Gateway Service,OU=Services,DC=corp,DC=example,DC=com"
bind_password: "${AD_BIND_PASSWORD}"
user_search_base: "OU=Users,DC=corp,DC=example,DC=com"
user_search_filter: "(sAMAccountName={{username}})"
attribute_mapping:
client_id: "sAMAccountName"
email: "mail"
display_name: "displayName"
group_search_base: "OU=Groups,DC=corp,DC=example,DC=com"
group_search_filter: "(member={{dn}})"
group_attribute: "cn"
tls:
skip_verify: falseWhen group_search_base is set, the gateway searches for groups the user belongs to. The group_search_filter supports {{dn}} (user's DN) and {{username}} placeholders. The group_attribute (default cn) is extracted from each group entry and included as roles in the identity claims.
- Connection pool: Maintains a pool of reusable LDAP connections (
pool_size, default 5). Stale connections older thanmax_conn_lifetimeare discarded. - Result cache: Successful authentications are cached using an LRU cache (capacity 10,000) with per-entry TTL (
cache_ttl, default 5m). Cache entries are evicted individually on TTL expiry — no thundering-herd evict-all.
GET /ldap/stats returns authentication statistics:
{
"attempts": 1234,
"successes": 1200,
"failures": 34,
"cache_hits": 950,
"cache_misses": 284,
"pool_size": 3
}Like basic auth, LDAP auth must be explicitly listed in auth.methods:
routes:
- id: "corp-api"
path: "/api/*"
backends:
- url: "http://backend:9000"
auth:
required: true
methods: ["ldap"]Delegates authentication decisions to an external HTTP or gRPC service. This is the "ForwardAuth" / "ext_authz" pattern used by Traefik, Envoy, and NGINX. The gateway sends request metadata to the external service and acts on its allow/deny response.
routes:
- id: "protected"
path: "/api"
path_prefix: true
backends:
- url: "http://backend:9000"
ext_auth:
enabled: true
url: "http://auth-service:8080/authorize"
timeout: 3s
fail_open: false
headers_to_send:
- "Authorization"
- "X-Forwarded-For"
- "Cookie"
headers_to_inject:
- "X-Auth-User"
- "X-Auth-Roles"
cache_ttl: 30sThe gateway sends a POST request to the auth URL with a JSON body containing the request method, path, and selected headers. A 200 response means allow; any other status means deny, and the status code and body are returned to the client.
ext_auth:
enabled: true
url: "grpc://auth-service:50051"
timeout: 3s
tls:
enabled: true
ca_file: "/certs/ca.pem"The gateway invokes /extauth.AuthService/Check using a JSON codec. The request and response are JSON-encoded CheckRequest/CheckResponse structs.
On allow, the auth service can return response headers that get injected into the upstream request. Use headers_to_inject to limit which headers are copied (empty = all headers from the auth response).
fail_open: false(default): If the auth service is unreachable or returns an error, the request is denied with502 Bad Gateway.fail_open: true: If the auth service is unreachable, the request is allowed to proceed.
Set cache_ttl to cache successful auth results. The cache key is computed from the request method, path, and selected headers. Only allow results are cached; deny results are never cached.
ExtAuth runs after built-in auth (JWT, API Key, OAuth), so identity information from built-in providers is available to the ext auth service via forwarded headers. Both can be used on the same route.
| Field | Type | Default | Description |
|---|---|---|---|
ext_auth.enabled |
bool | false | Enable external auth |
ext_auth.url |
string | Auth service URL (http://, https://, or grpc://) |
|
ext_auth.timeout |
duration | 5s | Request timeout |
ext_auth.fail_open |
bool | false | Allow on auth service error |
ext_auth.headers_to_send |
[]string | all | Request headers to forward |
ext_auth.headers_to_inject |
[]string | all | Auth response headers to inject upstream |
ext_auth.cache_ttl |
duration | 0 | Cache TTL (0 = disabled) |
ext_auth.tls.enabled |
bool | false | Enable TLS |
ext_auth.tls.ca_file |
string | CA certificate file | |
ext_auth.tls.cert_file |
string | Client certificate (mTLS) | |
ext_auth.tls.key_file |
string | Client key (mTLS) |
When mTLS is enabled on a listener, client certificate fields are extracted and made available as variables and in the rules engine:
| Variable | Description |
|---|---|
$client_cert_subject |
Certificate subject DN |
$client_cert_issuer |
Certificate issuer DN |
$client_cert_fingerprint |
SHA-256 fingerprint |
$client_cert_serial |
Serial number |
$client_cert_dns_names |
Comma-separated DNS SANs |
See Core Concepts — mTLS for listener TLS configuration.
Each route specifies whether auth is required and which methods to use:
routes:
- id: "protected-api"
path: "/api/protected"
path_prefix: true
backends:
- url: "http://backend:9000"
auth:
required: true
methods: ["jwt", "api_key"] # accepts either method
- id: "public-api"
path: "/api/public"
path_prefix: true
backends:
- url: "http://backend:9000"
auth:
required: false # no authentication neededWhen required: true and no valid credential is provided, the gateway returns 401 Unauthorized.
Forward JWT claims as request headers to backend services. Configured per-route; runs after authentication succeeds.
routes:
- id: my-route
path: /api/v1/*
auth:
required: true
methods: [jwt]
claims_propagation:
enabled: true
claims:
sub: "X-User-ID"
email: "X-User-Email"
org_id: "X-Org-ID"
user.role: "X-User-Role" # dot notation for nested claimsEach entry maps a JWT claim name to a request header name. The gateway reads claims from the authenticated identity (populated by the auth middleware) and sets the corresponding headers before forwarding to the backend.
- Dot notation:
user.roleextractsclaims["user"]["role"]from nested claim objects - Non-string values: Automatically converted via
fmt.Sprintf("%v", val) - Missing claims: Silently skipped (no error, no header set)
- No identity: If the request has no authenticated identity, propagation is skipped entirely
Admin endpoint: GET /claims-propagation returns per-route propagation statistics.
Blocklist revoked JWT tokens by JTI or token hash. Supports both in-memory and distributed (Redis) storage.
token_revocation:
enabled: true
mode: local # "local" (default) or "distributed"
default_ttl: 24h # max time to keep revoked tokens (default 24h)- After authentication, the gateway extracts the Bearer token from the
Authorizationheader - If the token has a
jticlaim, that value is used as the revocation key - If no
jti, the SHA256 hash of the full token is used (first 32 hex chars) - The key is checked against the revocation store — if found, the request is rejected with
401 Unauthorized
Token revocation only applies to routes where auth.required: true.
# Revoke by full JWT token (extracts JTI automatically)
curl -X POST http://localhost:8081/token-revocation/revoke \
-d '{"token":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyLTEiLCJqdGkiOiJhYmMtMTIzIn0.sig"}'
# Revoke by JTI directly (with custom TTL)
curl -X POST http://localhost:8081/token-revocation/revoke \
-d '{"jti":"abc-123","ttl":"2h"}'
# Unrevoke a token
curl -X POST http://localhost:8081/token-revocation/unrevoke \
-d '{"jti":"abc-123"}'
# Check revocation stats
curl http://localhost:8081/token-revocation- If the token has an
expclaim, the revocation TTL is capped atexp - now(no point storing a revocation longer than the token lives) - Explicit TTL in the revoke request is also capped at
default_ttl - If no TTL is specified,
default_ttlis used
With mode: distributed, the revocation store uses Redis (requires redis.address in config). This ensures revocations are shared across gateway instances. Redis keys are prefixed with gw:revoked: and use TTL-based expiration. The store fails open on Redis errors (allows the request through).
| Field | Type | Description |
|---|---|---|
authentication.api_key.enabled |
bool | Enable API key auth |
authentication.api_key.header |
string | Header name to check (default X-API-Key) |
authentication.jwt.algorithm |
string | HS256, RS256, etc. |
authentication.jwt.jwks_url |
string | JWKS endpoint for dynamic key fetching |
authentication.oauth.scopes |
[]string | Required OAuth scopes |
auth.required |
bool | Require auth on this route |
authentication.basic.enabled |
bool | Enable Basic auth |
authentication.basic.realm |
string | WWW-Authenticate realm (default Restricted) |
authentication.ldap.enabled |
bool | Enable LDAP auth |
authentication.ldap.url |
string | LDAP server URL (ldap:// or ldaps://) |
authentication.ldap.cache_ttl |
duration | Auth result cache TTL (default 5m) |
authentication.ldap.pool_size |
int | Connection pool size (default 5) |
auth.methods |
[]string | Allowed methods: jwt, api_key, oauth, basic, ldap |
claims_propagation.enabled |
bool | Enable claims propagation (per-route) |
claims_propagation.claims |
map | Claim-to-header mappings |
token_revocation.enabled |
bool | Enable token revocation (global) |
token_revocation.mode |
string | local or distributed |
token_revocation.default_ttl |
duration | Max revocation TTL (default 24h) |
| backend_auth.enabled | bool | Enable backend auth token injection (per-route) |
| backend_auth.type | string | oauth2_client_credentials (required) |
| backend_auth.token_url | string | Token endpoint URL |
See Configuration Reference for all fields.
The gateway can act as an OAuth2 client, fetching access tokens from an identity server using the client_credentials grant and injecting them as Authorization: Bearer <token> headers into backend requests.
Tokens are cached in memory and auto-refreshed 10 seconds before expiry. If a token refresh fails, the request proceeds without an Authorization header (logged as a warning).
routes:
- id: protected-api
path: /api/
path_prefix: true
backends:
- url: http://backend:8080
backend_auth:
enabled: true
type: oauth2_client_credentials
token_url: https://auth.example.com/oauth/token
client_id: "${GATEWAY_CLIENT_ID}"
client_secret: "${GATEWAY_CLIENT_SECRET}"
scopes:
- read
- write
extra_params:
audience: https://api.example.com
timeout: 5sThe middleware is positioned at step 16.25 in the chain — after request transforms and before backend signing. This ensures the Authorization header is included in HMAC signature computation when backend signing is also enabled.
Admin endpoint: GET /backend-auth returns per-route token refresh stats.
The gateway can act as a Security Token Service (STS) intermediary, accepting external IdP tokens and issuing internal service tokens. This enables zero-trust architectures where backends only trust gateway-issued tokens.
- Client sends a request with
Authorization: Bearer <external-token> - Gateway validates the external token (JWT via JWKS, or introspection)
- Gateway mints a new internal JWT with mapped claims
- Backend receives the gateway-issued token in the
Authorizationheader
routes:
- id: partner-api
path: /api/partner
auth:
required: true
methods: [jwt]
token_exchange:
enabled: true
validation_mode: jwt # "jwt" or "introspection"
jwks_url: https://partner-idp.example.com/.well-known/jwks.json
trusted_issuers:
- https://partner-idp.example.com
issuer: https://runway.internal.example.com
audience: [internal-services]
scopes: [read, write]
token_lifetime: 15m
signing_algorithm: RS256 # RS256, RS512, HS256, HS512
signing_key_file: /etc/runway/exchange-key.pem
cache_ttl: 14m
claim_mappings:
sub: sub
email: email
groups: roles- jwt: Validates tokens locally using JWKS. Requires
jwks_urlandtrusted_issuers. - introspection: Validates via OAuth2 introspection endpoint. Requires
introspection_url,client_id,client_secret.
The claim_mappings field maps claims from the subject token to the issued token. The key is the claim name in the subject token, the value is the claim name in the issued token.
Exchange results are cached by SHA-256 of the subject token. Set cache_ttl slightly less than token_lifetime to avoid serving expired tokens from cache.
The middleware is positioned at step 6.07 in the chain — after auth (6) and token revocation (6.05), before claims propagation (6.15).
Admin endpoint: GET /token-exchange returns per-route exchange metrics.
The gateway can act as a SAML 2.0 Service Provider (SP), enabling browser-based Single Sign-On with enterprise identity providers (Okta, Azure AD, ADFS, OneLogin, etc.). It also supports stateless token validation via SAML assertions passed in an HTTP header.
Browser SSO (SP-initiated): The user visits a protected route, gets redirected to the IdP login page, authenticates, and is redirected back to the gateway's ACS endpoint. The gateway validates the SAML response, creates a signed session cookie, and redirects the user to the original URL.
Header-based token validation: For API clients, a Base64-encoded SAML assertion can be passed in the X-SAML-Assertion header (configurable). The gateway validates the assertion's XML structure, time conditions, and checks for replay before granting access.
| Endpoint | Method | Description |
|---|---|---|
/saml/metadata |
GET | SP metadata XML for IdP registration |
/saml/login |
GET | Initiates SSO redirect to IdP |
/saml/acs |
POST | Assertion Consumer Service — processes IdP response |
/saml/slo |
GET/POST | Single Logout (SP-initiated and IdP-initiated) |
The path prefix (/saml/) is configurable via path_prefix.
authentication:
saml:
enabled: true
entity_id: "https://runway.example.com"
cert_file: /etc/runway/saml/sp.cert # SP X.509 certificate (PEM)
key_file: /etc/runway/saml/sp.key # SP private key (PEM)
idp_metadata_url: https://idp.example.com/metadata # OR idp_metadata_file
# idp_metadata_file: /etc/runway/saml/idp-metadata.xml
metadata_refresh_interval: 24h # auto-refresh IdP metadata (0 disables)
path_prefix: /saml/ # default "/saml/"
name_id_format: email # email, persistent, transient, unspecified
sign_requests: true # sign AuthnRequests (default true)
force_authn: false # force re-authentication at IdP
allow_idp_initiated: false # allow unsolicited IdP responses
assertion_header: X-SAML-Assertion # header for stateless mode
session:
signing_key: "${SAML_SESSION_KEY}" # HMAC key for session JWT (>= 32 bytes)
cookie_name: runway_saml # default "runway_saml"
max_age: 8h # session lifetime
domain: .example.com # cookie domain
secure: true # Secure flag (default true)
same_site: lax # lax, strict, none
attribute_mapping:
client_id: uid # SAML attribute → Identity.ClientID
email: email
display_name: displayName
roles: groups # multi-valued → []stringSAML is not included in the default auth methods (it requires browser redirects). Add it explicitly:
routes:
- id: dashboard
path: /dashboard
path_prefix: true
auth:
required: true
methods: [saml]
backends:
- url: http://dashboard-service:8080You can combine SAML with other methods. The gateway tries each method in order and uses the first that succeeds:
auth:
required: true
methods: [jwt, saml] # API clients use JWT; browsers use SAML sessionSP-initiated: A GET to /saml/slo clears the session cookie and redirects the user to the IdP's SLO endpoint.
IdP-initiated: The IdP sends a LogoutRequest to /saml/slo. The gateway clears the session and responds with a LogoutResponse.
- Start the gateway with SAML enabled
- Access
GET /saml/metadatato download the SP metadata XML - Register the SP in your IdP using this metadata (or manually configure ACS URL and Entity ID)
- Configure the IdP to release required attribute statements (at minimum:
uidor whichever attribute maps toclient_id) - Set
idp_metadata_urlto the IdP's metadata endpoint, or download the IdP metadata XML and useidp_metadata_file
- Session cookies are always
HttpOnly(not configurable) to prevent XSS access - Relay state is HMAC-signed to prevent CSRF/tampering on the return URL
return_toparameter on/saml/loginis validated as a relative path only — absolute URLs are rejected- Assertion replay protection: Each assertion ID is tracked in a bounded TTL cache; reuse is rejected
- IdP metadata auto-refresh: When using
idp_metadata_url, the gateway periodically re-fetches metadata (default 24h) to handle IdP certificate rotation
- Clock skew errors: The gateway allows up to 180 seconds of clock skew (matching Shibboleth defaults). Ensure your server's clock is synchronized via NTP.
- Certificate mismatch: Verify that the IdP metadata contains the certificate the IdP is currently using to sign assertions. Use
metadata_refresh_intervalto auto-update. - Relay state invalid: If users land on
/after login instead of their original page, check that the sessionsigning_keyhasn't changed between the login redirect and the ACS callback. - "SAML assertion already consumed": This indicates a replay attempt or the user refreshed the ACS POST page. The assertion ID cache prevents reuse.
Admin endpoint: GET /saml/stats returns authentication counters (SSO attempts/successes/failures, token validations, session auths, logout requests).