Sesuaikan autentikasi dengan Microsoft. Identity.Web

Microsoft. Identity.Web menyediakan default aman untuk autentikasi dan otorisasi dalam aplikasi ASP.NET Core yang terintegrasi dengan Microsoft Entra ID. Anda dapat menyesuaikan banyak aspek perilaku autentikasi sambil mempertahankan fitur keamanan bawaan pustaka.

Mengidentifikasi area yang dapat disesuaikan

Wilayah Opsi Kustomisasi
Configuration Semua MicrosoftIdentityOptions, OpenIdConnectOptions, JwtBearerOptions properti
Peristiwa Peristiwa OpenID Connect (OnTokenValidated, OnRedirectToIdentityProvider, dll.)
Akuisisi Token ID korelasi, parameter kueri tambahan
Klaim Menambahkan klaim kustom ke ClaimsPrincipal
UI Halaman keluar akun, perilaku pengalihan
Masuk Petunjuk masuk, petunjuk domain

Pilih metode kustomisasi

Tabel berikut ini meringkas area yang bisa Anda kustomisasi dan apa yang didukung setiap area.

Gunakan salah satu dari dua pendekatan untuk menyesuaikan opsi:

  1. Configure<TOptions> - Mengonfigurasi opsi sebelum digunakan
  2. PostConfigure<TOptions> - Mengonfigurasi opsi setelah semua panggilan Configure

Urutan eksekusi:

Configure → Configure → ... → PostConfigure → PostConfigure → ... → Options used

Konfigurasi pilihan autentikasi

Bagian ini memperlihatkan cara mengonfigurasi berbagai kelas opsi autentikasi yang Microsoft. Identity.Web menggunakan.

Memahami pemetaan konfigurasi

Bagian "AzureAd" dalam appsettings.json memetakan ke beberapa kelas:

Anda dapat menggunakan properti apa pun dari kelas ini dalam konfigurasi Anda.

Pola 1: Mengonfigurasi MicrosoftIdentityOptions

Kode berikut menyesuaikan MicrosoftIdentityOptions untuk mengaktifkan pengelogan PII, mengatur kemampuan klien, dan menyesuaikan parameter validasi token:

using Microsoft.Identity.Web;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));

// Customize Microsoft Identity options
builder.Services.Configure<MicrosoftIdentityOptions>(options =>
{
    // Enable PII logging (development only!)
    options.EnablePiiLogging = true;

    // Custom client capabilities
    options.ClientCapabilities = new[] { "CP1", "CP2" };

    // Override token validation parameters
    options.TokenValidationParameters.ValidateLifetime = true;
    options.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(5);
});

var app = builder.Build();

Pola 2: Mengonfigurasi OpenIdConnectOptions (Aplikasi web)

Kode berikut menyesuaikan OpenIdConnectOptions aplikasi web untuk mengatur jenis respons, menambahkan cakupan, dan mengonfigurasi pengaturan validasi cookie dan token:

builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));

// Customize OpenIdConnect options
builder.Services.Configure<OpenIdConnectOptions>(
    OpenIdConnectDefaults.AuthenticationScheme,
    options =>
{
    // Override response type
    options.ResponseType = "code id_token";

    // Add extra scopes
    options.Scope.Add("offline_access");
    options.Scope.Add("profile");

    // Customize token validation
    options.TokenValidationParameters.NameClaimType = "preferred_username";
    options.TokenValidationParameters.RoleClaimType = "roles";

    // Set redirect URI
    options.CallbackPath = "/signin-oidc";

    // Configure cookie options
    options.Cookie.HttpOnly = true;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    options.Cookie.SameSite = SameSiteMode.Lax;
});

Pola 3: Mengonfigurasi JwtBearerOptions (API Web)

Kode berikut menyesuaikan JwtBearerOptions API web untuk mengatur audiens, pemetaan klaim, dan validasi masa pakai token yang valid:

using Microsoft.AspNetCore.Authentication.JwtBearer;

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));

