Entra App Registration
Design and configure secure app registrations with least privilege, proper auth flows, and automated credential rotation.
Support single-tenant, multi-tenant, and B2B scenarios.
Use This Skill When
- The user needs to set up app authentication with Entra ID
- The user is building web apps, SPAs, APIs, or daemon services
- The user needs to integrate with Microsoft Graph or other APIs
- The user needs API scopes, app roles, or delegated permissions
Context: Identity Maturity
Immature: Hard-coded secrets, no expiration, shared credentials
Developing: App registrations created, some scoping of permissions
Managed: Certificate-based auth, credential rotation, least-privilege scopes → Target
Optimized: Managed identity, workload federation, passwordless, real-time token binding
Required Inputs
- App type: SPA (React, Angular), Web (ASP.NET, Node), Daemon (background job), API (resource), Mobile
- Auth flow: Authorization Code (most common), ROPC (legacy), Client Credentials (daemon), Device Flow (IoT)
- Tenant model: Single-tenant (internal), Multi-tenant (SaaS), B2B (partner)
- Redirect URIs: Login callback URLs (e.g.,
https://myapp.azurewebsites.net/auth/callback)
- API permissions: Which Microsoft Graph or custom APIs does it call?
- Credential strategy: Certificates (preferred) or secrets (ephemeral)?
Decision Tree
What type of application is this?
├─ Web app (server-side) → Authorization Code flow
├─ SPA (client-side React/Vue) → Authorization Code + PKCE
├─ Mobile/Desktop → Authorization Code + PKCE or Device Flow
├─ Daemon/Background job → Client Credentials flow
├─ API (resource server) → Define scopes for other apps to call
└─ Microsoft 365 add-in → OAuth/OpenID Connect with specific scopes
Is this single-tenant or multi-tenant?
├─ Single-tenant → Simpler: app only works in your organization
└─ Multi-tenant → Complex: other organizations can install/authorize it
Who authenticates: User or Application?
├─ User → Delegated permissions (on-behalf-of user)
└─ Application → Application permissions (app-to-app, no user)
Does it need to call other APIs?
├─ No → Minimal configuration (just sign-in)
├─ Yes → Add API permissions, request at sign-in or runtime
└─ Daemon → App permissions only (no user)
How to manage credentials?
├─ Development/Testing → Secrets (short-lived)
└─ Production → Certificates or managed identity (via workload federation)
Workflow
Phase 1: Create App Registration
Register application:
az ad app create \
--display-name "MyApp" \
--public-client-redirect-uris "https://localhost:3000/auth/callback" \
--enable-id-token-issuance true \
--enable-access-token-issuance true
APP_ID=$(az ad app list --display-name "MyApp" --query "[0].appId" -o tsv)
TENANT_ID=$(az account show --query tenantId -o tsv)
Register service principal (represents app in tenant):
az ad sp create --id $APP_ID
Set token configuration (what claims in token):
# Via portal: Token Configuration → Add optional claim
# Or via CLI: az ad app update --id $APP_ID --optional-claims ...
Phase 2: Configure Platform & Redirect URIs
For web apps:
# Add redirect URIs (where to send auth code after login)
az ad app update --id $APP_ID \
--reply-urls "https://myapp.azurewebsites.net/auth/callback" \
"http://localhost:3000/auth/callback" \
--logout-urls "https://myapp.azurewebsites.net/logout"
For SPAs (React, Vue, Angular):
# Treat as public client (no secret needed)
az ad app update --id $APP_ID \
--public-client-redirect-uris "https://myapp.azurewebsites.net/" \
"http://localhost:3000/"
# Enable PKCE: Portal → Authentication → Treat app as public client (✓)
For desktop/mobile apps:
# Mobile: myapp://auth
# Desktop: http://localhost:port/redirect
az ad app update --id $APP_ID \
--public-client-redirect-uris "myapp://auth" \
"http://localhost:7777/redirect"
Phase 3: Add API Permissions
Request Microsoft Graph access:
# Delegate (user's behalf): Read user profile, calendar
az ad app permission add --id $APP_ID \
--api 00000003-0000-0000-c000-000000000000 \
--api-permissions "e1fe6dd8-ba31-4d61-89e7-88639da4683d=Scope" \
"14dad69e-099b-42c9-810b-d002981feee1=Scope"
# Application (as itself): Read all users (admin consent required)
az ad app permission add --id $APP_ID \
--api 00000003-0000-0000-c000-000000000000 \
--api-permissions "9a6d68d0-fcf5-4d9f-b6c5-5ff2e6bba1d5=Role"
Request custom API access:
# If calling your own API, get its app ID
CUSTOM_API_ID=$(az ad app list --display-name "MyCustomAPI" --query "[0].appId" -o tsv)
az ad app permission add --id $APP_ID \
--api $CUSTOM_API_ID \
--api-permissions "scope-uuid=Scope"
Admin consent (required for app permissions):
# Portal: API Permissions → Grant admin consent for [Tenant]
# Or: Redirect user to consent URL with scope
# https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize?client_id=...&scope=...
Phase 4: Define Exposed Scopes (if this is an API)
Expose scopes for other apps to call:
# Portal: Expose an API → Add a scope
# Scope: api://myapp-id/Orders.Read
# Display name: "Read Orders"
# Admin consent description: "App can read orders on user's behalf"
# CLI (register scope)
az ad app update --id $APP_ID \
--api-definition '{
"requestedAccessTokenVersion": 2,
"oauth2PermissionScopes": [{
"adminConsentDescription": "Allow app to read orders",
"adminConsentDisplayName": "Read Orders",
"id": "scope-uuid",
"isEnabled": true,
"type": "User",
"userConsentDescription": "Read your orders",
"userConsentDisplayName": "Read Orders",
"value": "Orders.Read"
}]
}'
Define app roles (for role-based access):
{
"appRoles": [
{
"id": "admin-uuid",
"displayName": "Admin",
"description": "Can manage orders",
"value": "Orders.Manage",
"allowedMemberTypes": ["Application", "User"]
}
]
}
Phase 5: Configure Credentials
Certificate-based (preferred for production):
# Generate self-signed cert (1 year valid)
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes \
-subj "/CN=MyApp"
# Upload to app registration
az ad app credential reset --id $APP_ID --cert @cert.pem --append
Secret-based (development/testing):
# Create secret (2-year expiration)
SECRET=$(az ad app credential reset --id $APP_ID --display-name "dev-secret" \
--years 2 --query "credential.secretText" -o tsv)
# SAVE TO KEY VAULT (never in code/config)
az keyvault secret set --vault-name $KV --name "app-registration-secret" --value "$SECRET"
Rotate credentials:
# Create new secret before old one expires
az ad app credential reset --id $APP_ID --display-name "new-secret"
# Update apps to use new secret from Key Vault
# Delete old secret
az ad app credential delete --id $APP_ID --key-id $OLD_KEY_ID
Phase 6: Obtain Tokens in Application
Example 1: ASP.NET Core Web App
// Sign-in user with Authorization Code flow
services.AddMicrosoftIdentityWebAppAuthentication(Configuration)
.AddMicrosoftGraph(Configuration.GetSection("MicrosoftGraph"))
.AddInMemoryTokenCaches();
// In controller: User automatically authenticated via middleware
[Authorize]
public async Task<IActionResult> Profile() {
var user = User; // User object available
return Ok(user);
}
Example 2: React SPA
import React from 'react';
import { MsalProvider, useMsal, useAccount } from "@azure/msal-react";
import { PublicClientApplication } from "@azure/msal-browser";
const msalConfig = {
auth: {
clientId: "YOUR_CLIENT_ID",
authority: `https://login.microsoftonline.com/YOUR_TENANT_ID`,
redirectUri: window.location.origin,
}
};
const pca = new PublicClientApplication(msalConfig);
export function App() {
const { accounts, instance } = useMsal();
const handleLogin = async () => {
const response = await instance.loginPopup({
scopes: ["openid", "profile", "email"]
});
console.log(response);
};
return <button
}
ReactDOM.createRoot(document.getElementById('root')).render(
<MsalProvider instance={pca}>
<App />
</MsalProvider>
);
Example 3: Daemon (Client Credentials)
// No user involved; app authenticates as itself
var credential = new ClientSecretCredential(
tenantId, clientId, clientSecret,
new TokenCredentialOptions { AuthorityHost = AzureAuthorityHosts.AzurePublicCloud }
);
var client = new GraphServiceClient(credential);
var users = await client.Users.GetAsync();
Phase 7: Validate & Test
Test sign-in flow:
# Use MSAL playground: https://ms.dev/msal-js-playground
# Or local app: curl + redirect flow
# Get auth code
curl "https://login.microsoftonline.com/$TENANT_ID/oauth2/v2.0/authorize?client_id=$CLIENT_ID&response_type=code&redirect_uri=http://localhost&scope=openid%20profile%20email"
# Exchange code for token
curl -X POST \
-d "client_id=$CLIENT_ID&client_secret=$SECRET&grant_type=authorization_code&code=$CODE&redirect_uri=http://localhost" \
"https://login.microsoftonline.com/$TENANT_ID/oauth2/v2.0/token"
Inspect token:
# Decode JWT token (on jwt.ms or locally)
# Verify:
# - Correct issuer (https://login.microsoftonline.com/{tenant}/v2.0)
# - Correct audience (your client ID)
# - Requested scopes present
# - Expiration reasonable
Output Contract
App Registration Summary
- App ID (client ID), tenant ID, object ID
- Auth flow chosen and justification
- Single/multi-tenant mode
Configuration for Application
- Authority URL:
https://login.microsoftonline.com/{tenant-id}
- Scopes requested:
openid profile email api://myapp/Orders.Read
- Redirect URIs registered
- Secret location (Key Vault reference)
API Permissions & Scopes
- Microsoft Graph permissions (delegated + application)
- Custom API scopes exposed (if this is an API)
- Admin consent URLs or instructions
Credential Management Plan
- Certificate thumbprint or secret name in Key Vault
- Rotation schedule (every 12 months)
- Deprovisioning process
Implementation Checklist
- MSAL library added to project
- Sign-in UI/UX
- Token caching strategy
- Refresh token handling
Guardrails
- Prefer certificates over secrets: Self-signed cert rotated annually is safer than long-lived secret.
- Never output secrets in plain text: Store in Key Vault, reference with managed identity.
- Request minimal permissions: Only request scopes actually needed (principle of least privilege).
- Enable PKCE for SPAs: Prevents authorization code interception.
- Use implicit flow for SPA: Deprecated; use Authorization Code + PKCE instead.
- Test token expiration: Ensure apps handle 401 and request fresh token.
- Secure the refresh token: Store in secure, httpOnly cookie or OS credential store (never localStorage).
- Monitor token usage: Ensure tokens not being replayed or misused (Application Insights).
1---2name: entra-app-registration3description: Design and configure Microsoft Entra app registrations for secure authentication and authorization. Use this skill when users need OAuth, OpenID Connect, API permissions, app roles, or multi-tenant identity. Covers auth flows, credentials, API permissions, consent models, and zero-trust patterns.4---56# Entra App Registration78Design and configure secure app registrations with least privilege, proper auth flows, and automated credential rotation.9Support single-tenant, multi-tenant, and B2B scenarios.1011## Use This Skill When1213- The user needs to set up app authentication with Entra ID14- The user is building web apps, SPAs, APIs, or daemon services15- The user needs to integrate with Microsoft Graph or other APIs16- The user needs API scopes, app roles, or delegated permissions1718## Context: Identity Maturity1920**Immature**: Hard-coded secrets, no expiration, shared credentials 21**Developing**: App registrations created, some scoping of permissions 22**Managed**: Certificate-based auth, credential rotation, least-privilege scopes → **Target** 23**Optimized**: Managed identity, workload federation, passwordless, real-time token binding2425## Required Inputs2627- **App type**: SPA (React, Angular), Web (ASP.NET, Node), Daemon (background job), API (resource), Mobile28- **Auth flow**: Authorization Code (most common), ROPC (legacy), Client Credentials (daemon), Device Flow (IoT)29- **Tenant model**: Single-tenant (internal), Multi-tenant (SaaS), B2B (partner)30- **Redirect URIs**: Login callback URLs (e.g., `https://myapp.azurewebsites.net/auth/callback`)31- **API permissions**: Which Microsoft Graph or custom APIs does it call?32- **Credential strategy**: Certificates (preferred) or secrets (ephemeral)?3334## Decision Tree3536```37What type of application is this?38├─ Web app (server-side) → Authorization Code flow39├─ SPA (client-side React/Vue) → Authorization Code + PKCE40├─ Mobile/Desktop → Authorization Code + PKCE or Device Flow41├─ Daemon/Background job → Client Credentials flow42├─ API (resource server) → Define scopes for other apps to call43└─ Microsoft 365 add-in → OAuth/OpenID Connect with specific scopes4445Is this single-tenant or multi-tenant?46├─ Single-tenant → Simpler: app only works in your organization47└─ Multi-tenant → Complex: other organizations can install/authorize it4849Who authenticates: User or Application?50├─ User → Delegated permissions (on-behalf-of user)51└─ Application → Application permissions (app-to-app, no user)5253Does it need to call other APIs?54├─ No → Minimal configuration (just sign-in)55├─ Yes → Add API permissions, request at sign-in or runtime56└─ Daemon → App permissions only (no user)5758How to manage credentials?59├─ Development/Testing → Secrets (short-lived)60└─ Production → Certificates or managed identity (via workload federation)61```6263## Workflow6465### Phase 1: Create App Registration66671. **Register application**:68 ```bash69 az ad app create \70 --display-name "MyApp" \71 --public-client-redirect-uris "https://localhost:3000/auth/callback" \72 --enable-id-token-issuance true \73 --enable-access-token-issuance true74 75 APP_ID=$(az ad app list --display-name "MyApp" --query "[0].appId" -o tsv)76 TENANT_ID=$(az account show --query tenantId -o tsv)77 ```78792. **Register service principal** (represents app in tenant):80 ```bash81 az ad sp create --id $APP_ID82 ```83843. **Set token configuration** (what claims in token):85 ```bash86 # Via portal: Token Configuration → Add optional claim87 # Or via CLI: az ad app update --id $APP_ID --optional-claims ...88 ```8990### Phase 2: Configure Platform & Redirect URIs91921. **For web apps**:93 ```bash94 # Add redirect URIs (where to send auth code after login)95 az ad app update --id $APP_ID \96 --reply-urls "https://myapp.azurewebsites.net/auth/callback" \97 "http://localhost:3000/auth/callback" \98 --logout-urls "https://myapp.azurewebsites.net/logout"99 ```1001012. **For SPAs** (React, Vue, Angular):102 ```bash103 # Treat as public client (no secret needed)104 az ad app update --id $APP_ID \105 --public-client-redirect-uris "https://myapp.azurewebsites.net/" \106 "http://localhost:3000/"107 # Enable PKCE: Portal → Authentication → Treat app as public client (✓)108 ```1091103. **For desktop/mobile apps**:111 ```bash112 # Mobile: myapp://auth113 # Desktop: http://localhost:port/redirect114 az ad app update --id $APP_ID \115 --public-client-redirect-uris "myapp://auth" \116 "http://localhost:7777/redirect"117 ```118119### Phase 3: Add API Permissions1201211. **Request Microsoft Graph access**:122 ```bash123 # Delegate (user's behalf): Read user profile, calendar124 az ad app permission add --id $APP_ID \125 --api 00000003-0000-0000-c000-000000000000 \126 --api-permissions "e1fe6dd8-ba31-4d61-89e7-88639da4683d=Scope" \127 "14dad69e-099b-42c9-810b-d002981feee1=Scope"128 129 # Application (as itself): Read all users (admin consent required)130 az ad app permission add --id $APP_ID \131 --api 00000003-0000-0000-c000-000000000000 \132 --api-permissions "9a6d68d0-fcf5-4d9f-b6c5-5ff2e6bba1d5=Role"133 ```1341352. **Request custom API access**:136 ```bash137 # If calling your own API, get its app ID138 CUSTOM_API_ID=$(az ad app list --display-name "MyCustomAPI" --query "[0].appId" -o tsv)139 140 az ad app permission add --id $APP_ID \141 --api $CUSTOM_API_ID \142 --api-permissions "scope-uuid=Scope"143 ```1441453. **Admin consent** (required for app permissions):146 ```bash147 # Portal: API Permissions → Grant admin consent for [Tenant]148 # Or: Redirect user to consent URL with scope149 # https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize?client_id=...&scope=...150 ```151152### Phase 4: Define Exposed Scopes (if this is an API)1531541. **Expose scopes for other apps to call**:155 ```bash156 # Portal: Expose an API → Add a scope157 # Scope: api://myapp-id/Orders.Read158 # Display name: "Read Orders"159 # Admin consent description: "App can read orders on user's behalf"160 ```161162 ```bash163 # CLI (register scope)164 az ad app update --id $APP_ID \165 --api-definition '{166 "requestedAccessTokenVersion": 2,167 "oauth2PermissionScopes": [{168 "adminConsentDescription": "Allow app to read orders",169 "adminConsentDisplayName": "Read Orders",170 "id": "scope-uuid",171 "isEnabled": true,172 "type": "User",173 "userConsentDescription": "Read your orders",174 "userConsentDisplayName": "Read Orders",175 "value": "Orders.Read"176 }]177 }'178 ```1791802. **Define app roles** (for role-based access):181 ```json182 {183 "appRoles": [184 {185 "id": "admin-uuid",186 "displayName": "Admin",187 "description": "Can manage orders",188 "value": "Orders.Manage",189 "allowedMemberTypes": ["Application", "User"]190 }191 ]192 }193 ```194195### Phase 5: Configure Credentials1961971. **Certificate-based (preferred for production)**:198 ```bash199 # Generate self-signed cert (1 year valid)200 openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes \201 -subj "/CN=MyApp"202 203 # Upload to app registration204 az ad app credential reset --id $APP_ID --cert @cert.pem --append205 ```2062072. **Secret-based (development/testing)**:208 ```bash209 # Create secret (2-year expiration)210 SECRET=$(az ad app credential reset --id $APP_ID --display-name "dev-secret" \211 --years 2 --query "credential.secretText" -o tsv)212 213 # SAVE TO KEY VAULT (never in code/config)214 az keyvault secret set --vault-name $KV --name "app-registration-secret" --value "$SECRET"215 ```2162173. **Rotate credentials**:218 ```bash219 # Create new secret before old one expires220 az ad app credential reset --id $APP_ID --display-name "new-secret"221 222 # Update apps to use new secret from Key Vault223 # Delete old secret224 az ad app credential delete --id $APP_ID --key-id $OLD_KEY_ID225 ```226227### Phase 6: Obtain Tokens in Application228229**Example 1: ASP.NET Core Web App**230```csharp231// Sign-in user with Authorization Code flow232services.AddMicrosoftIdentityWebAppAuthentication(Configuration)233 .AddMicrosoftGraph(Configuration.GetSection("MicrosoftGraph"))234 .AddInMemoryTokenCaches();235236// In controller: User automatically authenticated via middleware237[Authorize]238public async Task<IActionResult> Profile() {239 var user = User; // User object available240 return Ok(user);241}242```243244**Example 2: React SPA**245```javascript246import React from 'react';247import { MsalProvider, useMsal, useAccount } from "@azure/msal-react";248import { PublicClientApplication } from "@azure/msal-browser";249250const msalConfig = {251 auth: {252 clientId: "YOUR_CLIENT_ID",253 authority: `https://login.microsoftonline.com/YOUR_TENANT_ID`,254 redirectUri: window.location.origin,255 }256};257258const pca = new PublicClientApplication(msalConfig);259260export function App() {261 const { accounts, instance } = useMsal();262 263 const handleLogin = async () => {264 const response = await instance.loginPopup({265 scopes: ["openid", "profile", "email"]266 });267 console.log(response);268 };269270 return <button onClick={handleLogin}>Login</button>;271}272273ReactDOM.createRoot(document.getElementById('root')).render(274 <MsalProvider instance={pca}>275 <App />276 </MsalProvider>277);278```279280**Example 3: Daemon (Client Credentials)**281```csharp282// No user involved; app authenticates as itself283var credential = new ClientSecretCredential(284 tenantId, clientId, clientSecret,285 new TokenCredentialOptions { AuthorityHost = AzureAuthorityHosts.AzurePublicCloud }286);287288var client = new GraphServiceClient(credential);289var users = await client.Users.GetAsync();290```291292### Phase 7: Validate & Test2932941. **Test sign-in flow**:295 ```bash296 # Use MSAL playground: https://ms.dev/msal-js-playground297 # Or local app: curl + redirect flow298 299 # Get auth code300 curl "https://login.microsoftonline.com/$TENANT_ID/oauth2/v2.0/authorize?client_id=$CLIENT_ID&response_type=code&redirect_uri=http://localhost&scope=openid%20profile%20email" 301 302 # Exchange code for token303 curl -X POST \304 -d "client_id=$CLIENT_ID&client_secret=$SECRET&grant_type=authorization_code&code=$CODE&redirect_uri=http://localhost" \305 "https://login.microsoftonline.com/$TENANT_ID/oauth2/v2.0/token"306 ```3073082. **Inspect token**:309 ```bash310 # Decode JWT token (on jwt.ms or locally)311 # Verify:312 # - Correct issuer (https://login.microsoftonline.com/{tenant}/v2.0)313 # - Correct audience (your client ID)314 # - Requested scopes present315 # - Expiration reasonable316 ```317318## Output Contract3193201. **App Registration Summary**321 - App ID (client ID), tenant ID, object ID322 - Auth flow chosen and justification323 - Single/multi-tenant mode3243252. **Configuration for Application**326 - Authority URL: `https://login.microsoftonline.com/{tenant-id}`327 - Scopes requested: `openid profile email api://myapp/Orders.Read`328 - Redirect URIs registered329 - Secret location (Key Vault reference)3303313. **API Permissions & Scopes**332 - Microsoft Graph permissions (delegated + application)333 - Custom API scopes exposed (if this is an API)334 - Admin consent URLs or instructions3353364. **Credential Management Plan**337 - Certificate thumbprint or secret name in Key Vault338 - Rotation schedule (every 12 months)339 - Deprovisioning process3403415. **Implementation Checklist**342 - MSAL library added to project343 - Sign-in UI/UX344 - Token caching strategy345 - Refresh token handling346347## Guardrails348349- **Prefer certificates over secrets**: Self-signed cert rotated annually is safer than long-lived secret.350- **Never output secrets in plain text**: Store in Key Vault, reference with managed identity.351- **Request minimal permissions**: Only request scopes actually needed (principle of least privilege).352- **Enable PKCE for SPAs**: Prevents authorization code interception.353- **Use implicit flow for SPA**: Deprecated; use Authorization Code + PKCE instead.354- **Test token expiration**: Ensure apps handle 401 and request fresh token.355- **Secure the refresh token**: Store in secure, httpOnly cookie or OS credential store (never localStorage).356- **Monitor token usage**: Ensure tokens not being replayed or misused (Application Insights).