Skip to main content

Enterprise SSO — Microsoft Azure AD (direct)

This guide covers configuring Microsoft Azure AD (Entra ID) as a direct identity provider for Unstract, with no identity broker in the authentication path. Unstract talks to Entra ID directly using MSAL (OAuth 2.0 / OIDC authorization code flow) and the Microsoft Graph API.

On-prem only

Direct Microsoft Azure AD (Entra ID) sign-in is available for on-prem deployments only. Unstract Cloud always authenticates through Auth0 — see the Enterprise SSO Guide — Cloud Edition.

Availability

Available from on-prem release v0.178.0 onward — see the v0.178.0 On-Prem Release Notes.

1. Choosing between the two Azure AD modes

Unstract supports two ways of authenticating against Microsoft Entra ID. Pick one — a deployment runs exactly one identity provider.

Brokered via Auth0Direct (this guide)
EditionsCloud and on-premOn-prem only
Auth pathUnstract → Auth0 → Entra IDUnstract → Entra ID
Auth0 tenant requiredYesNo
Redirect URI registered in EntraAn Auth0 callback URLYour own Unstract host
Group sourceAuth0 connection claimsMicrosoft Graph (/transitiveMemberOf)
ROLE_MAPPING group valueGroup nameGroup Object ID (GUID)
User invitations, password resetSupportedNot available — managed in Entra ID
Vulnerability managementShared-responsibility model — platform patching and vulnerability management are Auth0/Okta's responsibilityThe authentication path runs entirely within software you own, deploy and patch, so vulnerability management is handled in-house

Choose direct mode when the deployment must not depend on a third-party identity broker. Choose the brokered mode when you need Auth0's invitation and user management, or when you run Unstract Cloud.

For the security reasoning behind that choice — what the broker does and does not see, the credential model for each protocol, and what changes when the broker is removed — see Identity Broker Security Posture.

2. Steps to be Performed by the Customer

2.1 Register the application in Entra ID

REF: Microsoft — Register an application

  1. Go to Entra ID > App registrations and select New registration.
  2. Enter a meaningful Name. Users can see this name on the consent and sign-in screens.
  3. Under Supported account types, select Accounts in this organizational directory only — this integration assumes a single tenant.
  4. Under Redirect URI, select platform type Web and enter your deployment's callback URL (see 2.2).
  5. Click Register.

2.2 Redirect URI

Unlike the brokered mode, the redirect URI points at your own Unstract deployment, not at an Auth0 domain. It is your base URL plus the API path prefix and callback:

https://<your-unstract-domain>/api/v1/callback

For example, a deployment reachable at https://unstract.example.com registers:

https://unstract.example.com/api/v1/callback

The backend derives this URL at sign-in time from the request, so in a normal single-origin deployment nothing further is needed. Pin it explicitly with AZURE_AD_REDIRECT_URI (see Section 4) when the derived value cannot be trusted to match what you registered — a reverse proxy that rewrites the Host header, or a frontend served from a different origin than the backend.

warning

The redirect URI is matched byte-for-byte by Entra ID. A trailing slash, an http scheme, or a different host will fail sign-in with AADSTS500112 or AADSTS50011.

2.3 Grant Microsoft Graph API permissions

Unstract reads user profiles and group membership with an app-only (client credentials) token, so these must be granted as Application permissions — not Delegated.

  1. In the app registration, go to API permissions > Add a permission > Microsoft Graph > Application permissions.

  2. Add:

    PermissionUsed for
    User.Read.AllReading user profiles
    GroupMember.Read.AllReading a user's group membership via /transitiveMemberOf
  3. Click Grant admin consent for the tenant. Both permissions require it.

Application, not Delegated

Adding these as Delegated permissions has no effect on the app-only token Unstract uses. The symptom is a Graph Authorization_RequestDenied that fails the login outright — the user authenticates successfully and is then refused.

Do not add these to AZURE_AD_SCOPES