// Customize JWT Bearer options
builder.Services.Configure<JwtBearerOptions>(
    JwtBearerDefaults.AuthenticationScheme,
    options =>
{
    // Customize audience validation
    options.TokenValidationParameters.ValidAudiences = new[]
    {
        "api://your-api-client-id",
        "https://your-api.com"
    };

    // Set custom claim mappings
    options.TokenValidationParameters.NameClaimType = "name";
    options.TokenValidationParameters.RoleClaimType = "roles";

    // Customize token validation
    options.TokenValidationParameters.ValidateLifetime = true;
    options.TokenValidationParameters.ClockSkew = TimeSpan.Zero; // No tolerance
});

Kode berikut mengonfigurasi kebijakan cookie dan opsi autentikasi cookie untuk aplikasi Anda, termasuk pengaturan keamanan dan perilaku kedaluwarsa:

using Microsoft.AspNetCore.Authentication.Cookies;

// Configure cookie policy
builder.Services.Configure<CookiePolicyOptions>(options =>
{
    options.MinimumSameSitePolicy = SameSiteMode.Lax;
    options.Secure = CookieSecurePolicy.Always;
    options.HttpOnly = Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy.Always;
});

// Configure cookie authentication options
builder.Services.Configure<CookieAuthenticationOptions>(
    CookieAuthenticationDefaults.AuthenticationScheme,
    options =>
{
    options.Cookie.Name = "MyApp.Auth";
    options.Cookie.HttpOnly = true;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    options.Cookie.SameSite = SameSiteMode.Lax;
    options.ExpireTimeSpan = TimeSpan.FromHours(1);
    options.SlidingExpiration = true;
});

Mengkustomisasi penanganan aktivitas

Autentikasi OpenID Connect dan JWT Bearer mengekspos peristiwa yang dapat dihubungkan. Microsoft. Identity.Web menyiapkan penanganan aktivitasnya sendiri, jadi Anda harus menautkan handler kustom Anda dengan yang sudah ada untuk mempertahankan fungsionalitas bawaan.

Pertahankan handler yang ada

Saat Anda menambahkan penanganan aktivitas kustom, selalu simpan dan panggil handler yang ada terlebih dahulu. Contoh berikut menunjukkan pendekatan yang salah dan benar.

Kode berikut salah menimpa Microsoft. Handler Identity.Web:

services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
    options.Events.OnTokenValidated = async context =>
    {
        // Your code - but you LOST the built-in validation!
        await Task.CompletedTask;
    };
});

Kode berikut ditautkan dengan benar dengan handler yang ada:

services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
    var existingOnTokenValidatedHandler = options.Events.OnTokenValidated;

    options.Events.OnTokenValidated = async context =>
    {
        // Call Microsoft.Identity.Web's handler FIRST
        await existingOnTokenValidatedHandler(context);

        // Then your custom code
        // (executes AFTER built-in security checks)
        var identity = context.Principal.Identity as ClaimsIdentity;
        identity?.AddClaim(new Claim("custom_claim", "custom_value"));
    };
});

Menerapkan skenario peristiwa umum

Menambahkan klaim kustom setelah validasi token

Kode berikut menambahkan klaim kustom ke ClaimsPrincipal setelah validasi token dalam API web. Ini mencari departemen pengguna dari database dan menetapkan peran khusus aplikasi berdasarkan domain email:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using System.Security.Claims;

builder.Services.Configure<JwtBearerOptions>(
    JwtBearerDefaults.AuthenticationScheme,
    options =>
{
    var existingHandler = options.Events.OnTokenValidated;

    options.Events.OnTokenValidated = async context =>
    {
        // Preserve built-in validation
        await existingHandler(context);

        // Add custom claims
        var identity = context.Principal.Identity as ClaimsIdentity;

        // Example: Add department claim from database
        var userObjectId = context.Principal.FindFirst("oid")?.Value;
        if (!string.IsNullOrEmpty(userObjectId))
        {
            var department = await GetUserDepartment(userObjectId);
            identity?.AddClaim(new Claim("department", department));
        }

        // Example: Add application-specific role
        var email = context.Principal.FindFirst("email")?.Value;
        if (email?.EndsWith("@admin.com") == true)
        {
            identity?.AddClaim(new Claim(ClaimTypes.Role, "SuperAdmin"));
        }
    };
});

