For the complete documentation index, see llms.txt. Markdown versions of all docs pages are available by appending .md to any docs URL.
Authorization code flow
Redirect browser users to Keycloak to log in, then store their tokens in session cookies.
Protect a route with the OAuth2 authorization code flow. Unauthenticated browser requests are redirected to Keycloak to log in, the gateway exchanges the returned authorization code for tokens, and it stores those tokens in session cookies. Your upstream service does not need to know that any of this happened.
Before you begin
-
Complete the Keycloak setup page. This flow needs the realm, the confidential client, the test user, the
Backend, and theBackendConfigPolicythat it creates. -
Make sure your gateway has an HTTPS listener. Kgateway sets the OAuth2 nonce and code verifier cookies with the
Secureattribute, so browsers do not return them over plain HTTP and the callback fails CSRF validation. To add one, see HTTPS listener.
Configure the authorization code flow
Create the Kubernetes Secret that holds the Keycloak client secret, a GatewayExtension that configures the provider, and a TrafficPolicy that enforces the flow on a route.
-
Create a Kubernetes Secret with the Keycloak client secret. Kgateway reads the value from the
client-secretkey specifically, so the key name matters. ReplaceYOUR_CLIENT_SECRETwith the value that you copied from the Credentials tab of your Keycloak client during Keycloak setup.kubectl create secret generic keycloak-client-secret \ --from-literal=client-secret=YOUR_CLIENT_SECRET \ -n kgateway-system -
Create a GatewayExtension that holds everything the gateway needs to talk to Keycloak. The GatewayExtension is independent of routing, so you can reuse the same extension across multiple TrafficPolicy resources.
Note
Keycloak derives the
issclaim from the URL that the request arrives on, so the same realm reports a different issuer depending on how it is reached. Use one browser-facing Keycloak URL consistently acrossissuerURIand the endpoint fields. For local testing against a cluster-only Keycloak, port-forward Keycloak and use that address, such ashttps://localhost:9443/realms/myrealm. The gateway still reaches Keycloak throughbackendRef, so the two do not have to be the same address.kubectl apply -f- <<EOF apiVersion: gateway.kgateway.dev/v1alpha1 kind: GatewayExtension metadata: name: keycloak-oauth2 namespace: kgateway-system spec: oauth2: backendRef: group: gateway.kgateway.dev kind: Backend name: keycloak namespace: kgateway-system issuerURI: https://keycloak.example.com/realms/myrealm authorizationEndpoint: https://keycloak.example.com/realms/myrealm/protocol/openid-connect/auth tokenEndpoint: https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token endSessionEndpoint: https://keycloak.example.com/realms/myrealm/protocol/openid-connect/logout redirectURI: https://www.example.com/oauth2/redirect scopes: - openid - email - profile credentials: clientID: kgateway-client clientSecretRef: name: keycloak-client-secret EOFField Description backendRefPoints to the Backendfrom Keycloak setup. Kgateway uses it to reach Keycloak for token exchange and OIDC discovery.issuerURITriggers OIDC discovery. Kgateway fetches /.well-known/openid-configurationfrom this URL and fills in the authorization, token, and end-session endpoints. If you also set those explicitly (as in the example), the explicit values win. Setting both is fine if you want the config to be readable without relying on discovery.redirectURIThe callback URL that kgateway sends to Keycloak as the redirect_uriparameter, and the path that the gateway intercepts to complete the code exchange. If you omit this field, it defaults to<request-scheme>://<host>/oauth2/redirectderived from the original request, which is easy to mismatch with the value registered in Keycloak. Set it explicitly.scopesDefaults to userif not set. For OIDC you needopenidin the list. Addemailandprofileif your app needs those claims.endSessionEndpointHandles single logout. When a user hits /logout, kgateway clears their session cookies and sends their browser to this URL so Keycloak ends the session too. This is RP-initiated logout in the OIDC spec. Only set it ifopenidis in your scopes. RP-initiated logout is enabled by default in Keycloak version 18.0 and later.clientSecretRef.nameMust match the Secret name from the previous step. Kgateway reads the client-secretkey inside that Secret. -
Create a TrafficPolicy that references the extension by name. This policy tells the gateway to enforce the login flow on a specific route.
Warning
The OAuth2 filter does not protect against CSRF attacks on routes with cached authentication cookies. Pair it with a
CSRFPolicyon the same route, especially for browser-facing apps.kubectl apply -f- <<EOF apiVersion: gateway.kgateway.dev/v1alpha1 kind: TrafficPolicy metadata: name: keycloak-oauth2 namespace: httpbin spec: targetRefs: - group: gateway.networking.k8s.io kind: HTTPRoute name: httpbin oauth2: extensionRef: name: keycloak-oauth2 namespace: kgateway-system EOFImportant
targetRefshas nonamespacefield, so the TrafficPolicy can target only resources in its own namespace. Create the policy in the same namespace as the resource that you want to protect. The HTTPRoute from the Sample app guide is in thehttpbinnamespace, so this policy is created there too.extensionRefdoes take anamespace, so the GatewayExtension can stay inkgateway-system.If the namespaces do not match, the policy is still accepted but never attaches, and requests reach your app unauthenticated. Verify that the policy attached before you rely on it.
targetRefscan also point to a Gateway, which applies the policy to every route that the Gateway serves. In that case, create the policy in the Gateway’s namespace. -
Verify that the policy attached to the route.
kubectl get TrafficPolicy keycloak-oauth2 -n httpbin -o yamlIn the
status.ancestorssection of the output, confirm that theAcceptedandAttachedconditions are bothTrue. An empty status means that the policy did not attach to anything.- message: Policy accepted reason: Valid status: "True" type: Accepted - message: Attached to all targets reason: Attached status: "True" type: AttachedNote
Using path matching in the HTTPRoute? The HTTPRoute must also match the OAuth2 callback path that you set in
redirectURI. Otherwise, the redirect back from Keycloak returns a 404 error. The HTTPRoute from the Sample app guide has no path matches, so it already serves every path and needs no change.For example, if your route matches only
/status, add the callback path as a second match:rules: - matches: - path: type: PathPrefix value: /status - path: type: PathPrefix value: /oauth2/redirect
Verify
Use the verification steps below to confirm that the Authorization Code flow works. Send these requests to the HTTPS listener, because the session cookies that this flow relies on are set with the Secure attribute.
-
Send a request without a session cookie. The gateway redirects to Keycloak.
curl -vik "https://${INGRESS_GW_ADDRESS}:8443/headers" -H "host: www.example.com"Example output. Note that the
redirect_uriparameter matches the value that you registered on the Keycloak client.< HTTP/2 302 < location: https://keycloak.example.com/realms/myrealm/protocol/openid-connect/auth?client_id=kgateway-client&...&redirect_uri=https%3A%2F%2Fwww.example.com%2Foauth2%2Fredirect < set-cookie: OauthNonce-...;path=/;Max-Age=600;secure;HttpOnly -
Open a browser and go to your protected route, such as
https://www.example.com/headers. The gateway redirects you to the Keycloak login page. -
Log in with the test user credentials,
testuserandpassword. -
Verify that Keycloak returns you to the route and that the response shows the httpbin output. The gateway exchanged the authorization code for tokens and stored them in session cookies.
If you get a
401response withCSRF token validation failedin the gateway logs, you sent the request over HTTP. Retry over HTTPS.If Keycloak shows
Invalid parameter: redirect_uri, theredirectURIon theGatewayExtensiondoes not match a redirect URI that is registered on the Keycloak client. -
Optional: If you added the
denyRedirectsetting to your GatewayExtension, send the same request withAccept: application/json. BecausedenyRedirectmatches on this header, the gateway returns401directly instead of redirecting.curl -vik "https://${INGRESS_GW_ADDRESS}:8443/headers" \ -H "host: www.example.com" \ -H "Accept: application/json"Example output:
< HTTP/2 401
Cleanup
You can remove the resources that you created in this guide.kubectl delete TrafficPolicy keycloak-oauth2 -n httpbin
kubectl delete GatewayExtension keycloak-oauth2 -n kgateway-system
kubectl delete secret keycloak-client-secret -n kgateway-systemTo remove Keycloak and the shared resources, see the Cleanup section of the Keycloak setup page.
More authorization code examples
The authorization code flow works without the following settings. Add the ones your app needs, then re-apply the GatewayExtension.
Configure cookie settings
Kgateway stores the access and ID tokens in session cookies. The default SameSite policy is Lax. If you need custom cookie names (for example, to read them in downstream services or share across subdomains), set them explicitly under cookies on the GatewayExtension.
spec:
oauth2:
# ... rest of the provider config ...
cookies:
domain: example.com
sameSite: Strict
names:
accessToken: kgw-access
idToken: kgw-id| Field | Description |
|---|---|
domain | Sets the cookie domain, which makes the session cookies valid for that domain and all of its subdomains. Set it if your app spans subdomains. If you omit it, the cookies apply only to the host that set them. |
sameSite | Strict means the browser does not send cookies on any cross-site request, including top-level navigations. Use Lax, the default, if users arrive at your app through links from other origins, such as an email link. None requires HTTPS and should only be used when you explicitly need cross-site cookie sharing. |
names | Overrides the generated cookie names, which is useful if a downstream service reads them. |
Add this block to the GatewayExtension manifest from the previous step and re-apply it. Because the manifest replaces the resource, keep the other fields that you already set, including redirectURI.
Forward the access token to your app
By default the gateway keeps the tokens in cookies and does not pass them upstream. Set forwardAccessToken if your app needs the access token itself, for example to call another API on the user’s behalf. The token is forwarded in the Authorization header and in a cookie named BearerToken.
spec:
oauth2:
# ... rest of the provider config ...
forwardAccessToken: trueCopy token claims into request headers
Kgateway can verify the token signature and copy individual claims into headers that your app reads, which saves the app from parsing the token. Set jwksURI so the gateway can fetch the signing keys, then map each claim to a header.
spec:
oauth2:
# ... rest of the provider config ...
jwt:
jwksURI: https://keycloak.example.com/realms/myrealm/protocol/openid-connect/certs
idToken:
claimsToHeaders:
- name: sub
header: x-user-id
- name: email
header: x-user-emailUse accessToken in place of idToken to map claims from the access token instead. Both take the same claimsToHeaders list, where name is the JWT claim and header is the header to copy it to.
Stop redirecting API clients
This step is optional. By default, any unauthenticated request gets a 302 redirect to the Keycloak login page. That response works for a browser, but not for API clients. curl, mobile apps, and AJAX calls that hit an unauthenticated route silently follow the redirect, land on the Keycloak login HTML, and fail.
The denyRedirect field on OAuth2Provider lets you match specific requests and return 401 instead of redirecting them. It takes a list of HTTPHeaderMatch entries, and a request matches if it satisfies all of them.
Pattern for matching JSON API clients:
spec:
oauth2:
# ... rest of the provider config ...
denyRedirect:
headers:
- name: Accept
type: Exact
value: application/jsonFor requests that might send Accept: application/json; charset=utf-8 or similar variations, use RegularExpression:
denyRedirect:
headers:
- name: Accept
type: RegularExpression
value: "application/json.*"For AJAX requests from browser JavaScript:
denyRedirect:
headers:
- name: X-Requested-With
type: Exact
value: XMLHttpRequestThe full GatewayExtension with denyRedirect included:
kubectl apply -f- <<EOF
apiVersion: gateway.kgateway.dev/v1alpha1
kind: GatewayExtension
metadata:
name: keycloak-oauth2
namespace: kgateway-system
spec:
oauth2:
backendRef:
group: gateway.kgateway.dev
kind: Backend
name: keycloak
namespace: kgateway-system
issuerURI: https://keycloak.example.com/realms/myrealm
authorizationEndpoint: https://keycloak.example.com/realms/myrealm/protocol/openid-connect/auth
tokenEndpoint: https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token
endSessionEndpoint: https://keycloak.example.com/realms/myrealm/protocol/openid-connect/logout
redirectURI: https://www.example.com/oauth2/redirect
scopes:
- openid
- email
- profile
credentials:
clientID: kgateway-client
clientSecretRef:
name: keycloak-client-secret
denyRedirect:
headers:
- name: Accept
type: Exact
value: application/json
EOFImportant
This manifest replaces the GatewayExtension that you created earlier, so it must repeat every field that you want to keep. Omitting redirectURI here reverts it to the derived default, which no longer matches the redirect URI registered in Keycloak, and the login fails with Invalid parameter: redirect_uri.