Keycloak
Create a permanent Admin account
Navigate to the Keycloak Admin Console:
https://serendipity-identity-service.localhost/admin
And sign in using the SERENDIPITY_IDENTITY_SERVICE_ADMIN (temp-admin) and SERENDIPITY_IDENTITY_SERVICE_ADMIN_PASSWORD (secret) credentials (supplied via the .env file):

You should see something like:

When you first start Keycloak you log in using the Keycloak bootstrap username and password. You should create a permanent Admin account in the master realm and delete the temporary one.
For example:

Don't forget to assign the admin role (role_admin) to the permanent admin account:

And to set a password:

Now sign out and then sign back in using your permanent Admin account credentials.
After signing back in with the permanent admin account, the Admin Console loads as before but now the top-right user menu shows the permanent admin's name. The temporary bootstrap user no longer appears — delete it from Users in the master realm.
Configuring realms
A realm in Keycloak is analogous to a tenant. Each realm allows an administrator to create (isolated) groups of applications and users. Initially, Keycloak includes a single realm, the master realm.
The master realm should only be used to manage Keycloak.
Creating a realm
To create a new realm, in the side menu click the dropdown menu and then click the Create realm button:

Enter a Realm name (e.g., serendipity-dev) and then click the Create button.
After creation the realm selector (top-left of the Admin Console) now includes serendipity-dev alongside master. Selecting it takes you to that realm's dashboard. The realm's Realm settings page is reachable from the side menu and the Login tab shows the default login options for the realm.
To update a realm's settings, select Realm settings in the side menu:

On the realm's settings Login tab you can control the options for users, applications, roles, and groups in the current realm:

In newer versions of Keycloak (v19+ using the modern React-based Admin Console), toggles on the Login tab auto-save immediately via API requests, which is why there is no main Save button.
The new Admin Console heavily caches UI state. A hard refresh (Cmd + Shift + R on Mac, Ctrl + F5 on Windows) often reveals that the setting did save on the backend even though the frontend UI showed it toggled back off.
Managing users
Create a user
Verify that you are in the correct realm e.g., the Development Realm (serendipity-dev).
To create a new user, select Users in the side menu and then click the Create new user button:

Enable the Email verified slide toggle and enter an email address, a first name (i.e., given name), a last name (i.e., family name) and then click the Create button.
A user needs a password to sign in.
To create a password, select the Credentials tab and then click the Set password button:

Enter a password, confirm the password and disable the Temporary slide toggle (so that the user does not need to update the password the first time they sign in) and then click the Save button.
Navigate to the Keycloak Account Console:
https://serendipity-identity-service.localhost:8443/realms/serendipity-dev/account
And sign in using the credentials you created:

You should see something like:

