Azure AD + Keycloak — Integration Guide

Parameter Value

Keycloak version

26.0 (Docker)

Azure AD

Microsoft Entra ID

Backend protocol

SAML 2.0 (backend <→ Keycloak)

Broker protocol

OpenID Connect v1.0 (Keycloak <→ Azure AD)

1. Overview

This guide describes the Azure AD login type: how to register an application in Microsoft Entra ID (Azure AD), integrate it with Keycloak as an Identity Broker, and manage users through Keycloak after the integration is complete.

The OneIoT platform uses two separate Keycloak realms — one for the Support Center (SC) portal and one for the Management Console (MC) portal. Both realms are connected to the same Azure AD application.

On the backend this is the saml login type: the backend is a SAML Service Provider and Keycloak is its SAML Identity Provider. Keycloak, in turn, does not store the passwords — it brokers authentication to Azure AD over OIDC. In other words, the Azure AD login type is the SAML setup plus an OIDC Identity-Provider broker to Azure AD configured inside each Keycloak realm.

1.1. How it works

The platform uses a three-layer authentication architecture:

Layer Component Role

Application

OneIoT Backend (Spring Boot)

Initiates the SAML authentication request and receives the authenticated user session.

Broker

Keycloak

Acts as an intermediary between the application and the identity provider. Maps user attributes and adds custom fields required by the application.

Identity Provider

Microsoft Azure AD (Entra ID)

Performs the actual user authentication and identity verification.

1.2. Authentication flow

azure-keycloak-auth-flow

1.3. Attribute flow

Attribute Source Keycloak Mapper Type Description

email

Azure AD token

Attribute Importer

Imported directly from the Azure AD email claim.

username

Azure AD token

Username Template Importer

Derived from ${CLAIM.email} — the user’s email becomes their username.

portal-type

Keycloak (hardcoded)

Hardcoded Attribute

Set per realm: sc for Support Center, mc for Management Console.

user-group

Mode A: Keycloak (hardcoded)
Mode B: Azure AD token (per-user attribute)

Hardcoded Attribute / Attribute Importer

The application user-group name. Must exist in the application database for that portal; a missing or unknown value blocks the login.

domain

Mode A: Keycloak (hardcoded)
Mode B: Azure AD token (per-user attribute)

Hardcoded Attribute / Attribute Importer

The application domain name. Super domain (with a space) grants full access. Must exist in the application database; a missing or unknown value blocks the login.

Two ways to set user-group / domain
  • Mode A — one fixed value per realm. Every user of the realm gets the same user-group and domain from Hardcoded Attribute mappers. Simplest correct setup when one group and the super domain are acceptable for everyone; no Azure extension attributes are needed.

  • Mode B — per user. Each Azure user carries their own user-group / domain value, imported into Keycloak by an Attribute Importer mapper. This is what enables multiple roles and multiple domains across users, and requires setting the value on each user in Azure (Part 1b: Per-user attributes in Azure (userGroup / domain)).

portal-type is always a Hardcoded Attribute (it is per realm, sc / mc), regardless of the mode. Both attributes are mandatory in either mode — see Choose a mode: fixed or per-user user-group / domain.

1.4. Why Keycloak as a broker (not direct Azure AD integration)?

  • Keycloak adds the portal-type claim (per realm) that Azure AD does not provide, and imports the per-user user-group / domain values from Azure into the fixed claim names the application requires — without the application needing any Azure-specific code.

  • Administrators can manage user attributes, sessions, and access directly in Keycloak without needing Azure AD admin permissions.

  • Each portal (SC / MC) uses a separate Keycloak realm with its own default attribute values, allowing portal-specific configuration.

  • If the identity provider changes in the future (e.g., from Azure AD to Okta or ADFS), only the Keycloak broker configuration needs to be updated — the application remains unchanged.

2. Prerequisites

Before configuring the Azure AD broker:

  1. Deploy Keycloak — see Keycloak Deployment (Docker Compose, keycloak schema, TLS certificate, shared network, HA).

  2. Complete the SAML setup — create the oneiot-sc / oneiot-mc realms, their SAML clients (oneiot-sp-sc / oneiot-sp-mc), attribute mappers, and export the IdP signing certificates, following SAML SSO Setup. The steps below add the Azure AD Identity Provider on top of that SAML configuration.

3. Backend configuration

