LDAP Login via Keycloak (OIDC)

Parameter Value

Application version

Spring Boot 3.5.6 / Java 25

Library

Spring Security OAuth2 Client (OIDC Relying Party)

Standard

OpenID Connect 1.0, OAuth 2.0 Authorization Code flow + PKCE (S256)

Login type

ldap (credentials validated by Keycloak against LDAP / Active Directory)

1. Overview

When a portal’s authentication type is set to ldap, the login is delegated to Keycloak. The backend never receives or checks the user’s password: Keycloak presents the login form, validates the credentials against the company directory (Active Directory / LDAP, connected through Keycloak User Federation), and returns the authenticated identity to the backend via the OpenID Connect (OIDC) protocol.

Historically the backend authenticated against LDAP directly. That mechanism has been removed. The ldap authentication type now always means "OIDC redirect to Keycloak, with Keycloak validating credentials against LDAP". There is also no "insecure TLS" / "trust-all" switch anywhere in the system — certificate trust is handled with a proper truststore (see [part-c-tls-certificate-trust]).

This document covers only the ldap login type: the backend environment variables and the Keycloak Admin Console configuration. To deploy the Keycloak container itself (Docker Compose, keycloak schema, TLS certificate, shared network, HA), see Keycloak Deployment first.

1.1. Components

Component Role

Browser

Runs the Angular portals: Support Center (SC) and Management Console (MC). Initiates the login and follows the OIDC redirects.

Backend (ui-backend, Spring Boot)

The OIDC client (relying party). Starts the authorization flow, exchanges the authorization code for tokens, provisions the user, and issues the application’s own JWT.

Keycloak

The identity server. Shows the login form, validates credentials against LDAP, and issues OIDC tokens. Deployed per Keycloak Deployment.

LDAP / Active Directory

The company user directory (usernames, passwords, groups). Connected to Keycloak via User Federation.

Database (MySQL / Oracle)

The backend’s own database: application users, user-groups, and login sessions.

2. Login flow

ldap-oidc-login-sequence

Step by step:

  1. The frontend asks the backend for the portal’s authentication type (POST /iot-webservice/iotw/Auth/type). The type is stored per portal in the database; ldap (or windows) means "OIDC via Keycloak".

  2. The frontend requests the login URL (POST /iot-webservice/iotw/Auth/oidc/login-url) and opens it. The backend responds with {OIDC_REDIRECT_HOST}/iot-webservice/oauth2/authorization/support-portal (or …​/management-portal for MC).

  3. The backend redirects the browser to Keycloak’s authorize endpoint using the Authorization Code flow with PKCE (S256), including state and nonce.

  4. Keycloak shows its login form and validates the entered credentials against LDAP User Federation.

  5. On success, Keycloak redirects the browser to the backend callback {OIDC_REDIRECT_HOST}/iot-webservice/login/oauth2/code/<registrationId>?code=…​ with a short-lived, one-time authorization code.

  6. The backend exchanges the code for tokens on a back-channel call to Keycloak’s token endpoint, authenticating with its client_id and client secret. This step requires (a) the correct client secret and (b) JVM trust of Keycloak’s TLS certificate.

  7. From the ID token the backend reads the claims: preferred_username (the login name), user-group, domain, and portal-type. The user-group and domain values are application names that Keycloak has already translated from the user’s AD group membership; each must match a record that exists in the database — permissions and visibility are then resolved from the database by that user-group and domain.

  8. The backend provisions the user if it does not exist, or reuses and syncs the existing one (idempotent — no duplicates), then issues its own internal JWT (HS256).

  9. The browser is redirected to the frontend callback {OIDC_REDIRECT_HOST}/<portal>/login/auth-callback?token=<JWT>&sessionHash=<hash>. All subsequent API calls carry Authorization: Bearer <JWT>, validated by JwtFilter. Keycloak plays no part after login.

Authorization Code flow + PKCE

With the Authorization Code flow, tokens never pass through the browser — only a short-lived code does; the tokens are fetched by the backend over a secure back-channel. PKCE (Proof Key for Code Exchange) additionally binds the code to the client that started the flow: the backend sends a SHA-256 code_challenge at the start and reveals the original verifier at the token exchange, so a stolen code is useless on its own. The Keycloak client must therefore have its PKCE method set to S256.