Managing user attributes
In Keycloak a user is associated with a set of attributes. These attributes are used to describe and identify users within Keycloak as well as to provide additional information about users to applications.
Default managed attributes
By default, Keycloak provides a basic user profile configuration:
| Attribute | Notes |
|---|---|
username | The primary login name — what the user types to sign in. Stable until changed. Not used for ownership/assignment comparisons (the enforcement layer uses sub). |
email | The preferred email address. |
firstName | Given name. |
lastName | Surname / family name. |
In Keycloak, both the username and email attributes can be used to identify, authenticate, and link user accounts.
The behavior of both username and email attributes changes accordingly to the Login settings of your realm.
By default, Serendipity has enabled the Email as username setting.
Core fields (not profile attributes)
These are tracked by Keycloak on the core user model but are not configurable profile attributes:
| Field | Notes |
|---|---|
id | The system-generated UUID. This is the value emitted as sub in tokens. |
enabled | Whether the account is active. |
emailVerified | Whether the email has been verified. |
createdTimestamp | When the user was created. |
Custom attributes
These are added to the user's profile as custom attributes.
| Attribute | Value | Notes |
|---|---|---|
jobTitle | A string | Optional profile attribute. |
department | A string | Optional profile attribute. |
employeeType | A string | Optional profile attribute. |
manager | The sub (UUID) of the user's manager | The one attribute the access-control model depends on — it enables the manager hierarchy. |
Manually setting the manager attribute
The manager attribute stores the manager's sub (Keycloak UUID). To set it:
- Identify the manager's Keycloak
sub.
Open the (manager) user in the Admin Console and copy theidfield. Theidis the UUID you need. - Open the direct report user in the Admin Console.
- Go to the Attributes tab.
- Set the attribute with Key =
managerand Value = the manager'ssub(the UUID copied in step 1). - Click the Save button.
The sub is not labeled "sub" in the Admin Console. It is the id field on the user's Details tab. That is the value (UUID) you copy into the manager attribute.
Managing the User Profile
The user profile configuration is managed on a per-realm basis.
The user profile configuration is stored using a well-defined JSON schema:
{
"attributes": [
{
"name": "username",
"displayName": "${username}",
"validations": {
"length": {
"min": 3,
"max": 255
},
"username-prohibited-characters": {},
"up-username-not-idn-homograph": {}
},
"permissions": {
"view": [
"admin",
"user"
],
"edit": [
"admin",
"user"
]
},
"multivalued": false
},
{
"name": "email",
"displayName": "${email}",
"validations": {
"email": {},
"length": {
"max": 255
}
},
"required": {
"roles": [
"user"
]
},
"permissions": {
"view": [
"admin",
"user"
],
"edit": [
"admin",
"user"
]
},
"multivalued": false
},
{
"name": "firstName",
"displayName": "${firstName}",
"validations": {
"length": {
"max": 255
},
"person-name-prohibited-characters": {}
},
"required": {
"roles": [
"user"
]
},
"permissions": {
"view": [
"admin",
"user"
],
"edit": [
"admin",
"user"
]
},
"multivalued": false
},
{
"name": "lastName",
"displayName": "${lastName}",
"validations": {
"length": {
"max": 255
},
"person-name-prohibited-characters": {}
},
"required": {
"roles": [
"user"
]
},
"permissions": {
"view": [
"admin",
"user"
],
"edit": [
"admin",
"user"
]
},
"multivalued": false
},
{
"name": "jobTitle",
"displayName": "jobTitle",
"validations": {},
"annotations": {},
"permissions": {
"view": [],
"edit": [
"admin"
]
},
"multivalued": false
},
{
"name": "department",
"displayName": "department",
"validations": {},
"annotations": {},
"permissions": {
"view": [],
"edit": [
"admin"
]
},
"multivalued": false
},
{
"name": "employeeType",
"displayName": "employeeType",
"validations": {},
"annotations": {},
"permissions": {
"view": [],
"edit": [
"admin"
]
},
"multivalued": false
},
{
"name": "manager",
"displayName": "manager",
"validations": {},
"annotations": {},
"permissions": {
"view": [],
"edit": [
"admin"
]
},
"multivalued": false
}
],
"groups": [
{
"name": "user-metadata",
"displayHeader": "User metadata",
"displayDescription": "Attributes, which refer to user metadata"
}
]
}
Using external storage
Adding a provider
In Keycloak's Admin Console, for the serendipity-dev realm:
- User Federation → Add LDAP providers:

- Configure the General options:

- UI display name:
OpenLDAP - Vendor:
Other
- Configure the Connection and authentication settings:

- Connection URL:
ldap://openldap:389 - Bind DN:
cn=admin,dc=shane-longman,dc=org - Bind credendials:
secret
- Configure the LDAP searching and updating settings:

- Edit mode:
READ_ONLYif LDAP is the source of truth and Keycloak should not write back.WRITABLEif you want Keycloak to write changes back to OpenLDAP. - Users DN:
ou=people,dc=shane-longman,dc=org - Username LDAP attribute:
mailuser sign in using their email address - RDN LDAP attribute:
uid - UUID LDAP attribute:
entryUUID - User object classes:
inetOrgPerson, organizationalPerson, person - User LDAP filter:
(objectClass=inetOrgPerson)
- Configure the Synchronisation settings:

And then click the Save button.
Standard Built-In Mappers
Keycloak automatically creates standard mappers for core attributes when you set up an LDAP provider. Ensure their settings match your entry:
- username: Maps Keycloak
username→ LDAP attributeuid. - first name: Maps Keycloak
firstName→ LDAP attributegivenName - last name: Maps Keycloak
lastName→ LDAP attributesn - email: Maps Keycloak
email→ LDAP attributemail
Custom User Attribute Mappers
| Mapper Name | Mapper Type | Keycloak Attribute | LDAP Attribute |
|---|---|---|---|
| job-title | user-attribute-ldap-mapper | jobTitle | title |
| department | user-attribute-ldap-mapper | department | departmentNumber |
| employee-type | user-attribute-ldap-mapper | employeeType | employeeType |
| locality-city | user-attribute-ldap-mapper | l | |
| state-region | user-attribute-ldap-mapper | st | |
| manager-dn | user-attribute-ldap-mapper | manager | manager |
Custom attributes mapped this way will be stored on the Keycloak User Profile under User Attributes and can be exposed to your applications via OIDC token mappers.
The manager attribute
The default user-attribute-ldap-mapper performs a direct string copy, so it will store the raw LDAP DN string
(e.g., uid=lee.wolf,ou=people,dc=shane-longman,dc=org) as the custom user attribute value.
Custom Group Attribute Mapper
You need to configure the group-ldap-mapper if you want Keycloak to recognise group structures and assign users to them automatically.
Keycloak does not read group entries or member attributes from LDAP by default — it relies on the mapper to parse your schema.
In Keycloak's Admin Console, for the serendipity-dev realm:
- User Federation → Open the OpenLDAP provider:

- On the Mappers tab, click the Add mapper button:

- Configure the General settings:

- Name:
groups-dn - Mapper type:
group-ldap-mapper - LDAP Groups DN:
ou=groups,dc=shane-longman,dc=org - Group Name LDAP Attribute:
cn - Group Object Classes:
groupOfNames - Preserve Group Inheritance:
Off - Ignore Missing Groups:
Off
- Configure the group Membership settings:

- Membership LDAP Attribute:
member - Membership Attribute Type:
DN - Membership User LDAP Attribute:
uid - Mode:
IMPORT - User Groups Retrieve Strategy:
LOAD_GROUPS_BY_MEMBER_ATTRIBUTE - Member-Of LDAP Attribute:
memberOf - Drop non-existing groups during sync:
On
Save the mapper settings, then select Sync LDAP Groups from the Action dropdown menu.
Assigning permissions using roles and groups
Roles and groups are used to manage access to applications. Roles define specific applications permissions and typically apply to one type of user. Groups are a collection of users to which you assign one or more roles.
The Shane Longman sample organisation utilises the following roles:
| Tier | Keycloak role name | Common titles (the role covers all of these) | Who this is in the sample |
|---|---|---|---|
| Entry-level practitioner | analyst | Analyst, Associate Analyst | Graduate assistants and junior staff who handle data collection, market research, financial modeling, slide deck preparation, and primary task execution. |
| Mid-level professional | consultant | Consultant, Senior Consultant | Experienced professionals responsible for managing specific workstreams, conducting client interviews, designing solutions, and drafting deliverables. |
| Day-to-day project leader | manager | Manager, Engagement Manager, Project Leader | Experienced leaders who oversee day-to-day project operations, manage delivery timelines, lead consultant teams, and maintain primary client relationships. |
| Senior practice leader | senior-manager | Senior Manager, Director, Associate Partner | Senior leaders tasked with driving multi-project delivery, leading sector or functional practice areas, and actively generating new business. |
| Co-owner / senior executive | partner | Partner, Principal, Managing Director | Co-owners or senior executives of the firm focused on revenue generation, strategic client account management, firm governance, and practice development. |
Creating a realm role
Verify that you are in the correct realm e.g., the Development Realm (serendipity-dev).
To create a new role:
- Select Realm Roles in the side menu and then click the Create role button.
- Enter a Role Name, enter a Description and then click Save.
Assigning realm roles to a group
Verify that you are in the correct realm e.g., the Development Realm (serendipity-dev).
- Click Groups in the left sidebar menu.
- Click on the name of the group you want to configure. Open the Role mapping tab.
- Click Assign role.
- Use the filter dropdown (set to Filter by realm roles) to view available realm roles.
- Select the checkboxes for the roles you want to grant, then click Assign.
Managing OpenID Connect and SAML Clients
Creating an OpenID Connect client
Verify that you are in the correct realm e.g., the Development Realm (serendipity-dev).
To create a new client, select Clients in the side menu and then click the Create client button:

Enter a Client ID (e.g., serendipity-web-bff) and a Name (e.g., Serendipity Web BFF) and then click the Next button:

Enable the Client authentication (i.e., OAuth 2.0 Confidential Client) slide toggle and the Standard flow (i.e., OAuth 2.0 Authorization Code Flow) checkbox and then click the Next button:

