---
title: "Microsoft Entra ID"
description: "Configure Microsoft Entra ID for deployment"
source: "Guardian Platform"
---
## Overview

The Guardian Platform integrates with Microsoft Entra ID (formerly Azure Active Directory) for enterprise-grade authentication and authorization.

**Required app registrations:**

- **Guardian Platform Backend API** – Web application for backend API authentication and token validation
- **Microsoft Graph API Access** – App with `User.Read.All` for user lookups via Microsoft Graph; the bootstrap chart reuses the backend app
- **Governance Worker** – M2M confidential client for automated governance workflow execution
- **Guardian SPA** – Single Page Application for frontend authentication

The `entra-bootstrap` Helm chart automates most of this: it runs a one-time Kubernetes Job that creates the backend, frontend, and worker app registrations, the `access_as_user` scope, and the Microsoft Graph permissions. It uses the backend app for Graph access instead of a separate registration. Run it **before** deploying the platform chart.

**This guide covers:**

- Creating app registrations with the bootstrap chart, or by hand with the Azure CLI or Portal
- Configuring API permissions and scopes
- Setting up Microsoft Graph access
- Organization and admin user setup by the post-install hook
- Generating the token signing key
- Configuring the Helm chart for Entra ID

## Prerequisites

- Microsoft Entra ID tenant (Azure AD tenant)
- [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) installed and configured
- Permissions to create app registrations and grant admin consent in the tenant
- Tenant ID (found in Azure Portal > Microsoft Entra ID > Overview)
- `kubectl` and `helm` access to the deployment namespace
- [`jq`](https://jqlang.org/download/) 1.6 or later, to read the bootstrap's generated credentials
- A clone of the [deployment repository](./prerequisites.mdx#deployment-repository) for the `entra-bootstrap` chart

## Helm Configuration

Add the following to the `values.yaml` and `secrets.yaml` files. The setup steps below fill in the placeholders.

`values.yaml`:

```yaml
auth-service:
  config:
    idp:
      provider: "entra"
      issuer: "https://login.microsoftonline.com/<tenant-id>/v2.0"
      entra:
        tenantId: "<tenant-id>"
    tokenExchange:
      enabled: true
      keyId: "auth-service-001" # Identifier for the token signing key

governance-service:
  config:
    authProvider: "entra"
    entraTenantId: "<tenant-id>"

guardian:
  config:
    authProvider: "entra"
    entraTenantId: "<tenant-id>"
    entraClientId: "<spa-client-id>" # Frontend (SPA) app
    entraScopes: "openid profile email offline_access api://<backend-client-id>/access_as_user"
```

`secrets.yaml`:

```yaml
global:
  secrets:
    create: true
    auth:
      provider: "entra"
      entra:
        secretName: "platform-entra"
        values:
          clientId: "<backend-client-id>"
          clientSecret: "<backend-client-secret>"
          tenantId: "<tenant-id>"
          graphClientId: "<graph-client-id>" # The backend app when using the bootstrap chart
          graphClientSecret: "<graph-client-secret>"
          tokenExchangePrivateKey: | # Generate with: openssl genrsa 2048
            -----BEGIN PRIVATE KEY-----
            ...
            -----END PRIVATE KEY-----
    governanceWorker:
      secretName: "platform-governance-worker"
      values:
        encryptionKey: "<worker-encryption-key>" # Generate with: openssl rand -base64 32
        clientId: "<worker-client-id>"
        clientSecret: "<worker-client-secret>"
```

Entra deployments always use token exchange: the Auth Service signs the platform's access tokens with `tokenExchangePrivateKey`, which the chart stores as the `token-exchange-private-key` key of the `platform-entra` secret. Every Auth Service replica reads that key, so they all sign with the same one. With `global.secrets.create: true`, rendering fails without it; if you create `platform-entra` yourself, add the `token-exchange-private-key` key. [guardctl](./guardctl.mdx) generates the key and sets `tokenExchange`.

## Quick Start (Bootstrap Chart - Recommended)

Create a service principal for the bootstrap, run the `entra-bootstrap` chart, and copy the IDs and secrets it saves into the platform's chart values, in six steps.

### 1. Create the Bootstrap Service Principal

The bootstrap job calls Microsoft Graph as its own service principal, which needs `Application.ReadWrite.All` and `DelegatedPermissionGrant.ReadWrite.All` (application permissions). Sign in to the tenant and create it:

```bash
az login --tenant <tenant-id>

BOOTSTRAP_APP_ID=$(az ad app create --display-name "Governance Bootstrap SP" --query appId -o tsv)
az ad sp create --id $BOOTSTRAP_APP_ID

# Application.ReadWrite.All and DelegatedPermissionGrant.ReadWrite.All
az ad app permission add --id $BOOTSTRAP_APP_ID \
  --api 00000003-0000-0000-c000-000000000000 \
  --api-permissions 1bfefb4e-e0b5-418b-a88f-73c46d2cc8e9=Role 8e8e4742-1d2d-4f22-b4fa-e31d2a2e3798=Role

az ad app permission admin-consent --id $BOOTSTRAP_APP_ID

# Confirm both Graph permissions are listed
az ad app permission list --id $BOOTSTRAP_APP_ID -o table

az ad app credential reset --id $BOOTSTRAP_APP_ID --display-name "bootstrap" --years 2
```

Save the `password` from the last command as the bootstrap client secret. Admin consent needs a Global Administrator (or Privileged Role Administrator); if `admin-consent` fails, have one grant it in Azure Portal > Microsoft Entra ID > App registrations > Governance Bootstrap SP > API permissions.

### 2. Create the Service Principal Secret

The job reads the service principal's credentials from the `entra-bootstrap-sp` secret:

```bash
kubectl create secret generic entra-bootstrap-sp \
  --from-literal=client-id=<bootstrap-app-id> \
  --from-literal=client-secret=<bootstrap-client-secret> \
  --namespace guardian
```

This secret is only for the bootstrap; the platform services use `platform-entra`.

### 3. Prepare the Bootstrap Values

From the deployment repository checkout, start from the example values file:

```bash
cp charts/entra-bootstrap/examples/values.yaml bootstrap-values.yaml
```

Edit these keys in `bootstrap-values.yaml`:

```yaml title="bootstrap-values.yaml"
entra:
  tenantId: "<tenant-id>"

apps:
  frontend:
    redirectUris:
      - "https://<domain>"
      - "http://localhost:5173"
```

The SPA redirect URIs come only from `apps.frontend.redirectUris`; the `http://localhost:5173` entry is for local Guardian development and can be removed. If the tenant already has a Guardian Platform deployment, add a per-environment suffix to the `displayName` values under `apps`: the job reuses the app registration with exactly that display name and overwrites its redirect URIs, and fails if more than one app registration has the name.

### 4. Run the Bootstrap

```bash
helm upgrade --install entra-bootstrap ./charts/entra-bootstrap \
  --namespace guardian \
  --values bootstrap-values.yaml \
  --wait \
  --timeout 10m

kubectl logs job/entra-bootstrap-entra-bootstrap --namespace guardian

# Confirm the backend, frontend, and worker app registrations exist, beside Governance Bootstrap SP
az ad app list --filter "startswith(displayName,'Guardian Platform') or startswith(displayName,'Governance')" --query "[].{name:displayName, appId:appId}" -o table
```

The log ends with an **Entra ID Bootstrap Summary** of the backend, frontend, and worker app IDs. The job saves the app IDs, the tenant ID, and new client secrets to the `entra-bootstrap-credentials` secret (`<release>-credentials`, or `credentialsSecret.name`), not to its logs. Read them with `jq`:

```bash
kubectl get secret entra-bootstrap-credentials --namespace guardian -o json \
  | jq '.data | map_values(@base64d)'
```

| Key                                          | Value                         |
| -------------------------------------------- | ----------------------------- |
| `tenant-id`                                  | The directory (tenant) ID     |
| `backend-client-id`, `backend-client-secret` | The backend app's credentials |
| `frontend-client-id`                         | The frontend app ID           |
| `worker-client-id`, `worker-client-secret`   | The worker app's credentials  |

Helm creates the secret empty and keeps it on uninstall, and the job can only read and update it.

Instead of these commands, the deployment repository's helper script checks for the service principal secret named in the values file, runs the chart, prints the job logs and the command that reads the credentials, and then deletes the job: `./scripts/entra/bootstrap-entra.sh -f bootstrap-values.yaml -n guardian`.

The job is idempotent: re-running it reuses app registrations with the same display name and keeps their client secrets. For an existing app with no client secret, it creates one and saves it. The job fails if it cannot create or save a secret.

### 5. Finish the App Registrations

The bootstrap's own admin consent attempt fails without an administrator role, and it doesn't set the worker's Application ID URI. Signed in as a Global Administrator or Privileged Role Administrator:

```bash
# Graph User.Read.All, which the admin-user hook and the Auth Service need
az ad app permission admin-consent --id <backend-client-id>

# The SPA's delegated scopes, needed where user consent is turned off
az ad app permission admin-consent --id <frontend-client-id>

# The Auth Service requests worker tokens for api://<worker-client-id>
az ad app update --id <worker-client-id> \
  --identifier-uris "api://<worker-client-id>"
```

### 6. Update Chart Values

Fill in the [Helm Configuration](#helm-configuration) sections from the credentials secret:

- `<tenant-id>`: `tenant-id`
- `clientId` / `clientSecret` and `graphClientId` / `graphClientSecret`: `backend-client-id` and `backend-client-secret`
- `governanceWorker.values.clientId` / `clientSecret`: `worker-client-id` and `worker-client-secret`, plus an `encryptionKey` from `openssl rand -base64 32`
- `entraClientId`: `frontend-client-id`; `entraScopes`: the backend app ID in `api://<backend-client-id>/access_as_user`
- `tokenExchangePrivateKey`: a token signing key from `openssl genrsa 2048`, including its `BEGIN` and `END` lines

Add the organization and admin user as described in [Create Organization and Admin User](#create-organization-and-admin-user), then proceed to the [Helm Deployment guide](./helm-deployment.mdx).

## Alternative: Manual Setup via Azure CLI

Without the bootstrap chart, create the platform's four app registrations with the Azure CLI and generate the token signing key, in six steps.

### 1. Authenticate to Azure

The tenant ID can be found in Azure Portal > Microsoft Entra ID > Overview. Login to Azure with the tenant:

```bash
az login --tenant <tenant-id>
```

Verify the correct tenant:

```bash
az account show
```

Update `values.yaml` with the tenant ID.

`values.yaml` -> auth-service.config.idp:

```yaml
issuer: "https://login.microsoftonline.com/<tenant-id>/v2.0"
entra:
  tenantId: "<tenant-id>"
```

`values.yaml` -> governance-service.config:

```yaml
entraTenantId: "<tenant-id>"
```

`values.yaml` -> guardian.config:

```yaml
entraTenantId: "<tenant-id>"
```

Update `secrets.yaml` with the tenant ID.

`secrets.yaml` -> global.secrets.auth.entra.values:

```yaml
tenantId: "<tenant-id>"
```

### 2. Create Backend API App Registration

Create the backend API app registration:

```bash
az ad app create \
  --display-name "Guardian Platform Backend" \
  --sign-in-audience AzureADMyOrg
```

Save the `appId` and `id` from the output:

- `appId` is the Client ID
- `id` is the Object ID

Store both as variables for use in subsequent commands:

```bash
BACKEND_CLIENT_ID="<app-id-from-output>"
BACKEND_OBJECT_ID="<id-from-output>"
```

Create a service principal and set the application ID URI:

```bash
az ad sp create --id $BACKEND_CLIENT_ID

az ad app update --id $BACKEND_CLIENT_ID \
  --identifier-uris "api://$BACKEND_CLIENT_ID"
```

Generate a unique ID for the API scope:

```bash
uuidgen
```

Save this UUID — it will be used as `SCOPE_ID` in the next step.

Create a file named `backend-scope.json` with the following content. Replace `SCOPE_ID` with the UUID from above:

```json
{
  "api": {
    "requestedAccessTokenVersion": 2,
    "oauth2PermissionScopes": [
      {
        "id": "SCOPE_ID",
        "adminConsentDescription": "Allow the application to access Guardian Platform on behalf of the signed-in user",
        "adminConsentDisplayName": "Access Guardian Platform",
        "isEnabled": true,
        "type": "User",
        "userConsentDescription": "Allow the application to access Guardian Platform on your behalf",
        "userConsentDisplayName": "Access Guardian Platform",
        "value": "access_as_user"
      }
    ]
  }
}
```

Add the API scope to the app registration:

```bash
az rest --method PATCH \
  --uri "https://graph.microsoft.com/v1.0/applications/$BACKEND_OBJECT_ID" \
  --headers "Content-Type=application/json" \
  --body @backend-scope.json
```

Create a client secret for the backend app:

```bash
az ad app credential reset \
  --id $BACKEND_CLIENT_ID \
  --append \
  --query password \
  --output tsv
```

Save the client secret — it cannot be retrieved later.

Update `secrets.yaml` with the backend credentials.

`secrets.yaml` -> global.secrets.auth.entra.values:

```yaml
clientId: "<backend-client-id>"
clientSecret: "<client-secret-from-output>"
```

Update `values.yaml` with the backend client ID (used in the scopes string).

`values.yaml` -> guardian.config:

```yaml
entraScopes: "openid profile email offline_access api://<backend-client-id>/access_as_user"
```

### 3. Create Graph API Access App Registration

Create the Graph API access app registration:

```bash
az ad app create \
  --display-name "Guardian Platform Graph API" \
  --sign-in-audience AzureADMyOrg
```

Save the `appId` from the output — this is the Graph API Client ID.

Store it as a variable for use in subsequent commands:

```bash
GRAPH_CLIENT_ID="<app-id-from-output>"
```

Create a service principal for the app:

```bash
az ad sp create --id $GRAPH_CLIENT_ID
```

Add the Microsoft Graph API permission for reading users:

```bash
az ad app permission add \
  --id $GRAPH_CLIENT_ID \
  --api 00000003-0000-0000-c000-000000000000 \
  --api-permissions df021288-bdef-4463-88db-98f22de89214=Role
```

This adds `User.Read.All` (`df021288-bdef-4463-88db-98f22de89214`) – Read all users' full profiles.

Grant admin consent for the API permissions (requires admin privileges):

```bash
az ad app permission admin-consent --id $GRAPH_CLIENT_ID
```

Create a client secret for the Graph API app:

```bash
az ad app credential reset \
  --id $GRAPH_CLIENT_ID \
  --append \
  --query password \
  --output tsv
```

Save the client secret — it cannot be retrieved later.

Update `secrets.yaml` with the Graph API credentials.

`secrets.yaml` -> global.secrets.auth.entra.values:

```yaml
graphClientId: "<graph-client-id>"
graphClientSecret: "<client-secret-from-output>"
```

### 4. Create Governance Worker App Registration

The Governance Worker is a confidential M2M (machine-to-machine) client used for automated governance workflow execution.

Create the Governance Worker app registration:

```bash
az ad app create \
  --display-name "EQTY Governance Worker" \
  --sign-in-audience AzureADMyOrg
```

Save the `appId` from the output — this is the Worker Client ID.

Store it as a variable for use in subsequent commands:

```bash
WORKER_CLIENT_ID="<app-id-from-output>"
```

Create a service principal and set the application ID URI. The Auth Service requests worker tokens for `api://<worker-client-id>`, and Entra rejects that request if the URI is not set:

```bash
az ad sp create --id $WORKER_CLIENT_ID

az ad app update --id $WORKER_CLIENT_ID \
  --identifier-uris "api://$WORKER_CLIENT_ID"
```

Create a client secret for the Worker app:

```bash
az ad app credential reset \
  --id $WORKER_CLIENT_ID \
  --append \
  --query password \
  --output tsv
```

Save the client secret — it cannot be retrieved later.

Generate an encryption key for the worker:

```bash
openssl rand -base64 32
```

Update `secrets.yaml` with the worker credentials.

`secrets.yaml` -> global.secrets.governanceWorker.values:

```yaml
encryptionKey: "<generated-key>"
clientId: "<worker-client-id>"
clientSecret: "<client-secret-from-output>"
```

### 5. Create Frontend SPA App Registration

Create the SPA app registration:

```bash
az ad app create \
  --display-name "Guardian Platform Frontend" \
  --sign-in-audience AzureADMyOrg
```

Save the `appId` and `id` from the output:

- `appId` is the SPA Client ID
- `id` is the SPA Object ID

Store both as variables:

```bash
SPA_CLIENT_ID="<app-id-from-output>"
SPA_OBJECT_ID="<id-from-output>"
```

Configure SPA redirect URIs and set token version to v2.0. Replace `<domain>` with the Guardian Platform domain:

```bash
az rest --method PATCH \
  --uri "https://graph.microsoft.com/v1.0/applications/$SPA_OBJECT_ID" \
  --headers "Content-Type=application/json" \
  --body "{\"spa\":{\"redirectUris\":[\"https://<domain>\"]},\"api\":{\"requestedAccessTokenVersion\":2}}"
```

Create a service principal for the SPA app:

```bash
az ad sp create --id $SPA_CLIENT_ID
```

Get the backend API scope ID:

```bash
az ad app show --id $BACKEND_CLIENT_ID --query 'api.oauth2PermissionScopes[0].id' -o tsv
```

Save this scope ID — it will be used in the next command.

Grant the SPA access to the backend API. Replace `<scope-id>` with the value from above:

```bash
az ad app permission add \
  --id $SPA_CLIENT_ID \
  --api $BACKEND_CLIENT_ID \
  --api-permissions <scope-id>=Scope
```

Optionally, grant admin consent for the API permission:

```bash
az ad app permission admin-consent --id $SPA_CLIENT_ID
```

Note: Admin consent may require higher privileges. If this fails, grant consent later through the Azure Portal.

Update `values.yaml` with the SPA client ID.

`values.yaml` -> guardian.config:

```yaml
entraClientId: "<spa-client-id>"
```

### 6. Generate the Token Signing Key

Generate an RSA private key for the Auth Service to sign the platform's access tokens:

```bash
openssl genrsa 2048
```

Update `secrets.yaml` with the key, including its `BEGIN` and `END` lines.

`secrets.yaml` -> global.secrets.auth.entra.values:

```yaml
tokenExchangePrivateKey: |
  -----BEGIN PRIVATE KEY-----
  ...
  -----END PRIVATE KEY-----
```

## Alternative: Web UI Setup

<details>
<summary>Click to expand Azure Portal instructions</summary>

### Using the Azure Portal

#### 1. Create Backend API App Registration

- Navigate to Azure Portal > Microsoft Entra ID > App registrations
- Click "New registration"
- Name: "Guardian Platform Backend"
- Supported account types: "Accounts in this organizational directory only"
- Click "Register"
- Note the Application (client) ID and Directory (tenant) ID
- Navigate to "Manifest"
- Find "accessTokenAcceptedVersion" and set it to `2` (for v2.0 tokens)
- Click "Save"
- Navigate to "Expose an API"
- Set Application ID URI: `api://<client-id>`
- Add a scope:
  - Scope name: `access_as_user`
  - Admin consent display name: "Access Guardian Platform"
  - Admin consent description: "Allow the application to access Guardian Platform on behalf of the signed-in user"
  - State: Enabled
  - Click "Add scope"
- Navigate to "Certificates & secrets"
- Click "New client secret"
- Description: "Guardian Platform Secret"
- Expires: 24 months (or the organization's policy)
- Click "Add"
- Copy the secret Value immediately — it won't be shown again

Update `values.yaml` with the tenant ID and backend credentials.

`values.yaml` -> auth-service.config.idp:

```yaml
issuer: "https://login.microsoftonline.com/<tenant-id>/v2.0"
entra:
  tenantId: "<tenant-id>"
```

`values.yaml` -> governance-service.config:

```yaml
entraTenantId: "<tenant-id>"
```

`values.yaml` -> guardian.config:

```yaml
entraTenantId: "<tenant-id>"
entraScopes: "openid profile email offline_access api://<backend-client-id>/access_as_user"
```

Update `secrets.yaml` with the backend credentials.

`secrets.yaml` -> global.secrets.auth.entra.values:

```yaml
clientId: "<backend-client-id>"
clientSecret: "<client-secret>"
tenantId: "<tenant-id>"
```

#### 2. Create Graph API Access App Registration

- Create another app registration: "Guardian Platform Graph API"
- Navigate to "API permissions"
- Click "Add a permission"
- Select "Microsoft Graph" > "Application permissions"
- Add: `User.Read.All`
- Click "Add permissions"
- Click "Grant admin consent" (requires admin role)
- Navigate to "Certificates & secrets"
- Create a client secret
- Save the secret value

Update `secrets.yaml` with the Graph API credentials.

`secrets.yaml` -> global.secrets.auth.entra.values:

```yaml
graphClientId: "<graph-client-id>"
graphClientSecret: "<graph-client-secret>"
```

#### 3. Create Governance Worker App Registration

- Create app registration: "EQTY Governance Worker"
- Supported account types: "Accounts in this organizational directory only"
- No redirect URI needed (M2M client)
- Click "Register"
- Navigate to "Expose an API"
- Set Application ID URI: `api://<worker-client-id>`
- Navigate to "Certificates & secrets"
- Create a client secret
- Save the secret value

Generate an encryption key for the worker:

```bash
openssl rand -base64 32
```

Update `secrets.yaml` with the worker credentials.

`secrets.yaml` -> global.secrets.governanceWorker.values:

```yaml
encryptionKey: "<generated-key>"
clientId: "<worker-client-id>"
clientSecret: "<worker-client-secret>"
```

#### 4. Create SPA App Registration

- Create app registration: "Guardian Platform Frontend"
- Navigate to "Manifest"
- Find "accessTokenAcceptedVersion" and set it to `2` (for v2.0 tokens)
- Click "Save"
- Navigate to "Authentication"
- Click "Add a platform" > "Single-page application"
- Redirect URIs: `https://<domain>`
- Click "Configure"
- Navigate to "API permissions"
- Click "Add a permission"
- Select "My APIs"
- Select "Guardian Platform Backend"
- Check the `access_as_user` scope
- Click "Add permissions"
- Optional: Click "Grant admin consent"

Update `values.yaml` with the SPA client ID.

`values.yaml` -> guardian.config:

```yaml
entraClientId: "<spa-client-id>"
```

Generate the token signing key as in [step 6](#6-generate-the-token-signing-key) of the manual CLI setup.

</details>

## Create Organization and Admin User

Before deploying the platform, add the organization and admin user to `values.yaml`. The platform chart's post-install hook creates them during the install:

```yaml
# Add to values.yaml
entra:
  createOrganization: true
  organizationName: "your-org-name"
  displayName: "Organization Display Name"

  createPlatformAdmin: true
  platformAdminEmail: "admin@example.onmicrosoft.com" # Must exist in Entra
  tenantId: "<tenant-id>" # Omit to read tenant-id from the Entra secret
```

This will:

- Create an organization in the governance database
- Look up the Entra user by email via Graph API, using the Graph credentials in the Entra secret
- Create a platform-admin user record linked to the Entra user

The hook fails if the Graph credentials are missing or the user cannot be found. It runs on every install and upgrade and never creates duplicate records. On an existing install it points the organization and the platform admin's user record at Entra and makes the admin an active `organization_owner` again: other organization roles are kept, and project roles and project assignments in the organization are removed. It does not change an existing organization's display name.

## Verification

Verify the OIDC discovery endpoint:

```bash
curl "https://login.microsoftonline.com/<tenant-id>/v2.0/.well-known/openid-configuration"
```

Test token issuance with backend API credentials:

```bash
curl -X POST "https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id=<backend-client-id>" \
  -d "client_secret=<backend-client-secret>" \
  -d "scope=api://<backend-client-id>/.default" \
  -d "grant_type=client_credentials"
```

Test Graph API access:

```bash
curl -X POST "https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id=<graph-client-id>" \
  -d "client_secret=<graph-client-secret>" \
  -d "scope=https://graph.microsoft.com/.default" \
  -d "grant_type=client_credentials"
```

If both commands succeed and return access tokens, the Entra configuration is correct.

## Security Best Practices

- **Use Certificate Authentication:** For production, consider using certificates instead of client secrets
- **Rotate Secrets:** Regularly rotate client secrets (recommended: every 6-12 months), update the Kubernetes secrets, and restart Auth Service so it picks up the new worker secret
- **Conditional Access:** Implement Conditional Access policies for enhanced security
- **Multi-Factor Authentication:** Require MFA for all users
- **Least Privilege:** Only grant the minimum required Graph API permissions
- **Monitor Sign-ins:** Enable sign-in logs and monitor for suspicious activity
- **App Consent Policies:** Configure app consent policies to control which apps users can consent to

## Troubleshooting

- **Bootstrap job fails to authenticate** — Solution: Check the `client-id` and `client-secret` keys in `entra-bootstrap-sp`, `entra.tenantId`, and that admin consent was granted for the bootstrap service principal's permissions
- **Bootstrap log says "Admin consent may need to be granted manually"** — Expected: the job's consent attempt failed; run the consent commands in [step 5](#5-finish-the-app-registrations)
- **No client secret in the credentials secret** — Solution: The app already existed with a client secret, which the job keeps and cannot read. Create a new secret with `az ad app credential reset --id <app-id> --append --query password --output tsv`
- **`Found more than one app registration named ...`** — Solution: Delete or rename the duplicate app registrations, or set a unique `displayName` under `apps`, and re-run
- **"AADSTS65001: The user or administrator has not consented to use the application"** — Solution: Grant admin consent for API permissions in the Azure Portal
- **"AADSTS700016: Application not found in the directory"** — Solution: Verify the client ID is correct and the app registration exists in the tenant
- **"Invalid scope"** — Solution: Ensure the `entraScopes` configuration includes `api://<backend-client-id>/access_as_user`
- **Graph API returns 403 Forbidden** — Solution: Verify admin consent was granted for Graph API permissions

## Official Documentation

- [Azure CLI - App Registrations](https://learn.microsoft.com/en-us/cli/azure/ad/app)
- [Microsoft Entra ID Overview](https://learn.microsoft.com/en-us/entra/identity/)
- [Microsoft Graph API](https://learn.microsoft.com/en-us/graph/overview)
- [OAuth 2.0 and OpenID Connect in Entra ID](https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols)
- [Single-page application: Sign-in and Sign-out](https://learn.microsoft.com/en-us/entra/identity-platform/scenario-spa-sign-in)
- [Helm Chart Examples](https://github.com/eqtylab/deployment/tree/main/charts/guardian-platform/examples)

## Related

- [Authentication](./authentication.mdx) — how Entra ID compares with the other supported providers
- [Identity Providers](../auth-service/identity-providers.mdx) — what Auth Service supports in Entra mode
- [Token Exchange and Refresh Tokens](../auth-service/token-exchange.mdx) — Auth Service-issued tokens and refresh-token rotation for Entra
- [Azure Key Vault](./azure-key-vault.mdx) — the Azure key provider for DID signing keys
- [Azure Storage Account](./azure-storage-account.mdx) — the Azure object storage the platform needs
- [Helm Deployment](./helm-deployment.mdx) — deploying the platform once the app registrations exist