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.
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.
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 Auth0 | Direct (this guide) | |
|---|---|---|
| Editions | Cloud and on-prem | On-prem only |
| Auth path | Unstract → Auth0 → Entra ID | Unstract → Entra ID |
| Auth0 tenant required | Yes | No |
| Redirect URI registered in Entra | An Auth0 callback URL | Your own Unstract host |
| Group source | Auth0 connection claims | Microsoft Graph (/transitiveMemberOf) |
ROLE_MAPPING group value | Group name | Group Object ID (GUID) |
| User invitations, password reset | Supported | Not available — managed in Entra ID |
| Vulnerability management | Shared-responsibility model — platform patching and vulnerability management are Auth0/Okta's responsibility | The 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
- Go to Entra ID > App registrations and select New registration.
- Enter a meaningful Name. Users can see this name on the consent and sign-in screens.
- Under Supported account types, select Accounts in this organizational directory only — this integration assumes a single tenant.
- Under Redirect URI, select platform type Web and enter your deployment's callback URL (see 2.2).
- 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.
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.
-
In the app registration, go to API permissions > Add a permission > Microsoft Graph > Application permissions.
-
Add:
Permission Used for User.Read.AllReading user profiles GroupMember.Read.AllReading a user's group membership via /transitiveMemberOf -
Click Grant admin consent for the tenant. Both permissions require it.
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.
AZURE_AD_SCOPESAZURE_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
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
- In the app registration, go to Certificates & secrets > Client secrets > + New client secret.
- Add a description, set an expiration period, and click Add.
- Copy the secret Value (not the Secret ID).
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 center — Groups > All groups >
<group>> Overview > Object Id - Graph Explorer —
GET /groups?$filter=displayName eq 'Unstract-Admins'&$select=id - Azure CLI —
az ad group show --group "Unstract-Admins" --query id -o tsv
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.
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"
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.
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.
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:
groupis the Entra group's Object ID (a GUID), not its display name.providershould bewaad— the Entra directory. Matching ignores this field, so any accepted value works, butwaadis 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.
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
| Key | Location | Description |
|---|---|---|
AUTH_PROVIDER | backend.configMap | auth0 (default) or azure_ad. Selects the authentication provider for the whole deployment. |
authAzureAd.enabled | global.sharedConfigs | true to render and mount the Entra credentials. Must be set together with AUTH_PROVIDER: "azure_ad". |
AZURE_AD_TENANT_ID | global.sharedConfigs.authAzureAd | Directory (tenant) ID from the app registration's Overview blade. |
AZURE_AD_CLIENT_ID | global.sharedConfigs.authAzureAd | Application (client) ID from the app registration's Overview blade. |
AZURE_AD_CLIENT_SECRET | global.sharedConfigs.authAzureAd | Client secret value created in 2.4. Can be supplied through authAzureAd.existingSecret instead. |
ENABLE_ENTERPRISE_SSO_SETUP | backend.configMap | Must 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_NAME | backend.configMap | Organization display name, shown in the organization picker. Read only when the organization is first created. |
ROLE_MAPPING | backend.configMap | Entra group Object IDs mapped to Unstract roles, as a JSON array string. Defaults to '[]', which grants no one a role. |
ingress.hostLookup.unstract | top level | The 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.
| Key | Default | Description |
|---|---|---|
AZURE_AD_AUTHORITY | https://login.microsoftonline.com/<tenant-id> | Override only for a sovereign or national cloud. |
AZURE_AD_REDIRECT_URI | Derived per request | Pins the callback URL sent to Entra ID. Set it behind a Host-rewriting proxy or a split frontend/backend origin. |
AZURE_AD_SCOPES | User.Read | Delegated scopes requested at sign-in. Do not put Graph application permissions here. |
AZURE_AD_GRAPH_API_BASE | https://graph.microsoft.com/v1.0 | Microsoft Graph endpoint. |
AZURE_AD_HTTP_CONNECT_TIMEOUT | 30 | Connect timeout in seconds for Graph and token calls. |
AZURE_AD_HTTP_READ_TIMEOUT | 30 | Read timeout in seconds for Graph and token calls. |
AZURE_AD_GRAPH_MAX_PAGES | 50 | Maximum /transitiveMemberOf pages followed (100 objects each). Exceeding it fails rather than returning a truncated group list. |
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:
| Capability | Direct mode behaviour |
|---|---|
| User invitations | Not available in Unstract. Add users to the mapped Entra groups instead. |
| Password reset | Not available in Unstract. Handled entirely by Entra ID. |
| Managing organization membership from Unstract | Not available. Membership follows Entra group membership. |
| Sign-out | Clears 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
| Symptom | Likely cause |
|---|---|
AADSTS500112 or AADSTS50011 at sign-in | The 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-in | A Graph application permission was named in AZURE_AD_SCOPES. Remove it — that setting is for delegated sign-in scopes only. |
AADSTS7000215 | Invalid or expired client secret. Create a new one and update AZURE_AD_CLIENT_SECRET. |
Graph Authorization_RequestDenied after a successful sign-in | User.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 rejects | The 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 group | ingress.hostLookup.unstract is unset or does not match global.host.baseUrl, so no domain record exists. |
| Users sign in but no group check is applied | DISABLE_SSO_IDP_AUTHORIZATION is "True", which skips group-to-role authorization entirely. |
| Deployment still authenticates through Auth0 | AUTH_PROVIDER is not set to azure_ad, or the setup Job did not render because ENABLE_ENTERPRISE_SSO_SETUP was an unquoted YAML boolean. |