Federating Keycloak with Entra ID: Keycloak as Primary Identity Provider

In Part 4 I covered the three identity options — Entra ID, Entra External ID, and Red Hat Keycloak — and where each one makes sense. One of the nuances I flagged was that if you're moving workforce identity to Keycloak but you're still running Microsoft 365, you can't just unplug Entra ID. Teams, SharePoint, Exchange Online, Intune — they're all anchored to Entra ID as the identity backbone. But that doesn't mean you're stuck with Entra ID as your authoritative identity source.

The pattern that actually works in practice: Keycloak as your primary IdP (the thing that authenticates users and owns the identity record), with Entra ID as a downstream service provider that trusts SAML assertions from Keycloak. Your organisation's users authenticate through Keycloak, and Entra ID gets a signed token it trusts — so M365 keeps working. This is called domain federation or external federation in Entra ID terminology.

So that in mind, this post covers what the architecture actually looks like, how to set it up end-to-end with real commands, and — importantly — what degrades or breaks when you're no longer doing native Entra ID authentication. That last part tends to get omitted, which leads to unpleasant surprises post-migration.

The Architecture: How the Pieces Fit

The standard pattern is Entra ID domain federation. Here's what happens at a high level:

  1. Your Entra ID tenant's custom domain (e.g., corp.example.fi) is converted from "managed" to "federated". This tells Entra ID that authentication for users with that domain suffix should be handled externally.
  2. Entra ID stores the metadata for your external IdP — the SAML signing certificate and endpoint URLs — so it knows where to redirect authentication requests and how to validate the responses.
  3. When a user signs into a Microsoft 365 app with user@corp.example.fi, Entra ID detects the federated domain and redirects them to your Keycloak sign-in page.
  4. The user authenticates in Keycloak. Keycloak issues a SAML Response signed with its SP key, sending the user's UPN back to Entra ID.
  5. Entra ID validates the signature, finds the matching user record (by UPN or ImmutableID), and issues tokens for the M365 application.

The user record still exists in Entra ID — you're not removing it. Entra ID is still the directory that M365 uses for authorisation (groups, licenses, SharePoint permissions). You're just replacing the authentication step with an external one. Entra ID becomes a flat directory that trusts your Keycloak for login.

🔍 Click to zoom
Domain federation flow. Entra ID redirects auth to Keycloak; SAML response flows back through the browser; Entra ID validates and issues M365 tokens.

A few things to get clear before diving into setup:

  • ImmutableID matters: Each Entra ID user object has an ImmutableID attribute. In a pure-cloud tenant this is the Base64 encoding of the object's GUID. When Keycloak issues a SAML assertion, the NameID it sends must match the ImmutableID on the matching Entra ID object — that's how Entra ID knows which user account to sign in. If these don't align, authentication fails with an unhelpful error.
  • UPN suffix must match the federated domain: The UPN in Entra ID (e.g., anna@corp.example.fi) must use the domain you're federating. The default .onmicrosoft.com domain can't be federated — Microsoft requires at least one verified custom domain.
  • You can federate one domain at a time: If your tenant has multiple custom domains, you can choose to federate some and leave others managed. Good for phased migrations or multi-entity tenants.

Configuring Keycloak as a SAML IdP

On the Keycloak side, you're creating a SAML service provider (SP) configuration that represents Entra ID. Entra ID will be the relying party; Keycloak is the IdP that handles authentication and returns signed assertions.

Step 1: Create a SAML Client in Keycloak