2.1. URL reference

Purpose URL pattern

Login start (per portal)

{OIDC_REDIRECT_HOST}/iot-webservice/oauth2/authorization/support-portal
{OIDC_REDIRECT_HOST}/iot-webservice/oauth2/authorization/management-portal

Backend OAuth callback
(must be registered in Keycloak as a Valid Redirect URI, exact, no wildcard)

{OIDC_REDIRECT_HOST}/iot-webservice/login/oauth2/code/support-portal
{OIDC_REDIRECT_HOST}/iot-webservice/login/oauth2/code/management-portal

Frontend success callback
(where the user lands with the JWT)

{OIDC_REDIRECT_HOST}/support-portal/login/auth-callback
{OIDC_REDIRECT_HOST}/management-portal/login/auth-callback

Keycloak endpoints (derived from the issuer)

{issuer}/protocol/openid-connect/auth (login)
{issuer}/protocol/openid-connect/token (token exchange)
{issuer}/protocol/openid-connect/userinfo
{issuer}/protocol/openid-connect/certs

registrationId vs. Keycloak Client ID

support-portal and management-portal in the URLs above are the registrationId — an internal name fixed in the code. It never changes and is not configurable.

The Keycloak Client ID is a separate value, set via OIDC_SC_CLIENT_ID / OIDC_MC_CLIENT_ID, and must match the client created in Keycloak. The URL paths always stay support-portal / management-portal regardless of what the Client ID is.

3. Part A — Deploy Keycloak

The ldap login type uses the standard Keycloak container. Deploy it first following Keycloak Deployment (the OIDC compose file; the Windows/Kerberos mounts stay commented out), then continue with the backend variables and Keycloak Admin Console configuration below.

4. Part B — Backend environment variables

All variables below are set on the backend (ui-backend) — they ship commented out as the OIDC block in ui-backend/.env (FT_DISK — ONE_IOT_UI/docker/ui-backend); uncomment it and fill it in. For each variable: what to put in it, and where to find the value.

4.1. Core switches

Variable What to set

OIDC_ENABLED

true to enable the OIDC/Keycloak login flow, false to disable it entirely. Must be true for the ldap (and windows) authentication type to work.

OIDC_REDIRECT_HOST

The external base URL under which the browser reaches the backend: https://<host>; or https://<host>:<port>; — scheme, host, and port only; no path, no trailing slash.

Where to find it: it is the address users type to open the portals (the ingress / reverse-proxy hostname of the deployment). It anchors both the OAuth callback and the final frontend redirect, so it must be the URL as seen from outside, not an internal service address.

Set it explicitly: if left unset, the backend derives it from the incoming request, which is unreliable behind a proxy or load balancer.

4.2. Support Center (SC) client

Variable What to set

OIDC_SC_CLIENT_ID

The Client ID of the OIDC client created for the Support Center in Keycloak.

Where to find it: Keycloak Admin Console → select the SC realm → Clients → the Client ID column of the client you created for SC. Copy it verbatim (case-sensitive).

OIDC_SC_ISSUER_URI

The SC realm’s issuer URL: https://<keycloak-host>:<keycloak-port>/realms/<sc-realm-name>;. Include the port if it is non-standard. Must match the realm’s published issuer value character for character (scheme, host, port, no trailing slash).

Where to find it: Keycloak Admin Console → select the SC realm → Realm settings → Endpoints → open OpenID Endpoint Configuration and copy the issuer field. Alternatively, open https://<keycloak-host>:<keycloak-port>/realms/<sc-realm-name>/.well-known/openid-configuration in a browser.

4.3. Management Console (MC) client

Variable What to set

OIDC_MC_CLIENT_ID

The Client ID of the OIDC client created for the Management Console in Keycloak (MC realm → Clients → Client ID column).

OIDC_MC_ISSUER_URI

The MC realm’s issuer URL: https://<keycloak-host>:<keycloak-port>/realms/<mc-realm-name>;. Same rules and same way to verify as for the SC issuer, but from the MC realm’s OpenID Endpoint Configuration.

4.4. Attribute claims

The backend reads the user’s identity from claims in the ID token, using fixed default claim names — there are no environment variables for remapping them. Configure the Keycloak protocol mappers to emit exactly these claim names (see Protocol mappers):

Claim Carries

