Identity and Access

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

See the section bibliography for the reference material consulted while preparing these pages.

Commands and behaviour below are current for Azure CLI 2.90.0 (checked 2026-09-26); re-verify against az version and the linked reference pages before relying on them in a pipeline.

Every Bookshelf resource (rg-bookshelf, kv-bookshelf, stbookshelf, acrbookshelf, and so on — see Azure) is reached by something that first has to prove who it is and then be told what it may do. This page is that "who" and "what": Microsoft Entra ID as the identity provider, Azure RBAC as the authorization model layered on top of it, and managed identities plus DefaultAzureCredential as the mechanism that lets Bookshelf’s own services (catalog-api, orders-api, notifier) authenticate without ever holding a secret.

Microsoft Entra ID: the identity layer

Microsoft Entra ID is Azure’s cloud identity provider: it holds users, groups, and the identities that represent applications. Older material — including both books this section drew on for scope — calls this service "Azure Active Directory"; that name was retired and the current name is Microsoft Entra ID. The underlying tenant, object model and most az ad command names are unchanged, only the product name moved.

Three related-but-distinct objects represent an application in Entra ID:

App registration

The template for an application: a global definition (client ID, redirect URIs, requested API permissions, certificates/secrets, federated credentials) created once, typically in the tenant that owns the application’s source code.

Service principal

The local, usable identity created from an app registration (or a managed identity, see below) inside a specific tenant. Azure RBAC role assignments and Conditional Access policies target the service principal, never the app registration directly — an app registration has no permissions of its own until a service principal for it exists in the tenant where access is needed.

Enterprise application

The name the Azure portal gives to a service principal’s entry in Enterprise applications, whether that service principal backs an app registration owned in this tenant, a multi-tenant SaaS application a user consented to, or a managed identity. In other words, "enterprise application" and "service principal" are the same object viewed from two portal blades.

Bookshelf uses this pattern directly: the app registration bookshelf-github-actions-deploy (covered under workload identity federation below) has one service principal, which is what az role assignment create and Conditional Access actually reference.

Authentication features (brief)

Single sign-on (SSO)

One Entra ID sign-in covers every connected application (Microsoft 365, custom apps using OIDC/SAML/WS-Fed), so users authenticate once per session instead of per application.

Multi-factor authentication (MFA)

A second proof of identity (an authenticator app, a hardware key, a phone prompt) in addition to a password, enforced tenant-wide or per Conditional Access policy.

Passwordless authentication

Sign-in with no password at all — a passkey/FIDO2 security key, Windows Hello for Business, or the Microsoft Authenticator app’s phone sign-in — which removes password phishing as an attack vector entirely rather than just adding a second factor to it.

Conditional Access

If-this-then-that access policies evaluated at every sign-in ("if the user is in the Finance group and the sign-in is from an unmanaged device, require MFA and block downloads"), driven by signals such as group membership, device compliance, location and sign-in risk.

Zero Trust and defence-in-depth

Zero Trust is the operating principle behind all of the above: never trust a request by network location alone, verify every identity, device and session explicitly, and grant only the access a task needs. Defence-in-depth is the resulting architecture — Entra ID authentication, Conditional Access, Azure RBAC scoped to the narrowest resource, network controls and resource-level firewalls each act as an independent layer, so a failure in one does not expose Bookshelf’s data on its own.

Azure RBAC: who can do what, where

Azure role-based access control answers one question for every management-plane or data-plane call: does this principal have this role’s permissions at this scope. A role assignment is always the combination of the three:

  • Principal — a user, group, service principal, or managed identity.

  • Role definition — a named set of permitted (and, for a custom role, explicitly denied) operations, either built-in (Contributor, Reader, Owner, Key Vault Secrets User, …​) or custom.

  • Scope — a management group, subscription, resource group, or a single resource, with permissions inherited downward.

Control plane vs. data plane roles

Azure separates managing a resource from using it, and assigns roles for each separately:

  • Control-plane (Azure Resource Manager) roles govern the resource’s own lifecycle — creating, deleting, configuring kv-bookshelf itself. Owner, Contributor and Reader are the built-in examples.

  • Data-plane roles govern the data or operations inside a resource once it exists — reading a secret from kv-bookshelf, writing a blob to stbookshelf. Key Vault Secrets User and Storage Blob Data Contributor are examples.

