From 01fe3883c73be3348852fdccb85feb17b109faeb Mon Sep 17 00:00:00 2001 From: Tim Diekmann Date: Wed, 25 Mar 2026 14:26:32 +0100 Subject: [PATCH 1/2] BE-477: Add Microsoft/Azure AD OIDC SSO provider - Add Microsoft OIDC provider config in kratos.yml with tenant env var - Add Microsoft Jsonnet mapper (handles email/preferred_username/upn) - Add docker-compose env vars for Microsoft client_id, secret, tenant_id --- .../hash-external-services/docker-compose.yml | 3 +++ .../kratos/hooks/oidc.microsoft.jsonnet | 22 +++++++++++++++++++ apps/hash-external-services/kratos/kratos.yml | 10 +++++++++ 3 files changed, 35 insertions(+) create mode 100644 apps/hash-external-services/kratos/hooks/oidc.microsoft.jsonnet diff --git a/apps/hash-external-services/docker-compose.yml b/apps/hash-external-services/docker-compose.yml index c5a3b5636dd..d0647b077f5 100644 --- a/apps/hash-external-services/docker-compose.yml +++ b/apps/hash-external-services/docker-compose.yml @@ -85,6 +85,9 @@ services: SELFSERVICE_METHODS_OIDC_CONFIG_BASE_REDIRECT_URI: "http://localhost:5001/auth" SELFSERVICE_METHODS_OIDC_CONFIG_PROVIDERS_0_CLIENT_ID: "${KRATOS_OIDC_GOOGLE_CLIENT_ID}" SELFSERVICE_METHODS_OIDC_CONFIG_PROVIDERS_0_CLIENT_SECRET: "${KRATOS_OIDC_GOOGLE_CLIENT_SECRET}" + SELFSERVICE_METHODS_OIDC_CONFIG_PROVIDERS_1_CLIENT_ID: "${KRATOS_OIDC_MICROSOFT_CLIENT_ID}" + SELFSERVICE_METHODS_OIDC_CONFIG_PROVIDERS_1_CLIENT_SECRET: "${KRATOS_OIDC_MICROSOFT_CLIENT_SECRET}" + SELFSERVICE_METHODS_OIDC_CONFIG_PROVIDERS_1_MICROSOFT_TENANT: "${KRATOS_OIDC_MICROSOFT_TENANT_ID}" SELFSERVICE_FLOWS_VERIFICATION_UI_URL: "http://localhost:3000/verification" SELFSERVICE_FLOWS_RECOVERY_UI_URL: "http://localhost:3000/recovery" SELFSERVICE_FLOWS_SETTINGS_UI_URL: "http://localhost:3000/settings/security" diff --git a/apps/hash-external-services/kratos/hooks/oidc.microsoft.jsonnet b/apps/hash-external-services/kratos/hooks/oidc.microsoft.jsonnet new file mode 100644 index 00000000000..ca017ee2e54 --- /dev/null +++ b/apps/hash-external-services/kratos/hooks/oidc.microsoft.jsonnet @@ -0,0 +1,22 @@ +local claims = std.extVar('claims'); + +// Microsoft may provide email via 'email', 'preferred_username', or 'upn' +local email = + if "email" in claims && claims.email != "" then claims.email + else if "preferred_username" in claims && claims.preferred_username != "" then claims.preferred_username + else claims.upn; + +{ + identity: { + traits: { + emails: [email], + }, + // Microsoft Entra ID emails are verified by the directory + verified_addresses: [ + { + value: email, + via: "email", + }, + ], + }, +} diff --git a/apps/hash-external-services/kratos/kratos.yml b/apps/hash-external-services/kratos/kratos.yml index cd3e406a4bc..edfb535d6c5 100644 --- a/apps/hash-external-services/kratos/kratos.yml +++ b/apps/hash-external-services/kratos/kratos.yml @@ -68,6 +68,16 @@ selfservice: essential: true email_verified: essential: true + - id: microsoft + provider: microsoft + label: Microsoft + # Set `client_id` through the `SELFSERVICE_METHODS_OIDC_CONFIG_PROVIDERS_1_CLIENT_ID` environment variable + # Set `client_secret` through the `SELFSERVICE_METHODS_OIDC_CONFIG_PROVIDERS_1_CLIENT_SECRET` environment variable + # Set `microsoft_tenant` through the `SELFSERVICE_METHODS_OIDC_CONFIG_PROVIDERS_1_MICROSOFT_TENANT` environment variable + mapper_url: "file:///etc/config/kratos/hooks/oidc.microsoft.jsonnet" + scope: + - email + - profile flows: error: From 6dd8021437a2c40a847e77528dced70fe4f79677 Mon Sep 17 00:00:00 2001 From: Tim Diekmann Date: Wed, 25 Mar 2026 17:32:38 +0100 Subject: [PATCH 2/2] BE-477: Address review feedback - Style flow messages based on type (red for errors, blue for info) - Guard Microsoft Jsonnet upn claim with existence check - Check email_verified claim in Microsoft mapper when present - Add Microsoft env vars to .env.local setup comment --- apps/hash-external-services/docker-compose.yml | 3 +++ .../kratos/hooks/oidc.microsoft.jsonnet | 17 ++++++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/apps/hash-external-services/docker-compose.yml b/apps/hash-external-services/docker-compose.yml index d0647b077f5..c4a0d2400b3 100644 --- a/apps/hash-external-services/docker-compose.yml +++ b/apps/hash-external-services/docker-compose.yml @@ -79,6 +79,9 @@ services: # KRATOS_OIDC_ENABLED=true # KRATOS_OIDC_GOOGLE_CLIENT_ID= # KRATOS_OIDC_GOOGLE_CLIENT_SECRET= + # KRATOS_OIDC_MICROSOFT_CLIENT_ID= + # KRATOS_OIDC_MICROSOFT_CLIENT_SECRET= + # KRATOS_OIDC_MICROSOFT_TENANT_ID= SELFSERVICE_METHODS_OIDC_ENABLED: "${KRATOS_OIDC_ENABLED:-false}" SELFSERVICE_FLOWS_REGISTRATION_AFTER_OIDC_HOOKS_0_CONFIG_URL: "http://host.docker.internal:5001/kratos-after-registration" SELFSERVICE_FLOWS_REGISTRATION_AFTER_OIDC_HOOKS_0_CONFIG_AUTH_CONFIG_VALUE: "${KRATOS_API_KEY}" diff --git a/apps/hash-external-services/kratos/hooks/oidc.microsoft.jsonnet b/apps/hash-external-services/kratos/hooks/oidc.microsoft.jsonnet index ca017ee2e54..8b91ce25112 100644 --- a/apps/hash-external-services/kratos/hooks/oidc.microsoft.jsonnet +++ b/apps/hash-external-services/kratos/hooks/oidc.microsoft.jsonnet @@ -4,19 +4,22 @@ local claims = std.extVar('claims'); local email = if "email" in claims && claims.email != "" then claims.email else if "preferred_username" in claims && claims.preferred_username != "" then claims.preferred_username - else claims.upn; + else if "upn" in claims && claims.upn != "" then claims.upn + else error "Microsoft OIDC: no email claim found in token"; { identity: { traits: { emails: [email], }, - // Microsoft Entra ID emails are verified by the directory - verified_addresses: [ - { - value: email, - via: "email", - }, + // Microsoft Entra ID verifies directory emails; check email_verified + // claim if present, otherwise trust the directory. + verified_addresses: if "email_verified" in claims then ( + if claims.email_verified then [ + { value: email, via: "email" }, + ] else [] + ) else [ + { value: email, via: "email" }, ], }, }