Enter a Valid redirect URI (e.g., https://serendipity.localhost/*), a valid Web origins (e.g., *) and then click the Save button.
After saving, Keycloak takes you to the client's Settings page. The Client authentication field shows On and Standard flow shows On. A Client secret is generated and visible in the Credentials tab — copy it now; Keycloak does not show it again. The client appears in the Clients list with the name you entered.
Configure the BFF
The BFF must be configured with the client credentials the Keycloak client just generated. The OAuth2 client registration lives in backend/modules/web-bff/src/main/resources/application.yml:
security:
oauth2:
client:
registration:
keycloak:
client-id: serendipity-web-bff
client-secret: LPg1pb4vRmHLhYOrA60hiOASlMHNlnSI
scope:
- openid
- profile
- email
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
provider:
keycloak:
issuer-uri: https://serendipity-identity-service.localhost/realms/serendipity-dev
authorization-uri: https://serendipity-identity-service.localhost/realms/serendipity-dev/protocol/openid-connect/auth
token-uri: https://serendipity-identity-service.localhost/realms/serendipity-dev/protocol/openid-connect/token
user-info-uri: https://serendipity-identity-service.localhost/realms/serendipity-dev/protocol/openid-connect/userinfo
jwk-set-uri: https://serendipity-identity-service.localhost/realms/serendipity-dev/protocol/openid-connect/certs
user-name-attribute: preferred_username
Each field must match what Keycloak has for the client:
| Application.yml field | What it must match |
|---|---|
client-id | The Client ID set when creating the client (serendipity-web-bff) |
client-secret | The Client Secret shown in the Keycloak Credentials tab after the client is created (Keycloak shows this only once — copy it immediately) |
authorization-grant-type: authorization_code | Standard flow enabled (the 'Standard flow' checkbox in the client creation wizard) |
redirect-uri | Must be a valid redirect URI registered on the client. Spring Security's default code endpoint is /{baseUrl}/login/oauth2/code/{registrationId}, so register https://serendipity.localhost/login/oauth2/code/keycloak as a valid redirect URI in Keycloak (the wildcard https://serendipity.localhost/* covers it) |
issuer-uri | The realm URL (https://serendipity-identity-service.localhost/realms/serendipity-dev) |
The scopes (openid, profile, email) correspond to what the client is allowed to request; they must be valid scopes on the Keycloak realm's OIDC provider, but they are not separately configured on the client itself.
In production the client-secret should come from an environment variable or secret manager, not live in application.yml committed to source. The project's .env file is the intended place for environment-specific values during local development.
When the BFF is running and the PWA navigates to https://serendipity.localhost/, the user is redirected to Keycloak's login endpoint (https://serendipity-identity-service.localhost/realms/serendipity-dev/protocol/openid-connect/auth). After entering credentials, Keycloak redirects back to https://serendipity.localhost/login/oauth2/code/keycloak with an authorization code. The BFF exchanges the code for an access token and the user is taken to the contacts page. A subsequent visit to the PWA does not require re-authentication — the JSESSIONID session cookie is still valid.
Optional: enable OIDC global logout
If you want logging out of the PWA to also end the Keycloak session (so the user is signed out everywhere, not just from the BFF), register a Valid post logout redirect URI on the client in Keycloak:
https://serendipity.localhost/
This matches the postLogoutRedirectUri set on the BFF's OidcClientInitiatedLogoutSuccessHandler in the BFF's security configuration (SecurityConfig.java). Without it, the BFF can still log the user out locally, but the Keycloak session remains active and a subsequent login will not require re-authentication.
When the user clicks Logout in the PWA, the browser calls GET https://serendipity.localhost/logout. The BFF invalidates the session and the JSESSIONID cookie, then redirects the browser to Keycloak's logout endpoint (https://serendipity-identity-service.localhost/realms/serendipity-dev/protocol/openid-connect/logout). If the post-logout redirect URI is registered on the client, Keycloak destroys the SSO session and redirects the browser back to https://serendipity.localhost/. Without the post-logout redirect URI registered, the browser is still redirected to https://serendipity.localhost/ but the Keycloak session remains active — opening the PWA again logs the user back in without a credential prompt.
Export a realm
To export a realm, in the project's /backend directory, run
REALM_NAME=serendipity-dev docker compose run --rm serendipity-identity-service-export
The exported realm file is written to backend/services/identity-service/export/. Look for a file named serendipity-dev-realm.json there.
References
Keycloak
- Keycloak docs: Server Administration Guide - Creating the first administrator
- Keycloak docs: Server Administration Guide - Configuring realms
- Keycloak docs: Server Administration Guide - Using external storage
- Keycloak docs: Server Administration Guide - Managing users