Files
labhelper/Keycloak-installation.md

8.0 KiB

Keycloak SSO Integration

This document describes how Keycloak SSO was integrated into labhelper, replacing the built-in Django username/password authentication.

Overview

Authentication is handled via OpenID Connect (OIDC) using the mozilla-django-oidc library. When a user visits any protected page, they are redirected to Keycloak to authenticate. On return, Keycloak group memberships are synced to Django groups, controlling what the user can do in the app.


Keycloak Setup

1. Create a client

In the Keycloak admin console, go to Clients → Create client.

Field Value Why
Client type OpenID Connect The protocol mozilla-django-oidc speaks
Client ID labhelper Must match OIDC_RP_CLIENT_ID in the app
Client authentication On (confidential) Server-side apps use a client secret; this is more secure than a public client
Authentication flow Standard flow only labhelper uses the standard authorisation code flow

After saving, go to the Credentials tab and copy the Client secret — this is OIDC_RP_CLIENT_SECRET.

2. Set the redirect URI

In the client Settings tab:

Field Value Why
Valid redirect URIs https://your-app/oidc/callback/ Keycloak will only redirect back to whitelisted URIs after authentication. The trailing slash is required — without it Keycloak rejects the request. For local dev also add http://127.0.0.1:8000/oidc/callback/

3. Set the PKCE code challenge method

In the client Advanced tab, find "Proof Key for Code Exchange Code Challenge Method" and set it to S256.

Why: Keycloak 26 configures new clients with PKCE enforced. mozilla-django-oidc sends S256 as the challenge method (the more secure option). If Keycloak is set to plain, the two sides don't agree and authentication fails with code challenge method is not matching the configured one.

4. Add a Groups mapper

This makes Keycloak include the user's group memberships in the token so the app can sync them to Django groups.

Go to Clients → labhelper → Client scopes tab → click the dedicated scope (named labhelper-dedicated) → Add mapper → By configuration → Group Membership.

Field Value Why
Name groups Label for this mapper
Token Claim Name groups The claim name the app reads from the token
Full group path Off Sends Lab Administrators instead of /Lab Administrators. The app strips leading slashes anyway, but this is cleaner
Add to ID token On
Add to access token On
Add to userinfo On The app fetches userinfo after the token exchange

5. Create groups

Go to Groups (left sidebar) and create these three groups with exactly these names — they map to the existing Django groups:

  • Lab Administrators — gets is_staff=True in Django (admin access)
  • Lab Staff
  • Lab Viewers

6. Ensure users have an email address

mozilla-django-oidc requires the email claim to be present in the token. Every Keycloak user who will log into labhelper must have:

  • An Email address set (Users → select user → Details tab)
  • Email verified ticked

Without an email, authentication fails silently with Claims verification failed in the Django logs.


App Configuration

The app is configured entirely via environment variables.

Required variables

OIDC_OP_BASE_URL=https://keycloak.example.com/realms/your-realm
OIDC_RP_CLIENT_ID=labhelper
OIDC_RP_CLIENT_SECRET=<client-secret-from-keycloak-credentials-tab>

OIDC_OP_BASE_URL is the realm URL. All OIDC endpoints (authorisation, token, userinfo, JWKS, logout) are derived from it automatically in settings.py.

Other relevant variables

ALLOWED_HOSTS=your-app-hostname
CSRF_TRUSTED_ORIGINS=https://your-app

CSRF_TRUSTED_ORIGINS must include the app's origin. The OIDC callback goes through Django's CSRF middleware, and requests from untrusted origins are rejected.


How the App Side Works

Library

mozilla-django-oidc handles the full OIDC flow: redirecting to Keycloak, validating the returned token (RS256 signature verified against Keycloak's JWKS endpoint), exchanging the authorisation code, and fetching userinfo.

Key settings

Setting Value Why
OIDC_RP_SIGN_ALGO RS256 Keycloak signs tokens with RS256 by default
OIDC_RP_SCOPES openid email profile profile is needed to get preferred_username, given_name, family_name from Keycloak
OIDC_USE_PKCE True Required because Keycloak enforces PKCE on this client
OIDC_STORE_ID_TOKEN True The ID token is stored in the session and passed as id_token_hint when logging out, so Keycloak also ends its own session
OIDC_EXEMPT_URLS ['search_api'] The search endpoint is called via AJAX. The SessionRefresh middleware would return a redirect instead of JSON for unauthenticated AJAX calls, breaking the UI
LOGIN_URL oidc_authentication_init When @login_required intercepts an unauthenticated request, it redirects directly to the OIDC flow rather than a local login form

Authentication backend (labhelper/auth_backend.py)

Overrides OIDCAuthenticationBackend to:

  • Use preferred_username from Keycloak as the Django username
  • Set first_name and last_name from given_name / family_name claims
  • Sync group memberships on every login — if a user is added to or removed from a Keycloak group, it takes effect at their next login
  • Set is_staff=True for members of Lab Administrators (grants Django admin access)

django.contrib.auth.backends.ModelBackend is kept as a fallback so the Django admin login form still works with a local username/password (useful for emergency superuser access without Keycloak).

Session refresh middleware

mozilla_django_oidc.middleware.SessionRefresh is added after AuthenticationMiddleware. It periodically checks whether the user's OIDC session is still valid and forces re-authentication if the token has expired, rather than keeping a stale Django session alive indefinitely.

URLs

All OIDC routes are mounted under /oidc/:

URL Purpose
/oidc/authenticate/ Initiates the OIDC flow, redirects to Keycloak
/oidc/callback/ Keycloak redirects here after authentication
/oidc/logout/ Logs out of Django and ends the Keycloak session

/login/ is kept as a static landing page with a "Login with SSO" button, used when users navigate to it manually or are redirected there after logout.


Auth Flow

User visits protected page
        ↓
@login_required → redirect to /oidc/authenticate/?next=/original/url/
        ↓
Redirect to Keycloak (with code_challenge for PKCE)
        ↓
User authenticates in Keycloak
        ↓
Keycloak redirects to /oidc/callback/?code=...
        ↓
App exchanges code for tokens, verifies RS256 signature
        ↓
App fetches userinfo, syncs groups and attributes
        ↓
User redirected to original URL

Troubleshooting

Error Cause Fix
Invalid parameter: redirect_uri Redirect URI not in Keycloak whitelist, or trailing slash missing Add exact URI including trailing slash to Keycloak client Valid Redirect URIs
Missing parameter: code_challenge_method Keycloak requires PKCE but app wasn't sending it Set OIDC_USE_PKCE = True in settings
code challenge method is not matching the configured one Keycloak client set to plain, app sends S256 Set PKCE method to S256 in Keycloak client Advanced settings
Claims verification failed User has no email set in Keycloak Set email address and tick Email Verified on the Keycloak user
NoReverseMatch for OIDC_EXEMPT_URLS Regex pattern used instead of URL name Use the Django URL name ('search_api'), not a regex
Login loops without showing Keycloak Existing Keycloak session auto-authenticates Expected behaviour — Keycloak reuses its session. Log out of Keycloak admin console to test a clean login