On the backend, the Azure AD login type is configured exactly like the SAML login type — the backend talks SAML to Keycloak and is unaware of Azure AD. Set app.authentication-type to saml, enable SAML, and point the SAML IdP variables at the Keycloak realms:

SAML_ENABLED=true
SAML_REDIRECT_HOST=https://<PORTALS_HOST>:8843

# IdP = the Keycloak realms (which broker to Azure AD)
SAML_SC_IDP_ENTITY_ID=https://<KEYCLOAK_HOST>:<PORT>/realms/oneiot-sc
SAML_SC_IDP_SSO_URL=https://<KEYCLOAK_HOST>:<PORT>/realms/oneiot-sc/protocol/saml
SAML_MC_IDP_ENTITY_ID=https://<KEYCLOAK_HOST>:<PORT>/realms/oneiot-mc
SAML_MC_IDP_SSO_URL=https://<KEYCLOAK_HOST>:<PORT>/realms/oneiot-mc/protocol/saml

These variables live in the backend per-service env file ui-backend/.env (loaded after the shared .env via env_file: [.env, ./ui-backend/.env], shipped in FT_DISK — ONE_IOT_UI/docker/ui-backend), in its commented SAML block — there is no separate SAML env file. See SAML SSO Setup — Environment variables for the complete variable table and the IdP certificate mount (sc-idp.crt / mc-idp.crt). The backend configuration is identical whether Keycloak authenticates users locally or brokers them to Azure AD — only the Keycloak side differs, as described below.

4. Part 1: Creating an App Registration in Microsoft Entra ID

  1. Open the Microsoft Entra ID portal.

  2. In the left menu, navigate to Manage > App registrations.

  3. Click New registration.

  4. Fill in the following:

    • Name: a descriptive name, for example OneIoT-Keycloak

    • Supported account types: select the option that suits your organization

    • Redirect URI: leave empty for now

  5. Click Register.

After creation, the Overview page is displayed:

Azure App Registration Overview
Field Description

Application (client) ID

Unique identifier of the registered application. Will be used in Keycloak as the Client ID.

Directory (tenant) ID

Azure AD tenant identifier. Part of the Discovery endpoint URL.

Keep this page open — the values above will be needed in the following steps.

5. Choose a mode: fixed or per-user user-group / domain

The backend requires three attributes on every SAML assertion — portal-type, user-group and domain — and rejects the login if any of them is missing or carries a name it does not know. portal-type is always a hardcoded value per realm; how the other two are produced depends on what the deployment needs. Pick one mode and configure both attributes accordingly.

Mode Use when How user-group / domain are produced

A — Single user-group, single domain

Every Azure user should land in the same application user-group and in the super domain.

Hardcoded Attribute mappers on the Keycloak Identity Provider, exactly like portal-type (Mode A — Fixed user-group and domain (Hardcoded Attribute mappers)). Parts 1b and 1c do not apply — no Azure extension attributes, no per-user Graph calls.

B — Multi user-group / multi-domain

Different users must land in different user-groups, different domains, or both.

Azure directory-extension attributes per user, imported by Attribute Importer mappers (Parts 1b, 1c and user-group / domain in Part 2.6).

A missing attribute blocks the login — in both modes

SamlAuthService never provisions anything on the fly. If user-group or domain is absent from the assertion, or carries a name that does not exist in the application database for that portal, the user is rejected with NO_MATCH_AUTH (HTTP 403) after Azure and Keycloak have authenticated them.

This matters most for domain: the backend reads an unset domain as the super domain, so accepting an assertion without it would grant access to every domain to the user who was granted none. A user without a group was likewise created with no group at all, and permission checks default to "allowed" when a group has no permission rows — so neither attribute may be left to chance. Super-domain access must always be configured explicitly, by a hardcoded mapper (Mode A) or by the user’s Azure attribute value (Mode B).

Never configure both modes for the same attribute

A Hardcoded Attribute mapper and an Attribute Importer writing the same User Attribute Name must not coexist on one Identity Provider: the hardcoded value is written for every user and overrides the per-user import, silently collapsing a multi-domain deployment into a single domain. When switching modes, delete the mappers of the mode you are leaving.

5.1. Mode A — Fixed user-group and domain (Hardcoded Attribute mappers)

On each Identity Provider (Identity providers > Login with Microsoft > Mappers > Add mapper), add three Hardcoded Attribute mappers. They are configured exactly like the portal-type mapper in portal-type; only the name and value differ.