Kode berikut menambahkan klaim kustom di aplikasi web dengan memanggil Microsoft Graph untuk mengambil data profil pengguna tambahan setelah validasi token:

using Microsoft.AspNetCore.Authentication.OpenIdConnect;

builder.Services.Configure<OpenIdConnectOptions>(
    OpenIdConnectDefaults.AuthenticationScheme,
    options =>
{
    var existingHandler = options.Events.OnTokenValidated;

    options.Events.OnTokenValidated = async context =>
    {
        // Preserve built-in processing
        await existingHandler(context);

        // Call Microsoft Graph to get additional user data
        var graphClient = context.HttpContext.RequestServices
            .GetRequiredService<GraphServiceClient>();

        var user = await graphClient.Me.GetAsync();

        var identity = context.Principal.Identity as ClaimsIdentity;
        identity?.AddClaim(new Claim("jobTitle", user?.JobTitle ?? ""));
        identity?.AddClaim(new Claim("department", user?.Department ?? ""));
    };
});

Menambahkan parameter kueri ke permintaan otorisasi

Kode berikut menambahkan parameter kueri kustom ke permintaan otorisasi yang dikirim ke penyedia identitas Microsoft Entra:

builder.Services.Configure<OpenIdConnectOptions>(
    OpenIdConnectDefaults.AuthenticationScheme,
    options =>
{
    var existingHandler = options.Events.OnRedirectToIdentityProvider;

    options.Events.OnRedirectToIdentityProvider = async context =>
    {
        // Preserve existing behavior
        if (existingHandler != null)
        {
            await existingHandler(context);
        }

        // Add custom query parameters
        context.ProtocolMessage.Parameters.Add("slice", "testslice");
        context.ProtocolMessage.Parameters.Add("custom_param", "custom_value");

        // Conditional parameters based on request
        if (context.HttpContext.Request.Query.ContainsKey("prompt"))
        {
            context.ProtocolMessage.Prompt = context.HttpContext.Request.Query["prompt"];
        }
    };
});

Menyesuaikan penanganan kegagalan autentikasi

Kode berikut menangani kegagalan autentikasi dengan mencatat kesalahan dan mengembalikan respons kesalahan JSON kustom:

builder.Services.Configure<OpenIdConnectOptions>(
    OpenIdConnectDefaults.AuthenticationScheme,
    options =>
{
    options.Events.OnAuthenticationFailed = async context =>
    {
        // Log the error
        var logger = context.HttpContext.RequestServices
            .GetRequiredService<ILogger<Program>>();
        logger.LogError(context.Exception, "Authentication failed");

        // Customize error response
        context.Response.StatusCode = 401;
        context.Response.ContentType = "application/json";
        await context.Response.WriteAsync($$"""
            {
                "error": "authentication_failed",
                "error_description": "{{context.Exception.Message}}"
            }
            """);

        context.HandleResponse(); // Suppress default error handling
    };
});

Menangani akses ditolak

Kode berikut mengalihkan pengguna ke halaman kustom saat mereka menolak persetujuan:

builder.Services.Configure<OpenIdConnectOptions>(
    OpenIdConnectDefaults.AuthenticationScheme,
    options =>
{
    options.Events.OnAccessDenied = async context =>
    {
        // User denied consent
        context.Response.Redirect("/Home/AccessDenied");
        context.HandleResponse();
        await Task.CompletedTask;
    };
});

Menyesuaikan akuisisi token

Anda dapat menyesuaikan bagaimana token diperoleh saat memanggil API hilir dengan meneruskan opsi ke IDownstreamApi.