Log in to the Keycloak Admin Console for your realm. Create a new Client with the following settings (these match Entra ID's SAML federation requirements):

# Keycloak Admin CLI (kcadm.sh) — run from within the Keycloak pod or container
# Set your realm name
REALM="corp"

# Create the SAML client representing Entra ID
kcadm.sh create clients -r $REALM -s clientId="urn:federation:MicrosoftOnline" \
  -s protocol=saml \
  -s enabled=true \
  -s "redirectUris=[\"https://login.microsoftonline.com/login.srf\"]" \
  -s "attributes.saml_name_id_format=urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" \
  -s "attributes.saml.force.name.id.format=true" \
  -s "attributes.saml.authn.statement=true" \
  -s "attributes.saml.server.signature=true" \
  -s "attributes.saml.signing.certificate=true"

The clientId must be exactly urn:federation:MicrosoftOnline — that's the entity ID Entra ID uses when it sends the AuthnRequest.

Step 2: Add the ImmutableID Mapper

The SAML assertion's NameID must carry the user's ImmutableID. The simplest approach is to store the Entra ID ImmutableID value as a custom attribute on the Keycloak user object (immutableid), then map it into the assertion.

# Add a User Attribute mapper for the NameID
kcadm.sh create clients/$CLIENT_ID/protocol-mappers/models -r $REALM \
  -s name="immutableid-nameid" \
  -s protocol=saml \
  -s protocolMapper=saml-user-attribute-mapper \
  -s "config.\"user.attribute\"=immutableid" \
  -s "config.\"attribute.name\"=NameID" \
  -s "config.\"attribute.nameformat\"=Basic" \
  -s "config.\"friendly.name\"=ImmutableID"

Important: The immutableid attribute must be populated on every Keycloak user that will sign into M365. When provisioning users, compute the value as Base64(GUID_bytes) from the Entra ID user object's id property. You can pull this via Microsoft Graph: GET /users/{id}?$select=onPremisesImmutableId,id. For a cloud-only tenant, calculate it from the object GUID. For hybrid (AAD Connect-synced) users, the value is already set in onPremisesImmutableId.

Step 3: Export the Keycloak Signing Certificate

Entra ID needs Keycloak's public signing certificate to validate the SAML Response. Get it from the Keycloak realm's SAML metadata endpoint:

# Download Keycloak's SAML signing certificate
curl -s "https://keycloak.corp.example.fi/realms/corp/protocol/saml/descriptor" \
  | grep -oP '(?<=<ds:X509Certificate>)[^<]+' \
  | head -1 \
  | sed 's/.\{64\}/&\n/g' \
  | awk 'BEGIN{print "-----BEGIN CERTIFICATE-----"} {print} END{print "-----END CERTIFICATE-----"}' \
  > keycloak-signing.pem

Configuring Domain Federation in Entra ID

Now the Entra ID side. You're converting your custom verified domain from managed (Entra ID handles auth) to federated (external IdP handles auth). This is done via Microsoft Graph — the older Set-MsolDomainAuthentication PowerShell cmdlet works but is part of the deprecated MSOnline module. Use Graph where you can.

Prerequisites check

# Check that your custom domain is verified and currently managed
Connect-MgGraph -Scopes "Domain.ReadWrite.All"

Get-MgDomain | Select-Object Id, AuthenticationType, IsVerified | Format-Table
# Look for your domain with AuthenticationType = Managed and IsVerified = True
# If it shows Federated already, it was previously configured

Set the domain to federated

# Variables — fill in your values
$domain          = "corp.example.fi"
$issuerUri       = "https://keycloak.corp.example.fi/realms/corp"
$metadataUrl     = "https://keycloak.corp.example.fi/realms/corp/protocol/saml/descriptor"
$passiveSignInUrl = "https://keycloak.corp.example.fi/realms/corp/protocol/saml"
$signingCert     = (Get-Content keycloak-signing.pem | Where-Object { $_ -notmatch "---" }) -join ""

# Build the federation settings body
$body = @{
    issuerUri                            = $issuerUri
    metadataExchangeUri                  = $null
    passiveSignInUri                     = $passiveSignInUrl
    preferredAuthenticationProtocol      = "saml"
    signingCertificate                   = $signingCert
    isSignedAuthenticationRequestRequired = $false
} | ConvertTo-Json

# POST to Graph to configure federation
Invoke-MgGraphRequest -Method POST `
  -Uri "https://graph.microsoft.com/v1.0/domains/$domain/federationConfiguration" `
  -Body $body `
  -ContentType "application/json"

Heads up: When you set a domain to federated, all users with that UPN suffix immediately start being redirected to Keycloak on their next sign-in. There's no gradual rollout — it's a binary switch for the whole domain. Test this on a non-production domain first, or create a test domain alias (e.g., corp-test.example.fi) before cutting over production users.

Verify the federation configuration

# Check the domain's authentication type
Get-MgDomain -DomainId $domain | Select-Object Id, AuthenticationType

# Retrieve the federation configuration to confirm settings
Invoke-MgGraphRequest -Method GET `
  -Uri "https://graph.microsoft.com/v1.0/domains/$domain/federationConfiguration"

Testing end-to-end

# Quick sign-in test using a Graph token — authenticates through the federation
# Open this in a private browser window to force a fresh sign-in
$testUrl = "https://login.microsoftonline.com/<your-tenant-id>/oauth2/v2.0/authorize?" +
           "client_id=00000002-0000-0000-c000-000000000000" +
           "&response_type=token" +
           "&redirect_uri=https%3A%2F%2Flogin.microsoftonline.com%2Fcommon%2Foauth2%2Fnativeclient" +
           "&scope=openid" +
           "&login_hint=anna@corp.example.fi"
Start-Process $testUrl

When you open this link, Entra ID should redirect you to Keycloak's sign-in page. After authenticating in Keycloak, you should land back in the Entra ID session. If you see Entra ID's own login page instead, the domain federation isn't applied yet or the UPN doesn't match the federated domain.

What Keeps Working

The good news: for the core M365 experience, federation is surprisingly transparent to end users.

Standard M365 app access

Teams, SharePoint Online, Exchange Online, OneDrive, Outlook on the web — all of these work. The user signs in through Keycloak, gets redirected back to Entra ID with a valid SAML assertion, and Entra ID issues OAuth 2.0 tokens for the M365 applications as normal. From the app's perspective, nothing changed. Groups, SharePoint permissions, Teams channel memberships, Exchange mailboxes — all still controlled through Entra ID objects.

Azure RBAC and subscription access

Works fine. RBAC authorisation is still Entra ID's job. Authentication is delegated to Keycloak, but role assignments, subscription access, and resource group permissions all live in Entra ID as before. So an engineer who authenticates through Keycloak can still access their Azure subscriptions with the same roles they had before.

App registrations and first-party OAuth flows

Applications registered in Entra ID (app registrations) that use delegated user flows work with federation, as long as the user can successfully authenticate through Keycloak and come back with a valid SAML assertion. The app's access token and refresh token are issued by Entra ID as usual. Applications that use the on-behalf-of flow or client credentials (M2M, no user involved) are not affected — those don't go through the federated auth path at all.

MFA — but Keycloak's, not Entra's

If Keycloak enforces MFA (which it should), users complete MFA during the Keycloak authentication step. Entra ID will see the completed authentication and — if Keycloak includes the correct AuthnContextClassRef claim in the SAML assertion indicating multi-factor authentication — Entra ID's Conditional Access policies that require MFA will be satisfied. The assertion needs to include urn:oasis:names:tc:SAML:2.0:ac:classes:MultiFactor (the correct SAML standard class) or the Microsoft-specific claim http://schemas.microsoft.com/claims/multipleauthn. Note: urn:oasis:names:tc:SAML:2.0:ac:classes:MFA is not a recognised SAML class — Entra ID will ignore it. Use MultiFactor or the Microsoft claim URI.

What Actually Breaks (or Degrades)

This is the section that matters. I want to be direct about what you're trading away, because these aren't edge cases — several of them affect enterprise deployments meaningfully.

1. Continuous Access Evaluation stops working

Continuous Access Evaluation (CAE) is one of Entra ID's better security features from the last few years. It lets Entra ID push a real-time revocation signal to participating applications (Exchange Online, Teams, SharePoint) — so when a user is disabled, their active sessions in those apps are cut off within minutes rather than waiting for the token to expire (normally up to an hour). This doesn't work under domain federation. CAE depends on Entra ID as the issuing authority evaluating current user state on every token refresh. With a federated domain, Entra ID isn't doing live evaluation of the user's authentication state — it issued the session based on Keycloak's assertion and then largely trusts it. If you disable a user in Keycloak, their active M365 sessions continue until the Entra ID access tokens expire.

The workaround is to disable the user in both Keycloak and Entra ID, and explicitly revoke sessions via Graph (POST /users/{id}/revokeSignInSessions). That's a manual step you'll want in your offboarding runbook.

2. Entra ID Conditional Access loses device compliance signals

Conditional Access policies that require device compliance (Intune-managed, compliant device) evaluate those signals at the Entra ID token issuance layer. When a user authenticates through a federated domain, Entra ID doesn't perform its own interactive authentication — the SAML assertion arrives and Entra ID processes it. Entra ID can evaluate some signals from the assertion context (IP address, whether MFA was claimed), but the native device compliance check requires the device to present its authentication via Entra ID's own authentication flows, not via a federated SAML assertion.

In practice: policies requiring compliant device will fail for federated users unless you're also doing Entra ID hybrid join (Azure AD Connect sync) alongside the federation, and the device presents a Primary Refresh Token (PRT). Hybrid-joined devices can still get PRTs, so if you're in a hybrid environment with AD Connect running, device compliance works. Pure cloud-only with Keycloak federation and no Intune hybrid join — device compliance signals are gone.

3. Entra ID Governance JML workflows won't fire

Lifecycle workflows in Entra ID Governance (joiner, mover, leaver automations) are triggered on changes to the Entra ID user object's attributes — department, manager, employment start/end dates, job title. If your authoritative HR/identity source is Keycloak and you're not syncing attribute changes back into Entra ID user objects, those workflows won't trigger. The Entra ID user object needs to be kept current for Governance to function — which means you need a process to push relevant attributes from Keycloak (or your upstream HR system) into Entra ID via Graph.

4. passwordless and FIDO2 (passkeys) through Entra ID don't work

Entra ID's passwordless authentication — Windows Hello for Business, FIDO2 security keys, the Microsoft Authenticator app as a passwordless method — depends on Entra ID handling the interactive authentication. With domain federation, Entra ID forwards authentication to Keycloak; it's no longer evaluating the Windows Hello credential or FIDO2 assertion. If your security strategy depends on hardware-bound passkeys via Entra ID, you'd need to either implement FIDO2 directly in Keycloak (via Keycloak's built-in WebAuthn Authenticator, available in current Keycloak releases) or accept that passwordless only works for non-federated domains.