The reason they are assigned separately, rather than folded into one role, is that Contributor on a Key Vault grants full control over the vault’s configuration, network rules and access policies — which is enough to list which secrets exist and reconfigure who can read them — but grants no permission to read a secret’s value. Conversely, Key Vault Secrets User can read every secret’s value but cannot change the vault’s firewall or delete it. catalog-api’s managed identity gets only `Key Vault Secrets User on kv-bookshelf and only Storage Blob Data Contributor on stbookshelf; nothing in Bookshelf’s deployment pipeline needs control-plane rights on either resource at runtime.

In the Azure portal

  1. Open the target resource (or resource group/subscription for a broader scope) and select Access control (IAM).

  2. Select Add → Add role assignment.

  3. On the Role tab, search for the role (Key Vault Secrets User, Storage Blob Data Contributor, Contributor, or a custom role) and select it.

  4. On the Members tab, choose Managed identity, User, group, or service principal as appropriate, and pick the specific principal (e.g. the id-catalog-api user-assigned identity used in the CLI example below).

  5. Review and select Review + assign.

  6. To inspect existing assignments, return to Access control (IAM) → Role assignments and filter by scope or principal.

With the Azure CLI

# Data-plane: let catalog-api's managed identity read secrets from kv-bookshelf
az role assignment create \
  --assignee-object-id "$(az identity show --name id-catalog-api --resource-group rg-bookshelf --query principalId -o tsv)" \
  --assignee-principal-type ServicePrincipal \
  --role "Key Vault Secrets User" \
  --scope "$(az keyvault show --name kv-bookshelf --query id -o tsv)"

# Data-plane: same identity, blob access on stbookshelf
az role assignment create \
  --assignee-object-id "$(az identity show --name id-catalog-api --resource-group rg-bookshelf --query principalId -o tsv)" \
  --assignee-principal-type ServicePrincipal \
  --role "Storage Blob Data Contributor" \
  --scope "$(az storage account show --name stbookshelf --resource-group rg-bookshelf --query id -o tsv)"

# List everything assigned at the resource group scope
az role assignment list --resource-group rg-bookshelf --output table

A custom role is a JSON role definition with Actions (control-plane, ARM operations), DataActions (data-plane operations), and AssignableScopes:

{
  "Name": "Bookshelf Secret Reader (custom)",
  "Description": "Read-only access to secrets in kv-bookshelf; no configuration changes.",
  "Actions": [],
  "NotActions": [],
  "DataActions": [
    "Microsoft.KeyVault/vaults/secrets/getSecret/action"
  ],
  "NotDataActions": [],
  "AssignableScopes": [
    "/subscriptions/<subscription-id>/resourceGroups/rg-bookshelf"
  ]
}
az role definition create --role-definition custom-secret-reader.json

Managed identities

A managed identity is a service principal that Entra ID creates and rotates on behalf of an Azure resource, so that resource can authenticate to other Azure services without any secret ever appearing in code, configuration, or an environment variable. There are two kinds:

System-assigned

Tied to the lifecycle of one resource (e.g. `catalog-api’s Container App): created when the feature is enabled on that resource and deleted automatically when the resource is deleted. It cannot be shared with another resource.

User-assigned

A standalone Entra ID object created independently of any resource, which can then be attached to one or many resources (e.g. the same id-notifier identity attached to both the notifier job and a backup Function App). It outlives any single resource and is the better fit when several resources need the same permission set, or when an identity must exist before the resource that will use it.

Once a managed identity exists, a resource "picks it up" by having it attached at the resource level (system- assigned: a toggle on the resource; user-assigned: the identity’s resource ID added to the resource’s identity list). The platform then makes a local token endpoint available to that resource’s runtime (the Instance Metadata Service for VMs and AKS, or an equivalent sidecar/environment binding for Container Apps and App Service), and any SDK using DefaultAzureCredential (below) picks up a token from it automatically — there is no key to provision, rotate, or leak, which is exactly why managed identities remove the need for credentials in application code.

Bookshelf uses a user-assigned identity, id-catalog-api, for the catalog-api Container App, so the identity (and its role assignments above) can exist before the app is deployed and survive a redeploy. Because the Azure Identity libraries only pick a user-assigned identity when told which one, the app also needs the identity’s client ID — set as the AZURE_CLIENT_ID environment variable on the Container App, which DefaultAzureCredential reads.

In the Azure portal

  1. Search for Managed Identities and select + Create.

  2. Choose the subscription, resource group rg-bookshelf, the region, and enter id-catalog-api as the name. Select Review + create, then Create.

  3. Open the catalog-api Container App and, in the Settings group, select Identity.

  4. On the User assigned tab, select Add, search for and select id-catalog-api, then select Add.

  5. For a system-assigned identity instead, use the System assigned tab on the same Identity page: switch Status to On and select Save.