AZURE_AD_SCOPES lists the delegated scopes requested at the interactive sign-in leg, and defaults to User.Read — the default delegated permission on every app registration, which needs no consent. Naming an admin-consent permission such as User.Read.All there fails sign-in with AADSTS65001 on a tenant that granted only the Application permissions, for a scope nothing would have used.

If you change API permissions on a running deployment, drop the cached app-only token — it was minted before the new grant and stays valid for up to an hour, so Graph keeps rejecting it until it expires:

kubectl exec -n <namespace> deploy/unstract-backend -- .venv/bin/python manage.py clear_azure_ad_cache --type app_token
note

Use .venv/bin/python, not a bare python. The backend image has no .venv/bin on PATH, so python resolves to the system interpreter and fails with ModuleNotFoundError: No module named 'django', which reads like a broken install rather than a wrong command.

2.4 Create a client secret

  1. In the app registration, go to Certificates & secrets > Client secrets > + New client secret.
  2. Add a description, set an expiration period, and click Add.
  3. Copy the secret Value (not the Secret ID).
warning

The client secret value is only shown once at creation time. Copy and store it securely before navigating away, and plan for rotation before the expiry date — an expired secret fails every sign-in.

2.5 Create directory groups and collect their Object IDs

Create a security group in Entra ID for each Unstract role you intend to grant, and add the appropriate users to each. See the Unstract Roles Guide for the full list of roles.

Group membership is resolved transitively, so a user who reaches a mapped group through a nested group still gets its role.

Unstract matches groups by Object ID, not display name. Collect the Object ID (a GUID) for each group:

  • Entra admin centerGroups > All groups > <group> > Overview > Object Id
  • Graph ExplorerGET /groups?$filter=displayName eq 'Unstract-Admins'&$select=id
  • Azure CLIaz ad group show --group "Unstract-Admins" --query id -o tsv
Why Object IDs and not names

Entra ID does not enforce unique group display names, and creating groups is enabled for tenant members by default. Matching on a name would let any member create a second group named after a privileged mapping and be granted that role through membership in the wrong group. The Object ID is the only identifier Entra guarantees to be unique.

note

Renaming a group in Entra ID does not break its mapping — the Object ID is immutable. Deleting a group and recreating one with the same name does: the replacement gets a new Object ID, so ROLE_MAPPING must be updated.

3. Steps to be Performed by the Ops Team

Configure the deployment through your on-prem.values.yaml. Three things must be set together — the provider switch, the credentials group, and the enterprise SSO setup Job that seeds the group-to-role mappings.

global:
sharedConfigs:
authAzureAd:
enabled: true
# From the app registration's Overview blade.
AZURE_AD_TENANT_ID: "<directory-tenant-id>"
AZURE_AD_CLIENT_ID: "<application-client-id>"
AZURE_AD_CLIENT_SECRET: "<client-secret-value>"

backend:
configMap:
AUTH_PROVIDER: "azure_ad"
# Required under azure_ad — the Job it gates creates the domain and
# role-mapping rows that group resolution reads.
ENABLE_ENTERPRISE_SSO_SETUP: "True"
# Names the organization users see in the organization picker.
ORG_NAME: "Acme Corporation"
# Group Object IDs -> Unstract roles, as a JSON array string.
ROLE_MAPPING: '[{"group":"3fa85f64-5717-4562-b3fc-2c963f66afa6","unstract_role":"unstract_admin","provider":"waad"},{"group":"9c858901-8a57-4791-aab8-9c8e02b3e662","unstract_role":"unstract_user","provider":"waad"}]'
# Must be "False" for group membership to gate access.
DISABLE_SSO_IDP_AUTHORIZATION: "False"
Keep the client secret out of your values file

The three AZURE_AD_* credentials can instead come from a Secret you create yourself, by setting authAzureAd.existingSecret to its name and omitting the three keys above. Values files are routinely committed to git and shared in tickets, so prefer this wherever the deployment's secret management allows it.

