Skip to content

Authentication Flow

Updated 4 min read

Detailed explanation of how OIDC authentication works for Nebari Software Packs.

When a NebariApp has auth.enabled: true, the nebari-operator sets up a complete OIDC authentication flow using Keycloak and Envoy Gateway. Users are required to log in before accessing the application.

ComponentRole
Envoy GatewayReverse proxy that enforces the SecurityPolicy (OIDC filter)
KeycloakOIDC identity provider that handles login and issues tokens
nebari-operatorCreates and manages all the glue resources (HTTPRoute, SecurityPolicy, Certificate, Keycloak client)
cert-managerProvisions TLS certificates for the application hostname
Nebari Cluster
User Envoy Gateway Keycloak Your App
| | | |
|--1. GET /---->| | |
| |--2. No session---->| |
|<--3. 302 -----| cookie? | |
| redirect | | |
| | | |
|--4. Login ----|---------------+--->| |
| page | | | |
| | | | |
|--5. Submit----|---------------+--->| |
| credentials | | | |
| | | | |
|<--6. 302 -----|<--auth code--------| |
| redirect | | |
| | | |
|--7. GET / --->| | |
| (with code) |--8. Exchange------>| |
| | code for tokens | |
| |<--9. ID + Access---| |
| | tokens | |
| | | |
|<--10. Set ----| | |
| cookies + | | |
| redirect | | |
| | | |
|--11. GET / -->| | |
| (with |--12. Forward-------|---------------->|
| cookies) | request | |
| | | |
|<--13. Response from your app------|<-----------------|
  1. User visits the app at https://my-pack.nebari.example.com

  2. Envoy Gateway checks for session cookies. The OIDC filter (configured by the SecurityPolicy) looks for valid IdToken-* and AccessToken-* cookies.

  3. No valid session - redirect to Keycloak. Envoy Gateway sends a 302 redirect to the Keycloak authorization endpoint with the client ID, redirect URI, and requested scopes.

  4. Keycloak presents the login page. The user sees the Keycloak login form (or SSO if already authenticated with Keycloak).

  5. User submits credentials. Keycloak validates the username/password (or delegates to an external IdP if configured).

  6. Keycloak redirects back with an authorization code. The redirect goes to the redirectURI configured in the NebariApp (default: /oauth2/callback), which is handled by Envoy Gateway’s OIDC filter.

  7. Browser follows the redirect back to Envoy Gateway with the authorization code.

  8. Envoy Gateway exchanges the code for tokens. A server-to-server call from Envoy Gateway to Keycloak’s token endpoint.

  9. Keycloak returns ID token, access token, and refresh token.

  10. Envoy Gateway sets session cookies. The tokens are stored in cookies:

    • IdToken-<suffix> (JWT containing user claims)
    • AccessToken-<suffix>
    • OauthHMAC-<suffix>, OauthExpires-<suffix>, RefreshToken-<suffix>

    The <suffix> is an 8-character hex string derived from the SecurityPolicy’s Kubernetes UID (e.g., IdToken-a1b2c3d4).

  11. Browser retries the original request with the session cookies attached.

  12. Envoy Gateway validates the cookies and forwards the request to your service via the HTTPRoute.

  13. Your app receives the request. The IdToken cookies are available for your app to read if it needs user identity information.

Envoy Gateway’s OIDC filter sets cookies with the following naming convention:

IdToken-<suffix>
AccessToken-<suffix>
OauthHMAC-<suffix>
OauthExpires-<suffix>
RefreshToken-<suffix>
OauthNonce-<suffix>

The <suffix> is an 8-character hexadecimal string generated by FNV-32a hashing the SecurityPolicy resource’s Kubernetes UID. This ensures unique cookie names when multiple SecurityPolicies exist on the same domain.

For example: IdToken-a1b2c3d4, AccessToken-a1b2c3d4.

Cookie names can be customized via the cookieNames field in the SecurityPolicy’s OIDC configuration.

Find the cookie starting with IdToken-:

