Background
Sections
IntroductionFoundations1. Resource Hierarchy2. Resource Manager3. Identity and RBAC4. Regions and Availability5. Naming and TaggingVirtual Machines1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetVirtual Network1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetBlob Storage1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure SQL Database1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Kubernetes Service1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Container Registry1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetMicrosoft Entra ID1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure RBAC1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Functions1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAPI Management1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure App Configuration1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Machine Learning1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Monitor1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure AI Foundry1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and Cheatsheet

4. Getting Started

9 min read

The task, three ways: register an application, give it an identity it can authenticate with, grant it read access to one Azure resource, and prove it by fetching a token and using it. That is the smallest thing that exercises the whole model — a directory object, a credential, an Azure RBAC assignment, and a token with the right audience.

This page is deliberately throwaway. Hard-coded names, a client secret we'd never ship, no variables, no state, no pipeline. Everything production-shaped — federated credentials, a parameterised module, CI/CD — lives in Deployment.

Before you start

You need two different kinds of permission, and this is the first place people get stuck:

To do this You need
Create an app registration Application Developer directory role (or the tenant's "users can register applications" setting left on)
Create the role assignment User Access Administrator or Owner on the target scope, in Azure RBAC

Directory permission and resource permission are separate systems — that is the lesson of Architecture made concrete on your very first attempt. If you're using a personal or sandbox tenant you almost certainly hold both.

Check what you've got:

az login
az account show -o table
az ad signed-in-user show --query "{upn:userPrincipalName, oid:id}" -o table

Note that oid — the object ID. It's the identifier that matters, per Core Concepts.

Portal, CLI, and Terraform converging on the same app registration and service principal

Step 0 — a throwaway resource group and something to grant access to

Everything Azure-side goes in one resource group so teardown is a single command. The directory objects will not be in it — they can't be — which is worth noticing as it happens.

az group create -n rg-entra-demo -l uksouth

# something with a data plane worth protecting
az keyvault create \
  -n kv-entrademo-$RANDOM \
  -g rg-entra-demo \
  -l uksouth \
  --enable-rbac-authorization true

--enable-rbac-authorization true matters: it puts the vault's data plane under Azure RBAC rather than the legacy vault access policy model. Do this on every new vault.

Path 1 — Azure Portal

Short click-paths by blade name, because the portal's navigation changes often.

Register the application

  • Open the Microsoft Entra admin centreApp registrationsNew registration.
  • Name it demo-entra-app, leave it single-tenant, skip the redirect URI (this is a daemon).
  • On the Overview blade, copy the Application (client) ID and the Directory (tenant) ID.
  • On Certificates & secretsNew client secret, create one and copy the Value immediately — it is never shown again. (Copying the Secret ID instead of the Value is the single most common first mistake; it produces AADSTS7000215.)

Find its service principal

  • Go to Enterprise applications and search for demo-entra-app.
  • Copy the Object ID shown there. This is not the client ID, and it is what the role assignment needs. Two blades, two GUIDs, one object model — see Core Concepts.

Grant it access to the vault

  • Open the key vault → Access control (IAM)Add role assignment.
  • Role Key Vault Secrets User, member type User, group, or service principal, select demo-entra-app.

Notice what just happened across two systems: the first two steps wrote to Microsoft Graph, the third wrote to Azure Resource Manager.

Path 2 — Azure CLI

The same thing, copy-pasteable. This is the version worth actually running.

# --- directory side (Microsoft Graph) ---

# create the app registration AND its service principal in one command
az ad sp create-for-rbac --name demo-entra-app --skip-assignment

That prints appId, password, and tenant. Save them — the password is shown once.

APP_ID="<appId from above>"
TENANT_ID="<tenant from above>"
CLIENT_SECRET="<password from above>"

# the service principal's OBJECT id — different from the app (client) id
SP_OBJECT_ID=$(az ad sp show --id "$APP_ID" --query id -o tsv)
echo "client id : $APP_ID"
echo "object id : $SP_OBJECT_ID"

# --- resource side (Azure Resource Manager) ---

KV_NAME=$(az keyvault list -g rg-entra-demo --query "[0].name" -o tsv)
KV_ID=$(az keyvault show -n "$KV_NAME" -g rg-entra-demo --query id -o tsv)

az role assignment create \
  --assignee-object-id "$SP_OBJECT_ID" \
  --assignee-principal-type ServicePrincipal \
  --role "Key Vault Secrets User" \
  --scope "$KV_ID"

Use --assignee-object-id with an explicit --assignee-principal-type rather than --assignee. The friendly form makes a directory lookup that fails intermittently for freshly-created principals — the replication lag from Architecture, meeting you on day one.

Prove it works. Put a secret in as yourself, then read it as the application:

# grant yourself data-plane access first — being subscription Owner is NOT enough
MY_OID=$(az ad signed-in-user show --query id -o tsv)
az role assignment create \
  --assignee-object-id "$MY_OID" --assignee-principal-type User \
  --role "Key Vault Secrets Officer" --scope "$KV_ID"

az keyvault secret set --vault-name "$KV_NAME" -n demo-secret --value "it-works"

# now sign in AS the application
az login --service-principal \
  -u "$APP_ID" -p "$CLIENT_SECRET" --tenant "$TENANT_ID"

az keyvault secret show --vault-name "$KV_NAME" -n demo-secret --query value -o tsv
# -> it-works

That "grant yourself data-plane access first" line is the control-plane/data-plane split biting in under sixty seconds. You created the vault, you own the subscription, and you still cannot read the secret until an Azure RBAC data role says so.

Look at the token. This is the most instructive thirty seconds on the page:

az account get-access-token --resource https://vault.azure.net --query accessToken -o tsv

Paste it into jwt.ms and read aud, iss, tid, oid, appid, roles, exp. Then request a token for a different audience and note that they are not interchangeable:

az account get-access-token --resource https://graph.microsoft.com --query accessToken -o tsv

Two valid tokens for the same principal, useless to each other's APIs. That's the aud claim doing its job, and it explains most mystifying 401s.

Sign back in as yourself before continuing:

az logout && az login

PowerShell equivalent

Entra-heavy shops are frequently PowerShell-first, so this one earns its place. Note that the old AzureAD and MSOnline modules are retired — Microsoft Graph PowerShell is the current path.

Connect-MgGraph -Scopes "Application.ReadWrite.All"

$app = New-MgApplication -DisplayName "demo-entra-app"
$sp  = New-MgServicePrincipal -AppId $app.AppId

$app.AppId   # client id
$sp.Id       # object id — the one the role assignment wants

# resource side uses the Az module, not Graph
Connect-AzAccount
New-AzRoleAssignment `
  -ObjectId $sp.Id `
  -RoleDefinitionName "Key Vault Secrets User" `
  -Scope (Get-AzKeyVault -ResourceGroupName rg-entra-demo).ResourceId

Two modules for one task, because two systems. The vocabulary is consistent once you stop expecting them to be one thing.

Path 3 — Terraform, minimally

Entra ID needs the azuread provider. azurerm cannot create directory objects — the structural fact from What & Why, now as a line of HCL. Both providers are configured here because the role assignment lives in ARM.

terraform {
  required_providers {
    azuread = { source = "hashicorp/azuread" }
    azurerm = { source = "hashicorp/azurerm" }
  }
}

provider "azuread" {}

provider "azurerm" {
  features {}
}

data "azuread_client_config" "current" {}

# --- directory objects ---

resource "azuread_application" "demo" {
  display_name = "demo-entra-app"
  owners       = [data.azuread_client_config.current.object_id]
}

resource "azuread_service_principal" "demo" {
  client_id = azuread_application.demo.client_id
  owners    = [data.azuread_client_config.current.object_id]
}

resource "azuread_application_password" "demo" {
  application_id = azuread_application.demo.id
  display_name   = "demo-secret"
  end_date_relative = "168h" # a week; this is a throwaway
}

# --- Azure resources ---

resource "azurerm_resource_group" "demo" {
  name     = "rg-entra-demo-tf"
  location = "uksouth"
}

resource "azurerm_key_vault" "demo" {
  name                      = "kv-entrademotf"
  resource_group_name       = azurerm_resource_group.demo.name
  location                  = azurerm_resource_group.demo.location
  tenant_id                 = data.azuread_client_config.current.tenant_id
  sku_name                  = "standard"
  enable_rbac_authorization = true
}

resource "azurerm_role_assignment" "demo" {
  scope                = azurerm_key_vault.demo.id
  role_definition_name = "Key Vault Secrets User"
  principal_id         = azuread_service_principal.demo.object_id
}

output "client_id" { value = azuread_application.demo.client_id }
output "object_id" { value = azuread_service_principal.demo.object_id }

output "client_secret" {
  value     = azuread_application_password.demo.value
  sensitive = true
}
terraform init
terraform plan
terraform apply

Three things worth noticing in that file, because each one is a real gotcha:

  1. Two providers, one apply. Terraform is talking to Microsoft Graph and to ARM in the same run. That's the correct shape and it's why the azuread provider exists at all.
  2. client_id vs. object_id appear as separate outputs. They are different GUIDs for different purposes. azurerm_role_assignment.principal_id wants the object ID.
  3. azuread_application_password.value lands in state in plaintext. Marking the output sensitive hides it from the console, not from the state file. This is the argument for federated credentials over secrets, made by the tool itself — see Deployment.

The thing you should not do

Nothing on this page used a certificate or a federated credential, and the CLI path pasted a client secret onto the command line, where it lands in shell history. That is fine for a demo you delete in ten minutes and unacceptable anywhere else. If you take one habit from this page, make it the opposite one: inside Azure, use a managed identity; outside Azure, use workload identity federation; use a secret only when neither is possible, and give it a short expiry.

Teardown

Directory objects and Azure resources are deleted separately — one more reminder that they live in different systems.

# terraform path
terraform destroy

# manual path — Azure resources
az group delete -n rg-entra-demo --yes --no-wait

# manual path — directory objects (NOT covered by the resource group delete)
az ad app delete --id "$APP_ID"

Two footnotes on teardown that matter later:

  • Deleting the application deletes its service principal. Deleting the service principal alone leaves the registration behind, which is how orphaned registrations accumulate.
  • Deleted applications and users go to a soft-deleted state for a retention window (~30 days ⚠️ verify current retention) and are restorable. az ad app list --show-mine won't show them; the Deleted applications blade will. They still consume directory quota.
  • Role assignments are not cleaned up. Deleting the principal leaves an assignment pointing at a GUID that no longer resolves — it shows as "Identity not found" in the portal. Deleting the resource group removes the ones scoped inside it; anything you scoped higher survives.

Confirm you're clean:

az group list -o table
az ad app list --display-name demo-entra-app -o table

Next: the same objects, but parameterised, version-controlled, credential-free, and shippable.


Next: Deployment →

← Back to the Microsoft Entra ID overview · ← Previous: Architecture