Also ensure ingress.hostLookup.unstract is set and matches the host in global.host.baseUrl. It supplies the domain the mappings are attached to — without it there is no domain record, and every Entra user is refused however their groups are mapped.

The chart validates this pairing

AUTH_PROVIDER: "azure_ad" and authAzureAd.enabled: true must be set together, and enabling the group without the three credentials is rejected. A half-configured Entra install fails at helm install rather than at the first login.

Quote the boolean-looking strings

ENABLE_ENTERPRISE_SSO_SETUP must be the quoted string "True". Unquoted, True is a YAML boolean that stringifies to true, never matches "True", and the setup Job is silently not rendered. The chart rejects the unquoted form rather than letting it through.

ROLE_MAPPING under direct mode

The mapping format is the same as the Unstract Roles Guide describes, with two direct-mode specifics:

  • group is the Entra group's Object ID (a GUID), not its display name.
  • provider should be waad — the Entra directory. Matching ignores this field, so any accepted value works, but waad is the accurate one.

A user who belongs to several mapped groups gets the strongest role, resolved in the order unstract_admin > unstract_supervisor > unstract_reviewer > unstract_user.

note

ROLE_MAPPING is the complete desired state. Each run of the setup Job replaces all existing mappings for the domain; an empty array ('[]') clears them.

4. Configuration Reference

Required

KeyLocationDescription
AUTH_PROVIDERbackend.configMapauth0 (default) or azure_ad. Selects the authentication provider for the whole deployment.
authAzureAd.enabledglobal.sharedConfigstrue to render and mount the Entra credentials. Must be set together with AUTH_PROVIDER: "azure_ad".
AZURE_AD_TENANT_IDglobal.sharedConfigs.authAzureAdDirectory (tenant) ID from the app registration's Overview blade.
AZURE_AD_CLIENT_IDglobal.sharedConfigs.authAzureAdApplication (client) ID from the app registration's Overview blade.
AZURE_AD_CLIENT_SECRETglobal.sharedConfigs.authAzureAdClient secret value created in 2.4. Can be supplied through authAzureAd.existingSecret instead.
ENABLE_ENTERPRISE_SSO_SETUPbackend.configMapMust be the quoted string "True". The Job it gates creates the domain and role-mapping records that group resolution reads — at the "False" default, helm install succeeds, the Job never renders, and every user is refused at login.
ORG_NAMEbackend.configMapOrganization display name, shown in the organization picker. Read only when the organization is first created.
ROLE_MAPPINGbackend.configMapEntra group Object IDs mapped to Unstract roles, as a JSON array string. Defaults to '[]', which grants no one a role.
ingress.hostLookup.unstracttop levelThe deployment's domain, which the role mappings attach to. Must match the host in global.host.baseUrl.

Optional

All of these live under global.sharedConfigs.authAzureAd, alongside the credentials — they are ignored anywhere else, including backend.configMap. Each already has the default the backend applies, so leaving it unset changes nothing. A blank value ("") is rejected at render.

KeyDefaultDescription
AZURE_AD_AUTHORITYhttps://login.microsoftonline.com/<tenant-id>Override only for a sovereign or national cloud.
AZURE_AD_REDIRECT_URIDerived per requestPins the callback URL sent to Entra ID. Set it behind a Host-rewriting proxy or a split frontend/backend origin.
AZURE_AD_SCOPESUser.ReadDelegated scopes requested at sign-in. Do not put Graph application permissions here.
AZURE_AD_GRAPH_API_BASEhttps://graph.microsoft.com/v1.0Microsoft Graph endpoint.
AZURE_AD_HTTP_CONNECT_TIMEOUT30Connect timeout in seconds for Graph and token calls.
AZURE_AD_HTTP_READ_TIMEOUT30Read timeout in seconds for Graph and token calls.
AZURE_AD_GRAPH_MAX_PAGES50Maximum /transitiveMemberOf pages followed (100 objects each). Exceeding it fails rather than returning a truncated group list.
Timeouts sit on the login path

