This repository contains a Keycloak build with:
- a custom user federation provider (
Dienst2) - a custom login theme (
chtheme)
- A user logs in with Google or SURFconext.
- An IdP mapper turns external claims into a Keycloak username used for lookup.
- Keycloak calls the
Dienst2federation provider with that username. - The provider resolves the person in Dienst2 and returns a federated Keycloak user (
WISVCH.<id>), including membership-related attributes. - Google groups are fetched by Dienst2 from Google Workspace and returned to Keycloak as groups.
- On first broker login, Keycloak auto-links the IdP account to the existing federated user (without confirmation).
- Keycloak issues OIDC tokens to your applications.
This means users are not managed as local Keycloak users. Dienst2 is the source of truth.
Run from the keycloak-build repository root:
mvn package
KEYCLOAK_VERSION="$(mvn -q -DforceStdout help:evaluate -Dexpression=keycloak.version)"
docker build --build-arg KEYCLOAK_VERSION=${KEYCLOAK_VERSION} -t keycloak-wisvch .pom.xml (<keycloak.version>) is the source of truth for the Keycloak runtime version.
CI reads that value and passes it to Docker as --build-arg KEYCLOAK_VERSION=..., so provider dependencies and image base version stay in sync.
The image copies:
target/keycloak-wisvch-custom-providers.jarto/opt/keycloak/providers/themes/chthemeto/opt/keycloak/themes
Use your own URLs, client IDs, secrets, and API tokens.
Only settings that are required for this architecture are listed below.
Set these realm settings:
frontendUrl=https://<your-login-host>loginTheme=chtheme(if you want to use the bundled login theme from this repo)registrationAllowed=falserememberMe=falseresetPasswordAllowed=falseverifyEmail=falseloginWithEmailAllowed=falseduplicateEmailsAllowed=true
Disable all Required Actions.
Account linking in this setup is username-driven and automatic, so self-service and required-action interruptions should be off.
Add these custom user profile attributes:
google_usernamenetidmembership_statusformatted_name
Set them to non-editable.
Set visibility to match the current model:
google_username,netid,formatted_name: view byadminandusermembership_status: view byadminonly
Set membership_status as required for role user.
These attributes are populated from Dienst2 and used for authorization/state checks; users should not be able to edit them in Keycloak.
Create a top-level flow with alias link exisiting without confimation and add:
idp-detect-existing-broker-userasREQUIREDidp-auto-linkasREQUIRED
This makes first IdP login link directly to an existing user without a confirmation screen.
This flow must be top-level because IdPs reference it via firstBrokerLoginFlowAlias.
Add a User Federation provider:
- Provider ID:
Dienst2 baseUrl=https://<dienst2-host>apiEndpoint=/<dienst2-api-path>apiKey=<dienst2-api-token>enabled=true
Recommended cache policy:
EVICT_DAILYat00:00
Behavior that drives the rest of the setup:
surfconext.<netid>is resolved via Dienst2netidgoogle.<localpart>is resolved via Dienst2google_username- resolved users are exposed as
WISVCH.<id> - Google groups are fetched through Dienst2 (which syncs from Google Workspace) and mapped to Keycloak groups
For each IdP, set:
syncMode=FORCEfirstBrokerLoginFlowAlias=link exisiting without confimationtrustEmail=false
Google IdP:
- Use Keycloak
googleprovider. - Optional restriction:
hostedDomain=<your-domain> - Keep
disableUserInfo=false. - Add mapper
oidc-username-idp-mapper:- template
${ALIAS}.${CLAIM.email | localpart} - target
LOCAL - sync mode
IMPORT
- template
SURFconext (or another OIDC IdP):
- Use
oidcprovider with your own OIDC endpoints and issuer. - Keep signature validation enabled (
validateSignature=true,useJwksUrl=true). - Use
clientAuthMethod=client_secret_postunless your provider requires a different method. - In the SURF SP Dashboard, set Subject type to Persistent (not Transient). Returning-user linking depends on stable user identifiers; with transient IDs, returning users cannot be linked reliably.
- Add mapper
oidc-username-idp-mapper:- template
${ALIAS}.${CLAIM.uids} - target
LOCAL - sync mode
FORCE
- template
Important coupling with federation code:
- aliases and username templates must produce usernames that match the federation lookup patterns (
google.*,surfconext.*), unless you also change the provider code.