preferred_username

The login name of the user. A standard OIDC claim Keycloak emits out of the box — no custom mapper needed.

user-group

The application user-group name. Emitted by a hardcoded mapper (Mode A) or translated from the user’s AD role group (Mode B) — see Choose a mode.

portal-type

Which portal the token is for: sc or mc. A hardcoded-claim mapper in each realm, in both modes.

domain

The application domain name, from the same source as user-group. Super domain resolves to the super domain (domainId = 0, full access).

user-group and domain are both required

A token missing either claim is rejected with NO_MATCH_AUTH (HTTP 403) after Keycloak has authenticated the user. In particular, a missing domain claim is not treated as the super domain: super-domain access must be granted explicitly, by a domain group (Mode B) or by a hardcoded mapper (Mode A).

The claim value must exactly match a name in the backend database

The backend matches claim values by name: the user-group value must be identical to a user-group that already exists in the database for that portal, and the domain value to an existing domain (or the literal Super domain). Nothing is provisioned on the fly — if the name is unknown, access is blocked (NO_MATCH_AUTH, HTTP 403).

The AD group name itself does not have to match. Keycloak translates it: the AD group carries an attribute holding the application name, and that attribute is what reaches the token. This is what makes AD naming (AdminRole, SuperDomainRole) independent of application naming (admin, Super domain) — see Translation table.

One user-group and one domain per user

Both claims are single-valued: the backend stores one userGroupId and one domainId per user. A user must therefore belong to exactly one AD role group and exactly one AD domain group. Membership in two groups of the same kind makes the resulting value non-deterministic.

4.5. Client secrets (files, not environment variables)

The client secret — the credential the backend presents to Keycloak at the token exchange — is deliberately kept out of the environment and read from files on disk:

File Holds

${APP_HOME}/conf/sc-oidc-secret

The SC client secret — the raw value, one line, nothing else.

${APP_HOME}/conf/mc-oidc-secret

The MC client secret — same format.

Where to find the secret: Keycloak Admin Console → the realm → Clients → open the client → Credentials tab → Client secret. The tab exists only if Client authentication is ON for the client.

If the secret is regenerated in Keycloak, the matching file must be updated; otherwise the code-for-token exchange fails with invalid_client.

5. Part C — TLS / certificate trust

During the token exchange the backend calls Keycloak server-to-server over HTTPS, so the backend’s JVM must trust the certificate Keycloak presents. If it does not, the backend logs:

PKIX path building failed: unable to find valid certification path to requested target
Keycloak’s certificate is…​ What to do

Issued by a public CA (e.g. Let’s Encrypt)

Nothing — the JVM trusts it out of the box.

Self-signed or issued by a private / internal CA

Add the certificate to the backend’s Java truststore.

To trust a self-signed or private-CA certificate, import Keycloak’s certificate into a Java truststore and point the backend JVM at it:

# Import Keycloak's certificate into a truststore
keytool -importcert -noprompt -alias keycloak \
  -file keycloak.crt \
  -keystore ${APP_HOME}/conf/truststore.jks \
  -storepass changeit

# Start the backend with the truststore
JAVA_OPTS="-Djavax.net.ssl.trustStore=${APP_HOME}/conf/truststore.jks \
           -Djavax.net.ssl.trustStorePassword=changeit"

The truststore.jks file lives in the backend conf/ directory (mounted at /opt/app/conf in Docker), alongside the sc-oidc-secret / mc-oidc-secret files.

The certificate’s SAN (or a covering wildcard) must include the exact Keycloak hostname used in the issuer URIs. Otherwise TLS fails hostname verification even though the certificate is trusted.

6. Part D — Keycloak Admin Console configuration

This is the click-by-click walkthrough for the ldap login type. Perform every step in both realms — one per portal (oneiot-sc for the Support Center, oneiot-mc for the Management Console) — unless a step is explicitly per-portal.

The realm names (oneiot-sc / oneiot-mc) are examples. They only need to match the OIDC_SC_ISSUER_URI / OIDC_MC_ISSUER_URI values configured on the backend.

6.1. D.1 Create the realms

  1. Open the Keycloak Admin Console ({KC_HOSTNAME}/admin) and log in as admin.

  2. In the realm selector (top-left), click Create realm.

  3. Enter Realm name oneiot-sc and click Create.

  4. Repeat for oneiot-mc.