for name, value in request.cookies.items():
if name.startswith("IdToken-"):
full_token = value
break

The IdToken is a standard JWT with three base64url-encoded sections separated by dots: header.payload.signature

Since Envoy Gateway already verified the signature, you can safely decode just the payload to extract claims:

import base64, json
parts = full_token.split(".")
payload = parts[1]
# Add base64 padding
payload += "=" * (4 - len(payload) % 4)
claims = json.loads(base64.urlsafe_b64decode(payload))
ClaimDescription
preferred_usernameKeycloak username
emailUser’s email address
nameDisplay name
given_nameFirst name
family_nameLast name
groupsKeycloak group memberships (if groups scope requested)
realm_access.rolesKeycloak realm roles
subUnique subject identifier
issToken issuer URL (Keycloak realm)
expToken expiration timestamp

When auth.enabled: true, the nebari-operator creates these resources:

1. Keycloak Client (when provisionClient: true)

Section titled “1. Keycloak Client (when provisionClient: true)”

The operator calls the Keycloak Admin API to create an OIDC client:

  • Client ID: <namespace>-<nebariapp-name>
  • Client protocol: openid-connect
  • Access type: confidential
  • Redirect URIs: https://<hostname><redirectURI>
  • Scopes: As configured in spec.auth.scopes

Client credentials are stored in a Secret:

apiVersion: v1
kind: Secret
metadata:
name: <nebariapp-name>-oidc-client
data:
client-id: <base64-encoded>
client-secret: <base64-encoded>

3. Envoy Gateway SecurityPolicy (when enforceAtGateway: true)

Section titled “3. Envoy Gateway SecurityPolicy (when enforceAtGateway: true)”
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
name: <nebariapp-name>-oidc
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: <nebariapp-name>
oidc:
provider:
issuer: https://<keycloak-host>/realms/<realm>
clientID: <from-secret>
clientSecret:
name: <nebariapp-name>-oidc-client
redirectURL: https://<hostname><redirectURI>
scopes: [openid, profile, email]
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: <nebariapp-name>
spec:
parentRefs:
- name: <gateway-name>
namespace: <gateway-namespace>
hostnames:
- <hostname>
rules:
- backendRefs:
- name: <service-name>
port: <service-port>

5. cert-manager Certificate (when routing.tls.enabled: true)

Section titled “5. cert-manager Certificate (when routing.tls.enabled: true)”

Created in the gateway’s namespace (envoy-gateway-system), not the app’s:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: <nebariapp-name>-<namespace>-cert
namespace: envoy-gateway-system
spec:
secretName: <nebariapp-name>-<namespace>-tls
dnsNames:
- <hostname>
issuerRef:
name: <cluster-issuer>
kind: ClusterIssuer

App-Native OAuth (enforceAtGateway: false)

Section titled “App-Native OAuth (enforceAtGateway: false)”

Some applications handle OAuth natively (e.g., Grafana, GitLab). For these apps, set enforceAtGateway: false:

auth:
enabled: true
provider: keycloak
provisionClient: true
enforceAtGateway: false

The operator will:

  • Provision a Keycloak client
  • Store credentials in a Secret
  • NOT create a SecurityPolicy

Your app reads the credentials from the Secret and handles the OAuth flow itself. This is useful when the app needs deeper integration with the OAuth flow (like Grafana mapping groups to roles).

  • Local development: The OIDC flow requires Keycloak and Envoy Gateway. When developing locally with kind, set nebariapp.enabled=false and test without auth. The FastAPI example shows “Not Authenticated” when no IdToken cookie is present.

  • Token expiration: Envoy Gateway handles token refresh automatically via refresh tokens stored in cookies. Your app does not need to handle token refresh.

  • Cookie size: Very large JWTs (many groups/roles) may exceed browser cookie size limits (typically 4KB). If this is an issue, reduce token size by limiting scopes/claims at the Keycloak level, or use disableIdToken/disableAccessToken in the SecurityPolicy OIDC config to skip setting cookies for tokens your app doesn’t need.