Intermediate AZ-204

AZ-204: Securing Applications and Identities

Authentication, the Microsoft identity platform, managed identities, SAS and App Configuration.

Certification: Azure Developer Associate (AZ-204) Services covered: Microsoft Identity Platform, MSAL, Azure Key Vault, Managed Identities, Azure App Configuration


Table of Contents

  1. User Authentication and Authorization in Azure
  2. Secret Management and Secure Access
  3. 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:

FlowUsageCode
Authorization Code + PKCEWeb apps, mobile appsAcquireTokenInteractive
Client CredentialsDaemon apps, services without usersAcquireTokenForClient
Device CodeDevices without browsers (IoT, CLI)AcquireTokenWithDeviceCode
ImplicitDeprecated — legacy SPA
On-behalf-ofAPI calling other APIsAcquireTokenOnBehalfOf
Resource Owner PasswordDiscouraged — tests onlyAcquireTokenByUsernamePassword

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:

TypeContextConsentUsage
DelegatedWith a logged-in userUser + Admin (if required)Web apps, mobile
ApplicationWithout user (daemon)Admin requiredBackground 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 version
  • https://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:

TypeIdentityRevocableSecurity
Service SASStorage account keyVia Stored Access PolicyModerate
Account SASStorage account keyNo (except key rotation)Low
User Delegation SASEntra IDVia 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:

ParameterDescription
svService version
stStart time (optional)
seExpiry time
srResource scope (b=blob, c=container, s=service)
spPermissions (r=read, w=write, d=delete, l=list, c=create)
sprProtocol (https)
sigCryptographic signature
skoidObject 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:

RolePermissions
Key Vault AdministratorEverything (admin)
Key Vault Secrets OfficerCRUD on secrets
Key Vault Secrets UserRead secrets (recommended for apps)
Key Vault ReaderRead 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?

ApplicationRecommended flow
Web app with usersAuthorization Code + PKCE
API (daemon/service)Client Credentials
Mobile appAuthorization Code + PKCE
IoT / devices without browserDevice Code
API calling another APIOn-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

FlowUsageClient TypeExample
Authorization Code + PKCEWeb apps, SPAs, mobilePublic + ConfidentialReact SPA, .NET MVC
Client CredentialsServices without usersConfidentialDaemon, service worker
Device CodeDevices without browsersPublicIoT, CLI tools
On-Behalf-OfAPI calling another APIConfidentialMicroservices chain
ImplicitDeprecated (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.


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

TermDefinition
Access TokenShort-lived JWT token (1h) to access a protected API
Admin ConsentAdministrator consent for Application permissions
App ConfigurationAzure centralized service for parameter and feature flag management
Application PermissionPermission without users (daemon apps) — requires admin consent
Authorization Code FlowMain OAuth 2.0 flow for interactive apps with users
Client Credentials FlowOAuth 2.0 flow for service-to-service without users
Confidential ClientApplication that can securely store a secret (Web App, API)
DefaultAzureCredential.NET class that tries multiple auth methods in order
Delegated PermissionPermission on behalf of a logged-in user
Device Code FlowFlow for devices without browsers (IoT, CLI)
Feature FlagFeature toggle managed in App Configuration
ID TokenJWT containing user identity claims (OpenID Connect)
Key Vault ReferenceReference to a Key Vault secret stored in App Configuration
Managed IdentityService principal without credentials to manage, provided by Azure
MSALMicrosoft Authentication Library — official library for Entra ID auth
On-Behalf-Of FlowOAuth flow for API calling other APIs on behalf of a user
PKCEProof Key for Code Exchange — secure extension of Authorization Code Flow
Public ClientApplication that cannot store a secret (mobile, desktop, SPA)
Refresh TokenLong-lived token to obtain new Access Tokens
ScopeSpecific permission requested by an application
Service SASSAS for a specific Azure Storage service, signed with account key
Stored Access PolicyCentralized policy for SAS, enabling group revocation
System-Assigned MIManaged Identity tied to a specific Azure resource (deleted with it)
User-Assigned MIIndependent Managed Identity, shareable between multiple resources
User Delegation SASSAS 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

Interested in this course?

Contact us to book it or get a custom training plan for your team.