6.2. D.2 Create the OIDC client (per realm)

Create one confidential OIDC client in each realm. The client for the Support Center lives in oneiot-sc; the client for the Management Console lives in oneiot-mc.

  1. Switch to the target realm using the realm selector.

  2. Navigate to Clients > Create client.

  3. General settings:

    • Client type: OpenID Connect

    • Client ID: the value you will put in OIDC_SC_CLIENT_ID (SC) or OIDC_MC_CLIENT_ID (MC), e.g. support-portal / management-portal.

  4. Click Next.

  5. Capability config:

    • Client authentication: On (makes the client confidential — it gets a secret).

    • Authorization: Off.

    • Authentication flow: enable Standard flow (Authorization Code). Leave Direct access grants off (not used).

  6. Click Next, then Save.

  7. On the Settings tab, set Valid redirect URIs to the exact backend callback — no wildcards:

    • SC: {OIDC_REDIRECT_HOST}/iot-webservice/login/oauth2/code/support-portal

    • MC: {OIDC_REDIRECT_HOST}/iot-webservice/login/oauth2/code/management-portal

  8. Click Save.

The redirect URI path segment (support-portal / management-portal) is the fixed registrationId, not the Client ID. Keep it exactly as shown regardless of the Client ID value.

6.3. D.3 Enable PKCE (S256)

  1. Open the client and go to the General Settings tab.

  2. Under Capability config, set PKCE Method to S256.

  3. Click Save.

6.4. D.4 Copy the client secret

  1. Open the client and go to the Credentials tab.

  2. Copy the Client secret value.

  3. Paste it — raw value, one line, nothing else — into the matching file on the backend:

    • SC: ${APP_HOME}/conf/sc-oidc-secret

    • MC: ${APP_HOME}/conf/mc-oidc-secret

If you click Regenerate later, update the corresponding secret file too, otherwise the token exchange fails with invalid_client.

6.5. D.5 LDAP User Federation

This connects Keycloak to Active Directory so the login form validates real AD credentials and the user’s group membership is available.

  1. Switch to the target realm.

  2. Navigate to User federation > Add LDAP providers.

  3. Fill in the connection settings:

    Field Value

    Vendor

    Active Directory (Keycloak pre-fills the AD-specific defaults)

    Connection URL

    ldaps://<ad-host>:636 (use LDAPS in production)

    Bind type

    simple

    Bind DN

    The service account used to read the directory, e.g. CN=svc-keycloak,OU=Service,DC=corp,DC=local

    Bind credentials

    The service account password

    Edit mode

    READ_ONLY (Keycloak never writes back to AD)

    Users DN

    The base DN to search users under, e.g. OU=Users,DC=corp,DC=local

    Username Attribute

    sAMAccountName for Active Directory (Keycloak pre-fills it when Vendor = Active Directory). This is the attribute users type as their login name; it flows into the preferred_username claim. Keep the exact spelling and case.

  4. Click Test connection and Test authentication — both must succeed.

  5. Click Save.

  6. Optionally click Synchronize all users to pre-import users (they are also imported lazily on first login).

LDAPS requires Keycloak to trust the AD certificate

With a ldaps:// Connection URL, Keycloak is a TLS client of the domain controller and must trust its certificate, or Test connection fails with a PKIX / TLS-handshake error — before any DN is even evaluated. Fetch the AD certificate and mount it into Keycloak’s truststore before testing: LDAPS certificate trust has the openssl one-liner, the compose mount, the KC_TRUSTSTORE_PATHS variable, and the extra_hosts tip for a certificate issued to a hostname rather than an IP.

Get the DN fields exactly right — case, and object type (CN= vs OU=)

Active Directory matches DNs exactly. The two most common reasons Test connection / Test authentication fails, or no users are found even though everything "looks right", are the wrong case and the wrong object-type prefix:

  • CN= vs OU= — do not mix them up. The built-in AD Users container is a container (CN=Users), not an organizational unit (OU=Users). Writing OU=Users when the objects actually live under CN=Users (or the reverse) makes the Users DN / Bind DN resolve to nothing. The same applies to every level of the path.

  • Case matters in the RDN values. Match the directory’s real casing for each component — CN=Svc-Keycloak, OU=Service, DC=corp,DC=local, and the sAMAccountName Username Attribute.

  • Copy DNs verbatim from AD. In Active Directory Users and Computers enable View → Advanced Features, open the object’s Attribute Editor, and copy its distinguishedName exactly — component types and case included — into the Bind DN and Users DN fields.