5. Seamless SSO and modern authentication edge cases

Entra ID Seamless SSO allows Windows domain-joined machines to authenticate silently (Kerberos ticket via AD) without a login prompt. This is an on-premises AD feature that works alongside Entra ID sync. It doesn't apply to Keycloak federation — if you want silent SSO for Windows machines, you'd implement Kerberos federation directly in Keycloak (Keycloak supports this via the Kerberos/SPNEGO authenticator) rather than relying on Entra ID's Seamless SSO mechanism.

6. Access reviews and entitlement management lose some automation

Entra ID Governance's access reviews can still run — they review Entra ID group memberships and app assignments, so as long as those objects exist in Entra ID the reviews work. What you lose is any automation that's triggered by identity lifecycle events happening in your federated IdP. Entitlement management's connected organisation feature (auto-granting external users access via a pre-approved package) still functions for guest users, but internal federated users won't benefit from new entitlement request flows that require Entra ID native auth.

Summary: What Changes Under Federation

Feature / Behaviour Native Entra ID auth Federated via Keycloak
M365 apps (Teams, SharePoint, Exchange) ✓ Works ✓ Works
Azure RBAC and subscription access ✓ Works ✓ Works
MFA (if Keycloak enforces it) ✓ Entra ID MFA ✓ Keycloak MFA (claimed in assertion)
Conditional Access — IP/location policies ✓ Works ⚠ Works (evaluated on assertion context)
Conditional Access — device compliance ✓ Works ✗ Breaks (unless hybrid joined with AD Connect)
Continuous Access Evaluation (CAE) ✓ Works ✗ Does not work
Windows Hello for Business / FIDO2 via Entra ✓ Works ✗ Breaks (use Keycloak WebAuthn instead)
Entra ID Seamless SSO (Kerberos) ✓ Supported ⚠ Use Keycloak SPNEGO directly
Entra ID Governance lifecycle workflows ✓ Works ⚠ Only if Entra ID attributes are kept in sync
Access reviews (group membership) ✓ Works ✓ Works (reviews Entra ID groups, not Keycloak state)
Entra ID PIM for Azure roles ✓ Works ✓ Works (PIM operates on Entra ID objects)
Real-time session revocation (disable user → kill M365) ✓ <1 min via CAE ✗ Manual — must call revokeSignInSessions in Graph