Menggunakan IDownstreamApi dengan opsi kustom

Kode berikut meneruskan ID korelasi dan parameter kueri tambahan saat memperoleh token melalui IDownstreamApi:

using Microsoft.Identity.Abstractions;

public class TodoListController : ControllerBase
{
    private readonly IDownstreamApi _downstreamApi;

    public TodoListController(IDownstreamApi downstreamApi)
    {
        _downstreamApi = downstreamApi;
    }

    [HttpGet("{id}")]
    public async Task<ActionResult> GetTodo(int id, Guid correlationId)
    {
        var result = await _downstreamApi.GetForUserAsync<Todo>(
            "TodoListService",
            options =>
            {
                options.RelativePath = $"api/todolist/{id}";

                // Customize token acquisition
                options.TokenAcquisitionOptions = new TokenAcquisitionOptions
                {
                    CorrelationId = correlationId,
                    ExtraQueryParameters = new Dictionary<string, string>
                    {
                        { "slice", "test_slice" }
                    }
                };
            });

        return Ok(result);
    }
}

Menyesuaikan UI

Anda dapat mengontrol di mana pengguna mendarat setelah masuk dan keluar, serta menyesuaikan pengalaman setelah keluar.

Mengalihkan ke halaman tertentu setelah masuk

redirectUri Gunakan parameter untuk mengirim pengguna ke halaman tertentu setelah mereka masuk:

<!-- Razor view -->
<a href="/MicrosoftIdentity/Account/SignIn?redirectUri=/Dashboard">Sign In</a>

<!-- Or in controller -->
[HttpGet]
public IActionResult SignInToDashboard()
{
    return RedirectToAction("SignIn", "Account", new
    {
        area = "MicrosoftIdentity",
        redirectUri = "/Dashboard"
    });
}

Mengkustomisasi halaman setelah keluar

Opsi 1: Ambil alih Halaman Razor

Buat file di Areas/MicrosoftIdentity/Pages/Account/SignedOut.cshtml dengan konten kustom Anda:

@page
@model Microsoft.Identity.Web.UI.Areas.MicrosoftIdentity.Pages.Account.SignedOutModel
@{
    ViewData["Title"] = "Signed out";
}

<div class="container text-center mt-5">
    <h1>You have been signed out</h1>
    <p>Thank you for using our application.</p>
    <a asp-area="" asp-controller="Home" asp-action="Index" class="btn btn-primary">
        Return to Home
    </a>
</div>

Opsi 2: Mengalihkan ke halaman kustom

Kode berikut mengalihkan pengguna ke halaman keluar kustom alih-alih default:

builder.Services.Configure<OpenIdConnectOptions>(
    OpenIdConnectDefaults.AuthenticationScheme,
    options =>
{
    options.Events.OnSignedOutCallbackRedirect = context =>
    {
        context.Response.Redirect("/Home/SignedOut");
        context.HandleResponse();
        return Task.CompletedTask;
    };
});

Mengkustomisasi pengalaman masuk

Menggunakan petunjuk masuk dan petunjuk domain

Sederhanakan pengalaman masuk dengan mengisi nama pengguna sebelumnya dan mengarahkan pengguna ke penyewa Microsoft Entra tertentu.

Memahami petunjuk

Petunjuk Kegunaan Example
loginHint Bidang nama pengguna/email diisi sebelumnya "user@contoso.com"
domainHint Langsung ke halaman masuk penyewa tertentu "contoso.com"

Menerapkan pola petunjuk

Pola 1: Berbasis pengontrol

Kode berikut menunjukkan tindakan pengontrol untuk proses masuk standar, masuk dengan petunjuk login, petunjuk domain, atau keduanya:

using Microsoft.AspNetCore.Mvc;

public class AuthController : Controller
{
    [HttpGet]
    public IActionResult SignIn()
    {
        // Standard sign-in
        return RedirectToAction("SignIn", "Account", new
        {
            area = "MicrosoftIdentity",
            redirectUri = "/Dashboard"
        });
    }