With the Azure CLI

# Create the user-assigned identity
az identity create \
  --name id-catalog-api \
  --resource-group rg-bookshelf \
  --location eastus2

# Attach it to the catalog-api Container App
az containerapp identity assign \
  --name catalog-api \
  --resource-group rg-bookshelf \
  --user-assigned "$(az identity show --name id-catalog-api --resource-group rg-bookshelf --query id -o tsv)"

# Tell DefaultAzureCredential which user-assigned identity to use
az containerapp update \
  --name catalog-api \
  --resource-group rg-bookshelf \
  --set-env-vars AZURE_CLIENT_ID="$(az identity show --name id-catalog-api --resource-group rg-bookshelf --query clientId -o tsv)"

# Alternative: a system-assigned identity, created and deleted with the app itself
az containerapp identity assign --name catalog-api --resource-group rg-bookshelf --system-assigned

# Inspect what is attached
az containerapp identity show --name catalog-api --resource-group rg-bookshelf

DefaultAzureCredential and the credential chain

DefaultAzureCredential, from the azure-identity library (used directly, or via Spring Cloud Azure’s Spring Boot starter, on the Java side), tries a sequence of credential sources in order and uses the first one that succeeds — environment variables, a workload identity, a managed identity, then developer tooling such as the Azure CLI’s own logged-in session — so the same code authenticates unmodified whether it runs on a developer’s machine, in CI, or as a deployed Container App with a managed identity attached. The exact order, and how to narrow it, is documented per language on the reference pages linked below.

Trying every source in turn costs a little latency and can pick up the wrong credential source in an environment that has more than one available (a developer machine that is both az login-authenticated and has a stale environment-variable credential set, for instance). The AZURE_TOKEN_CREDENTIALS environment variable pins the chain to a named subset — for example, AZURE_TOKEN_CREDENTIALS=ManagedIdentityCredential in a Container App forces exactly that source and fails fast instead of silently falling through to something else.

Java (Spring Boot / Spring Cloud Azure)

DefaultAzureCredential credential = new DefaultAzureCredentialBuilder().build();

SecretClient secretClient = new SecretClientBuilder()
    .vaultUrl("https://kv-bookshelf.vault.azure.net/")
    .credential(credential)
    .buildClient();

String connectionString = secretClient.getSecret("orders-db-connection-string").getValue();

With the Spring Cloud Azure starter (spring-cloud-azure-starter-keyvault), the same credential chain is wired through configuration instead of code:

spring:
  cloud:
    azure:
      keyvault:
        secret:
          endpoint: https://kv-bookshelf.vault.azure.net/
      credential:
        managed-identity-enabled: true

.NET (Azure.Identity)

var credential = new DefaultAzureCredential();

var secretClient = new SecretClient(
    new Uri("https://kv-bookshelf.vault.azure.net/"),
    credential);

KeyVaultSecret secret = await secretClient.GetSecretAsync("orders-db-connection-string");

Token flow: managed identity to a data-plane call

sequenceDiagram participant App as catalog-api participant MI as Managed identity participant Entra as Microsoft Entra ID (token endpoint) participant KV as Key Vault / Storage App->>MI: Request token (DefaultAzureCredential) MI->>Entra: Authenticate identity, request access token Entra-->>MI: Access token (scoped, short-lived) MI-->>App: Access token App->>KV: Data-plane call with bearer token KV->>Entra: Validate token, check Azure RBAC assignment Entra-->>KV: Token valid, principal has required role KV-->>App: Secret / blob data

Workload identity federation

Workload identity federation lets a workload outside Azure (a GitHub Actions job, a Kubernetes pod, another cloud’s workload) exchange its own platform-issued OIDC token for an Entra ID access token, via a federated credential configured on an app registration — with no client secret stored or rotated anywhere. GitHub Actions is the common case: the azure/login action authenticates using a federated credential tied to the GitHub repository, environment, or branch, so the workflow authenticates as Bookshelf’s deployment app registration without ever holding AZURE_CLIENT_SECRET. The workflow mechanics — job permissions (id-token: write), the azure/login step itself, and how it fits alongside the rest of a build/push pipeline — are covered on Docker CI/CD with GitHub Actions; this page only covers the identity side: creating the app registration, its service principal, the role assignment, and the federated credential subject that must match the workflow’s repository/branch/environment exactly for the exchange to succeed.