User Attribute Name Value Notes

portal-type

sc in the SC realm, mc in the MC realm

Always hardcoded, in both modes.

user-group

The application user-group every SSO user should get, e.g. Admin

Must already exist in the application database for that portal (Settings > User groups). It is never created automatically.

domain

Super domain

Resolves to domainId = 0 — access to every domain. Any other value must be an existing domain name.

Set Sync mode override = Force on all three, so the values are refreshed on every login rather than only at user creation. Then skip Parts 1b and 1c entirely and continue at Part 2: Integrating Azure AD into Keycloak; in Part 2.6, create only the email, username and the three hardcoded mappers above.

Mode A is a valid permanent configuration for single-tenant deployments, not just a starting point. Moving to Mode B later means registering the Azure extension attributes and replacing two mappers — the realm, the Identity Provider and the flows stay as they are.

6. Part 1b: Per-user attributes in Azure (userGroup / domain)

Parts 1b and 1c apply to Mode B only. In Mode A both values come from hardcoded mappers and Azure needs no extension attributes — see Choose a mode: fixed or per-user user-group / domain.

This part makes each Azure user carry their own user-group and domain, delivered to Keycloak as claims in the Azure ID token. It is what enables multiple roles and multiple domains across users.

The value is stored in Azure AD directory-extension attributes on each user and emitted as optional claims. Setting the value per user is done through Microsoft Graph (Graph Explorer or PowerShell) — the Entra portal UI does not expose these fields for editing.

Screenshots

The screenshots in this part were captured on the Keycloak-OneIoT App registration in the Friendly Technologies tenant. The user principal name is shown as <USER_UPN> — substitute the real UPN of the user you are configuring.

6.1. 1b.1 Register two directory-extension attributes on the app

Register the userGroup and domain extension attributes once, on the App registration from Part 1. In Microsoft Graph Explorer, signed in as a directory admin, run:

POST https://graph.microsoft.com/v1.0/applications/{APP_OBJECT_ID}/extensionProperties
Content-Type: application/json

{ "name": "userGroup", "dataType": "String", "targetObjects": ["User"] }

Repeat with "name": "domain". Each response contains a name field of the form extension_{APP_ID_NO_DASHES}_userGroup / extension_{APP_ID_NO_DASHES}_domain — copy both. This full name is the one Microsoft Graph, the app manifest and PowerShell use (Parts 1b.2, 1b.3 and 1c.3). It is not the name the claim carries inside the token — read the warning in 1b.3 Emit the attributes as optional claims (ID token) before configuring the Keycloak mappers in Part 2.6.

{APP_OBJECT_ID} is the App registration’s Object ID (App registration > Overview > Object ID), not the Application (client) ID. {APP_ID_NO_DASHES} in the returned claim name is the Application (client) ID with the dashes removed.

Graph Explorer — extensionProperties registered

6.2. 1b.2 Set the value on each user

For every user that logs in through Azure, set the two attributes to names that exactly match records in the application database for that portal:

PATCH https://graph.microsoft.com/v1.0/users/{USER_ID_OR_UPN}
Content-Type: application/json

{
  "extension_{APP_ID_NO_DASHES}_userGroup": "operator",
  "extension_{APP_ID_NO_DASHES}_domain":    "Super domain"
}
  • userGroup — an existing application user-group name for that portal (e.g. admin, operator).

  • domain — an existing application domain name, or exactly Super domain (with a space) for full access.

The super-domain literal is Super domain — with a space

The backend recognises the super domain only by the exact string Super domain (CommonUtils.SUPER_DOMAIN_NAME). SuperDomain (no space) is not recognised: it is looked up as an ordinary domain name, found nowhere, and the login is rejected with NO_MATCH_AUTH (HTTP 403). The same applies to any user-group or domain value that does not exist in the database for that portal.

To set the values in bulk, use the Microsoft Graph PowerShell SDK: Update-MgUser -UserId <upn> -AdditionalProperties @{ "extension_…​userGroup" = "operator"; "extension…​_domain" = "Super domain" }.
Graph Explorer — PATCH /users returns 204 No Content

6.3. 1b.3 Emit the attributes as optional claims (ID token)

  1. In the App registration, go to Manage > Token configuration.

  2. Click Add optional claim.

  3. Token type: select ID.

  4. In the list, find and check the two extension attributes (userGroup, domain).

  5. Click Add. If prompted to turn on the necessary Microsoft Graph permissions, accept.