The two timeout values bound every outbound call to Graph and to login.microsoftonline.com. Raise them only when the deployment's network genuinely needs it — a longer wait holds a backend worker longer, trading one failure mode for a slower one.

ORG_NAME is read only while the organization does not yet exist. Changing it later will not rename an organization that has already been created.

5. Verify the Configuration

After the Helm install or upgrade completes:

# Confirm the setup Job ran
kubectl get jobs -n <namespace> -l app.kubernetes.io/component=enterprise-sso-init-domain

# Review its logs
kubectl logs -n <namespace> job/<enterprise-sso-init-domain-job-name>

# List the configured domain and role mappings
kubectl exec -n <namespace> <backend-pod-name> -- \
bash -c "source /app/.venv/bin/activate && python manage.py list_domains"

Then sign in to the Unstract UI as a user who belongs to one of the mapped groups, and confirm the assigned role matches the mapping.

6. Behaviour Differences from the Brokered Mode

Direct mode implements sign-in, sign-out, sessions, user info, and group-based role assignment. Some capabilities that the Auth0-brokered mode provides are handled in Entra ID instead, or are not available:

CapabilityDirect mode behaviour
User invitationsNot available in Unstract. Add users to the mapped Entra groups instead.
Password resetNot available in Unstract. Handled entirely by Entra ID.
Managing organization membership from UnstractNot available. Membership follows Entra group membership.
Sign-outClears the Unstract session only.

Sign-out ends the Unstract session, not the Microsoft session

Signing out of Unstract deliberately does not perform a sign-out against Entra ID. Doing so would end the user's Microsoft session for every application in that browser — Outlook, Teams, and the rest — which enterprise tenants generally do not want from one application's logout button.

The practical consequence is that signing back in is an account-picker click rather than a credential prompt. It is never silent: Unstract always asks Entra ID to prompt for account selection.

Group changes take effect at the next sign-in

Group membership is read once per login and carried for the life of that session. Adding or removing a user from a mapped group takes effect the next time they sign in, not immediately.

If Microsoft Graph is unreachable when a user signs in, Unstract falls back to the membership already recorded locally rather than revoking access — an outage is not a statement about that user. Removals from mapped groups are therefore not enforced while Graph is failing.

7. Troubleshooting

SymptomLikely cause
AADSTS500112 or AADSTS50011 at sign-inThe redirect URI does not match one registered on the app registration. Compare it byte-for-byte, and set AZURE_AD_REDIRECT_URI if the deployment sits behind a Host-rewriting proxy or splits frontend and backend origins.
AADSTS65001 at sign-inA Graph application permission was named in AZURE_AD_SCOPES. Remove it — that setting is for delegated sign-in scopes only.
AADSTS7000215Invalid or expired client secret. Create a new one and update AZURE_AD_CLIENT_SECRET.
Graph Authorization_RequestDenied after a successful sign-inUser.Read.All / GroupMember.Read.All are missing, were added as Delegated rather than Application permissions, or admin consent was not granted.
Permissions look correct but Graph still rejectsThe cached app-only token predates the grant. Clear it with clear_azure_ad_cache --type app_token (see 2.3).
"Your user is not associated with any required role"The user is in no mapped group, or ROLE_MAPPING holds group display names instead of Object IDs.
Every user is refused regardless of groupingress.hostLookup.unstract is unset or does not match global.host.baseUrl, so no domain record exists.
Users sign in but no group check is appliedDISABLE_SSO_IDP_AUTHORIZATION is "True", which skips group-to-role authorization entirely.
Deployment still authenticates through Auth0AUTH_PROVIDER is not set to azure_ad, or the setup Job did not render because ENABLE_ENTERPRISE_SSO_SETUP was an unquoted YAML boolean.