OAuth2 vs OIDC: What Actually Matters#

OAuth2 is an authorization framework. It answers the question “what is this client allowed to do?” by issuing access tokens. It does not tell you who the user is. OIDC (OpenID Connect) is a layer on top of OAuth2 that adds authentication. It answers “who is this user?” by adding an ID token – a signed JWT containing user identity claims like email, name, and group memberships.

Most infrastructure tooling uses OIDC, not plain OAuth2. When you configure Kubernetes API server authentication, ArgoCD SSO, or Grafana login, you are using OIDC. The access token authorizes API calls. The ID token identifies the user so the system can make RBAC decisions.

OAuth2:  Client -> Authorization Server -> Access Token (opaque, for API access)
OIDC:    Client -> Authorization Server -> Access Token + ID Token (JWT with user claims)

The ID token is a JWT you can decode and inspect. It contains claims like sub (subject identifier), email, groups, and preferred_username. These claims drive authorization decisions downstream.

OAuth2 Flows: Choosing the Right One#

Authorization Code flow is the standard for web applications. The user is redirected to the identity provider, authenticates, and is redirected back with an authorization code. The backend exchanges that code for tokens. This keeps tokens out of the browser URL.

Authorization Code with PKCE extends the Authorization Code flow for public clients (SPAs, mobile apps, CLI tools). PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks. Use this for any client that cannot securely store a client secret.

Client Credentials flow is for service-to-service communication where no user is involved. The service authenticates directly with its client ID and client secret to get an access token. Use this for background jobs, CI pipelines, and automated tooling.

Device Code flow is for CLI tools and devices without a browser. The tool displays a URL and code, the user opens a browser on another device, enters the code, and authenticates. The CLI polls the authorization server until authentication completes.

Never use the Implicit flow. It was designed for SPAs before PKCE existed and returns tokens directly in the URL fragment. It is deprecated in OAuth 2.1 and insecure because tokens are exposed in browser history and server logs. Use Authorization Code with PKCE instead.

OIDC for the Kubernetes API Server#

Kubernetes does not have a built-in identity store. It delegates authentication to external providers via OIDC. Configure the API server with these flags:

kube-apiserver \
  --oidc-issuer-url=https://keycloak.example.com/realms/infrastructure \
  --oidc-client-id=kubernetes \
  --oidc-username-claim=email \
  --oidc-groups-claim=groups \
  --oidc-ca-file=/etc/kubernetes/pki/oidc-ca.pem

The --oidc-username-claim maps an ID token claim to the Kubernetes username. The --oidc-groups-claim maps a claim to Kubernetes groups. You then create RBAC ClusterRoleBindings that reference these groups:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: platform-admins
subjects:
  - kind: Group
    name: platform-team
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: cluster-admin
  apiGroup: rbac.authorization.k8s.io

Users authenticate through the IdP, receive an ID token with groups: ["platform-team"], and Kubernetes grants them the cluster-admin role.

Keycloak is an open-source identity and access management solution. It provides user management, SSO, user federation with LDAP and Active Directory, social login, and fine-grained authorization. Deploy it when you need a full identity provider that manages users directly or federates with existing directories.

Deploy on Kubernetes using the Bitnami Helm chart:

helm install keycloak oci://registry-1.docker.io/bitnamicharts/keycloak \
  --namespace identity --create-namespace \
  --set auth.adminUser=admin \
  --set auth.adminPassword=changeme \
  --set postgresql.enabled=true \
  --set ingress.enabled=true \
  --set ingress.hostname=keycloak.example.com

Key Keycloak concepts: Realms are isolated tenants – create one realm per environment or team. Clients represent applications that authenticate against Keycloak – each application (Kubernetes, ArgoCD, Grafana) gets its own client with specific redirect URIs. User federation connects Keycloak to LDAP or Active Directory so users authenticate with their existing corporate credentials.

Choose Keycloak when: you need a full identity provider, you need to manage users directly, you need LDAP/AD federation, you want a single SSO portal for all infrastructure tools.

Dex: Lightweight OIDC Federation#

Dex is a lightweight OIDC provider that acts as a gateway to upstream identity providers. It does not manage users itself. Instead, it uses connectors to authenticate against GitHub, GitLab, LDAP, SAML, and other identity sources, and presents a unified OIDC interface to downstream applications.

# Dex configuration
issuer: https://dex.example.com
connectors:
  - type: github
    id: github
    name: GitHub
    config:
      clientID: $GITHUB_CLIENT_ID
      clientSecret: $GITHUB_CLIENT_SECRET
      orgs:
        - name: my-org
          teams:
            - platform-team
            - developers
  - type: ldap
    id: ldap
    name: Corporate LDAP
    config:
      host: ldap.internal:636
      rootCA: /etc/dex/ldap-ca.pem
      bindDN: cn=serviceaccount,dc=example,dc=com
      bindPW: $LDAP_BIND_PASSWORD
      userSearch:
        baseDN: ou=Users,dc=example,dc=com
        username: uid
        emailAttr: mail
      groupSearch:
        baseDN: ou=Groups,dc=example,dc=com
        userMatchers:
          - userAttr: DN
            groupAttr: member
        nameAttr: cn
