Configuring a Keycloak Realm for GeoNode SSO

This procedure walks through building a Keycloak realm and OIDC client that lets GeoNode delegate its entire login to Keycloak, so users authenticate once and land in the portal without a local password.

This is a hands-on companion to Keycloak vs Dex for OIDC Federation in Geospatial Portals and sits inside the wider Core Portal Architecture & Security Boundaries practice; read the parent guide if you still need to decide which provider fits. Here the provider is settled — Keycloak — and the goal is a working authorization-code login where GeoNode is a relying party and Keycloak issues the tokens.

Prerequisites

Confirm each of the following before you touch the admin console. A skipped item here is the usual cause of a login that half-works.

  • Keycloak 24+ reachable over HTTPS at a stable hostname (examples use https://id.agency.gov). Behind a proxy, set KC_PROXY_HEADERS=xforwarded and KC_HOSTNAME so issued URLs match the public origin.
  • A running GeoNode 4.x you can restart, with shell or environment access to its Django settings.
  • Realm-admin rights in Keycloak (the built-in admin account or a delegated realm admin).
  • The portal’s public base URL, e.g. https://portal.agency.gov, and its OIDC callback path — /#/oidc/callback/ or /o/callback/ depending on your GeoNode auth integration.
  • Decide the integration path up front: GeoNode’s Django social login (via mozilla-django-oidc or a django-allauth OpenID Connect provider), or mod_auth_openidc at the Apache/NGINX proxy. This guide shows the Django path and notes the proxy alternative.
  • curl and a JWT decoder (jwt-cli, or jq plus base64 -d) for verification.

The sequence below is the authorization-code flow you are about to configure: the browser bounces from GeoNode to Keycloak and back with a code that GeoNode exchanges for tokens over a back channel.

Authorization-code flow from the browser through GeoNode to Keycloak and back Three vertical lifelines: the user's browser on the left, GeoNode as the OIDC relying party in the middle, and Keycloak as the OIDC provider on the right. The browser requests a protected portal page. GeoNode redirects the browser to the Keycloak authorize endpoint. Keycloak presents the login form and the user authenticates. Keycloak redirects the browser back to the GeoNode callback with an authorization code. GeoNode exchanges that code at the Keycloak token endpoint over a back channel, receiving an ID token and access token. GeoNode validates the token signature against the JWKS, provisions or matches the Django user, and establishes a session, returning the portal page to the browser. Browser GeoNoderelying party KeycloakOIDC provider GET protected portal page 302 to /authorize (client_id, scope) follow redirect to authorize endpoint present login form submit credentials + MFA 302 to /oidc/callback?code=… callback with authorization code back-channel: code → /token ID token + access token validate JWKS · set session portal page + session cookie

Step-by-step implementation

1. Create the realm

A dedicated realm isolates the portal’s identity from Keycloak’s master administrative realm, which should never host application users. Create geoportal from the console, or import it declaratively so the realm is reproducible.

{
  "realm": "geoportal",
  "enabled": true,
  "sslRequired": "external",
  "loginWithEmailAllowed": true,
  "duplicateEmailsAllowed": false,
  "registrationAllowed": false,
  "accessTokenLifespan": 300,
  "ssoSessionIdleTimeout": 1800,
  "ssoSessionMaxLifespan": 36000
}

Import it with kcadm.sh create realms -f geoportal-realm.json or via Realm settings → Partial import. Keeping the realm as a checked-in JSON file means staging and production start from the same definition rather than hand-clicked drift.

2. Create a confidential OIDC client

GeoNode runs server-side and can keep a secret, so register a confidential client using the standard authorization-code flow. Public clients (no secret) are for browser-only apps and are the wrong choice here.

{
  "clientId": "geonode-portal",
  "protocol": "openid-connect",
  "publicClient": false,
  "standardFlowEnabled": true,
  "directAccessGrantsEnabled": false,
  "serviceAccountsEnabled": false,
  "attributes": {
    "post.logout.redirect.uris": "https://portal.agency.gov/*",
    "pkce.code.challenge.method": "S256"
  }
}

After creating the client, open its Credentials tab and copy the generated secret — GeoNode needs it in the next steps. Enabling PKCE with S256 hardens the code exchange even for a confidential client.

3. Set valid redirect URIs and web origins

Keycloak refuses to return a code to any callback that is not on the client’s allow-list, so these values must match GeoNode’s callback exactly. An over-broad wildcard is a security risk; an incorrect path produces an Invalid redirect_uri error at login.

{
  "clientId": "geonode-portal",
  "rootUrl": "https://portal.agency.gov",
  "baseUrl": "https://portal.agency.gov/",
  "redirectUris": [
    "https://portal.agency.gov/oidc/callback/",
    "https://portal.agency.gov/o/callback/"
  ],
  "webOrigins": [
    "https://portal.agency.gov"
  ]
}

Set webOrigins to the portal’s exact origin (not *) so the CORS headers Keycloak emits on its endpoints permit the browser calls GeoNode’s frontend makes during login.

4. Add protocol mappers

Mappers decide which claims land in the token. For SSO you need the user’s email and preferred username reliably present; add a groups mapper too if you plan to drive roles from group membership, which the companion guide covers in depth.

{
  "protocolMappers": [
    {
      "name": "username",
      "protocolMapper": "oidc-usermodel-property-mapper",
      "protocol": "openid-connect",
      "config": {
        "user.attribute": "username",
        "claim.name": "preferred_username",
        "id.token.claim": "true",
        "access.token.claim": "true"
      }
    },
    {
      "name": "groups",
      "protocolMapper": "oidc-group-membership-mapper",
      "protocol": "openid-connect",
      "config": {
        "claim.name": "groups",
        "full.path": "false",
        "id.token.claim": "true",
        "access.token.claim": "true"
      }
    }
  ]
}

Setting full.path to false emits bare group names (gis-editors) rather than hierarchical paths (/agency/gis-editors), which keeps the downstream mapping simpler. How GeoNode consumes that claim is detailed in Mapping OIDC Group Claims to GeoNode Roles.

5. Wire GeoNode social login

With the client in place, point GeoNode at the realm. Using mozilla-django-oidc, the Django settings reference Keycloak’s discovery-derived endpoints and the confidential client’s credentials.

# geonode local_settings.py — OIDC relying-party configuration
OIDC_RP_CLIENT_ID = "geonode-portal"
OIDC_RP_CLIENT_SECRET = os.environ["OIDC_RP_CLIENT_SECRET"]
OIDC_RP_SIGN_ALGO = "RS256"

_ISSUER = "https://id.agency.gov/realms/geoportal"
OIDC_OP_AUTHORIZATION_ENDPOINT = f"{_ISSUER}/protocol/openid-connect/auth"
OIDC_OP_TOKEN_ENDPOINT = f"{_ISSUER}/protocol/openid-connect/token"
OIDC_OP_USER_ENDPOINT = f"{_ISSUER}/protocol/openid-connect/userinfo"
OIDC_OP_JWKS_ENDPOINT = f"{_ISSUER}/protocol/openid-connect/certs"

OIDC_RP_SCOPES = "openid email profile groups"
LOGIN_REDIRECT_URL = "/"

AUTHENTICATION_BACKENDS = (
    "mozilla_django_oidc.auth.OIDCAuthenticationBackend",
    "django.contrib.auth.backends.ModelBackend",
)

Keeping the client secret in an environment variable rather than the settings file lets you rotate it without editing code. If you prefer to authenticate at the proxy instead of in Django, mod_auth_openidc consumes the same realm — set OIDCProviderMetadataURL to https://id.agency.gov/realms/geoportal/.well-known/openid-configuration, OIDCClientID geonode-portal, and OIDCRedirectURI to the callback — and passes the authenticated user to GeoNode through a request header.

6. Enable and expose SSO login

Restart GeoNode so the OIDC backend loads, then add the login route to the URL config and surface a “Sign in with agency SSO” link so users reach the flow.

# urls.py — expose the OIDC login/callback routes
from django.urls import path, include

urlpatterns = [
    path("oidc/", include("mozilla_django_oidc.urls")),
    # ... existing GeoNode routes ...
]

Restart with your process manager (systemctl restart geonode or a rolling pod restart) and the “Sign in” button now redirects to Keycloak. For the multi-tenant permission model these authenticated sessions plug into, align the realm’s groups with Implementing RBAC for Multi-Tenant GIS Portals.

Verification

Confirm the provider metadata, the token exchange, and an end-to-end login before declaring SSO live.

# 1. The realm's discovery document resolves and lists the expected endpoints
curl -fsS https://id.agency.gov/realms/geoportal/.well-known/openid-configuration | jq '.issuer, .token_endpoint, .jwks_uri'
#   "https://id.agency.gov/realms/geoportal"
#   "https://id.agency.gov/realms/geoportal/protocol/openid-connect/token"
#   "https://id.agency.gov/realms/geoportal/protocol/openid-connect/certs"

# 2. The confidential client can obtain a token (use a test user; direct grant enabled only for this check)
curl -fsS -X POST https://id.agency.gov/realms/geoportal/protocol/openid-connect/token \
  -d grant_type=password -d client_id=geonode-portal \
  -d client_secret="$OIDC_RP_CLIENT_SECRET" \
  -d username=testuser -d password="$TEST_PW" -d scope="openid email" | jq '.access_token != null'
#   true

# 3. The JWKS endpoint serves the signing keys the token was signed with
curl -fsS https://id.agency.gov/realms/geoportal/protocol/openid-connect/certs | jq '.keys[0].kid'

# 4. Browser check: visiting the portal redirects to Keycloak and returns authenticated
curl -fsS -I https://portal.agency.gov/oidc/authenticate/ | grep -i location
#   location: https://id.agency.gov/realms/geoportal/protocol/openid-connect/auth?...

A non-null access_token in step 2 and a location header pointing at the realm’s authorize endpoint in step 4 confirm the client, secret, and redirect wiring are correct end to end. Disable the temporary direct-access grant after this check.

Troubleshooting matrix

Symptom Likely cause Fix
Invalid redirect_uri on the Keycloak login page Callback URL not in the client’s redirectUris Add the exact GeoNode callback (trailing slash included) to the client
invalid_client at the token endpoint Wrong or rotated client secret in GeoNode Copy a fresh secret from the client’s Credentials tab into OIDC_RP_CLIENT_SECRET
Login succeeds at Keycloak but GeoNode shows anonymous OIDC backend not in AUTHENTICATION_BACKENDS, or URLs not included Add the backend and include("mozilla_django_oidc.urls"); restart GeoNode
Issuer URL in tokens uses an internal hostname KC_HOSTNAME/proxy headers not set behind the reverse proxy Set KC_HOSTNAME and KC_PROXY_HEADERS=xforwarded so issued URLs match the public origin
Signature verification failed in Django logs JWKS endpoint wrong or algorithm mismatch Point OIDC_OP_JWKS_ENDPOINT at /certs; set OIDC_RP_SIGN_ALGO=RS256
CORS error during the frontend callback Portal origin missing from client webOrigins Add the exact portal origin to webOrigins (never use *)
Users authenticate but have no groups Groups mapper not added to the client scope Add the group-membership mapper from step 4 and re-test the token

Up one level: Keycloak vs Dex for OIDC Federation in Geospatial Portals.