Azure — the two extension attributes added as optional claims on the ID token

After this, the Azure ID token that Keycloak receives carries both per-user values. Keycloak reads them in Part 2.6.

The claim name inside the token is NOT the extension name — read this before Part 2.6

A directory-extension attribute has two different names. They are not interchangeable, and using the wrong one is the single most common reason this setup silently does not work.

  • Microsoft Graph, the app manifest and PowerShell (Parts 1b.2, 1b.3, 1c.3) use the full registered name: extension_{APP_ID_NO_DASHES}_userGroup.

  • Inside the issued JWT — and therefore in the Keycloak mapper Claim field (Part 2.6) — Microsoft emits the same attribute as extn.userGroup. The long extension_…​_userGroup string never appears in the token at all.

Put the long name into the Keycloak Claim field and it matches nothing: the mapper saves without complaint, the Azure login succeeds, but the Attribute Importer writes no value. The user ends up with an empty user-group / domain, and the backend rejects the SAML login with NO_MATCH_AUTH (HTTP 403).

On top of that, Keycloak reads a dot in the Claim field as a nested-path separator (as in address.locality), so the dot must be escaped with a backslash. The exact strings to type into the two mappers are:

extn\.userGroup
extn\.domain

7. Part 1c: Multi-domain and multi-group access (per-user assignment)

Part 1b is the one-time setup of the mechanism. This part is the recurring operational procedure: giving one Azure user their own application user-group and their own domain, so that different users of the same portal land in different domains with different permission sets.

This is what makes the installation multi-domain: the domain is no longer a single realm-wide constant, it travels with the user in the Azure ID token.

What you set Where it lives What it controls

userGroup

Directory-extension attribute on the Azure user

The application user-group (permission set), matched by name in the OneIoT database.

domain

Directory-extension attribute on the Azure user

The application domain the user works in, matched by name; the literal Super domain grants access to all domains.

7.1. 1c.1 Prerequisites

Before assigning attributes to a user, all of the following must already be true:

  1. The two extension attributes are registered on the App registration — see 1b.1 Register two directory-extension attributes on the app.

  2. Both are emitted as optional claims on the ID token — see 1b.3 Emit the attributes as optional claims (ID token).

  3. Both Keycloak realms have user-group and domain as Attribute Importer mappers with Sync mode override = Force, and the old Hardcoded Attribute mappers for those two names are deleted — see 2.6. Configuring Identity Provider Mappers.

  4. The account performing the change holds the User Administrator or Global Administrator directory role in Entra ID.

  5. Graph Explorer has admin consent for Directory.ReadWrite.All (or User.ReadWrite.All).

  6. The user-group name and the domain name already exist in the OneIoT database for the target portal.

Application Administrator is not enough

The Application Administrator role lets you register the extension attributes (Part 1b.1) and edit Token configuration, but it does not allow writing to user objects. PATCH /users/…​ then fails with:

{ "error": { "code": "Authorization_RequestDenied",
             "message": "Insufficient privileges to complete the operation." } }

Granting User.ReadWrite.All consent does not fix this — a delegated permission never exceeds the rights of the signed-in user. You need the User Administrator role (or Global Administrator) on the account itself.

To check which roles the signed-in account holds:

GET https://graph.microsoft.com/v1.0/me/transitiveMemberOf/microsoft.graph.directoryRole?$select=displayName

7.2. 1c.2 Choosing the values

userGroup

An existing application user-group name for that portal, for example admin or operator. The name is matched case-sensitively against the database.

domain

Either an existing application domain name, or exactly Super domain for access to all domains.

The super-domain literal is Super domain — with a space

The backend recognises the super domain by the exact string Super domain (CommonUtils.SUPER_DOMAIN_NAME), compared case-insensitively. SuperDomain (no space) is not recognised: it is looked up as an ordinary domain name, found nowhere, and the login is rejected with NO_MATCH_AUTH (HTTP 403). Any other value must already exist in the database for that portal, for user-group as well as for domain — an unknown name locks the user out rather than granting a default.

A typical multi-domain assignment looks like this:

User Portal userGroup domain

Platform administrator

SC

admin

Super domain

Operator, one customer

SC

operator

AussieBB