6.5.1. What must exist in Active Directory

Keycloak never writes to the directory. Two kinds of groups must already exist, and every user must belong to exactly one of each:

Kind Example AD name Produces

Role group

AdminRole, UserRole

the user-group claim

Domain group

SuperDomainRole

the domain claim

A typical layout:

dc=example,dc=com
+-- ou=people                     <-- users
|   +-- uid=user1
|   +-- uid=user2
+-- ou=groups                     <-- groups
    +-- cn=AdminRole              <-- produces user-group
    +-- cn=UserRole               <-- produces user-group
    +-- cn=SuperDomainRole        <-- produces domain

Requirements per object:

Object Attribute Notes

User

objectClass

user on Active Directory; inetOrgPerson (plus organizationalPerson, person, top) on OpenLDAP.

login attribute

sAMAccountName on AD, uid on OpenLDAP. Must match the federation’s Username LDAP attribute.

Group

objectClass

group on Active Directory; groupOfNames on OpenLDAP.

cn

The group name as Keycloak will show it.

member

The full DN of the user, not the bare login.

On OpenLDAP test stands the structure can be created with LDIF:

dn: ou=groups,dc=example,dc=com
objectClass: organizationalUnit
ou: groups

dn: cn=AdminRole,ou=groups,dc=example,dc=com
objectClass: groupOfNames
cn: AdminRole
member: uid=user1,ou=people,dc=example,dc=com

dn: cn=SuperDomainRole,ou=groups,dc=example,dc=com
objectClass: groupOfNames
cn: SuperDomainRole
member: uid=user1,ou=people,dc=example,dc=com
ldapadd -c -x -H ldap://<ldap-host>:389 -D "cn=admin,dc=example,dc=com" -W < groups.ldif
-c continues past an "Already exists" error on ou=groups, which is expected when the container already exists. On a real Active Directory the groups are created with the standard tooling (ADUC or PowerShell) instead.

6.5.2. Group membership (LDAP group mapper)

AD groups must be visible to Keycloak before they can carry the translation attributes.

  1. Open the LDAP provider and go to its Mappers tab. Keycloak’s AD template usually includes a group mapper already; if not, click Add mapper and choose Mapper type: group-ldap-mapper.

LDAP federation mappers — the group mapper has type group-ldap-mapper
  1. Open it and verify the configuration against the directory:

Field Value Why

LDAP Groups DN

OU=Groups,DC=corp,DC=local

Where the groups live. Covered by the provider’s Search scope — with Subtree, groups in nested containers are found as well.

Group Name LDAP Attribute

cn

Where the group name comes from.

Group Object Classes

group (AD) / groupOfNames (OpenLDAP)

Must match the directory.

Membership LDAP Attribute

member

Attribute listing the members.

Membership Attribute Type

DN

member holds full DNs.

User Groups Retrieve Strategy

LOAD_GROUPS_BY_MEMBER_ATTRIBUTE

Keycloak resolves membership by scanning member.

Mode

LDAP_ONLY

Groups are read from the directory. Translation attributes are still stored on the Keycloak side, so this mode is fine.

Group mapper — Groups DN
Group mapper — membership attribute
The memberOf overlay is not required

With LOAD_GROUPS_BY_MEMBER_ATTRIBUTE, Keycloak walks the groups and reads their member attribute, so an OpenLDAP memberof overlay is unnecessary. The Member-Of LDAP Attribute field belongs to the other retrieval strategy and should be left alone.

  1. Click Action → Sync LDAP groups to Keycloak. Repeat this after every group change in the directory.

Action menu — Sync LDAP groups to Keycloak
Mind the sync direction

Sync Keycloak groups to LDAP writes into the directory. The direction needed here is Sync LDAP groups to Keycloak.

The result line reports imported (new groups), updated (existing groups whose membership changed) and removed. Confirm the groups are present under Groups:

Realm groups after synchronisation

6.5.3. Translation table — attributes on groups