staticClients:
  - id: kubernetes
    name: Kubernetes
    redirectURIs:
      - http://localhost:8000
    secret: kubernetes-client-secret

Choose Dex when: you already have identity sources (GitHub, GitLab, LDAP) and need a unified OIDC interface for Kubernetes, ArgoCD, or other tools. Dex is the default identity layer for ArgoCD.

OAuth2 Proxy: SSO for Any Web Application#

OAuth2 Proxy is a reverse proxy that authenticates users via OIDC before forwarding requests to the backend. Put it in front of any web application to add SSO without modifying the application itself.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: oauth2-proxy
spec:
  template:
    spec:
      containers:
        - name: oauth2-proxy
          image: quay.io/oauth2-proxy/oauth2-proxy:v7.6.0
          args:
            - --provider=oidc
            - --oidc-issuer-url=https://keycloak.example.com/realms/infrastructure
            - --client-id=dashboard
            - --client-secret=$(CLIENT_SECRET)
            - --cookie-secret=$(COOKIE_SECRET)
            - --email-domain=*
            - --upstream=http://kubernetes-dashboard.kubernetes-dashboard.svc:443
            - --pass-authorization-header=true
            - --http-address=0.0.0.0:4180

With Kubernetes ingress, use auth annotations to put OAuth2 Proxy in front of any service:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: protected-app
  annotations:
    nginx.ingress.kubernetes.io/auth-url: "https://oauth2-proxy.example.com/oauth2/auth"
    nginx.ingress.kubernetes.io/auth-signin: "https://oauth2-proxy.example.com/oauth2/start?rd=$scheme://$host$escaped_request_uri"

Practical Integration Patterns#

Kubernetes Dashboard behind OAuth2 Proxy and Keycloak: Deploy Keycloak as the IdP. Create a client in Keycloak for the dashboard. Deploy OAuth2 Proxy configured with that client. Point the dashboard ingress auth annotations at OAuth2 Proxy. Users authenticate via Keycloak, OAuth2 Proxy validates the token, and forwards the authenticated request to the dashboard.

ArgoCD with Dex and GitHub Teams: ArgoCD ships with Dex built in. Configure the Dex connector to use GitHub OAuth with org and team filters. Map GitHub teams to ArgoCD RBAC roles in the argocd-rbac-cm ConfigMap. Members of my-org/platform-team get admin access; members of my-org/developers get read-only.

Grafana with OIDC: Grafana has built-in OIDC support. Configure it in grafana.ini – no proxy needed:

[auth.generic_oauth]
enabled = true
name = Keycloak
client_id = grafana
client_secret = grafana-secret
scopes = openid email profile
auth_url = https://keycloak.example.com/realms/infrastructure/protocol/openid-connect/auth
token_url = https://keycloak.example.com/realms/infrastructure/protocol/openid-connect/token
api_url = https://keycloak.example.com/realms/infrastructure/protocol/openid-connect/userinfo
role_attribute_path = contains(groups[*], 'grafana-admins') && 'Admin' || 'Viewer'

kubectl with OIDC via kubelogin: The kubelogin plugin enables browser-based authentication for kubectl. Install it as a kubectl plugin, then configure the kubeconfig:

users:
  - name: oidc-user
    user:
      exec:
        apiVersion: client.authentication.k8s.io/v1beta1
        command: kubectl
        args:
          - oidc-login
          - get-token
          - --oidc-issuer-url=https://keycloak.example.com/realms/infrastructure
          - --oidc-client-id=kubernetes

Running kubectl get pods opens a browser for authentication. After login, kubelogin caches the token and refreshes it automatically on expiry.

Token Management#

Access token lifetime should be short: 5 to 15 minutes. This limits the window of exposure if a token is leaked. The client uses the refresh token to get a new access token without requiring the user to log in again.

Refresh token lifetime is longer: hours to days depending on the security requirements. Refresh tokens should be rotated on use (the IdP issues a new refresh token each time one is used).

Token storage: never store tokens in localStorage – any JavaScript on the page can read them (XSS attack vector). Use httpOnly cookies with the Secure and SameSite flags. OAuth2 Proxy handles this correctly by default.

Common Gotchas#

Callback URL mismatch. The redirect URI configured in the IdP client must exactly match the URL the application sends during the OAuth2 flow. A trailing slash, different port, or http vs https mismatch will cause a cryptic error. Always verify both sides match.

Group claim not in token. By default, many IdPs do not include group memberships in the ID token. In Keycloak, you must add a “groups” mapper to the client scope. In Dex, groups come from the connector configuration. If Kubernetes is not recognizing user groups, decode the JWT and verify the groups claim is present.

Token expiry during long kubectl sessions. Without kubelogin, a manually configured OIDC token in kubeconfig expires and kubectl fails silently or with obscure errors. Always use kubelogin for automatic token refresh. Configure it once in kubeconfig and never think about token management again.