# Connecting your identity provider to KodeMed For an organisation that already has an identity provider (IdP) — Microsoft Entra ID, ADFS, Okta, its own Keycloak or any other OpenID Connect or SAML 2.0 provider — and wants its staff to sign in to KodeMed with the account they already have. This is the one guide for both kinds of installation: **on-premises** (your servers, installed with the deployment wizard or `setup.sh`) and **KodeMed SaaS** (an instance we host for you). Where the two differ, the section says so; [section 6](#6-on-premises-and-kodemed-saas) lists every difference in one place. Contents: 1. [Which option to choose](#1-which-option-to-choose) 2. [Before you start](#2-before-you-start) 3. [Option A: federation into KodeMed's Keycloak](#3-option-a-federation-into-kodemeds-keycloak) 4. [Option B: your IdP as the direct issuer](#4-option-b-your-idp-as-the-direct-issuer) 5. [Roles and group mapping](#5-roles-and-group-mapping) 6. [On-premises and KodeMed SaaS](#6-on-premises-and-kodemed-saas) 7. [Making it the only way in](#7-making-it-the-only-way-in) 8. [Verifying it](#8-verifying-it) 9. [When it does not work](#9-when-it-does-not-work) 10. [Doing it from the command line](#10-doing-it-from-the-command-line) > **Windows sign-in without a form (Kerberos)** is a separate guide: `KERBEROS-SSO.md` > (French: `KERBEROS-SSO.fr.md`), on the documentation portal next to this one. When your users > come through your own IdP as described here, Kerberos is configured in **that** IdP. --- ## 1. Which option to choose Both options give the user the same result: they sign in on your provider's page, with your MFA and Conditional Access policies, and KodeMed stores no password of theirs. | | **Option A — federation** (recommended) | **Option B — direct issuer** | |---|---|---| | How it works | Your IdP is added as an *identity provider* inside the Keycloak that comes with KodeMed. Your IdP authenticates the person; KodeMed's Keycloak issues the token KodeMed's services validate. | KodeMed's bundled Keycloak is removed and the services trust your IdP's tokens directly. | | Protocols | OpenID Connect (A1) or SAML 2.0 (A2) | OpenID Connect only | | Roles | Your groups are translated into KodeMed roles inside KodeMed's realm. You create, rename or restructure nothing in your directory. | Your IdP must deliver the KodeMed roles in the token itself (see [section 4](#4-option-b-your-idp-as-the-direct-issuer)). | | KodeMed SaaS | Yes — the only option | No: every hosted instance has its own Keycloak | | On-premises | Yes | Yes, with `setup.sh --no-keycloak --oidc-issuer=` | | Microsoft Entra ID | Works with configuration only | Needs instance-specific configuration on our side; agree it with us first | **Choose option A unless you have a firm reason not to.** It is the only option that works with Entra ID out of the box, the only one available on KodeMed SaaS, and it keeps the mapping from your groups to KodeMed's roles in one place, applied identically by every KodeMed service. --- ## 2. Before you start ### Who does the work The person who registers KodeMed in your IdP needs enough rights there. This is what most often stretches a connection over weeks, so check it first. For Microsoft Entra ID: | Action | Entra role needed | |---|---| | Create the app registration (options A1 and B) | **Application Developer** | | Create the enterprise application and configure SAML single sign-on (option A2) | **Cloud Application Administrator** | | Grant admin consent; configure group claims | **Cloud Application Administrator** | In practice **Cloud Application Administrator** covers all three options. An application owner alone cannot grant admin consent: the configuration then looks complete, and the first sign-in fails with a consent prompt the user cannot approve. If the person with that role is not your technical contact, involve them from the start rather than halfway through. ### One registration per environment If you run more than one KodeMed environment — typically a test (acceptance) instance and a production instance — connect **each** of them, with **one app registration per environment** rather than one registration carrying both sets of URLs. You can then change the test configuration, renew a secret or open access to a different group without touching production. The procedure is identical for each environment; only the host name changes. ### The addresses used in this guide Every URL below is built from the base address of KodeMed's Keycloak, written ``: | Installation | `` | |---|---| | KodeMed SaaS | `https:///auth` | | On-premises, single domain (`--single-domain`) | `https:///auth` | | On-premises, subdomains | `https://` (no `/auth`) | | On-premises, IP address only | `https://:9443` | From it: | | | |---|---| | Realm (the SAML entity ID) | `/realms/kodemed` | | Redirect / reply URL | `/realms/kodemed/broker//endpoint` | | Admin console | `/admin/` | `` is the short name you give your provider in KodeMed's Keycloak (see below). **Pick it first**: it is part of the redirect URL, and renaming it later means re-registering that URL on your side. The examples in this guide use `entra`. If you are unsure of your ``, ask us; we send the exact URLs with your instance's host, realm and alias already filled in. Copy those character for character — a redirect URL that differs by one character is rejected. --- ## 3. Option A: federation into KodeMed's Keycloak Two steps, in this order: register KodeMed in your IdP (A1 **or** A2), then add your IdP in KodeMed's Keycloak ([3.3](#33-add-the-provider-in-kodemeds-keycloak)). On KodeMed SaaS you can do the second step yourself, or send us the values listed under *What to send us* and we do it. ### 3.1 A1 — OpenID Connect (the simplest) **In Entra ID:** *App registrations → New registration*: | Setting | Value | |---|---| | Name | `KodeMed` (one registration per environment, e.g. `KodeMed Test`, `KodeMed`) | | Supported account types | Accounts in this organizational directory only | | Redirect URI | Platform **Web**: `/realms/kodemed/broker//endpoint` | Then: 1. *Certificates & secrets → New client secret.* The value is shown once; store it in your password manager before leaving the page. 2. *Token configuration → Add optional claim → ID token*: add `email` and `upn`, and accept the Microsoft Graph permissions the portal offers to add. 3. *API permissions → Grant admin consent.* > **Do not mix up the client ID and the client secret.** The secret is long and random; the > client ID (*Application (client) ID*) is a UUID. Pasting the secret into the ID field is the > most common mistake here, and it fails later with a message that mentions neither. For another OpenID Connect provider the steps have different names but the same content: a **confidential** client, the redirect URL above, and a client secret. **What to send us** (if we do the Keycloak side for you): - the tenant ID (*Directory (tenant) ID*); - the client ID (*Application (client) ID*); - the client secret — through a secure channel, never in a plain email; - the discovery URL: `https://login.microsoftonline.com//v2.0/.well-known/openid-configuration` ### 3.2 A2 — SAML 2.0 If your organisation standardises on SAML, use it: Entra ID supports both protocols, and KodeMed's Keycloak brokers both. **In Entra ID:** *Enterprise applications → New application → Create your own application → Integrate any other application you don't find in the gallery*, then *Single sign-on → SAML*: | Entra setting | Value | |---|---| | Identifier (Entity ID) | `/realms/kodemed` | | Reply URL (Assertion Consumer Service URL) | `/realms/kodemed/broker//endpoint` | | Sign on URL | the address your users open KodeMed at, e.g. `https:///` | | Unique User Identifier (Name ID) | `user.mail`, format *Email address* | Claims to emit (*Attributes & Claims*): the email address, given name and surname — Entra emits them by default — and the group claim if you map roles from groups (see [section 5](#5-roles-and-group-mapping)). > If some accounts have no `mail` attribute, use `user.userprincipalname` as the Name ID. An empty > Name ID makes sign-in fail for those accounts only, which is hard to diagnose. **What to send us:** the **App Federation Metadata Url**, of the form `https://login.microsoftonline.com//federationmetadata/2007-06/federationmetadata.xml?appid=`. That is all: the signing certificate and the endpoints are read from it. ### 3.3 Add the provider in KodeMed's Keycloak 1. Open the admin console, `/admin/`, and sign in (where the credentials are: [section 6](#6-on-premises-and-kodemed-saas)). 2. **Switch to the `kodemed` realm** with the selector at the top left. 3. *Identity providers* → **OpenID Connect v1.0** (A1) or **SAML v2.0** (A2). When your provider is itself a Keycloak, *Keycloak OpenID Connect* works too. > **The realm matters more than anything else in this section.** The console opens in > `master`, which exists only to administer Keycloak itself. A provider added there lets *you* > sign in to the console and does nothing at all for your users. If you finish the setup and > the KodeMed sign-in page is unchanged, this is almost always why. | Field | Value | |---|---| | Alias | Short, lowercase, no spaces, e.g. `entra`. Case-sensitive; it is part of the redirect URL. | | Display name | The text on the sign-in button. Write it for a clinical coder, not an administrator: *Sign in with your hospital account*, not `oidc-prod`. | | Discovery endpoint (A1) | The discovery URL from 3.1. Keycloak fills in the endpoints from it. | | Client ID / Client secret (A1) | From 3.1. | | Use entity descriptor / SAML entity descriptor (A2) | On; the App Federation Metadata Url from 3.2. | | Service provider entity ID (A2) | `/realms/kodemed` — must equal the Entra *Identifier*. | | NameID policy format (A2) | *Email*; *Principal type*: *Subject NameID*. | | Trust Email | **On** (see below). | Then the four settings below. They are not optional, and none has a useful default. #### Trust Email Turn **Trust Email** on. Your provider is yours and the addresses are your own, so Keycloak may treat the email it sends as verified. On **KodeMed SaaS** this is essential. We create your users in advance from the contacts you gave us, each with the KodeMed roles they need, and the instance's realm links a person arriving through your IdP to that prepared account **by email address**, carrying the roles across. With Trust Email off that match cannot be made safely: Keycloak creates a second, separate account with **no roles at all**. The person signs in successfully and then cannot do anything, while the prepared account sits unused. On **on-premises** installations users are normally created by their first sign-in, so there is no prepared account to link. If you did create local accounts beforehand, Keycloak's standard first-login flow asks the person to confirm the link to the existing account with that account's own credentials. #### Scopes (A1) **Field:** *Scopes* (under *Advanced*) → `openid email profile` (add `groups` if your provider needs that scope to emit group claims). Left empty, Keycloak requests only `openid`. The token then carries no `email`, no `given_name` and no `family_name`, and every mapping of those claims silently resolves to nothing. Nothing errors; the fields simply arrive blank. Check the other side too: the client must actually be granted `email` and `profile`. Requesting a scope the provider does not grant changes nothing. #### What becomes the username *Mappers* → *Add mapper* → **Username Template Importer**: | | A1 (OpenID Connect) | A2 (SAML) | |---|---|---| | Template | `${CLAIM.email}` | `${NAMEID}` (the Name ID is the email, see 3.2) | | Target | `LOCAL` | `LOCAL` | | Sync mode override | `Force` | `Force` | Without this, Keycloak derives the username from whatever the provider sends, which is often a display name — *"Anna Müller"*, with a space in it. **Keycloak's default username validation rejects spaces**, so the user is left with an invalid profile and the login stops at a *Review Profile* form asking them to fix a username they never chose. It looks like a broken login and generates support calls. An email address is unique, stable and always a valid username. If your users have no email in the directory, map something else that is — `${CLAIM.preferred_username}` (the UPN, in Entra ID) or `${CLAIM.sub}` — but map something deliberately. **Where a bad username usually comes from.** It is rarely invented by the provider you connect; it is carried in from a directory behind it. When that provider federates an LDAP or Active Directory, its *Username LDAP attribute* decides the username there — and if it points at a display-name attribute such as `cn` instead of `uid`, `sAMAccountName` or `mail`, every user whose `cn` is "Firstname Lastname" arrives with a space in their username. Only some users are affected (those whose chosen attribute happens to be empty fall back to something else), so it looks arbitrary from this side. Fix it at the source as well, but map the username here regardless: this is the boundary you control. For SAML, map the name and email too: *Add mapper* → **Attribute Importer**, one per attribute, from the claim name Entra lists under *Attributes & Claims* (for example `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname`) to the user attribute `firstName`, `lastName` or `email`, sync mode `Force`. #### Sync mode **Field:** *Sync mode* (under *Advanced*) → `Force`. `Import` (the default) copies the profile once, at first login, and never looks again. Someone who changes their surname, or whose email is corrected in your directory, keeps the old values in KodeMed for ever. `Force` re-reads them on every login — and it is what makes the username mapper above repair accounts created before you added it. #### The Review Profile step Even with the above correct, Keycloak's **first broker login** flow can stop a new user at a profile-confirmation form. *Authentication → Flows → first broker login* → the gear icon on *Review Profile* → `update.profile.on.first.login`: | Value | Behaviour | |---|---| | `missing` (default) | Shown when the profile Keycloak built is incomplete **or invalid** | | `on` | Shown to everyone, always | | `off` | Never shown | Keep `missing`. If the form appears with every field already filled in, it is telling you something is *invalid* rather than missing — most often the username problem above. Fix the mapping first; setting `off` without fixing it hides the symptom and leaves unusable usernames. This setting belongs to the **realm**, not to one provider: changing it affects every identity provider in the realm. ### 3.4 Test it Open KodeMed in a **private browsing window**: the sign-in page now shows your provider's button. Sign in with a real account from your organisation and confirm you land in KodeMed with the right permissions. A private window matters: an ordinary one may still hold an earlier session and show you a success that is not the thing you just configured. Then go through [section 8](#8-verifying-it). --- ## 4. Option B: your IdP as the direct issuer On-premises only. KodeMed's bundled Keycloak is left out and every service validates your IdP's tokens directly: ```bash bash setup.sh --domain=kodemed.hospital.ch --no-keycloak \ --oidc-issuer=https://sso.hospital.ch/realms/kodemed ``` (The deployment wizard offers the same choice.) Your IdP then has to provide everything the bundled Keycloak would have: - **The issuer address.** The Coding UI derives its sign-in address from the issuer by splitting it at `/realms/` — the Keycloak convention, as in the example above. - **The roles, in the token.** The services read roles from the `realm_access.roles` claim — the Keycloak layout. The KodeMed Server falls back to a `groups` claim when `realm_access` is absent; the DataServer and GrouperServer do not. Names that are not KodeMed's own can be translated with `role-mapping.json` (see [5.4](#54-role-mappingjson-option-b)). - **The clients.** A public client for the Coding UI (authorization code + PKCE, redirect URIs `https:///` and `https:///silent-renew.html`), and a public client for the Windows client (`kodemed-dll`, PKCE, redirect `http://localhost:*`). See the Integration Guide, section *Authentication - OAuth2 and OIDC*. An IdP that is itself a Keycloak meets all three. **Microsoft Entra ID meets none of them as it stands**: its issuer is `https://login.microsoftonline.com//v2.0` (no `/realms/`), and its tokens carry roles in `roles` and groups in `groups`, never in `realm_access`. Pointing KodeMed directly at Entra ID therefore needs configuration specific to your instance on our side, and with it a delay. **Agree it with us before you commit to this path**; option A needs none of it. If option B with Entra ID is agreed, this is what your technical contact prepares: ### 4.1 App registration — SPA platform *App registrations → New registration*, with redirect URIs of platform type **SPA** (not *Web*), two per environment: - `https:///` - `https:///silent-renew.html` — silent token renewal; without it users are signed out in the middle of coding. The SPA type is required: it enables the authorization code flow with **PKCE** and no client secret, the only acceptable flow for an application that runs in the browser. ### 4.2 Expose an API (the step most often forgotten) By default Entra ID issues no access token a third-party API can use. In the same registration, *Expose an API*: - set the Application ID URI (e.g. `api://`); - add a scope, e.g. `access_as_user`, consentable by admins; - let the application itself use that scope (*API permissions → My APIs*), and grant admin consent. Without this, the access token Entra issues is meant for Microsoft Graph and KodeMed rejects it: the user signs in "successfully" and every API call answers 401. ### 4.3 Force v2 tokens In the application's **manifest** set: ```json "accessTokenAcceptedVersion": 2 ``` (Depending on the manifest view the portal shows, the same setting appears as `api.requestedAccessTokenVersion`.) Without it Entra issues v1 tokens whose issuer is `https://sts.windows.net//` instead of `https://login.microsoftonline.com//v2.0`, and the issuer must match exactly what KodeMed validates. ### 4.4 What to send us - the tenant ID; - the client ID; - the Application ID URI and the exposed scope (`api:///access_as_user`); - confirmation that the tokens are v2. No client secret is involved: the clients are public and use PKCE. --- ## 5. Roles and group mapping A user who authenticates but holds no KodeMed role reaches the application and can do nothing. This is the step people forget, because the login itself works. ### 5.1 The roles | Role | What it grants | |---|---| | `kodemed-viewer` | Read-only access to cases | | `kodemed-coder` | Create and edit cases | | `kodemed-coder-controller` | Supervisor coder; may correct the Administratives block (audited) | | `kodemed-approver` | Approve coded cases | | `kodemed-auditor` | Audit trail and session logs | | `kodemed-data-admin` | Classifications and imports | | `kodemed-admin` | Full system administration | A `kodemed-`prefixed role grants both the prefixed and the plain authority — `kodemed-admin` satisfies `ADMIN` as well as `KODEMED_ADMIN` — so a provider that publishes plain `admin` / `coder` / `auditor` works without renaming anything. One group may be mapped to several roles. The full reference is `KodeMed_Roles_and_Permissions.md`. ### 5.2 Where roles come from with option A The token KodeMed's services validate is issued by **KodeMed's** Keycloak, so what counts is the roles the user holds in its **`kodemed` realm**. Your directory's groups do not become roles by themselves; Keycloak mappers on your identity provider translate them, before the token reaches any service — so the mapping applies uniformly across the product and can change without interrupting the service. **You do not need to create, rename or restructure any group.** Three ways to assign roles, each a mapper under *Identity providers → `` → Mappers*: **1. A baseline for everyone from your provider.** *Add mapper* → **Hardcoded Role**, role `kodemed-coder`, sync mode `Force`. Anyone your directory lets in can code; the narrower roles are then granted deliberately. A good default where your IdP is already the gate. **2. From one of your groups.** *Add mapper* → **Advanced Claim to Role** (A1) or **Advanced Attribute to Role** (A2): | | | |---|---| | Claims / Attributes | `groups` = your group's value (A1); for A2 the attribute is `http://schemas.microsoft.com/ws/2008/06/identity/claims/groups` | | Role | e.g. `kodemed-approver` | | Sync mode override | `Force` | For this to fire the claim has to be in what your IdP sends (see 5.3), and with A1 `groups` may need to be among the requested *Scopes*. If you are not sure what arrives, see [section 8](#8-verifying-it). **3. Directly, for the few.** *Users → the user → Role mapping.* Appropriate for two or three administrators; it does not scale and it is not reproducible, so do not run a hospital this way. **Sync mode, again.** Set `Force` on these mappers too. With `Import`, a role granted by a mapper is applied once, at first login, and never re-evaluated: moving someone out of a group in your directory leaves their KodeMed access exactly as it was. On KodeMed SaaS we can set these mappers up for you: send us which group should hold which role. ### 5.3 Emitting groups from Entra ID Where to switch it on depends on the option: - **A1 and B (OpenID Connect)** — in the app registration: *Token configuration → Add groups claim*. - **A2 (SAML)** — in the enterprise application: *Attributes & Claims → Add a group claim*. The groups then arrive as the SAML attribute `http://schemas.microsoft.com/ws/2008/06/identity/claims/groups`. One point decides the rest: > By default Entra ID puts the **object IDs (GUIDs)** of the groups in the `groups` claim, not > their names. The name format (`sAMAccountName`) is offered only for groups **synchronised from > an on-premises Active Directory**. For groups created in the cloud, only the GUIDs exist. Both work: - **Groups synchronised from your on-premises AD** → choose `sAMAccountName`, and map by name. - **Cloud-only groups** → keep the default (GUID), and map by GUID. Keep a list of name + GUID for each group concerned; if we do the mapping for you, that list is what we need. Two operational precautions: 1. Choose **Groups assigned to the application** rather than *All groups*. Above roughly 200 groups per user (150 in SAML) Entra stops emitting the list and replaces it with a reference to Microsoft Graph: the roles then disappear silently and the user loses their access. Restricting the claim to the groups assigned to the application removes that risk. 2. Enable the group claim **for every token type** used (ID token and access token). Enabling it on one only is a classic cause of "signed in, but no rights at all". > Prefer mapping a handful of groups over pushing your whole directory into the token. A site > that carried hundreds of AD groups grew its tokens past the HTTP header limit, and every API > call began failing with a 400 that read, in the browser, as a CORS error. **Alternative: application roles.** Instead of group claims you can define *App roles* in the app registration — for example with the KodeMed role names as values — and assign your groups to them. Entra then emits them in the `roles` claim; with option A, map that claim with an **Advanced Claim to Role** mapper exactly as for `groups`. A little more work in Entra, but the mapping becomes visible in the Entra portal. Both approaches are equivalent for KodeMed. ### 5.4 `role-mapping.json` (option B) With option B there is no KodeMed realm to hold mappers: the role and group names your IdP puts in the token reach the services as they are. KodeMed can alias each such name to existing KodeMed roles from **admin Settings** (*Role mapping*), which writes `role-mapping.json` beside the deployment: ```json { "roleMappings": { "MedCoders": ["kodemed-coder"], "DRG-Team": ["kodemed-admin"] }} ``` The services read the file named by `KODEMED_ROLE_MAPPING_FILE` (the generated compose sets `/app/role-mapping.json`) and reload it within about 30 seconds of a change — no restart. Names are matched case-insensitively, and the mapping only ever **adds** roles. It applies to the names the services read from the token (section 4): `realm_access.roles`, and in the Server the `groups` fallback. With option A, map in Keycloak instead (5.2) — the token then carries KodeMed's realm roles, not your group names. --- ## 6. On-premises and KodeMed SaaS | | On-premises | KodeMed SaaS | |---|---|---| | Options | A or B | A only | | `` | depends on the deployment mode ([section 2](#the-addresses-used-in-this-guide)) | `https:///auth` | | Admin console | `/admin/` | `https:///auth/admin/` | | Admin credentials | User `KEYCLOAK_ADMIN` (default `admin`), password `KEYCLOAK_ADMIN_PASSWORD`, both in the `.env` of the installation | In the KodeMed customer portal, on your instance's row. **Never sent by email.** | | Users | Created at their first sign-in through your IdP | Prepared by us from your contacts, with their roles, and linked by email at first sign-in (Trust Email on) | | Who configures Keycloak | You | You, with the credentials above — or send us the values in section 3 and we do it | | Command line | Available ([section 10](#10-doing-it-from-the-command-line)) | Not available; use the admin console | The instance's own administrator account lives in the `master` realm and is unaffected by anything in this guide, so federating the `kodemed` realm cannot lock you out of the console. Tell your KodeMed contact before you begin and we will go through it with you. The mapping between your groups and KodeMed's roles is the part worth agreeing up front rather than discovering afterwards. --- ## 7. Making it the only way in Optional, and only after a real sign-in through your provider has worked. You can remove the username/password form so everybody goes through your provider: 1. *Authentication → Flows → browser*. 2. On the **Identity Provider Redirector** row, open its settings (the gear). 3. Set **Default Identity Provider** to your alias, and save. Users then go straight to your provider's sign-in page and never see a KodeMed password field. A sign-in request can also carry `kc_idp_hint=` to the same effect for that request. The administrator account in the `master` realm is unaffected, so you will not be locked out — but a federation that is both broken and the only route in makes for a bad afternoon. --- ## 8. Verifying it Sign in once as a test user, in a private browsing window, then check: 1. The login completes without a profile form. 2. *Users →* the new user: the username is the email, first and last name are filled in. 3. *Users →* the user *→ Role mapping*: the expected KodeMed roles are there. 4. Sign in a second time. `Force` sync means no "account already exists" prompt and no duplicate. 5. In KodeMed, the user can do what their role allows. To see exactly what your provider sent, turn on *Store tokens* on the identity provider temporarily and read the token from the user's *Identity provider links*. Turn it off again afterwards: stored tokens are credentials. --- ## 9. When it does not work | Symptom | Cause | |---|---| | The sign-in page is unchanged | The provider was added to the `master` realm instead of `kodemed`. Check the realm selector at the top left. | | `AADSTS50011` (redirect URI mismatch) | The redirect URL registered in Entra does not match what Keycloak sent. The error quotes the URL actually sent: compare it character by character. Usually the alias differs (it is case-sensitive), `/auth` is missing or extra, or a trailing slash crept in. | | `invalid_redirect_uri` on another IdP | Same cause: the registered URL is not exactly `/realms/kodemed/broker//endpoint`. | | Profile form with every field filled in | The username is invalid — a space, usually. Fix the Username Template Importer. | | First/last name blank | `profile` missing from *Scopes*, or not granted to the client by your IdP; with SAML, no Attribute Importer for them. | | Email blank | `email` missing from *Scopes*, the optional claim not added in Entra, or the directory has no email for that user. | | SAML sign-in fails for some accounts only | Those accounts have an empty Name ID — no `mail` attribute. Use `user.userprincipalname` as the Name ID. | | Consent prompt the user cannot approve | Admin consent was not granted, or was granted by someone without the right Entra role (section 2). | | "Invalid username or password" after your provider accepted you (SaaS) | The account could not be linked. Check that **Trust Email** is on and that the address your provider sends matches the contact address you gave us. | | Signed in, but nothing is permitted | No role arrived. SaaS: a second account was created instead of linked (Trust Email off) — tell us and we merge it; do not delete anything first. Otherwise: no role mapper, the group claim missing from the token (or from that token type), group overage (section 5.3), or a mapper set to `Import`. | | Every API call fails with 400, shown in the browser as a CORS error | The token grew past the HTTP header limit from too many group claims. Restrict to *Groups assigned to the application*. | | Option B: signed in, every API call answers 401 | The access token is meant for Microsoft Graph (no exposed API, section 4.2), or it is a v1 token whose issuer does not match (section 4.3). | | Login loops back to the KodeMed login page (on-premises) | KodeMed's Keycloak is behind a proxy that does not send `X-Forwarded-Proto: https`; Keycloak then issues session cookies without `Secure`, which the browser drops. See `EXTERNAL-PROXY.md`. | | Changes to a user in your directory never appear | *Sync mode* is `Import`, not `Force`. | --- ## 10. Doing it from the command line On-premises only. The scopes, sync mode and username mapper of section 3.3, for an instance you cannot click through (`` is the provider's alias; run on the host that holds the deployment): ```bash cd /opt/kodemed PW=$(grep '^KEYCLOAK_ADMIN_PASSWORD=' .env | cut -d= -f2-) KC=/opt/keycloak/bin/kcadm.sh docker exec -e KCPW="$PW" kodemed-keycloak sh -c " $KC config credentials --server http://localhost:8080 --realm master \ --user admin --password \"\$KCPW\" $KC update identity-provider/instances/ -r kodemed \ -s 'config.defaultScope=openid email profile' \ -s 'config.syncMode=FORCE' $KC create identity-provider/instances//mappers -r kodemed \ -s name=email-as-username \ -s identityProviderAlias= \ -s identityProviderMapper=oidc-username-idp-mapper \ -s 'config.\"template\"=\${CLAIM.email}' \ -s 'config.\"target\"=LOCAL' \ -s 'config.\"syncMode\"=FORCE' " ``` Use `podman exec` on a Podman host. With `--single-domain` Keycloak runs under `/auth`, so use `--server http://localhost:8080/auth`. To read back what is configured, `get` the provider **without** `--fields config` — that option does not render nested maps, so the configuration looks empty when it is not. `kodemed-doctor.sh` checks every identity provider in the `kodemed` realm for an explicit username mapper and names this guide when one is missing.