Once the groups are synced into Keycloak, set a translation attribute on each one — this is what turns an AD group name into the application user-group / domain value, and it is how multi-domain / multi-role support works. The full procedure — the translation table with screenshots, plus the per-client protocol mappers that emit the claims — lives in the shared, login-type-neutral guide:

Keycloak Role & Domain Mappers (Step 1 — translation attributes, Step 2 — protocol mappers, Step 3 — verify).

Translation attributes are needed only when different users must land in different user-groups or domains (Mode B). If every SSO user should get the same user-group and the super domain, take Mode A instead: three hardcoded mappers, no group attributes, no group sync — Mode A.

6.6. D.6 Protocol mappers (per client)

The ID token must carry three custom claims — portal-type, user-group, and domain — added to each client’s dedicated scope. The username arrives in the standard preferred_username claim and needs no mapper.

How user-group and domain are produced depends on the deployment:

  • Mode A — single user-group and domain. All three claims are Hardcoded claim mappers (portal-type = sc/mc, user-group = the one application group, domain = Super domain). AD groups need no attributes.

  • Mode B — multi user-group / multi-domain. user-group and domain are User Attribute mappers fed by the translation attributes on the user’s AD groups; portal-type stays hardcoded.

Pick one mode per realm and never mix them for the same claim — a hardcoded mapper overrides the translation for every user. Mode selection and the field-by-field walkthrough, with screenshots, are in the shared guide: Keycloak Role & Domain Mappers.

6.7. D.7 Browser flow — show the LDAP login form

For the ldap login type the user must see Keycloak’s username/password form (which validates against LDAP). Ensure the realm’s bound Browser flow does not have an Identity Provider Redirector configured with a default IdP — otherwise the user is bounced straight to an external identity provider instead of the LDAP form.

  1. Navigate to Authentication > Flows.

  2. Confirm the bound Browser flow is the default one (no auto-redirect step), or that any Identity Provider Redirector step has no Default Identity Provider set.

Auto-redirect to an external IdP is the opposite requirement and is used by the Azure AD login type — see Azure AD + Keycloak.

6.8. D.8 Checklist

For each portal (SC and MC):

  • One realm per portal.

  • A confidential OIDC client: Client authentication = On, Standard flow = On, PKCE = S256, exact backend callback as the only Valid Redirect URI (no wildcard).

  • Client secret copied into conf/sc-oidc-secret / conf/mc-oidc-secret.

  • LDAP User Federation pointing at Active Directory (with a group mapper — member / DN / LOAD_GROUPS_BY_MEMBER_ATTRIBUTE — when using Mode B).

  • Mode chosen for this realm (A: hardcoded claims / B: per-user translation).

  • Mode A: portal-type, user-group and domain all emitted by Hardcoded claim mappers, and no User Attribute mapper left for user-group / domain.

  • Mode B: one role group and one domain group per user in AD, groups synced into Keycloak, translation attributes set (user-group on role groups, domain on domain groups), user-group and domain emitted by User Attribute mappers, portal-type hardcoded.

  • No leftover Group Membership mapper, and no claim served by two mappers at once.

  • Every emitted user-group / domain value matches a name that exists in the backend database for that portal — a token missing either claim, or carrying an unknown name, is rejected with 403.

  • Browser flow shows the LDAP username/password form (no Identity Provider Redirector).

7. Selecting the authentication type per portal

The ldap type is selected per portal (application UI → Settings → Interface → Authentication, or in the iotw_client_interface table). See Selecting the authentication type per portal for the full table of types.

If a portal shows a local password form when the Keycloak form was expected — or jumps to an external (e.g. Microsoft) login instead — check two things first: (a) the AuthenticationType setting for that portal, and (b) whether the realm’s browser flow has an Identity Provider Redirector enabled.

8. Verifying the setup

8.1. Checking the claims without logging in

Keycloak can assemble a token for any user from the Admin Console (Clients > client > Client scopes > Evaluate > Generated ID token), which verifies the whole chain without disturbing anyone’s session — both user-group and domain must appear as plain strings. The step-by-step, with screenshots, is in Keycloak Role & Domain Mappers — Step 3.

8.2. Checking a real login