The Hybrid Design That Actually Works in Practice

From what I've seen in real deployments, the organisations that make this work well don't try to do everything through federation. They split responsibilities cleanly:

  • Keycloak owns authentication — login flows, MFA, password policy, session management, token signing. This is the part you want under EU jurisdiction and your control.
  • Entra ID owns the M365 directory and authorisation — groups, licenses, SharePoint permissions, Intune enrollment, Azure RBAC. These are kept in Entra ID and synced from your authoritative attribute source (HR system, Keycloak attributes via SCIM provisioning, or a custom sync process).
  • SCIM provisioning from Keycloak to Entra ID keeps user objects in Entra ID current — new hires get created, leavers get disabled, attribute changes (department, manager) propagate. This is what triggers Governance lifecycle workflows. Keycloak has a SCIM provider plugin for outbound provisioning; alternatively you write a lightweight sync process that reads Keycloak's Admin API and writes to Microsoft Graph.
  • Custom apps use Keycloak directly via OIDC — they never touch Entra ID. Their tokens are issued by Keycloak, signed with keys in your HSM or Keycloak's own key store, and validated by the app against Keycloak's JWKS endpoint. No US cloud in the authentication path.
  • M365 surface uses federated auth — goes through Keycloak but Entra ID is still the directory and authorisation layer for that surface. You've moved the authentication step to EU jurisdiction while accepting that Entra ID is still in the M365 path for authorisation.

