This documentation has moved
GPT-RAG is now Agent Landing Zone. The maintained documentation is at azure.github.io/AI-Landing-Zones/agent-landing-zone. This page is kept for reference only and is no longer updated.
Authentication and Document-Level Security
Develop adoption, not a release
Adoption status and approval scope supersede the historical “unmerged” and pending-exception labels below. Source pins remain implementation evidence; released manifest pins are unchanged. See that record for active rules, reference runs and remaining validation gaps.
This page explains how authentication works in GPT-RAG, from the GPT-RAG UI sign-in to querying Azure AI Search with document-level access control enabled (POSIX-like ACL / RBAC scopes). The classic runtime forwards a user token to the orchestrator, which produces the correct user-context token for Azure AI Search. The published hosted component matrix defines a stricter UI BFF ownership boundary described below.
In OAuth mode, the orchestrator receives a user access token (for the orchestrator API) and then performs an On-Behalf-Of (OBO) exchange to obtain a separate token for Azure AI Search. The two tokens have different audiences and are not interchangeable.
Hosted continuity remains disabled
Live OQ-OWN evidence supersedes the capability-first contract from
PR #630. The delegated
x-ms-user-identity platform pivot merged in
PR #633. UI v2.6.2,
orchestrator v4.1.1, ingestion v2.7.3, and AILZ v2.5.1 are pinned by
GPT-RAG v3.8.3.
Keep HOSTED_CONTINUITY_ENABLED=false; compatible history endpoints return
HTTP 503 until HOSTED_CONVERSATION_OWNER_BINDING_VALIDATED=true.
Embed GPT-RAG in a portal
To add GPT-RAG chat to an existing portal, follow the Chainlit Copilot embedding guide. It covers the opt-in configuration, deployment topology, anonymous and Entra authentication modes, browser security controls, and rollout.
Zero Trust deployments
The GPT-RAG UI Container App has no public endpoint in a Zero Trust deployment. Route widget traffic through the environment's existing authenticated front door or gateway. Do not expose the internal Container App directly, and do not treat CORS or an origin allowlist as authentication.
Concepts
Hosted Conversation ownership boundary
For the hosted component matrix, the trusted UI BFF validates and canonicalizes
the signed-in user's Entra oid, derives x-ms-user-identity only from that
server-side value, and uses its own service token for managed-Conversation
operations. Browser-supplied owner/header values are never authoritative, and
the hosted runtime must not source or replace the identity header.
This owner header is distinct from OBO. x-ms-user-identity binds Foundry
Conversation ownership; OBO obtains a delegated bearer token for a downstream
retrieval audience. Activation requires direct assignments to the UI BFF, at
the individual hosted-agent scope, of Foundry Agent Consumer and the exact
GPT-RAG custom role GPT-RAG Hosted Agent User Identity Impersonation
(bef66abe-a495-530a-be1d-5d882fecff03) containing only
Microsoft.CognitiveServices/accounts/AIServices/agents/endpoints/UserIdentityImpersonation/action.
The hosted runtime receives no Conversation-capability key, Conversation or
impersonation RBAC, or Cosmos DB in hosted/no-panel. Capability/HMAC remains a
disabled fallback and is not provisioned or required on the primary path. See the
hosted-agent release matrix
for exact component behavior and the
hosted conversation continuity platform contract
for the owner-validation, protocol, role, and lifecycle requirements.
Hosted application identity versus document identity
The hosted bootstrap fix
targets the deployed agent's own instance_identity.principal_id for
configuration reads, model inference, and the exact audit HMAC secret only when
a Key Vault reference is configured. Audit signing is separate from the disabled
Conversation-capability/HMAC fallback. The bootstrap patch retains the v3.8.3
component pins and leaves continuity and panel evidence gates unchanged.
These application permissions are not a signed-in user's permissions and do not establish OBO, Conversation ownership, or document-level authorization. Bootstrap never grants Search, Blob, managed Conversation, or impersonation roles. Keep the caller identity supplied to permission-aware retrieval separate from the service identity used to call Azure APIs.
A service-identity-only synthetic test is an explicit, environment-specific authorization decision, not evidence that different end users are isolated. An approved container-level grant covers future files in that container as well as existing files; do not introduce protected real documents on the basis of that test or make its grants a default. A greeting only tests model and configuration access. Validate end-user document permissions separately, including requests that must be denied.
Classic Container Apps token flow
GPT-RAG uses Microsoft Entra ID authentication end-to-end, and Azure AI Search document-level access control (POSIX-like ACL / RBAC scopes) relies on three related steps:
1) The UI signs the user in and sends an access token to the orchestrator. This token is meant for the orchestrator API.
2) The orchestrator validates the incoming token (signature, issuer, and audience) and extracts the user identity (for example, user object ID). This is how the orchestrator knows which user is calling.
3) When the orchestrator queries Azure AI Search with document-level security enabled, it must send a different delegated token whose audience is Azure AI Search. The orchestrator obtains that token by performing an On-Behalf-Of exchange with Entra ID.
The diagram below shows where each token is used. Azure AI Search receives the orchestrator credential (to authorize the call) and a separate “query source authorization” token (to enforce document-level access control).
+---------------------------+ +----------------------------------+
| Microsoft Entra ID | | Azure AI Search |
| Tenant | | Index with ACL/RBAC enforcement |
| | | permissionFilterOption=enabled |
+---------------------------+ +----------------------------------+
^ ^
| |
| (4) user access | (6) Search query
| aud: https://search.azure.com | Authorization: (orchestrator identity)
| scope: https://search.azure.com/user_impersonation | x-ms-query-source-authorization: (user access token)
| |
| |
| |
+---------------------------+ (3) OBO exchange |
| Orchestrator |----------------------------------+
| Container App | to Entra ID token endpoint
| |
| Validates API token | Inputs
| Uses client secret | - incoming user access token
| Uses MI or admin key | - client id and client secret
+---------------------------+ - requested scope for Search
^
|
| (2) API token
| aud: api://<CLIENT_ID>
| scope: api://<CLIENT_ID>/user_impersonation
|
+---------------------------+
| Frontend |
| Container App |
| Chainlit OAuth |
+---------------------------+
^
|
| (1) User sign-in
| Entra issues API token
|
+---------------------------+
| App Registration |
| Single registration for |
| UI and orchestrator |
+---------------------------+
When authentication is configured (OAuth enabled), the UI includes the user access token for the orchestrator API on calls to the orchestrator. This is the token shown as (2) in the diagram. If OAuth is not configured and the UI is running in anonymous mode, the UI calls the orchestrator without an Authorization header. In OAuth mode, the UI sends the token like this:
Authorization: Bearer <orchestrator_api_user_access_token>
On the orchestrator side, this user access token is validated before any downstream call is made. The orchestrator verifies the token signature and issuer using the tenant signing keys (JWKS), checks the expected audience for the orchestrator API, and extracts the user identity (for example, user object ID). That identity can be used to enforce access to the orchestrator itself.
To query Azure AI Search with document-level access control enabled, the orchestrator performs an On-Behalf-Of (OBO) exchange using the incoming user access token as the user assertion. This returns a delegated user token whose audience is Azure AI Search. The orchestrator includes that token in the header below when running queries.
x-ms-query-source-authorization: Bearer <search_user_access_token>
Document-level access control is enforced by Azure AI Search when the index is configured for document permissions (see permissionFilterOption in the index definition) and documents include permission metadata.
Unmerged P3: required-user retrieval fails closed
Orchestrator ea61bc7cd7b2ac21c957bee5db959337fedd8760
supersedes the earlier 31348dc fallback evidence for maintained MAF and
multimodal providers, on MCP and non-MCP paths. A user assertion,
ALLOW_ANONYMOUS=false, or a user-only source selects required-user mode.
Missing/failed OBO stops retrieval: no application fallback, no multimodal
retry with the authorization header stripped. Legitimate service-only
requests remain; anonymous mode does not waive source-specific restrictions.
See mode selection and recovery.
This is unmerged, not released behavior. Scopes and token/header tests
alone do not prove live ACL enforcement; validate each source with allowed
and denied principals before deployment.
Foundry IQ retrieval
When RETRIEVAL_BACKEND=foundry_iq, there are two security paths. Native
Foundry IQ sources use the x-ms-query-source-authorization OBO header only
when permissions were ingested from a supported source such as ADLS Gen2
ACLs, SharePoint, OneLake/Fabric, or Purview labels. The custom ingestion
path, where the existing GPT-RAG index is registered as a searchIndex
knowledge source, uses an OData filterAddOn over GPT-RAG security fields
instead. Plain Blob storage is container-level RBAC for this purpose unless
Purview labels or an equivalent per-document permission source are used. See
Foundry IQ: Documents, security modes.
Work IQ and Fabric auth
Work IQ,
Fabric ontology, and
Fabric Data Agent always
require the same OBO
token flow described above: the orchestrator forwards the signed-in
user's delegated token in x-ms-query-source-authorization, and the
remote service (Microsoft 365 or Microsoft Fabric) evaluates
per-user permissions natively. Managed identity fallback does not
apply. Earlier connector behavior skips sources without a user token;
the unmerged ea61bc7 provider policy instead rejects required-user and
mixed-source requests before retrieval. It does not silently answer from
local documents. SharePoint remote and MCP OBO query headers also require
a user under that policy.
Legacy API-key recovery (H1)
Compatibility retained: rotate both key sources
At the same unmerged ea61bc7 source pin, src/dependencies.py
validate_auth retains the existing ORCHESTRATOR_APP_APIKEY environment
fallback when configuration lookup fails. It logs the safe constant
diagnostic API key configuration unavailable; using environment fallback,
without the key or provider exception. Missing credentials, an absent expected
key or a mismatch return 401. DISABLE_AUTH bypass and Dapr-token
precedence are unchanged; an invalid supplied Dapr token does not fall
through to API-key authentication.
A stale environment key can remain valid during configuration failure. Restore App Configuration access and rotate/remove both the configured and environment sources, updating callers and deployed revisions together. Do not assume changing only the configured key revokes the environment key. There is no new environment opt-in for this authentication path; ingestion's environment-fallback opt-in is a different contract.
H1 is the compatible existing behavior selected by the agent under the
user's delegated decision authority, not added runtime code in
25f1986..ea61bc7. Existing
tests/test_dependency_boundary_dispositions.py covers fallback match,
missing/mismatch, safe diagnostics, cancellation and precedence. This
recovery choice is not new authorization or a claim that the candidate shipped.
GPT-RAG uses these field names consistently across ingestion paths:
metadata_security_user_ids
metadata_security_group_ids
metadata_security_rbac_scope
For GPT-RAG, the document-level access control model is POSIX-style ACL plus RBAC scopes. Each document can carry ACL metadata such as user and group object IDs. When applicable, a document can also carry an RBAC scope such as a storage container resource ID.
GPT-RAG ingestion is responsible for collecting and attaching the permission metadata for each document automatically. This repo does not rely on the built-in Azure AI Search indexers for permission extraction. Permissions are handled by the pipeline implemented in gpt-rag-ingestion.
When ACL and RBAC scope metadata are present, Azure AI Search evaluates them as alternatives. Access is granted when any one permission type matches: userIds, groupIds, or rbacScope.
In normal usage, userIds and groupIds contain Microsoft Entra ID object IDs as strings. They also support special values.
- `["all"]` makes every caller match this ACL type.
- `["none"]` and `[]` mean no caller matches this ACL type.
These values only apply to that ACL type. For example, userIds set to ["none"] does not block access through groupIds or RBAC scope.
RBAC scopes are applied at the storage container level. When RBAC is used for a document, users must have the appropriate Azure role assignment on the container scope, typically Storage Blob Data Reader.
In test environments, if you do not want Azure AI Search to enforce document permissions, set permissionFilterOption to disabled in the index definition. This is the default.
Azure AI Search enforces limits for document permission fields. The userIds and groupIds fields accept up to 32 values each. The rbacScope field is limited to five distinct values across the entire index.
Prerequisites
For user authentication - A Microsoft Entra ID App Registration used by both the UI and the orchestrator. The UI uses it to sign users in and request an access token for the orchestrator API. The orchestrator then validates that token and uses the app's client secret to perform the On-Behalf-Of (OBO) token exchange when the Azure AI Search index is configured for document-level access control. - You have permissions in the tenant to create or update the App Registration, add API permissions.
For document-level security
- User authentication is configured.
- Your Azure AI Search index is configured for document-level access control and includes the required document permission fields. *
- Your documents are indexed with the correct security metadata for each document (user object IDs, group object IDs, and RBAC scope). **
- Tenant admin consent is granted for the Azure AI Search delegated permission user_impersonation in the App Registration.
* If you use the GPT-RAG provisioning workflow, it already creates the index with this configuration.
** If you use gpt-rag data ingetion pipeline documents are automatically indexed with ACL when they're set in the blobs metadata fields
Setup
1) Create one App Registration in Microsoft Entra ID.
After creating it, go to the App Registration Overview page and copy the Application (client) ID and the Directory (tenant) ID. You will use them later as OAUTH_AZURE_AD_CLIENT_ID and OAUTH_AZURE_AD_TENANT_ID.
If you plan to use group-based access control, in the App Registration go to Manage > Token configuration and add a groups claim.
If users belong to many groups, Entra may emit an overage indicator instead of the full group list, which typically requires resolving membership via Microsoft Graph.
2) In the App Registration, go to Manage > Authentication.
Under Platform configurations > Web, add the frontend redirect URI.
https://<YOUR-APP-URL>/auth/oauth/azure-ad/callback
or
https://<YOUR-APP-URL>/.auth/login/aad/callback
3) In the App Registration, go to Manage > Expose an API.
Set the Application ID URI to api://<OAUTH_AZURE_AD_CLIENT_ID>.
Then create a delegated scope named user_impersonation and add this scope to the app.
api://<OAUTH_AZURE_AD_CLIENT_ID>/user_impersonation
4) In the App Registration, go to Manage > Certificates & secrets.
Create a new client secret and copy the secret value.
You can only copy the secret value once. Store it securely (for example, in Key Vault) and use it later for the orchestrator configuration.
5) Add the Azure AI Search delegated permission user_impersonation and grant tenant consent.
In the App Registration, go to Manage > API permissions.
Add a permission
APIs my organization uses
Azure Cognitive Search
Delegated permissions
user_impersonation
Grant admin consent
CLI alternative if Azure Cognitive Search does not appear in the picker. First, discover the Azure AI Search resource application ID and the scope ID.
az rest --method GET --url "https://graph.microsoft.com/v1.0/servicePrincipals?$filter=servicePrincipalNames/any(s:s eq 'https://search.azure.com')&$select=appId,displayName,oauth2PermissionScopes" --query "value[0].{resourceAppId:appId,scopeId:oauth2PermissionScopes[?value=='user_impersonation'].id | [0],name:displayName}"
Then add the permission and grant consent.
az ad app permission add --id <replace-by-app-client-id> --api <SEARCH_RESOURCE_APP_ID> --api-permissions <SCOPE_ID>=Scope
az ad app permission admin-consent --id <replace-by-app-client-id>
Validate the configured permissions.
az ad app permission list --id <replace-by-app-client-id>
6) Configure the following app configuration settings used by the authentication flow.
GPT-RAG authentication settings are configured in Azure App Configuration. Create the keys below in App Configuration and apply the gpt-rag label. For non-secret settings, store the value as plain text. For secret settings, store the value in Key Vault and add the key in App Configuration as a Key Vault reference (also under the gpt-rag label).
At a minimum, you only need to set the settings marked as Required in the table below. The remaining settings are optional and only needed if you want to customize behavior.
Key Vault access for every
gpt-raglabel reader. App Configuration does not resolve Key Vault references on behalf of the application: each component that loads thegpt-raglabel resolves them with its own identity. After you add a Key Vault reference (for example,OAUTH_AZURE_AD_CLIENT_SECRET) under that shared label, grant Key Vault Secrets User on the vault to the managed identity of every component that reads the label, not just the UI. This includes the UI container app and the orchestrator, including its hosted-agent identity when you use hosted topologies. A component without this role fails at startup or on its first request (for example, the orchestrator returns HTTP 500) because it cannot resolve the reference. Prefer granting the role on the individual secret rather than on the whole vault.az role assignment create --assignee-object-id <component-principal-id> --assignee-principal-type ServicePrincipal \ --role "Key Vault Secrets User" \ --scope "<key-vault-resource-id>/secrets/<secret-name>"Client secrets also expire. Your tenant's policy may limit their lifetime, sometimes to less than 30 days. Record the expiration date and rotate the secret in Key Vault before it expires. Because the App Configuration reference points to the unversioned secret, rotation does not require changing App Configuration. Restart the UI and orchestrator revisions after rotating so they load the new value.
| Setting | Required | Secret? | What it controls |
|---|---|---|---|
OAUTH_AZURE_AD_CLIENT_ID |
Yes (for OAuth) | No | Entra App Registration application (client) ID used by the UI OAuth provider to request tokens for the orchestrator API. |
OAUTH_AZURE_AD_TENANT_ID |
Yes (for OAuth) | No | Entra tenant ID used to validate/target the tenant for the OAuth flow (single-tenant by default). |
OAUTH_AZURE_AD_CLIENT_SECRET |
Yes (for OAuth) | Yes | Client secret used by the app for confidential-client flows (for example, completing the OAuth code flow and performing the OBO exchange). Store as a Key Vault secret and reference it from App Configuration (or from App Settings if you bypass App Configuration). |
CHAINLIT_AUTH_SECRET |
Yes | Yes | Secret used by Chainlit to sign its session JWT. If missing, the UI generates a temporary value (sessions reset on restart). Store as a Key Vault secret and reference it from App Configuration (or from App Settings if you bypass App Configuration). |
CHAINLIT_URL |
No | No | Public base URL of the UI. Used to build the OAuth redirect/callback URL (and normalized without a trailing slash). |
OAUTH_AZURE_AD_SCOPES |
No | No | Scopes requested during interactive login. If omitted, the UI defaults to the orchestrator API scope plus OpenID Connect scopes. Setting this explicitly helps avoid accidentally getting Microsoft Graph tokens. |
OAUTH_AZURE_AD_ENABLE_SINGLE_TENANT |
No | No | Defaults to true. When true, the UI enforces single-tenant behavior for the OAuth flow. Set to false only for multi-tenant scenarios. |
ALLOW_ANONYMOUS |
No | No | UI anonymous-mode setting; defaults to true locally without OAuth. The unmerged orchestrator candidate also uses it for retrieval mode selection, never to override a user assertion or user-only source. See candidate policy. |
7) Configure the Azure AI Search index for document-level access control and ensure your documents include permission metadata.
In the index definition, set permissionFilterOption to enabled.
These are the Azure AI Search index fields used for document-level access control:
metadata_security_user_ids Collection(Edm.String) permissionFilter=userIds
metadata_security_group_ids Collection(Edm.String) permissionFilter=groupIds
metadata_security_rbac_scope Edm.String permissionFilter=rbacScope
During Blob Storage ingestion, the GPT-RAG ingestion pipeline reads the following blob metadata keys and adds them to the corresponding documents in the index. If you want to specify which users or groups can access a document, set these metadata fields on each blob.
metadata_security_user_ids
metadata_security_group_ids
Note: For SharePoint ingestion, you don't need any additional steps. The GPT-RAG ingestion pipeline typically derives the user and group ACLs from the source SharePoint document or list item permissions and populates the same fields automatically.
Examples of how to populate metadata_security_user_ids and metadata_security_group_ids.
["11111111-1111-1111-1111-111111111111"]
["11111111-1111-1111-1111-111111111111","22222222-2222-2222-2222-222222222222"]
11111111-1111-1111-1111-111111111111,22222222-2222-2222-2222-222222222222
['11111111-1111-1111-1111-111111111111','22222222-2222-2222-2222-222222222222']
Note: For Blob Storage ingestion, the GPT-RAG ingestion pipeline will populate
metadata_security_rbac_scopeautomatically. The value is the Azure resource ID of the container, for example: /subscriptions/<subscriptionId>/resourceGroups/<resourceGroup>/providers/Microsoft.Storage/storageAccounts/<storageAccount>/blobServices/default/containers/<container>
Admin Access for the Operator Dashboards
In the currently released classic Container Apps topology, the orchestrator
and ingestion service each ship a small operator dashboard. The orchestrator
dashboard is mounted at /dashboard on the orchestrator Container App and the
ingestion dashboard is mounted at /dashboard on the ingestion Container App.
Both surfaces let an operator inspect runs, conversations, and a curated set of
runtime settings, and the ingestion dashboard also lets an operator trigger a
scheduled job on demand. The
published hosted component matrix
defines hosted/no-panel, which omits these administrative surfaces and requires
DEPLOY_ADMINISTRATIVE_PANEL=false. Hosted-panel is an explicit supported
topology in the pinned matrix, but its user-history and operator routes remain
off/503 behind separate deployment-published evidence gates.
Admin access is enforced through an Entra ID App Role named Admin added to the same App Registration you created for user sign-in. The token the caller presents to /api/dashboard/* (or the ingestion equivalents) must carry Admin in its roles claim.
The ingestion v2.7.3 metadata-only Overview and Curation APIs use their own
explicit PANEL_OPERATOR_APP_ROLE or PANEL_OPERATOR_GROUP_ID gate. Their Vite
tabs do not acquire a browser token or add an Authorization header, so they
require an approved reverse-auth proxy to supply the delegated bearer and
otherwise expose the real 401/403/503 response. This is not the orchestrator
dashboard's MSAL flow.
This section explains how to add the role, assign users, request the correct scope, and debug the most common error (a 403 response even though the role is assigned).
The orchestrator dashboard SPA performs its own Microsoft Entra ID sign-in (MSAL, Authorization Code + PKCE) independent of the chat UI. If you are enabling sign-in specifically for the orchestrator dashboard, follow Admin Dashboard Sign-in for the SPA-specific setup: adding a Single-page application redirect URI, exposing the
access_as_userscope, and theOAUTH_AZURE_AD_TENANT_ID,OAUTH_AZURE_AD_CLIENT_ID, and optionalOAUTH_AZURE_AD_API_SCOPEApp Configuration keys. The rest of this section covers theAdminapp role itself, which is shared by both dashboards.
How each dashboard is gated
The two dashboards apply the gate at slightly different layers. The HTML shell is always served (it is just static files) so the page can render its loading state before the user signs in, but every endpoint that returns data or performs an action enforces the gate.
| Surface | HTML at /dashboard |
Data endpoints | Job actions | Configuration tab |
|---|---|---|---|---|
| Orchestrator | Open (loading shell only) | Admin role required on every /api/dashboard/* call |
Not applicable | Entire tab requires Admin (read and write) |
| Ingestion | Open (loading shell only) | Open (read-only run summaries) | Admin required for POST /api/jobs/{job_type}/run |
GET /api/config open in read-only mode for non-admins; PUT /api/config, POST /api/config/reload, and POST /api/config/apply require Admin |
In all cases, when OAUTH_AZURE_AD_TENANT_ID is not configured the gate is a no-op. This matches the rest of the orchestrator and ingestion behavior in local development and lets contributors run both services without an Entra app registration.
The orchestrator dashboard is only mounted when ENABLE_DASHBOARD is set to true. In default deployments the routes do not exist at all. Set ENABLE_DASHBOARD=true under the gpt-rag-orchestrator label in App Configuration to enable it.
flowchart LR
User[Operator] -->|Bearer token<br/>api://CLIENT_ID/.default| Orch[Orchestrator /api/dashboard/*]
User -->|Bearer token<br/>api://CLIENT_ID/.default| Ing[Ingestion /api/jobs/*/run<br/>and /api/config writes]
Orch -->|require_admin| RoleCheck{roles claim<br/>contains 'Admin'?}
Ing -->|require_admin| RoleCheck
RoleCheck -->|yes| Allow[Authorized to attempt operation]
RoleCheck -->|no| Deny[403 Admin role required]
1) Add the Admin app role to the App Registration
You add the role to the same App Registration created in the Setup section above. You can do this in the Azure portal or with the Azure CLI.
Portal
In the App Registration, go to Manage > App roles, then Create app role and fill the form as follows.
| Field | Value |
|---|---|
| Display name | Admin |
| Allowed member types | Users/Groups |
| Value | Admin |
| Description | Grants access to the GPT-RAG operator dashboards. |
| Do you want to enable this app role? | Checked |
The Value field is what the dashboards check. It must be exactly Admin (case-sensitive).
CLI
az ad app update --id <replace-by-app-client-id> --app-roles '[{
"allowedMemberTypes": ["User"],
"description": "Grants access to the GPT-RAG operator dashboards.",
"displayName": "Admin",
"isEnabled": true,
"value": "Admin"
}]'
Note:
az ad app update --app-rolesreplaces the full list. If the App Registration already defines other app roles, include them in the same JSON array.
2) Assign users or groups to the Admin role
App role assignments are done on the Enterprise application, not on the App Registration.
- In the Entra portal go to Microsoft Entra ID > Enterprise applications.
- Open the application that matches your App Registration name (same client ID).
- Go to Users and groups > Add user/group.
- Pick the user or group, select the Admin role, and confirm.
Only users assigned the role will receive Admin in the roles claim of their access token. Users who are not assigned the role can still sign in and use the chat UI; they only see a 403 when calling a gated dashboard endpoint.
Tip: For production, assign a security group instead of individual users so you can manage membership without touching Entra.
3) Request a scope that returns the roles claim
This is the most common source of confusion. Entra only includes the roles claim in an access token when the token is requested for your own API, not for Microsoft Graph. The scope you request decides the audience of the token and therefore whether the claim is present.
| Scope you request | Token audience | roles claim present? |
|---|---|---|
api://<OAUTH_AZURE_AD_CLIENT_ID>/.default |
Your API | Yes |
api://<OAUTH_AZURE_AD_CLIENT_ID>/user_impersonation |
Your API | Yes |
User.Read (or any https://graph.microsoft.com/* scope) |
Microsoft Graph | No |
openid profile email only |
ID token only, no API access token | No (and no API call possible) |
If the UI requests only Graph scopes or only OpenID Connect scopes, the access token sent to the orchestrator will not contain roles, and require_admin will reject the call with 403 Admin role required even when the role is correctly assigned in Entra.
The GPT-RAG UI already requests api://<OAUTH_AZURE_AD_CLIENT_ID>/user_impersonation by default. If you override OAUTH_AZURE_AD_SCOPES, make sure the API scope is still in the list. For example:
OAUTH_AZURE_AD_SCOPES = api://<OAUTH_AZURE_AD_CLIENT_ID>/user_impersonation openid profile email
4) ENABLE_DASHBOARD and labels in App Configuration
The dashboards and their Configuration tabs both read and write App Configuration. Each service uses its own label so an operator can filter cleanly in the portal.
| Setting | Service | Label | Purpose |
|---|---|---|---|
ENABLE_DASHBOARD |
Orchestrator | gpt-rag-orchestrator |
When true, mounts the orchestrator dashboard at /dashboard. Defaults to false. |
| Curated runtime settings | Orchestrator | gpt-rag-orchestrator |
The 12 keys exposed on the Configuration tab (see below). |
| Curated runtime settings | Ingestion | gpt-rag-ingestion |
The 17 keys exposed on the Configuration tab (see below). |
The ingestion dashboard is always mounted; there is no equivalent feature flag.
5) Configuration tab: what it can change, and what Apply actually does
The Configuration tab in each dashboard is intentionally narrow. It only exposes a curated allowlist of settings, never secrets. The backend enforces both an allowlist (only listed keys can be read or written) and a denylist as defense in depth (any key whose name suggests a secret is rejected even if it sneaks into the allowlist).
The denylist rejects any key that:
- starts with
OAUTH_, or - ends with
_APIKEY,_API_KEY,_SECRET,_PASSWORD,_CONNECTION_STRING,_CONNSTRING,_PRIVATE_KEY, or_TOKEN, or - is one of the known sensitive keys hard-coded in the registry (for example
AZURE_CLIENT_ID,KEY_VAULT_URI,APP_CONFIG_ENDPOINT).
So even if an operator types a Key Vault reference into a field, the API rejects the write.
Orchestrator allowlist (12 keys)
AGENT_STRATEGY, REASONING_EFFORT, CHAT_TEMPERATURE, CHAT_TOP_P, MAX_COMPLETION_TOKENS, CHAT_HISTORY_MAX_MESSAGES, CONVERSATION_HISTORY_COMPACTION_ENABLED, SEARCH_RETRIEVAL_ENABLED, SEARCH_RAGINDEX_TOP_K, BING_RETRIEVAL_ENABLED, INFERENCE_MAX_RETRIES, MULTIMODAL_CLASSIFY_IMAGES.
Ingestion allowlist (17 keys)
The 7 cron expressions (CRON_RUN_SHAREPOINT_INDEX, CRON_RUN_SHAREPOINT_PURGE, CRON_RUN_IMAGES_PURGE, CRON_RUN_BLOB_INDEX, CRON_RUN_BLOB_PURGE, CRON_RUN_NL2SQL_INDEX, CRON_RUN_NL2SQL_PURGE) plus chunking, indexing, throughput, limits, multimodal, and SharePoint-source settings.
Read-only mode in the ingestion Configuration tab
GET /api/config on the ingestion service is open to any caller, even unauthenticated, so the tab can render without a second round-trip. The response includes a canEdit boolean that is true only when auth is off, or when the caller presents a valid token carrying the Admin role. A non-admin operator sees the current values but the fields and the Save button are disabled. Writes (PUT /api/config), cache reload (POST /api/config/reload), and Apply (POST /api/config/apply) all require Admin. The orchestrator dashboard hides the tab entirely for non-admins instead, since the whole /api/dashboard/* surface is gated.
What Apply does, and what it does not do
The button labeled Apply calls POST /api/dashboard/config/apply (orchestrator) or POST /api/config/apply (ingestion). On a successful call, these endpoints perform a soft refresh; passing the authorization gate alone does not guarantee that the operation succeeds:
- The in-process App Configuration cache is refreshed so subsequent reads in this container instance see the new values immediately.
- On the ingestion service, every known cron job is rescheduled in APScheduler using the latest expression in App Configuration. This means a cron change takes effect on the running container without a restart.
The endpoint is intentionally called /config/apply and not /restart so the response honestly reflects what happens. The Container App revision is not recycled. Other replicas of the same service refresh their own cache on their normal cadence; if you want every replica updated immediately, restart the Container App revision in the portal.
Unmerged ingestion failure-handling preview
Azure/gpt-rag-ingestion#296,
at 3a46472,
corrects a genuine schedule-application failure in POST /api/config/apply
to return HTTP 500, not a successful Apply response. This is candidate
behavior, not a claim about the currently released service.
PUT /api/config preserves HTTP 200 and the keys in applied when the
durable writes and schedule handling succeed but the subsequent best-effort
local cache refresh fails. That refresh failure alone does not add a key
to failed. Explicit Reload and Apply are separate operations whose
outcomes must still be checked.
Unmerged orchestrator configuration and authentication clarification
At b93fb58,
unexpected configuration-read failures no longer become default dashboard
values or an "authentication not configured" decision. The dependency
checkpoint 7541c3e
likewise propagates unexpected required JWT tenant/client read failures.
Actual App Configuration missing-key and exhausted-retry fallback behavior
remains unchanged. Known authentication HTTPException statuses remain;
unexpected token-validation failures retain a generic 401.
Orchestrator PUT /api/dashboard/config still returns 500 for per-key
write failures, with the same error shape and the bounded message
Unable to persist setting. Other keys in that request may already persist.
A cache-refresh failure after durable writes also remains 500, unlike
ingestion's refresh-only 200/applied contract above. Neither status nor
code rollback undoes durable writes. The existing API-key environment
fallback was retained under an inactive proposal at this checkpoint;
subsequent initial administrative acceptance is not new identity approval.
These are historical checkpoints; roles, labels, defaults and SSE schemas
are not redefined by this preview.
6) Operator workflow: verifying the role lands in the token
When access works in Entra but the dashboard still returns 403, the access token sent to the API is almost always missing the roles claim. The fastest way to confirm is to inspect the actual token.
- Sign in to the GPT-RAG UI as the user assigned to the role.
- Open the browser developer tools, switch to the Network tab, and filter by
dashboardorconfig. - Trigger any dashboard request, then open the request and copy the value of the
Authorizationheader (the part afterBearer). - Paste the token into https://jwt.ms and look at the claims.
You should see:
"aud": "api://<OAUTH_AZURE_AD_CLIENT_ID>",
"roles": ["Admin"],
"scp": "user_impersonation"
If roles is missing, work down this checklist:
| Symptom | Likely cause | Fix |
|---|---|---|
aud is 00000003-0000-0000-c000-000000000000 or starts with https://graph.microsoft.com |
The UI requested a Microsoft Graph scope | Make sure OAUTH_AZURE_AD_SCOPES contains api://<CLIENT_ID>/user_impersonation (or leave it unset to use the default) |
aud is correct but roles is missing |
The user is not assigned to the Admin app role |
Assign the user (or their group) in Enterprise applications > Users and groups |
aud is correct, roles is ["Admin"], but the API still returns 403 |
Stale cached token in the browser | Sign out, clear the site cookies, and sign back in to force Entra to issue a new token |
aud is correct, roles is missing, and the user was just assigned the role |
Token was issued before the assignment | Sign out and back in; access tokens are typically cached for 60 to 90 minutes |
Orchestrator returns 404 at /dashboard |
ENABLE_DASHBOARD is not set |
Set ENABLE_DASHBOARD=true under the gpt-rag-orchestrator label in App Configuration and restart the container |
| Local dev returns 401 instead of being open | OAUTH_AZURE_AD_TENANT_ID is set in your local environment |
Unset it (or remove the value from your local .env) so require_admin becomes a no-op |
Tip: The ingestion service also exposes
GET /api/identity, which returns{"authEnabled": true, "isAdmin": true}when the caller carries theAdminrole. Curling that endpoint with your token is a quick way to confirm the gate sees the role without triggering a job run.