Certification: Azure Developer Associate (AZ-204) Services covered: Microsoft Identity Platform, MSAL, Azure Key Vault, Managed Identities, Azure App Configuration
Table of Contents
- User Authentication and Authorization in Azure
- Secret Management and Secure Access
- AZ-204 Certification Preparation
1. User Authentication and Authorization in Azure
1.1 Microsoft Identity Platform
┌──────────────────────────────────────────────────────────────────┐
│ MICROSOFT IDENTITY PLATFORM │
│ │
│ 3 components: │
│ │
│ 1. Microsoft Entra ID (formerly Azure AD) │
│ → Professional and school Microsoft accounts │
│ → Personal Microsoft accounts │
│ → Identities managed by the organization │
│ │
│ 2. Azure AD B2C (Business-to-Consumer) │
│ → Social accounts (Google, Facebook, Twitter) │
│ → Local accounts (email + password) │
│ → Full UI customization │
│ │
│ 3. Microsoft Entra External ID │
│ → New product, gradually replacing B2C │
│ → CIAM (Customer Identity & Access Management) │
└──────────────────────────────────────────────────────────────────┘
Supported authentication flows:
| Flow | Usage | Code |
|---|---|---|
| Authorization Code + PKCE | Web apps, mobile apps | AcquireTokenInteractive |
| Client Credentials | Daemon apps, services without users | AcquireTokenForClient |
| Device Code | Devices without browsers (IoT, CLI) | AcquireTokenWithDeviceCode |
| Implicit | Deprecated — legacy SPA | — |
| On-behalf-of | API calling other APIs | AcquireTokenOnBehalfOf |
| Resource Owner Password | Discouraged — tests only | AcquireTokenByUsernamePassword |
1.2 MSAL — Microsoft Authentication Library
Supported languages: .NET, Android, iOS/macOS, JavaScript/TypeScript, Java, Python, Go
Two client types:
// PUBLIC CLIENT — applications that run without a secure secret
// (mobile apps, desktop, SPA)
var publicClient = PublicClientApplicationBuilder
.Create("CLIENT_ID")
.WithAuthority(AzureCloudInstance.AzurePublic, "TENANT_ID")
.WithRedirectUri("https://localhost")
.Build();
// CONFIDENTIAL CLIENT — applications that can store secrets
// (server-side web apps, APIs, daemon services)
var confidentialClient = ConfidentialClientApplicationBuilder
.Create("CLIENT_ID")
.WithClientSecret("CLIENT_SECRET")
.WithAuthority($"https://login.microsoftonline.com/TENANT_ID")
.Build();
// Full example — ASP.NET Core web application
// Program.cs
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(new[] { "User.Read" })
.AddMicrosoftGraph()
.AddInMemoryTokenCaches();
builder.Services.AddAuthorization();
// appsettings.json
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "contoso.onmicrosoft.com",
"TenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"ClientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"ClientSecret": "[Use a secure secret in prod, not here]",
"CallbackPath": "/signin-oidc",
"SignedOutCallbackPath": "/signout-callback-oidc"
}
}
1.3 App Registration in Entra ID
Azure Portal → Microsoft Entra ID → App registrations → New registration
Steps:
────────────────────────────────────────────────────────────────────
1. Name: MyApp
2. Supported account types:
- Single tenant → Accounts in this directory only
- Multi-tenant → Accounts in any Entra ID
- Multi-tenant + personal Microsoft accounts
- Personal Microsoft accounts only
3. Redirect URI: Web → https://localhost:5001/signin-oidc
After creation, configure:
→ Authentication → Implicit Grant (if needed, discouraged)
→ Certificates & Secrets → New client secret (copy immediately!)
→ API permissions → Add permission
→ Expose an API → Add custom scopes
Delegated vs Application permissions:
| Type | Context | Consent | Usage |
|---|---|---|---|
| Delegated | With a logged-in user | User + Admin (if required) | Web apps, mobile |
| Application | Without user (daemon) | Admin required | Background services |
// Acquire a token with delegated permissions
var result = await confidentialClient.AcquireTokenOnBehalfOf(
scopes: new[] { "User.Read", "Mail.Send" },
userAssertion: new UserAssertion(incomingToken))
.ExecuteAsync();
// Acquire a token with application permissions (client credentials)
var result = await confidentialClient.AcquireTokenForClient(
scopes: new[] { "https://graph.microsoft.com/.default" })
.ExecuteAsync();
Admin consent:
# Grant admin consent for an application
az ad app permission admin-consent \
--id "CLIENT_ID"
1.4 Microsoft Graph
Microsoft Graph = unified REST API to access Microsoft 365 data (users, emails, calendar, files, Teams, etc.)
Endpoints:
https://graph.microsoft.com/v1.0/→ Stable versionhttps://graph.microsoft.com/beta/→ Preview features
// Use Microsoft Graph SDK in an ASP.NET Core app
// Package: Microsoft.Graph
// Option 1: via DI (recommended with Microsoft.Identity.Web.MicrosoftGraph)
// Program.cs (see section 1.2)
// Option 2: manual creation
var scopes = new[] { "User.Read" };
var options = new PublicClientApplicationOptions
{
ClientId = "CLIENT_ID",
TenantId = "TENANT_ID"
};
var publicClientApp = PublicClientApplicationBuilder
.CreateWithApplicationOptions(options)
.Build();
// Authentication provider for Graph
var authProvider = new InteractiveAuthenticationProvider(publicClientApp, scopes);
var graphClient = new GraphServiceClient(authProvider);
// Graph calls
var user = await graphClient.Me.GetAsync();
Console.WriteLine($"Hello, {user.DisplayName}!");
// List emails
var messages = await graphClient.Me.Messages.GetAsync(config =>
{
config.QueryParameters.Top = 10;
config.QueryParameters.Select = new[] { "subject", "from", "receivedDateTime" };
config.QueryParameters.Orderby = new[] { "receivedDateTime desc" };
});
foreach (var message in messages.Value)
{
Console.WriteLine($"{message.ReceivedDateTime}: {message.Subject}");
}
// Send an email
await graphClient.Me.SendMail.PostAsync(new SendMailPostRequestBody
{
Message = new Message
{
Subject = "Test via Graph",
Body = new ItemBody
{
ContentType = BodyType.Text,
Content = "Sent from the application"
},
ToRecipients = new List<Recipient>
{
new Recipient { EmailAddress = new EmailAddress { Address = "recipient@example.com" } }
}
}
});
1.5 Shared Access Signatures (SAS)
SAS = temporary URL with limited permissions to access Azure Storage resources.
3 SAS types:
| Type | Identity | Revocable | Security |
|---|---|---|---|
| Service SAS | Storage account key | Via Stored Access Policy | Moderate |
| Account SAS | Storage account key | No (except key rotation) | Low |
| User Delegation SAS | Entra ID | Via user permissions | ✅ Recommended |
# Create a User Delegation SAS (recommended — uses Entra ID)
az storage blob generate-sas \
--account-name "myappstorage" \
--container-name "assets" \
--name "image.jpg" \
--permissions r \
--expiry "2024-12-31T23:59:59Z" \
--auth-mode login # Uses Entra ID, not the account key
# The generated SAS looks like:
# https://myappstorage.blob.core.windows.net/assets/image.jpg
# ?sv=2021-06-08
# &se=2024-12-31T23%3A59%3A59Z
# &sr=b → Scope = Blob
# &sp=r → Permission = Read
# &spr=https → HTTPS only
# &sig=XXXXX → HMAC-SHA256 Signature
SAS Parameters:
| Parameter | Description |
|---|---|
sv | Service version |
st | Start time (optional) |
se | Expiry time |
sr | Resource scope (b=blob, c=container, s=service) |
sp | Permissions (r=read, w=write, d=delete, l=list, c=create) |
spr | Protocol (https) |
sig | Cryptographic signature |
skoid | Object ID (User Delegation SAS only) |
Stored Access Policy:
# Create a Stored Access Policy (to be able to revoke the SAS)
az storage container policy create \
--account-name "myappstorage" \
--container-name "assets" \
--name "ReadOnlyPolicy" \
--permissions r \
--expiry "2024-12-31T23:59:59Z"
# Revoke by modifying/deleting the policy
az storage container policy delete \
--account-name "myappstorage" \
--container-name "assets" \
--name "ReadOnlyPolicy"
2. Secret Management and Secure Access
2.1 Managed Identities
Managed Identity = identity automatically managed by Azure for a resource (no secrets to manage!).
┌──────────────────────────────────────────────────────────────────────┐
│ TYPES OF MANAGED IDENTITIES │
│ │
│ SYSTEM-ASSIGNED USER-ASSIGNED │
│ ════════════════ ═════════════ │
│ ✓ Tied to the resource ✓ Independent Azure resource │
│ ✓ Deleted with the resource ✓ Can be shared │
│ ✓ 1 per resource ✓ Multiple per resource │
│ ✓ Lifecycle = resource ✓ Independent lifecycle │
│ │
│ Typical usage: Typical usage: │
│ → VM, simple App Service → Reuse an identity │
│ → Single service accessing → Multi-resources same identity │
│ Key Vault │
└──────────────────────────────────────────────────────────────────────┘
# Enable System-Assigned Managed Identity on an App Service
az webapp identity assign \
--name "my-webapp" \
--resource-group "rg-myapp"
# → Returns a principalId (object ID of the identity)
# Create and assign a User-Assigned Managed Identity
az identity create \
--name "id-myapp" \
--resource-group "rg-myapp"
az webapp identity assign \
--name "my-webapp" \
--resource-group "rg-myapp" \
--identities "/subscriptions/SUB/resourceGroups/rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/id-myapp"
// Use ManagedIdentityCredential in code
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
// System-assigned
var credential = new ManagedIdentityCredential();
// User-assigned
var credential = new ManagedIdentityCredential("CLIENT_ID_OF_USER_ASSIGNED_IDENTITY");
// DefaultAzureCredential — tries multiple auth methods (recommended for dev)
// In dev: Azure CLI, VS, VSCode, environment
// In prod: Managed Identity
var credential = new DefaultAzureCredential();
var client = new SecretClient(
new Uri("https://myapp-vault.vault.azure.net/"),
credential);
var secret = await client.GetSecretAsync("DatabasePassword");
Console.WriteLine($"Secret: {secret.Value.Value}");
2.2 Azure Key Vault
Key Vault stores 3 types of objects:
- Secrets: passwords, connection strings, API keys
- Keys: cryptographic keys (RSA, EC) — encryption, signing
- Certificates: TLS/SSL certificates
# Create a Key Vault
az keyvault create \
--name "myapp-vault" \
--resource-group "rg-myapp" \
--location "eastus" \
--enable-rbac-authorization true # Use RBAC (recommended)
# Add a secret
az keyvault secret set \
--vault-name "myapp-vault" \
--name "DatabasePassword" \
--value "SecurePassword123!"
# List secrets
az keyvault secret list --vault-name "myapp-vault" -o table
# Read a secret
az keyvault secret show \
--vault-name "myapp-vault" \
--name "DatabasePassword" \
--query value -o tsv
# Grant access via RBAC (Managed Identity or user)
az role assignment create \
--role "Key Vault Secrets User" \
--assignee "OBJECT_ID_OF_PRINCIPAL" \
--scope "/subscriptions/SUB/resourceGroups/rg/providers/Microsoft.KeyVault/vaults/myapp-vault"
RBAC roles for Key Vault:
| Role | Permissions |
|---|---|
| Key Vault Administrator | Everything (admin) |
| Key Vault Secrets Officer | CRUD on secrets |
| Key Vault Secrets User | Read secrets (recommended for apps) |
| Key Vault Reader | Read metadata only |
// Key Vault integration in ASP.NET Core
// Program.cs — Automatically load secrets as configuration
builder.Configuration.AddAzureKeyVault(
new Uri("https://myapp-vault.vault.azure.net/"),
new DefaultAzureCredential());
// Secrets are accessible like any other config
var dbPassword = builder.Configuration["DatabasePassword"];
// Key Vault → ASP.NET Core naming convention
// Key Vault: "ConnectionStrings--DefaultConnection" (-- replaces :)
// ASP.NET Core: Configuration["ConnectionStrings:DefaultConnection"]
// Read a secret directly via SDK
var client = new SecretClient(
new Uri("https://myapp-vault.vault.azure.net/"),
new DefaultAzureCredential());
// Read current value
KeyVaultSecret secret = await client.GetSecretAsync("DatabasePassword");
string password = secret.Value.Value;
// Read a specific version
KeyVaultSecret specificVersion = await client.GetSecretAsync(
"DatabasePassword",
"version-guid");
// List all versions of a secret
await foreach (SecretProperties properties in client.GetPropertiesOfSecretVersionsAsync("DatabasePassword"))
{
Console.WriteLine($"Version: {properties.Version}, Enabled: {properties.Enabled}");
}
Key Vault best practices:
✅ One Key Vault per environment (DEV / TEST / PROD)
→ Access isolation, separate audit logs
✅ Use Managed Identity (never hardcode auth secrets)
✅ Enable soft-delete and purge protection
→ Avoid irreversible accidental deletion
✅ RBAC rather than Access Policies (more granular)
✅ Monitor access with Azure Monitor / Diagnostic Logs
❌ Never store sensitive information in App Configuration
→ Use Key Vault references in App Configuration
2.3 Azure App Configuration
App Configuration = centralized service for configuration (not for secrets).
┌────────────────────────────────────────────────────────────────────┐
│ APP CONFIGURATION vs KEY VAULT │
│ │
│ APP CONFIGURATION KEY VAULT │
│ ═════════════════ ═════════ │
│ ✓ Non-sensitive key/values ✓ Secrets, keys, certs │
│ ✓ Feature flags ✓ Encryption │
│ ✓ Labels per environment ✓ Access auditing │
│ ✓ Hierarchy with : ✓ Secret rotation │
│ ✓ App Config → Key Vault ref ✓ HSM (Premium) │
│ │
│ Use App Config FOR config parameters. │
│ Use Key Vault FOR sensitive values. │
│ Combine: Key Vault references in App Config. │
└────────────────────────────────────────────────────────────────────┘
# Create an App Configuration store
az appconfig create \
--name "myapp-config" \
--resource-group "rg-myapp" \
--location "eastus" \
--sku Free # or Standard
# Add key/values
az appconfig kv set \
--name "myapp-config" \
--key "App:Theme:Background" \
--value "blue"
# With labels (per environment)
az appconfig kv set \
--name "myapp-config" \
--key "App:MaxProducts" \
--value "100" \
--label "Development"
az appconfig kv set \
--name "myapp-config" \
--key "App:MaxProducts" \
--value "1000" \
--label "Production"
# Reference to Key Vault (for sensitive values)
az appconfig kv set-keyvault \
--name "myapp-config" \
--key "ConnectionStrings:Default" \
--secret-identifier "https://myapp-vault.vault.azure.net/secrets/DatabaseConnectionString"
Key naming convention:
App:Theme:Background → uses : for hierarchy
App/Theme/Background → uses / (alternative)
In ASP.NET Core → Configuration["App:Theme:Background"]
// App Configuration integration in ASP.NET Core
// Package: Microsoft.Extensions.Configuration.AzureAppConfiguration
// Program.cs
builder.Configuration.AddAzureAppConfiguration(options =>
{
options.Connect(builder.Configuration.GetConnectionString("AppConfig"))
// Or with Managed Identity:
// .Connect(new Uri("https://myapp-config.azconfig.io"), new ManagedIdentityCredential())
.Select(KeyFilter.Any, LabelFilter.Null) // Config without label
.Select(KeyFilter.Any, "Production") // Config with Production label
// Feature flags
.UseFeatureFlags(flagOptions =>
{
flagOptions.CacheExpirationInterval = TimeSpan.FromMinutes(5);
})
// Key Vault references
.ConfigureKeyVault(kvOptions =>
{
kvOptions.SetCredential(new DefaultAzureCredential());
});
});
// Feature Flags with App Configuration
// Package: Microsoft.FeatureManagement.AspNetCore
builder.Services.AddFeatureManagement();
// In a controller or service
public class ProductController : Controller
{
private readonly IFeatureManager _featureManager;
public ProductController(IFeatureManager featureManager)
{
_featureManager = featureManager;
}
public async Task<IActionResult> Index()
{
if (await _featureManager.IsEnabledAsync("NewProductLayout"))
{
return View("IndexV2");
}
return View("Index");
}
}
@* Feature Flag in views *@
@inject IFeatureManager FeatureManager
@if (await FeatureManager.IsEnabledAsync("ShowBetaBanner"))
{
<div class="alert alert-info">You are using the beta version!</div>
}
3. AZ-204 Certification Preparation
3.1 Exam Questions — Identities
Q1: What is the difference between delegated permissions and application permissions?
Delegated permissions:
→ The application acts ON BEHALF OF a logged-in user
→ The user must consent (+ admin if required)
→ Ex: read a specific user's emails
Application permissions:
→ The application acts IN ITS OWN NAME (without a user)
→ Admin consent REQUIRED
→ Ex: read all users' emails (reporting)
Q2: Which MSAL flows for which application types?
| Application | Recommended flow |
|---|---|
| Web app with users | Authorization Code + PKCE |
| API (daemon/service) | Client Credentials |
| Mobile app | Authorization Code + PKCE |
| IoT / devices without browser | Device Code |
| API calling another API | On-behalf-of |
Q3: Which SAS is the most secure?
User Delegation SAS > Service SAS > Account SAS
User Delegation SAS:
✓ Uses Entra ID credentials (not account keys)
✓ Revocable via Entra ID permissions
✓ Audit trail in Entra ID
3.2 Exam Questions — Secrets and Access
Q4: System-assigned vs User-assigned Managed Identity?
System-assigned:
→ Tied to the resource lifecycle
→ Automatically deleted with the resource
→ Only 1 per resource
→ Ideal for: a single resource accessing Key Vault
User-assigned:
→ Independent Azure resource
→ Can be assigned to multiple resources
→ Survives deletion of associated resources
→ Ideal for: multiple resources sharing the same identity
Q5: What is the main advantage of Managed Identity?
→ No secrets/credentials to manage in code or config
→ Automatic credential rotation by Azure
→ Cannot accidentally be leaked
→ Compliant with security best practices
Q6: How to access Key Vault securely from an App Service?
1. Enable Managed Identity on the App Service
2. Grant "Key Vault Secrets User" role to the identity
3. In code: use DefaultAzureCredential()
→ In prod → automatically uses Managed Identity
→ In dev → uses local credentials (az login, VS, VSCode)
Q7: How many Key Vaults do you recommend?
→ Official recommendation: 1 Key Vault per environment
→ Minimum: 3 (DEV / TEST / PROD)
→ Reasons: access isolation, resilience, regulations
Q8: App Configuration vs Key Vault — what goes where?
App Configuration:
✓ Feature flags (enable/disable features)
✓ Non-sensitive settings (URLs, timeouts, pagination)
✓ Per-environment configuration (labels)
❌ DO NOT store: passwords, connection strings, API keys
Key Vault:
✓ Secrets (passwords, tokens)
✓ DB connection strings
✓ Cryptographic keys
✓ TLS certificates
Combination (recommended):
→ Store Key Vault references in App Configuration
→ Sensitive values remain in Key Vault
→ App configures both simultaneously
3.3 Essential Azure CLI Commands
# Entra ID / App Registration
az ad app create --display-name "MyApp"
az ad app list --display-name "MyApp" -o table
az ad app permission admin-consent --id "CLIENT_ID"
# Managed Identity
az webapp identity assign --name "app" --resource-group "rg"
az identity create --name "id-name" --resource-group "rg"
# Key Vault
az keyvault create --name "vault" --resource-group "rg" --enable-rbac-authorization true
az keyvault secret set --vault-name "vault" --name "MySecret" --value "value"
az keyvault secret show --vault-name "vault" --name "MySecret" --query value -o tsv
az role assignment create --role "Key Vault Secrets User" --assignee "OBJECT_ID" \
--scope "/subscriptions/SUB/resourceGroups/rg/providers/Microsoft.KeyVault/vaults/vault"
# App Configuration
az appconfig create --name "config" --resource-group "rg" --sku Free
az appconfig kv set --name "config" --key "App:Setting" --value "value"
az appconfig kv set --name "config" --key "App:Setting" --value "prod-value" --label "Production"
az appconfig kv set-keyvault --name "config" --key "Secrets:DB" \
--secret-identifier "https://vault.vault.azure.net/secrets/DB"
# Storage SAS
az storage blob generate-sas \
--account-name "storage" \
--container-name "container" \
--name "blob.jpg" \
--permissions r \
--expiry "2024-12-31T23:59:59Z" \
--auth-mode login
# Microsoft Graph (via CLI)
az rest --method GET --url "https://graph.microsoft.com/v1.0/me"
Summary — Security Decisions
┌────────────────────────────────────────────────────────────────────────┐
│ AZURE SECURITY DECISION TABLE │
│ │
│ NEED SOLUTION │
│ ────────────────────────────────────────────────────────────────── │
│ Authenticate users → Microsoft Identity Platform │
│ (Azure/Microsoft accounts) + MSAL │
│ │
│ Authenticate customers → Entra External ID / Azure AD B2C │
│ (Google, Facebook, custom) │
│ │
│ Call Microsoft Graph → GraphServiceClient SDK │
│ (emails, calendars, Teams) + delegated permissions │
│ │
│ Temporary Storage access → User Delegation SAS │
│ (without exposing keys) │
│ │
│ Service accesses Key Vault → Managed Identity (System-assigned) │
│ (without hardcoded credentials) │
│ │
│ Multiple services same → User-assigned Managed Identity │
│ Key Vault identity │
│ │
│ Store passwords / certs → Azure Key Vault │
│ │
│ Centralized non-sensitive → Azure App Configuration │
│ config + Feature flags │
│ │
│ Sensitive values in → Key Vault references │
│ App Configuration in App Configuration │
└────────────────────────────────────────────────────────────────────────┘
Section 3 — Microsoft Identity Platform: Complete Guide
3.1 OAuth 2.0 and OpenID Connect
sequenceDiagram
participant U as User
participant APP as Application
participant IDP as Microsoft Entra ID\n(Identity Provider)
participant API as Backend API
Note over APP,IDP: Authorization Code Flow (with PKCE)
U->>APP: Click "Sign in"
APP->>IDP: Authorization Request\n(client_id, scope, redirect_uri, code_challenge)
IDP->>U: Login page
U->>IDP: Credentials
IDP->>APP: Authorization Code (via redirect)
APP->>IDP: Token Request\n(code, code_verifier, client_secret)
IDP->>APP: Access Token + Refresh Token + ID Token
APP->>API: Request with Bearer Token
API->>IDP: Validate the token (JWKS endpoint)
IDP->>API: Token valid (userId, roles, scopes)
API->>APP: Protected data
3.2 OAuth 2.0 Flow Types
| Flow | Usage | Client Type | Example |
|---|---|---|---|
| Authorization Code + PKCE | Web apps, SPAs, mobile | Public + Confidential | React SPA, .NET MVC |
| Client Credentials | Services without users | Confidential | Daemon, service worker |
| Device Code | Devices without browsers | Public | IoT, CLI tools |
| On-Behalf-Of | API calling another API | Confidential | Microservices chain |
| Implicit | Deprecated (replaced by PKCE) | — | — |
3.3 MSAL (Microsoft Authentication Library) in .NET
// Program.cs - ASP.NET Core configuration
using Microsoft.Identity.Web;
var builder = WebApplication.CreateBuilder(args);
// Authentication with Microsoft Entra ID
builder.Services.AddMicrosoftIdentityWebApiAuthentication(
builder.Configuration, "AzureAd");
// For a Web App (not an API)
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(new[] { "User.Read" })
.AddMicrosoftGraph(builder.Configuration.GetSection("MicrosoftGraph"))
.AddInMemoryTokenCaches();
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "yourtenant.onmicrosoft.com",
"TenantId": "your-tenant-id",
"ClientId": "your-client-id",
"ClientSecret": "your-client-secret",
"CallbackPath": "/signin-oidc"
},
"MicrosoftGraph": {
"BaseUrl": "https://graph.microsoft.com/v1.0",
"Scopes": "User.Read Mail.Read"
}
}
// Service that calls Microsoft Graph
public class GraphService
{
private readonly GraphServiceClient _graphClient;
public GraphService(GraphServiceClient graphClient)
{
_graphClient = graphClient;
}
// Retrieve the current user's profile
public async Task<User> GetCurrentUserAsync()
{
return await _graphClient.Me
.GetAsync(config => config.QueryParameters.Select =
new[] { "id", "displayName", "mail", "jobTitle" });
}
// Send an email
public async Task SendEmailAsync(string recipientEmail, string subject, string body)
{
var message = new Message
{
Subject = subject,
Body = new ItemBody { ContentType = BodyType.Html, Content = body },
ToRecipients = new List<Recipient>
{
new Recipient { EmailAddress = new EmailAddress { Address = recipientEmail } }
}
};
await _graphClient.Me.SendMail
.PostAsync(new SendMailPostRequestBody { Message = message });
}
// List members of a group
public async Task<List<User>> GetGroupMembersAsync(string groupId)
{
var members = await _graphClient.Groups[groupId].Members
.GetAsync(config =>
{
config.QueryParameters.Top = 100;
config.QueryParameters.Select = new[] { "id", "displayName", "mail" };
});
return members?.Value?.OfType<User>().ToList() ?? new List<User>();
}
}
3.4 App Registrations — Configuration
flowchart TD
subgraph "Application Registration"
APP_ID["Application (Client) ID"]
TENANT_ID["Directory (Tenant) ID"]
REDIRECT[Redirect URIs\nhttps://app.com/callback]
CREDS[Credentials\nClient Secret OR Certificate]
PERMS[API Permissions\nRequested scopes]
EXPOSE[Expose an API\nExposed scopes]
end
subgraph "Application Types"
SINGLE[Single-tenant\nOne Entra ID tenant only]
MULTI[Multi-tenant\nAll Entra ID tenants]
PERSONAL[Accounts + Personal\nPersonal Microsoft accounts too]
end
subgraph "Permission Types"
DELEGATED[Delegated Permissions\nOn behalf of a logged-in user]
APP_PERM[Application Permissions\nWithout user - daemon app]
end
# Create an App Registration via Azure CLI
az ad app create \
--display-name "MyApp" \
--sign-in-audience "AzureADMyOrg" \ # single-tenant
--web-redirect-uris "https://localhost:5001/signin-oidc"
# Create a client secret
az ad app credential reset \
--id "app-client-id" \
--years 1
# Add Microsoft Graph permissions
az ad app permission add \
--id "app-client-id" \
--api "00000003-0000-0000-c000-000000000000" \ # Microsoft Graph
--api-permissions "e1fe6dd8-ba31-4d61-89e7-88639da4683d=Scope" # User.Read (delegated)
# Grant admin consent (Application permissions)
az ad app permission admin-consent --id "app-client-id"
# Expose an API (for protected APIs)
az ad app update \
--id "app-client-id" \
--identifier-uris "api://app-client-id"
3.5 Client Credentials Flow (Service-to-Service)
// Daemon application without users
var confidentialClient = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithClientSecret(clientSecret) // Or .WithCertificate(cert) — more secure
.WithAuthority(new Uri($"https://login.microsoftonline.com/{tenantId}"))
.Build();
// Acquire a token to call an API
var scopes = new[] { "https://myapi.com/.default" }; // .default = all App permissions
var authResult = await confidentialClient
.AcquireTokenForClient(scopes)
.ExecuteAsync();
// Use the token
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
var response = await httpClient.GetAsync("https://myapi.com/api/data");
3.6 On-Behalf-Of Flow (API Chaining)
// Middle API calls a downstream API on behalf of the user
public class MiddleApiController : ControllerBase
{
private readonly ITokenAcquisition _tokenAcquisition;
[HttpGet("data")]
public async Task<IActionResult> GetData()
{
// Acquire a token for the downstream API on behalf of the logged-in user
string downstreamToken = await _tokenAcquisition
.GetAccessTokenForUserAsync(
scopes: new[] { "api://downstream-api-id/Data.Read" },
user: HttpContext.User);
// Call the downstream API with this token
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", downstreamToken);
var response = await httpClient.GetAsync("https://downstream-api.com/data");
var data = await response.Content.ReadAsStringAsync();
return Ok(data);
}
}
Section 4 — Managed Identities: Complete Guide
4.1 Managed Identity with DefaultAzureCredential
// DefaultAzureCredential tries in order:
// 1. Environment Variables (AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID)
// 2. Workload Identity (AKS)
// 3. Managed Identity (in production on Azure)
// 4. Azure CLI (in local development with `az login`)
// 5. Azure PowerShell (in local development)
// 6. Visual Studio / VS Code credentials
// Configuration in Program.cs
builder.Services.AddAzureClients(clientBuilder =>
{
// Key Vault Secrets Client
clientBuilder.AddSecretClient(
new Uri("https://my-keyvault.vault.azure.net/"));
// Blob Storage Client
clientBuilder.AddBlobServiceClient(
new Uri("https://mystorage.blob.core.windows.net/"));
// App Configuration Client
clientBuilder.AddConfigurationClient(
new Uri("https://my-appconfig.azconfig.io"));
// Use DefaultAzureCredential for all
clientBuilder.UseCredential(new DefaultAzureCredential());
});
// Injection into services
public class SecureDataService
{
private readonly SecretClient _secretClient;
private readonly BlobServiceClient _blobClient;
public SecureDataService(SecretClient secretClient, BlobServiceClient blobClient)
{
_secretClient = secretClient;
_blobClient = blobClient;
}
public async Task<string> GetSecretAsync(string secretName)
{
var secret = await _secretClient.GetSecretAsync(secretName);
return secret.Value.Value;
}
}
4.2 System-Assigned vs User-Assigned
# ─── System-Assigned Managed Identity ──────────────────────────────────
# Enable on App Service
az webapp identity assign \
--name "my-app" \
--resource-group "rg"
# Get the Object ID
APP_IDENTITY=$(az webapp identity show \
--name "my-app" \
--resource-group "rg" \
--query principalId -o tsv)
# ─── User-Assigned Managed Identity ────────────────────────────────────
# Create the identity
az identity create \
--name "mi-shared-identity" \
--resource-group "rg"
MI_CLIENT_ID=$(az identity show \
--name "mi-shared-identity" \
--resource-group "rg" \
--query clientId -o tsv)
MI_OBJECT_ID=$(az identity show \
--name "mi-shared-identity" \
--resource-group "rg" \
--query principalId -o tsv)
MI_ID=$(az identity show \
--name "mi-shared-identity" \
--resource-group "rg" \
--query id -o tsv)
# Assign to multiple resources
az webapp identity assign \
--name "app1" \
--resource-group "rg" \
--identities $MI_ID
az functionapp identity assign \
--name "func1" \
--resource-group "rg" \
--identities $MI_ID
# ─── Assign roles (for both types) ───────────────────────────────────
# Key Vault Secrets User
az role assignment create \
--role "Key Vault Secrets User" \
--assignee-object-id $MI_OBJECT_ID \
--assignee-principal-type ServicePrincipal \
--scope "/subscriptions/{sub}/resourceGroups/rg/providers/Microsoft.KeyVault/vaults/my-kv"
# Storage Blob Data Reader
az role assignment create \
--role "Storage Blob Data Reader" \
--assignee-object-id $MI_OBJECT_ID \
--assignee-principal-type ServicePrincipal \
--scope "/subscriptions/{sub}/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/mystorage"
Section 5 — Shared Access Signatures (SAS): Complete Guide
5.1 SAS Types in Detail
flowchart TD
SAS_TYPES[SAS Types] --> SERVICE[Service SAS\nAccess to a specific service\nBlob, Queue, Table, File]
SAS_TYPES --> ACCOUNT[Account SAS\nAccess to multiple services\nand operations]
SAS_TYPES --> USER_DEL[User Delegation SAS\nBased on Entra ID\nBlob only\nRecommended]
USER_DEL --> SECURE[✅ More secure\nNo account key exposed]
SERVICE --> POLICY[Stored Access Policy\nCentrally revocable]
5.2 Generate a SAS Token in .NET
// User Delegation SAS (recommended - based on Entra ID)
public async Task<Uri> GenerateUserDelegationSasAsync(
string containerName,
string blobName,
TimeSpan validity)
{
var blobServiceClient = new BlobServiceClient(
new Uri($"https://{storageAccountName}.blob.core.windows.net"),
new DefaultAzureCredential());
// Get a User Delegation Key (valid max 7 days)
var userDelegationKey = await blobServiceClient.GetUserDelegationKeyAsync(
DateTimeOffset.UtcNow.AddMinutes(-5), // Start slightly in the past
DateTimeOffset.UtcNow.Add(validity));
// Generate the SAS
var sasBuilder = new BlobSasBuilder
{
BlobContainerName = containerName,
BlobName = blobName,
Resource = "b", // "b" = blob, "c" = container
ExpiresOn = DateTimeOffset.UtcNow.Add(validity)
};
sasBuilder.SetPermissions(BlobSasPermissions.Read); // Read-only
var blobClient = new BlobClient(
new Uri($"https://{storageAccountName}.blob.core.windows.net/{containerName}/{blobName}"),
new DefaultAzureCredential());
var sasUri = blobClient.GenerateSasUri(sasBuilder);
return sasUri;
}
// Service SAS with Stored Access Policy
public async Task<Uri> GenerateServiceSasWithPolicyAsync(
string containerName,
string blobName,
string policyName)
{
var blobClient = new BlobClient(
$"DefaultEndpointsProtocol=https;AccountName={storageAccountName};AccountKey={storageAccountKey}",
containerName,
blobName);
var sasBuilder = new BlobSasBuilder
{
BlobContainerName = containerName,
BlobName = blobName,
Identifier = policyName // References the Stored Access Policy
};
// No need to define permissions (defined in the policy)
return blobClient.GenerateSasUri(sasBuilder);
}
5.3 Stored Access Policies
// Create and manage Stored Access Policies
// A policy can be revoked to invalidate ALL SAS that reference it
public async Task CreateAccessPolicyAsync(
string containerName,
string policyName)
{
var containerClient = new BlobContainerClient(
connectionString, containerName);
var policies = new List<BlobSignedIdentifier>
{
new BlobSignedIdentifier
{
Id = policyName,
AccessPolicy = new BlobAccessPolicy
{
StartsOn = DateTimeOffset.UtcNow.AddMinutes(-5),
ExpiresOn = DateTimeOffset.UtcNow.AddHours(24),
Permissions = "r" // Read only
}
}
};
await containerClient.SetAccessPolicyAsync(
permissions: policies);
}
// Revoke a Stored Access Policy (invalidates all associated SAS)
public async Task RevokeAccessPolicyAsync(string containerName, string policyName)
{
var containerClient = new BlobContainerClient(connectionString, containerName);
// Get existing policies
var properties = await containerClient.GetAccessPolicyAsync();
var updatedPolicies = properties.Value.SignedIdentifiers
.Where(p => p.Id != policyName) // Remove the revoked policy
.ToList();
await containerClient.SetAccessPolicyAsync(permissions: updatedPolicies);
}
Section 6 — Azure App Configuration Advanced
6.1 Feature Flags
// Program.cs
builder.Services.AddFeatureManagement()
.AddFeatureFilter<PercentageFilter>() // Enable for X% of users
.AddFeatureFilter<TargetingFilter>() // Enable for specific groups/users
.AddFeatureFilter<TimeWindowFilter>(); // Enable during a time window
// Injection into a Controller
[ApiController]
[Route("api/[controller]")]
public class OrderController : ControllerBase
{
private readonly IFeatureManager _featureManager;
public OrderController(IFeatureManager featureManager)
{
_featureManager = featureManager;
}
[HttpGet("checkout")]
public async Task<IActionResult> Checkout([FromBody] CartModel cart)
{
if (await _featureManager.IsEnabledAsync("NewCheckoutExperience"))
{
return await ProcessNewCheckout(cart);
}
return await ProcessLegacyCheckout(cart);
}
}
// Attribute on an action (returns 404 if feature disabled)
[FeatureGate("BetaFeature")]
[HttpGet("beta")]
public IActionResult BetaEndpoint()
{
return Ok("Beta feature active!");
}
// Configuration in Azure App Configuration (JSON import)
{
"feature_management": {
"feature_flags": [
{
"id": "NewCheckoutExperience",
"enabled": true,
"conditions": {
"client_filters": [
{
"name": "Microsoft.Targeting",
"parameters": {
"Audience": {
"Users": ["admin@company.com"],
"Groups": [
{ "Name": "BetaTesters", "RolloutPercentage": 50 }
],
"DefaultRolloutPercentage": 10
}
}
}
]
}
}
]
}
}
6.2 Dynamic Refresh (without restart)
// Program.cs - Enable dynamic refresh
builder.Configuration.AddAzureAppConfiguration(options =>
{
options.Connect(connectionString)
.ConfigureRefresh(refresh =>
{
// This "sentinel" key watches for changes
// Changing this value triggers a reload of ALL config
refresh.Register("Application:Sentinel", refreshAll: true)
.SetCacheExpiration(TimeSpan.FromSeconds(30)); // Poll every 30s
})
.UseFeatureFlags(flagOptions =>
{
flagOptions.CacheExpirationInterval = TimeSpan.FromSeconds(30);
});
});
// Enable the refresh middleware
builder.Services.AddAzureAppConfiguration();
var app = builder.Build();
app.UseAzureAppConfiguration(); // IMPORTANT: must be before UseRouting()
# Trigger a configuration refresh
az appconfig kv set \
--name "my-appconfig" \
--key "Application:Sentinel" \
--value "$(date +%s)" # Change the value to trigger a refresh
Review Questions — AZ-204 Security
Q1 — MSAL: Client Types
Question: Your iOS mobile application uses MSAL to authenticate. The user logs in, and the app needs to access their Microsoft calendar. What type of MSAL client do you use, and what OAuth 2.0 flow?
- A. Confidential Client + Client Credentials Flow
- B. Public Client + Implicit Flow
- C. Public Client + Authorization Code Flow with PKCE ✅
- D. Confidential Client + Authorization Code Flow
Explanation: A mobile application is a Public Client because it cannot securely store secrets (the code can be decompiled). The recommended flow is Authorization Code + PKCE (Proof Key for Code Exchange) which replaces Implicit Flow. PKCE protects against authorization code interception attacks without requiring a client secret.
Q2 — Admin Consent
Question: Your daemon application needs to read all users’ emails in the tenant (not on behalf of a user). What type of permission and what consent are required?
- A. Delegated permission + User consent
- B. Application permission + Admin consent ✅
- C. Delegated permission + Admin consent
- D. Application permission + User consent
Explanation: To access resources without a logged-in user (daemon app, service), you need Application permissions (not Delegated). These permissions always require administrator consent because they grant broad access to all tenant resources. Only an admin can consent on behalf of the whole organization.
Q3 — User Delegation SAS
Question: You need to generate a SAS token to give temporary access to a blob in Azure Storage. Your security team requires that storage account keys are NEVER exposed. What type of SAS do you use?
- A. Service SAS based on the account key
- B. Account SAS based on the account key
- C. Service SAS with Stored Access Policy
- D. User Delegation SAS based on Entra ID ✅
Explanation: The User Delegation SAS uses Microsoft Entra ID credentials to generate the User Delegation Key, not the storage account key. This completely avoids exposing the account key. This is Microsoft’s recommended method for Azure Blob Storage. The Account Key is never touched.
Q4 — Managed Identity: System vs User
Question: You have 5 Azure Functions that all need to access the same Azure Key Vault. If one of the functions is deleted and recreated, it must automatically have access to Key Vault without reconfiguration. What type of Managed Identity do you use?
- A. System-assigned Managed Identity on each Function
- B. User-assigned Managed Identity shared between the 5 Functions ✅
- C. Service Principal with certificate
- D. System-assigned Managed Identity with inherited RBAC
Explanation: A User-assigned Managed Identity is independent of Azure resources. It survives the deletion/recreation of Functions. When you create a new Function, simply assign the existing identity to it and it immediately has access to Key Vault. With System-assigned, if a Function is deleted, the identity disappears and the role assignment needs to be recreated.
Q5 — Stored Access Policy
Question: You have generated 1000 SAS tokens that give access to an Azure Storage container. You discover a security breach and need to immediately invalidate ALL these tokens. Which approach works?
- A. Rename the container (breaks all SAS)
- B. Revoke the Stored Access Policy that all SAS reference ✅
- C. Regenerate the storage account key
- D. Delete and recreate the container
Explanation: If all SAS tokens were generated by referencing a Stored Access Policy (via
Identifier), revoking/deleting this policy immediately invalidates all tokens. This is precisely why Stored Access Policies exist: they enable centralized revocation. Regenerating the account key would also work but disrupts all legitimate access.
Q6 — App Configuration vs Key Vault
Question: You need to store the following configuration for your application: payment API URL, connection timeout, “EnableNewCheckout” feature flag, and the Azure SQL connection string. How do you distribute these elements?
- A. Everything in Azure Key Vault (maximum security)
- B. Everything in Azure App Configuration
- C. App Configuration for everything, except secrets in Key Vault
- D. URL and timeout in App Configuration, Feature flag in App Configuration, SQL Connection String in Key Vault referenced via App Configuration ✅
Explanation: The best practice:
- App Configuration: non-sensitive settings (URLs, timeouts), Feature Flags
- Key Vault: secrets (connection strings, passwords, API keys)
- Combination: store a Key Vault Reference in App Configuration — the app loads everything from App Configuration, but App Config automatically resolves Key Vault references.
Glossary — AZ-204 Security
| Term | Definition |
|---|---|
| Access Token | Short-lived JWT token (1h) to access a protected API |
| Admin Consent | Administrator consent for Application permissions |
| App Configuration | Azure centralized service for parameter and feature flag management |
| Application Permission | Permission without users (daemon apps) — requires admin consent |
| Authorization Code Flow | Main OAuth 2.0 flow for interactive apps with users |
| Client Credentials Flow | OAuth 2.0 flow for service-to-service without users |
| Confidential Client | Application that can securely store a secret (Web App, API) |
| DefaultAzureCredential | .NET class that tries multiple auth methods in order |
| Delegated Permission | Permission on behalf of a logged-in user |
| Device Code Flow | Flow for devices without browsers (IoT, CLI) |
| Feature Flag | Feature toggle managed in App Configuration |
| ID Token | JWT containing user identity claims (OpenID Connect) |
| Key Vault Reference | Reference to a Key Vault secret stored in App Configuration |
| Managed Identity | Service principal without credentials to manage, provided by Azure |
| MSAL | Microsoft Authentication Library — official library for Entra ID auth |
| On-Behalf-Of Flow | OAuth flow for API calling other APIs on behalf of a user |
| PKCE | Proof Key for Code Exchange — secure extension of Authorization Code Flow |
| Public Client | Application that cannot store a secret (mobile, desktop, SPA) |
| Refresh Token | Long-lived token to obtain new Access Tokens |
| Scope | Specific permission requested by an application |
| Service SAS | SAS for a specific Azure Storage service, signed with account key |
| Stored Access Policy | Centralized policy for SAS, enabling group revocation |
| System-Assigned MI | Managed Identity tied to a specific Azure resource (deleted with it) |
| User-Assigned MI | Independent Managed Identity, shareable between multiple resources |
| User Delegation SAS | SAS for Blob Storage based on Entra ID (not the account key) |
Search Terms
az-204 · securing · applications · identities · azure · developer · microsoft · access · app · sas · configuration · identity · managed · authentication · flow · msal · questions · security · types · user · .net · 2.0 · client · exam