Operator, another customer

SC

operator

Acme Telecom

Read-only auditor

MC

viewer

Acme Telecom

7.3. 1c.3 Setting the values on a user

The Entra portal UI does not expose directory-extension attributes for editing — they can only be written through Microsoft Graph.

  1. Open Microsoft Graph Explorer and sign in with the account that holds the User Administrator role.

  2. Set the HTTP method to PATCH and the version to v1.0.

  3. Enter the request URL, where <USER_UPN> is the user’s UPN (or their object ID):

    https://graph.microsoft.com/v1.0/users/<USER_UPN>
  4. Open the Request body tab and paste the payload, substituting the exact claim names returned in Part 1b.1 and the values chosen in 1c.2:

    {
      "extension_{APP_ID_NO_DASHES}_userGroup": "admin",
      "extension_{APP_ID_NO_DASHES}_domain":    "Super domain"
    }
  5. Click Run query.

A response of 204 No Content means the attributes were written.

Graph Explorer — PATCH /users returns 204 No Content

Both attributes can be set independently. Sending only one key in the payload leaves the other one untouched.

7.4. 1c.4 Verifying the values in Azure

Directory-extension attributes are not returned by a plain GET /users/{id} — they must be requested explicitly with $select:

GET https://graph.microsoft.com/v1.0/users/<USER_UPN>?$select=userPrincipalName,extension_{APP_ID_NO_DASHES}_userGroup,extension_{APP_ID_NO_DASHES}_domain

The response must contain both values:

Graph Explorer — both extension attributes read back from the user