    [HttpGet]
    public IActionResult SignInWithLoginHint()
    {
        // Pre-populate username
        return RedirectToAction("SignIn", "Account", new
        {
            area = "MicrosoftIdentity",
            redirectUri = "/Dashboard",
            loginHint = "user@contoso.com"
        });
    }

    [HttpGet]
    public IActionResult SignInWithDomainHint()
    {
        // Direct to Contoso tenant
        return RedirectToAction("SignIn", "Account", new
        {
            area = "MicrosoftIdentity",
            redirectUri = "/Dashboard",
            domainHint = "contoso.com"
        });
    }

    [HttpGet]
    public IActionResult SignInWithBothHints()
    {
        // Pre-populate AND direct to tenant
        return RedirectToAction("SignIn", "Account", new
        {
            area = "MicrosoftIdentity",
            redirectUri = "/Dashboard",
            loginHint = "user@contoso.com",
            domainHint = "contoso.com"
        });
    }
}

Pola 2: Berbasis tampilan

HTML berikut menunjukkan tautan masuk dengan konfigurasi petunjuk yang berbeda:

<div class="sign-in-options">
    <h2>Sign In Options</h2>

    <!-- Standard sign-in -->
    <a href="/MicrosoftIdentity/Account/SignIn?redirectUri=/Dashboard"
       class="btn btn-primary">
        Sign In
    </a>

    <!-- With login hint -->
    <a href="/MicrosoftIdentity/Account/SignIn?redirectUri=/Dashboard&loginHint=user@contoso.com"
       class="btn btn-secondary">
        Sign In as user@contoso.com
    </a>

    <!-- With domain hint -->
    <a href="/MicrosoftIdentity/Account/SignIn?redirectUri=/Dashboard&domainHint=contoso.com"
       class="btn btn-secondary">
        Sign In (Contoso)
    </a>
</div>

Pola 3: Pemrograman dengan OnRedirectToIdentityProvider

Kode berikut secara dinamis menetapkan petunjuk berdasarkan parameter kueri dan cookie selama pengalihan ke penyedia identitas:

builder.Services.Configure<OpenIdConnectOptions>(
    OpenIdConnectDefaults.AuthenticationScheme,
    options =>
{
    var existingHandler = options.Events.OnRedirectToIdentityProvider;

    options.Events.OnRedirectToIdentityProvider = async context =>
    {
        if (existingHandler != null)
        {
            await existingHandler(context);
        }

        // Add hints based on application logic
        if (context.HttpContext.Request.Query.TryGetValue("tenant", out var tenant))
        {
            context.ProtocolMessage.DomainHint = tenant;
        }

        // Get suggested user from cookie or session
        var suggestedUser = context.HttpContext.Request.Cookies["LastSignedInUser"];
        if (!string.IsNullOrEmpty(suggestedUser))
        {
            context.ProtocolMessage.LoginHint = suggestedUser;
        }
    };
});

Skenario penggunaan

Platform E-niaga:

// Pre-fill returning customer email
loginHint = customerEmail

Aplikasi B2B:

// Direct to customer's tenant
domainHint = customerDomain

Multi-Tenant SaaS:

// Route based on subdomain
domainHint = GetTenantFromSubdomain(Request.Host)

Ikuti praktik terbaik

Yang Harus Dilakukan

1. Selalu pertahankan penanganan aktivitas yang ada. Simpan dan panggil handler yang ada sebelum menjalankan logika kustom Anda:

var existingHandler = options.Events.OnTokenValidated;
options.Events.OnTokenValidated = async context =>
{
    await existingHandler(context); // Call Microsoft.Identity.Web's handler
    // Your custom code
};

2. Gunakan ID korelasi untuk pelacakan. Lampirkan ID korelasi ke permintaan akuisisi token untuk diagnostik:

var tokenOptions = new TokenAcquisitionOptions
{
    CorrelationId = Activity.Current?.Id ?? Guid.NewGuid()
};

3. Memvalidasi klaim khusus. Verifikasi bahwa klaim kustom berisi nilai yang diharapkan sebelum memberikan akses:

var department = context.Principal.FindFirst("department")?.Value;
if (!IsValidDepartment(department))
{
    throw new UnauthorizedAccessException("Invalid department");
}

Kesalahan kustomisasi log. Bungkus logika kustom dalam blok try-catch dan catat kesalahan.

try
{
    // Custom logic
}
catch (Exception ex)
{
    logger.LogError(ex, "Custom authentication logic failed");
    throw;
}

5. Uji jalur keberhasilan dan kegagalan. Mencakup semua skenario autentikasi dalam pengujian Anda:

// Test with valid tokens
// Test with missing claims
// Test with expired tokens
// Test with wrong audience

Hal-hal yang Tidak Boleh Dilakukan

1. Jangan lewati Microsoft. Penanganan aktivitas Identity.Web:

//  Wrong - loses built-in security checks
options.Events.OnTokenValidated = async context => { /* your code */ };

//  Correct - preserves security
var existing = options.Events.OnTokenValidated;
options.Events.OnTokenValidated = async context =>
{
    await existing(context);
    /* your code */
};

2. Jangan aktifkan pencatatan log PII dalam produksi:

//  Wrong
options.EnablePiiLogging = true; // In production!

//  Correct
if (builder.Environment.IsDevelopment())
{
    options.EnablePiiLogging = true;
}

3. Jangan melewati validasi token:

//  Wrong - insecure!
options.TokenValidationParameters.ValidateLifetime = false;
options.TokenValidationParameters.ValidateAudience = false;

//  Correct - maintain security
options.TokenValidationParameters.ValidateLifetime = true;
options.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(5);

4. Jangan hardcode nilai sensitif:

//  Wrong
options.ClientSecret = "mysecret123";

//  Correct
options.ClientSecret = builder.Configuration["AzureAd:ClientSecret"];

5. Jangan ubah autentikasi di middleware:

//  Wrong - configure in Startup, not middleware
app.Use(async (context, next) =>
{
    // Modifying auth options here is too late!
});

Pemecahan Masalah Umum

Mengatasi permasalahan penyesuaian tidak diterapkan

Periksa urutan eksekusi:

  1. AddMicrosoftIdentityWebApp / AddMicrosoftIdentityWebApi mengatur default
  2. Panggilan Anda Configure sedang berlangsung
  3. PostConfigure panggilan berjalan (jika ada)
  4. Opsi digunakan

Solusi: Gunakan PostConfigure jika panggilan Anda Configure tidak berlaku, karena PostConfigure berjalan setelah semua Configure panggilan:

services.PostConfigure<OpenIdConnectOptions>(
    OpenIdConnectDefaults.AuthenticationScheme,
    options => { /* your changes */ }
);

Memperbaiki klaim kustom yang hilang

Verifikasi hal berikut jika klaim kustom tidak muncul:

  1. Handler OnTokenValidated ditautkan dengan benar dengan handler yang ada.
  2. Autentikasi berhasil sebelum kode Anda menambahkan klaim.
  3. Klaim ditambahkan ke ClaimsIdentity yang benar.

Kode berikut merekam semua klaim untuk pendebugan.

var claims = context.Principal.Claims.ToList();
logger.LogInformation($"Claims count: {claims.Count}");
foreach (var claim in claims)
{
    logger.LogInformation($"{claim.Type}: {claim.Value}");
}

Memperbaiki peristiwa yang tidak diaktifkan

Jika peristiwa tidak diaktifkan, verifikasi bahwa middleware autentikasi dan otorisasi terdaftar dalam urutan yang benar:

app.UseAuthentication(); // Must be first
app.UseAuthorization();  // Must be second
app.MapControllers();    // Then endpoints