A healthy ldap-via-Keycloak login looks like this:

  • On login, the portal does not show a local username/password form — it redirects to Keycloak.

  • The address bar shows the Keycloak host with Keycloak’s login form.

  • Wrong AD credentials are rejected by Keycloak (the backend is never reached).

  • After a correct password, the browser passes through …​/iot-webservice/login/oauth2/code/<registrationId>?code=…​ and lands on …​/<portal>/login/auth-callback?token=…​.

  • The user is inside the portal with permissions matching their AD group.

  • The backend logs show the user being provisioned or reused and an internal JWT issued — no PKIX, invalid_client, or NO_MATCH_AUTH errors.

  • Reloading the portal keeps the session (the JWT is stored); Keycloak is not contacted again.

Common prerequisite

The value the translation produces must name a user-group that already exists in the backend database for that portal. The backend never creates user-groups on the fly: if the name is unknown, login is rejected even though the password was correct and the token was well-formed. Pre-create the matching user-group before the first login.

9. Troubleshooting

Symptom Likely cause Fix

invalid_client during the code-for-token exchange

Wrong or missing client secret in conf/sc-oidc-secret / conf/mc-oidc-secret

Copy the secret from Keycloak → client → Credentials into the file, then redeploy.

HTTP 500 right after entering the password (NO_MATCH_AUTH in the logs)

The user-group name from the user-group claim does not exist in the DB for that portal

Create the application user-group with exactly that name, or correct the group attribute value to match an existing one.

The user-group or domain claim is missing from the token entirely

The mapper was not created, or was created outside the client’s dedicated scope

Add it under Clients > client > Client scopes > <client>-dedicated.

The claim is present but empty

The user belongs to no group carrying that attribute

Check Users > user > Groups, then the group’s Attributes tab.

The user has no groups at all in Keycloak

The group mapper reads membership the wrong way

Set member / DN / LOAD_GROUPS_BY_MEMBER_ATTRIBUTE on the group mapper.

New AD groups do not appear in Keycloak

Synchronisation was not run after the directory change

Mappers > group mapper > Action → Sync LDAP groups to Keycloak.

Groups exist in AD but are never found

Search scope is One Level while the groups live in a nested container

Set Search scope to Subtree on the LDAP provider.

The claim value never changes, whatever the user’s groups

A leftover Hardcoded claim mapper with the same claim name overrides the translation

Delete it from the dedicated scope.

The claim is an array instead of a string

Multivalued is enabled on the mapper

Switch Multivalued and Aggregate attribute values off.

PKIX path building failed in the backend logs

The backend’s JVM does not trust Keycloak’s certificate

Set trustKeycloakCert: true (import the cert into the truststore) or use a CA-issued certificate. Ensure the cert’s SAN covers the Keycloak host.

redirect_uri did not match on the Keycloak page

The exact callback URL is not registered in the client’s Valid Redirect URIs

Add {OIDC_REDIRECT_HOST}/iot-webservice/login/oauth2/code/<registrationId> (exact scheme/host/port, no wildcard).

Error about a missing code_challenge_method

The Keycloak client’s PKCE method is not S256

Set Advanced → PKCE method = S256 on the client.

Browser jumps to an external (e.g. Microsoft) login instead of the Keycloak form

The realm’s browser flow has an Identity Provider Redirector enabled

Disable it so the LDAP username/password form is shown.

Login loops or lands on the wrong host

OIDC_REDIRECT_HOST unset or pointing at an internal address instead of the external portal URL

Set it to the exact external base URL the browser uses (scheme + host + port, no trailing slash).

Test connection / Test authentication fails in LDAP User Federation (PKIX / SSL handshake)

Keycloak does not trust the AD LDAPS certificate, or the certificate is issued for a hostname you are connecting to by IP

Mount the AD certificate into Keycloak’s truststore (KC_TRUSTSTORE_PATHS); connect by the certificate’s hostname and add an extra_hosts entry. See LDAPS certificate trust.

Test connection succeeds but Test authentication fails, or no users are found

Wrong case in a DN, or OU= used where the objects live under CN= (e.g. the built-in CN=Users)

Copy each DN level verbatim from AD (Attribute Editor → distinguishedName); fix CN vs OU and the case, in both Bind DN and Users DN.

Diagnose from the token backwards

Work the chain in this order: the generated ID token (Client scopes → Evaluate), then the user’s Groups tab, then the group’s Attributes tab, then the federation’s group mapper. The first step that shows the wrong value is the layer that is broken.