The Bookshelf deployment identity is the app registration bookshelf-github-actions-deploy, trusted for pushes to main of the <org>/bookshelf-catalog-api repository (replace <org> with your GitHub organization or user). The federated credential’s subject must match the GitHub token’s sub claim exactly — no wildcards: repo:<org>/<repo>:ref:refs/heads/<branch> for a branch, repo:<org>/<repo>:environment:<name> for a job tied to a GitHub environment, or repo:<org>/<repo>:pull-request for pull-request-triggered workflows. A wrong subject is accepted when the credential is created and only fails later, at token exchange time.

In the Azure portal

  1. In the Microsoft Entra admin center (or Microsoft Entra ID in the Azure portal), open App registrations and select New registration. Enter bookshelf-github-actions-deploy, keep Single tenant, and select Register. Registering through the portal also creates the app’s service principal in your tenant.

  2. On the new registration, select Certificates & secrets, open the Federated credentials tab, and select Add credential.

  3. For Federated credential scenario, choose GitHub Actions deploying Azure resources; enter the Organization and Repository, set Entity type to Branch with main, and give the credential a Name (for example github-main). Issuer, Audiences and Subject identifier fill in automatically. Select Add.

  4. Open rg-bookshelf, select Access control (IAM) → Add → Add role assignment, choose Contributor (or a narrower role that covers only what the pipeline deploys), and on Members pick User, group, or service principal and select bookshelf-github-actions-deploy. Select Review + assign.

  5. From the app registration’s Overview, copy the Application (client) ID and Directory (tenant) ID; with the subscription ID, store them as the AZURE_CLIENT_ID, AZURE_TENANT_ID and AZURE_SUBSCRIPTION_ID GitHub secrets that azure/login reads. None of them is a secret credential — there is no client secret.

With the Azure CLI

# App registration and its service principal
APP_ID="$(az ad app create --display-name bookshelf-github-actions-deploy --query appId -o tsv)"
SP_OBJECT_ID="$(az ad sp create --id "$APP_ID" --query id -o tsv)"

# Let the pipeline deploy into rg-bookshelf only
az role assignment create \
  --assignee-object-id "$SP_OBJECT_ID" \
  --assignee-principal-type ServicePrincipal \
  --role Contributor \
  --scope "$(az group show --name rg-bookshelf --query id -o tsv)"

# Trust GitHub's OIDC tokens for pushes to main of <org>/bookshelf-catalog-api
cat > github-main.json <<'EOF'
{
  "name": "github-main",
  "issuer": "https://token.actions.githubusercontent.com",
  "subject": "repo:<org>/bookshelf-catalog-api:ref:refs/heads/main",
  "description": "GitHub Actions deploys from main",
  "audiences": ["api://AzureADTokenExchange"]
}
EOF
az ad app federated-credential create --id "$APP_ID" --parameters github-main.json

# Values for the AZURE_CLIENT_ID / AZURE_TENANT_ID / AZURE_SUBSCRIPTION_ID GitHub secrets
echo "$APP_ID"
az account show --query "{tenant: tenantId, subscription: id}" -o table

Microsoft Entra ID vs. Microsoft Entra Domain Services

Microsoft Entra ID is a modern, internet-native identity provider built around OIDC, OAuth 2.0 and SAML — it has no concept of an LDAP bind, Kerberos ticket, or NTLM domain join, and Bookshelf’s own applications never need one. Microsoft Entra Domain Services provides a separate, managed service that does speak those legacy protocols: a classic Active Directory Domain Services-compatible domain (LDAP, Kerberos, NTLM, group policy) running as a managed Azure service, synchronized from the Entra ID tenant. It exists for the case Bookshelf does not have — a lift-and-shifted legacy application, or third-party software, that can only authenticate against a traditional Windows domain and cannot be updated to speak OIDC/SAML directly. Reaching for Domain Services is a sign that an application needs to be modernized onto Entra ID directly, not a permanent architecture choice.

Clean up

Remove what this page created, in dependency order: the app registration (deleting it also removes its service principal and federated credential), the custom role (which can only be deleted once nothing is assigned to it), and the user-assigned identity; deleting the resource group, if nothing else uses it, removes the role assignments scoped to it along with every resource inside.

az ad app delete --id "$(az ad app list --display-name bookshelf-github-actions-deploy --query '[0].appId' -o tsv)"

az role definition delete \
  --name "Bookshelf Secret Reader (custom)" \
  --custom-role-only true \
  --scope "$(az group show --name rg-bookshelf --query id -o tsv)"

az identity delete --name id-catalog-api --resource-group rg-bookshelf

# Only when tearing down the whole Bookshelf environment
az group delete --name rg-bookshelf --yes --no-wait