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 (the AD group name), and portal-type. The user-group name coming from AD must match an application user-group that already exists in the database — everything else (role, domain, permissions) is resolved from the database by that user-group.

  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). 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 settingsEndpoints → 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 → ClientsClient 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 D.6 Protocol mappers (per client)):

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 user’s group name coming from LDAP / Active Directory.

portal-type

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

domain

The user’s domain name. Hardcoded to Super domain for now (see below).

The LDAP group name must exactly match the DB user-group

There is no name translation between LDAP and the application. The user-group claim value must be identical to a user-group that already exists in the backend database for that portal. If the group in LDAP is not the same as the group name in the database, login will not work — the backend blocks access (NO_MATCH_AUTH, HTTP 403) rather than provisioning the user. Name the LDAP / AD groups exactly like the DB user-groups.

Domain is hardcoded to the super domain (for now)

The backend does not derive a user’s domain from their user-group. Until the full implementation exists, the domain claim is hardcoded to Super domain at the Keycloak level so every user lands in a valid domain (domainId = 0, full access). This is a Keycloak protocol mapper — see D.6 Protocol mappers (per client).

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. Group membership (LDAP group mapper)

So the user-group claim can carry the AD group name, AD groups must be synced into Keycloak groups:

  1. Open the LDAP provider you just created and go to its Mappers tab.

  2. Keycloak’s AD template usually includes a group mapper already. If not, click Add mapper, choose Mapper type: group-ldap-mapper, and set the Groups DN to where the relevant groups live (e.g. OU=Groups,DC=corp,DC=local).

  3. Click Save, then Sync LDAP groups to Keycloak.

The AD group names synced here must exactly match application user-group names that already exist in the backend database for that portal. The backend performs no name translation and never creates user-groups on the fly.

6.6. D.6 Protocol mappers (per client)

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

  1. Switch to the target realm and open Clients > your client > Client scopes.

  2. Click the client’s dedicated scope (e.g. support-portal-dedicated).

  3. Click Add mapper > By configuration and create the mappers below.

6.6.1. portal-type (Hardcoded claim)

Field Value

Mapper type

Hardcoded claim

Name

portal-type

Token Claim Name

portal-type

Claim value

sc in realm oneiot-sc, mc in realm oneiot-mc

Claim JSON Type

String

Add to ID token

On

6.6.2. user-group (Group Membership)

Field Value

Mapper type

Group Membership

Name

user-group

Token Claim Name

user-group

Full group path

Off (emit the plain group name, not /path/name)

Add to ID token

On

6.6.3. domain (Hardcoded claim — temporary)

Because the backend does not derive the domain from the user-group (see Attribute claims), emit the domain explicitly. Until the full implementation exists, use a hardcoded value so every user lands in a valid domain — Super domain places them in the super domain (full access).

Field Value

Mapper type

Hardcoded claim

Name

domain

Token Claim Name

domain

Claim value

Super domain (temporary — resolves to the super domain, domainId = 0)

Claim JSON Type

String

Add to ID token

On

The Token Claim Name values must be exactly portal-type, user-group, and domain — the backend expects these fixed names and has no override for them. The user-group value must equal a DB user-group name (which must equal the LDAP group name), and domain stays hardcoded to Super domain until the full implementation lands.

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.

  • Protocol mappers emitting user-group, portal-type (hardcoded sc/mc), and domain (hardcoded Super domain, temporary).

  • AD group names identical to the application user-group names.

  • 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

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 user’s Active Directory group must resolve to an application user-group that already exists in the backend database for that portal. The backend never creates user-groups on the fly: if the group is missing, login is rejected even though the password was correct. 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 rename the AD group to match an existing one.

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.