This isn't a pure-sovereignty design. If your requirement is that no US-operated infrastructure touches any part of your identity flow, federation with Entra ID doesn't satisfy that — Entra ID is still processing the SAML assertion and issuing M365 tokens. What it does achieve is moving authentication (and the credential store, session management, MFA) out of Entra ID's control while keeping the M365 surface functional.

For most organisations that have landed here from a genuine sovereignty concern rather than a "we must not touch anything American" policy position, this hybrid is a reasonable place to be. You control how users authenticate. You control the token signing keys. You control the session policy. The M365 apps they use are still US-operated (that's just the reality of using Teams and Exchange Online), but the identity layer is yours.

Keeping Entra ID in Sync via SCIM

For the hybrid design to work properly — especially for Governance lifecycle workflows and access reviews — Entra ID user objects must reflect current state from your authoritative source. Entra ID supports inbound SCIM provisioning via its Inbound Provisioning API, which writes directly to user objects from an external SCIM provider.

# Example: sync a user from Keycloak to Entra ID via Microsoft Graph SCIM-compatible endpoint
# Use a Managed Identity or Service Principal with User.ReadWrite.All on Graph

# Create or update a user in Entra ID from Keycloak attributes
curl -s -X PATCH "https://graph.microsoft.com/v1.0/users/$(UPN)" \
  -H "Authorization: Bearer $GRAPH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "displayName": "'"$DISPLAY_NAME"'",
    "department": "'"$DEPT"'",
    "jobTitle": "'"$TITLE"'",
    "manager@odata.bind": "https://graph.microsoft.com/v1.0/users/'"$MANAGER_UPN"'",
    "employeeHireDate": "'"$HIRE_DATE"'",
    "accountEnabled": '"$ENABLED"'
  }'

Note: Entra ID's Lifecycle Workflows in Governance trigger on attribute presence and change — specifically on employeeHireDate (joiner), manager change (mover), and employeeLeaveDateTime (leaver). If these attributes aren't being populated from your HR/Keycloak sync, the automated workflows won't fire. The sync process needs to write these fields for Governance to do its job.

Keycloak's SCIM provider plugin (maintained by the community; also available in some Red Hat builds) can be configured to push user create, update, and delete events from Keycloak to an external SCIM endpoint. Entra ID's inbound provisioning endpoint accepts standard SCIM 2.0 calls, so this is the most direct integration path for keeping both directories aligned.

Where This Leads

The pattern I've described here — Keycloak as primary IdP, Entra ID as the M365 directory with a federated domain — is the closest you can get to EU sovereignty for identity while keeping Microsoft 365 operational. It's not frictionless. You carry the operational responsibility for Keycloak, the federation configuration, the SCIM sync, and the manual session revocation process that replaces CAE. But for organisations where the sovereignty requirement is real and the M365 dependency isn't going away, it's a workable architecture.

What it does most clearly is move the credential and session layer to the EU. Your users' passwords, MFA devices, session tokens, and signing keys sit in Keycloak running on infrastructure you operate or contract with an EU provider. Entra ID becomes a downstream consumer of that identity rather than the source of truth.

In the next part of this series, I'll cover the SCIM federation pattern in more depth — specifically the Keycloak-to-Entra ID provisioning flow, managing attribute mapping, and handling edge cases like user renames and identity conflicts between the two directories.

Archives