7.5. 1c.5 Verifying end to end in Keycloak

  1. Sign the user out completely — both from the portal/Keycloak session and from Microsoft (https://login.microsoftonline.com/logout). A cached session keeps serving the previous token and the new values will not appear.

  2. Log in to the portal through Login with Microsoft.

  3. In the Keycloak admin console, open the realm for that portal (oneiot-sc / oneiot-mc) and go to Users > the user > Attributes.

  4. Confirm the attributes:

    user-group

    the value set in 1c.3

    domain

    the value set in 1c.3

    portal-type

    sc or mc, from the hardcoded mapper

  5. Confirm in the portal that the user sees exactly the data of their domain and the permissions of their group.

Because the mappers use Sync mode override = Force, the values are refreshed on every login, not only when the Keycloak user is first created. Any values left over from the old Hardcoded Attribute mappers are overwritten on the next login.

7.6. 1c.6 Changing or revoking access later

To move a user to another domain or group, repeat 1c.3 with the new values. The change takes effect at the user’s next login.

To clear an attribute, send null:

{ "extension_{APP_ID_NO_DASHES}_domain": null }

Keycloak keeps the previously imported attribute on the user until the next successful broker login. When revoking access, also terminate the user’s active sessions (Users > the user > Sessions, or Sessions at realm level), otherwise the current session continues with the old domain until it expires.

7.7. 1c.7 Bulk assignment

For more than a handful of users, use the Microsoft Graph PowerShell SDK instead of Graph Explorer:

Connect-MgGraph -Scopes "User.ReadWrite.All"

Update-MgUser -UserId "<USER_UPN>" -AdditionalProperties @{
  "extension_{APP_ID_NO_DASHES}_userGroup" = "operator"
  "extension_{APP_ID_NO_DASHES}_domain"    = "AussieBB"
}

Driving it from a CSV with UserPrincipalName,UserGroup,Domain columns:

Import-Csv .\users.csv | ForEach-Object {
  Update-MgUser -UserId $_.UserPrincipalName -AdditionalProperties @{
    "extension_{APP_ID_NO_DASHES}_userGroup" = $_.UserGroup
    "extension_{APP_ID_NO_DASHES}_domain"    = $_.Domain
  }
}

7.8. 1c.8 Troubleshooting

Symptom Cause Fix

PATCH returns 403 Authorization_RequestDenied

The signed-in account lacks the User Administrator role, or Graph Explorer has no admin consent for Directory.ReadWrite.All / User.ReadWrite.All.

Check the roles with the GET /me/transitiveMemberOf/…​ query in 1c.1, and grant consent on the Modify permissions tab. The role is the more common cause — consent alone does not help.

PATCH returns 400 Unable to read JSON request payload

Malformed body — typically a stray quote left in the editor when the previous payload was overwritten.

Clear the Request body field completely and retype the JSON.

PATCH returns 404

Wrong UPN, or the guest user’s UPN differs from their email address.

Use the user’s object ID instead of the UPN.

The attribute is missing from the GET response

Directory-extension attributes are only returned when requested.

Add them to $select as shown in 1c.4.

user-group / domain are empty in Keycloak after login

Almost always the mapper Claim field: it still holds the long extension_…​_userGroup name (which never appears in the token), or the dot is not escaped. Less often, the optional claim was never added in Token configuration.

Set the mapper Claim to exactly extn\.userGroup / extn\.domain, backslash included, then re-check Token configuration (1b.3). See the warning in 1b.3 Emit the attributes as optional claims (ID token).

The value in Keycloak does not change after being updated in Azure

The mapper Sync mode is not Force, or a leftover Hardcoded Attribute mapper with the same name is overwriting the imported value.

Set Sync mode override = Force and delete the hardcoded user-group / domain mappers (2.6. Configuring Identity Provider Mappers).

The user logs in but the portal returns NO_MATCH_AUTH / 403

The user-group or domain attribute is missing from the assertion, or the imported name does not exist in the OneIoT database for that portal (including Super domain written without the space). The backend logs which of the two it was.

Correct the value in Azure to a name that exists in the database and have the user log in again.

8. Part 2: Integrating Azure AD into Keycloak

For initial Keycloak setup and realm creation, see SAML SSO — Integration Guide.

The steps below must be performed for each realm (oneiot-sc and oneiot-mc).

8.1. 2.1. Creating the Identity Provider

  1. Open the Keycloak Admin Console at https://<KEYCLOAK_HOST>:8500.

  2. Select the target realm (e.g., oneiot-sc).

  3. In the left menu, navigate to Identity providers.

  4. Click Add provider and select OpenID Connect v1.0.

  5. In the Alias field, change the default value from oidc to microsoft.

    Keycloak — Identity Provider General Settings
    Changing the Alias is mandatory. The Alias determines the broker endpoint URL path.
  6. The Redirect URI field at the top is auto-generated based on the Alias. Copy this value — it will be added to Azure in the next step.

8.2. 2.2. Adding the Redirect URI in Azure

  1. Go back to the Azure App Registration Overview page.

  2. Click Add a Redirect URI.

  3. Click Add a platform and select Web.

  4. Paste the Redirect URI copied from Keycloak.

  5. Click Save.

Repeat steps 2.1—​2.2 for the second realm (e.g., oneiot-mc), so that Azure has two Redirect URIs:

  • https://rd.friendly-tech.com:8500/realms/oneiot-sc/broker/microsoft/endpoint

  • https://rd.friendly-tech.com:8500/realms/oneiot-mc/broker/microsoft/endpoint

8.3. 2.3. Creating a Client Secret in Azure

A Client Secret is required for Keycloak to authenticate with Azure AD. One secret is shared across both Keycloak realms.

  1. In the Azure App Registration, navigate to Manage > Certificates & secrets.

  2. Go to the Client secrets tab.

  3. Click + New client secret.

    Azure — Creating a Client Secret
  4. Enter a description and choose an expiration period.

  5. Click Add.

Copy the secret Value immediately after creation. It is only displayed once. If you lose it, you will need to create a new one.

8.4. 2.4. Obtaining the Discovery Endpoint from Azure

  1. Go back to the Azure App Registration Overview page.

  2. Click the Endpoints button in the top toolbar.

  3. Copy the OpenID Connect metadata document URL.

    The URL format is:

    https://login.microsoftonline.com/<TENANT_ID>/v2.0/.well-known/openid-configuration

8.5. 2.5. Completing the Identity Provider Configuration in Keycloak

Go back to the Keycloak Identity Provider configuration page and fill in the remaining fields:

  1. Discovery endpoint — paste the OpenID Connect metadata document URL from Azure.

    Keycloak — Discovery Endpoint
  2. Client ID — enter the Application (client) ID from the Azure App Registration Overview.

  3. Client Secret — enter the secret Value created in Azure.

    Keycloak — Client Secret
  4. Click Save.

Repeat this step for the second realm using the same Discovery endpoint, Client ID, and Client Secret.

After completing both realms, each Keycloak realm should have a configured microsoft Identity Provider linked to the same Azure AD application.

8.6. 2.6. Configuring Identity Provider Mappers

Mappers define how user attributes from Azure AD are imported into Keycloak. This configuration must be done in both realms on each Identity Provider.

  1. Open the Identity Provider (Login with Microsoft).

  2. Go to the Mappers tab.

  3. Click Add mapper and create the mappers described below.

After all mappers are created, the list should look like this:

Keycloak — Identity Provider Mappers

8.6.1. email

Imports the user email from Azure AD.

Mapper — email

Name

email

Sync mode override

Force

Mapper type

Attribute Importer

Claim

email

User Attribute Name

email

8.6.2. username

Sets the Keycloak username based on the email claim from Azure AD.

Mapper — username

Name

username

Sync mode override

Inherit

Mapper type

Username Template Importer

Template

${CLAIM.email}

Target

LOCAL

8.6.3. portal-type

A hardcoded attribute that defines which portal the user belongs to. This value is not provided by Azure AD — it is assigned by default for every user logging in through this realm.

Mapper — portal-type

Name

portal-type

Sync mode override

Import

Mapper type

Hardcoded Attribute

User Attribute

portal-type

User Attribute Value

sc (for Support Center realm) / mc (for Management Console realm)

In the oneiot-sc realm set the value to sc. In the oneiot-mc realm set the value to mc.
Use the token claim names extn\.userGroup / extn\.domain — with the backslash

The two Attribute Importers below must be given the token claim name, not the long extension_{APP_ID_NO_DASHES}_…​ name from Part 1b.1, and the dot must be escaped with a backslash. Getting this wrong fails silently: the mapper saves, the login succeeds, and the user simply has no user-group / domain. Full explanation in 1b.3 Emit the attributes as optional claims (ID token).

8.6.4. user-group

Imports the per-user application user-group from the Azure extension claim registered in Part 1b: Per-user attributes in Azure (userGroup / domain). Sync mode override = Force so the value is refreshed on every login (not only at user creation).

Name

user-group

Sync mode override

Force

Mapper type

Attribute Importer

Claim

extn\.userGroup (token claim name, backslash-escaped — not the long extension_…​_userGroup name)

User Attribute Name

user-group

Keycloak — user-group Attribute Importer (Claim

8.6.5. domain

Imports the per-user application domain from the Azure extension claim. Identical to the user-group mapper, with domain substituted.

Name

domain

Sync mode override

Force

Mapper type

Attribute Importer

Claim

extn\.domain (token claim name, backslash-escaped — not the long extension_…​_domain name)

User Attribute Name

domain

Keycloak — domain Attribute Importer (Claim
Remove any Hardcoded Attribute mappers for user-group / domain first

These Attribute Importers replace the Mode A mappers. If Hardcoded Attribute mappers for user-group or domain still exist on the Identity Provider — from a Mode A setup or from an earlier revision of this guide — delete them before adding the importers above. A leftover hardcoded mapper writes the same value for every user and overrides the per-user import. Keep only the portal-type hardcoded mapper.

portal-type stays a Hardcoded Attribute in both modes (it is per realm, sc / mc). Only user-group and domain differ: Attribute Importer in Mode B, Hardcoded Attribute in Mode A.
The imported values must exactly match a user-group / domain that already exists in the application database for that portal. A user whose assertion carries an unknown name — or no user-group / domain attribute at all — is rejected with NO_MATCH_AUTH (HTTP 403); nothing is created on the fly and no implicit default is applied. See Choose a mode: fixed or per-user user-group / domain.

9. Part 3: Configuring Authentication Flows

All authentication flow settings described below must be configured in each realm separately.

9.1. 3.1. Browser Flow — Auto-redirect to Azure AD

By default, Keycloak shows its own login page. To skip it and redirect users directly to the Azure AD login page, create a custom Browser flow.

  1. In the left menu, navigate to Authentication.

  2. Find the browser flow in the list.

  3. Click the three-dot menu on the right and select Duplicate.

    Keycloak — Duplicate browser flow
  4. Enter a name for the new flow (e.g., browser-with-auto-idp) and click Duplicate.

  5. Open the new flow.

    Keycloak — Browser flow details
  6. Click the gear icon on the Identity Provider Redirector step.

  7. In the configuration dialog, set Default Identity Provider to microsoft (the same Alias used when creating the Identity Provider).

    Keycloak — Identity Provider Redirector config
  8. Click Save.

  9. Go back to the Authentication flows list.

  10. Find the browser-with-auto-idp flow, click the three-dot menu and select Bind flow.

    Keycloak — Bind flow
  11. Select Browser flow and confirm.

After binding, the Authentication page should show browser-with-auto-idp as the active Browser flow:

Keycloak — Authentication flows final state

9.2. 3.2. Identity Provider — Prompt Setting

To configure how Azure AD prompts users during login:

  1. Open the Identity Provider (microsoft) in the current realm.

  2. Scroll down and expand the Advanced section.

  3. Find the Prompt dropdown and select the preferred option.

    Keycloak — Prompt setting
    Option Behavior

    Select account

    Azure AD shows a list of logged-in Microsoft accounts and allows choosing another one. Recommended.

    Login

    Always shows the Azure AD login form, even if the user is already signed in.

    Consent

    Asks the user for consent to share data with the application.

    None

    No prompt — silently authenticates if a session exists.

Select account is recommended because it allows users to choose between multiple Microsoft accounts or log in with a different one.

By default, when a user logs in via Azure AD for the first time, Keycloak asks to review the profile and verify the email. Since Keycloak acts only as a broker and we trust Azure AD to verify users, these steps should be disabled.

  1. In the Authentication flows list, find the first broker login flow.

    Keycloak — Duplicate first broker login
  2. Click the three-dot menu and select Duplicate.

  3. Enter a name (e.g., first broker login - auto-link) and click Duplicate.

    Keycloak — Duplicate dialog
  4. Open the new flow and change the following steps to Disabled:

    Keycloak — First broker login auto-link settings
    Step Set to Reason

    Review Profile

    Disabled

    Users should not be asked to fill in profile fields on first login.

    Confirm link existing account

    Disabled

    Skip the confirmation prompt when linking to an existing account.

    Verify existing account by Email

    Disabled

    Skip email verification — Azure AD already verified the user.

  5. Save the flow.

9.4. 3.4. Assigning the First Login Flow to the Identity Provider

  1. Open the Identity Provider (microsoft) settings.

  2. Scroll down to the First login flow override field.

  3. Select the newly created flow (first broker login - auto-link).

    Keycloak — First login flow override
  4. Click Save.

Both authentication flows (Browser flow and First login flow) must be configured in each realm separately. Repeat all steps from 3.1 to 3.4 for the second realm.

10. Part 4: Managing Users in Keycloak

10.1. User Attributes

Users created through the Azure AD broker receive:

  • portal-type — sc or mc depending on the realm (Hardcoded Attribute mapper).

  • user-group — imported per user from the Azure userGroup extension claim (Attribute Importer).

  • domain — imported per user from the Azure domain extension claim (Attribute Importer).

Change per-user values in Azure, not in Keycloak

Because the user-group / domain mappers use Sync mode = Force, Keycloak re-imports the Azure value on every login and overwrites any manual edit made on the Keycloak user. To change a user’s group or domain permanently, update the corresponding extension attribute on the user in Azure (Part 1b.2). Editing it only in Keycloak lasts until the next login.

10.2. Editing User Attributes

  1. In the left menu, navigate to Users.

  2. Select the user.

  3. Go to the Attributes tab.

  4. Update the attribute values as needed and click Save.

For user-group and domain this is only a temporary override — the next login re-imports the Azure value (Sync mode = Force). Change these permanently in Azure (Part 1b.2). Use the Keycloak Attributes tab for a one-off, until-next-login adjustment.

10.3. Terminating User Sessions

  1. In the left menu, navigate to Users.

  2. Select the user.

  3. Go to the Sessions tab.

  4. Click Sign out to terminate the active session.

10.4. Deleting Users

  1. In the left menu, navigate to Users.

  2. Select the user.

  3. Click Delete and confirm.

Each realm manages its users independently. Changes made in one realm do not affect the other.

10.5. Recommendations

  • Set every Azure user’s userGroup / domain extension attribute (Part 1b.2) to names that already exist in the application database for that portal. An empty or unknown value — for either attribute — blocks the login with NO_MATCH_AUTH (HTTP 403).

  • Super domain (with a space) grants access to every domain. Assign it deliberately — most users should carry a specific domain instead.

  • To restrict a user, change their userGroup extension attribute in Azure to a group with limited permissions and have them re-login.

  • Every user must belong to exactly one group and one domain value; the claims are single-valued.