Skip to content

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

Page as Markdown

    

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

  1. Complete the Keycloak setup page. This flow needs the realm, the confidential client, the test user, the Backend, and the BackendConfigPolicy that it creates.

  2. Make sure your gateway has an HTTPS listener. Kgateway sets the OAuth2 nonce and code verifier cookies with the Secure attribute, 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.

  1. Create a Kubernetes Secret with the Keycloak client secret. Kgateway reads the value from the client-secret key specifically, so the key name matters. Replace YOUR_CLIENT_SECRET with 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
  2. 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 iss claim 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 across issuerURI and the endpoint fields. For local testing against a cluster-only Keycloak, port-forward Keycloak and use that address, such as https://localhost:9443/realms/myrealm. The gateway still reaches Keycloak through backendRef, 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
    EOF
    Field Description
    backendRefPoints to the Backend from Keycloak setup. Kgateway uses it to reach Keycloak for token exchange and OIDC discovery.
    issuerURITriggers OIDC discovery. Kgateway fetches /.well-known/openid-configuration from 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_uri parameter, and the path that the gateway intercepts to complete the code exchange. If you omit this field, it defaults to <request-scheme>://<host>/oauth2/redirect derived from the original request, which is easy to mismatch with the value registered in Keycloak. Set it explicitly.
    scopesDefaults to user if not set. For OIDC you need openid in the list. Add email and profile if 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 if openid is 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-secret key inside that Secret.
  3. 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 CSRFPolicy on 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
    EOF

    Important

    targetRefs has no namespace field, 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 the httpbin namespace, so this policy is created there too. extensionRef does take a namespace, so the GatewayExtension can stay in kgateway-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.

    targetRefs can 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.

  4. Verify that the policy attached to the route.

    kubectl get TrafficPolicy keycloak-oauth2 -n httpbin -o yaml

    In the status.ancestors section of the output, confirm that the Accepted and Attached conditions are both True. 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: Attached

    Note

    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.

  1. 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_uri parameter 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
  2. 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.

  3. Log in with the test user credentials, testuser and password.

  4. 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 401 response with CSRF token validation failed in the gateway logs, you sent the request over HTTP. Retry over HTTPS.

    If Keycloak shows Invalid parameter: redirect_uri, the redirectURI on the GatewayExtension does not match a redirect URI that is registered on the Keycloak client.

  5. Optional: If you added the denyRedirect setting to your GatewayExtension, send the same request with Accept: application/json. Because denyRedirect matches on this header, the gateway returns 401 directly 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-system

To 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
domainSets 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.
sameSiteStrict 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.
namesOverrides 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: true

Copy 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-email

Use 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/json

For 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: XMLHttpRequest

The 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
EOF

Important

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.

Was this page helpful?