Microsoft.Identity.Web 提供自訂驗證

Microsoft。Identity.Web 為與 Microsoft Entra ID 整合的 ASP.NET Core 應用程式提供安全的認證與授權預設值。 你可以自訂許多認證行為的面向,同時保留函式庫內建的安全功能。

識別可自訂區域

適用範圍 自訂選項
Configuration 所有 MicrosoftIdentityOptions、 OpenIdConnectOptions、 JwtBearerOptions 屬性
活動 OpenID Connect 事件(OnTokenValidated、 OnRedirectToIdentityProvider等)
代幣獲取 相關性 ID,額外查詢參數
宣告 新增自訂宣告至 ClaimsPrincipal
UI(使用者介面) 登出頁面、重定向行為
登入 登入提示、網域提示

選擇客製化方式

下表總結了你可以自訂的區域以及每個區域所支援的內容。

可採用兩種方式之一來自訂選項:

  1. Configure<TOptions> - 在選項使用前設定
  2. PostConfigure<TOptions> - 在所有 Configure 呼叫完成後設定選項

執行順序:

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

設定認證選項

本節說明如何配置 Microsoft.Identity.Web 使用的各種認證選項類別。

理解配置映射

"AzureAd"區段在appsettings.json映射到多個類別:

你可以在配置中使用這些類別中的任何屬性。

模式一:配置 MicrosoftIdentityOptions

以下程式碼可 MicrosoftIdentityOptions 自訂啟用個人識別資訊(PII)記錄、設定用戶端功能及調整令牌驗證參數:

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();

模式二:配置 OpenIdConnectOptions(網頁應用程式)

以下程式碼可 OpenIdConnectOptions 自訂網頁應用程式,以設定回應類型、新增範圍,以及配置 Cookie 與令牌驗證設定:

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;
});

模式 3:配置 JwtBearerOptions(網頁 API)

以下程式碼可 JwtBearerOptions 自訂網路 API 以設定有效受眾、聲明映射及代幣存續驗證:

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
});

以下程式碼配置您的應用程式的 Cookie 政策與 Cookie 認證選項,包括安全設定與過期行為:

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;
});

自訂事件處理程式

OpenID Connect 和 JWT 持有者認證會公開一些事件,讓你可以連接和使用。 Microsoft。Identity.Web 會自行設定事件處理程序,因此你必須將自訂處理程序與現有的串接,以保留內建功能。

保留現有的處理者

當你新增自訂事件處理器時,務必先儲存並呼叫現有的處理器。 以下範例展示了錯誤與正確的方法。

以下程式碼 不正確地 覆蓋 Microsoft.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;
    };
});

以下 程式碼正確 地與現有處理器串接:

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"));
    };
});

應用常見事件情境

憑證驗證後新增自訂聲明

以下程式碼在令牌驗證之後的網路 API 中新增自訂聲明 ClaimsPrincipal。 它會從資料庫查詢使用者的部門,並根據電子郵件網域指派應用程式特定的角色:

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"));
        }
    };
});

以下程式碼透過呼叫 Microsoft Graph,在令牌驗證後取得更多使用者資料,在網頁應用程式中新增自訂權利要求:

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 ?? ""));
    };
});

在授權請求中新增查詢參數

以下程式碼為發送給 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"];
        }
    };
});

自訂認證失敗處理

以下程式碼透過記錄錯誤並回傳自訂的 JSON 錯誤回應來處理認證失敗:

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
    };
});

處理存取拒絕

以下程式碼會在用戶拒絕同意時,將重新導向至自訂頁面:

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;
    };
});

自訂代幣取得方式

你可以透過將選項傳遞給 IDownstreamApi,來自訂呼叫下游 API 時如何取得令牌。

使用 IDownstreamApi 並自訂選項

以下程式碼在透過 IDownstreamApi 取得令牌時會傳遞關聯 ID 及額外查詢參數:

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);
    }
}

自訂 UI

你可以控制用戶登入與登出後的停留地點,並自訂登出體驗。

登入後會重新導向到特定頁面

請使用 redirectUri 參數將使用者在登入後導向特定頁面:

<!-- 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"
    });
}

自訂登出頁面

選項一:覆寫剃刀頁面

建立一個包含自訂內容的檔案:Areas/MicrosoftIdentity/Pages/Account/SignedOut.cshtml

@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>

選項二:重定向至自訂頁面

以下程式碼將使用者重新導向至自訂登出頁面,而非預設頁面:

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

自訂登入體驗

使用登入提示和網域提示

透過預先填入使用者名稱並引導使用者到特定的 Microsoft Entra 租戶,簡化登入體驗。

理解提示

Hint Purpose 範例
登入提示 預先填入使用者名稱/電子郵件欄位 "user@contoso.com"
domainHint 直接前往特定租戶登入頁面 "contoso.com"

套用提示模式

模式一:控制器式

以下程式碼顯示標準登入、帶登入提示的登入、網域提示或兩者皆有的控制器操作:

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"
        });
    }
}

模式二:基於視圖

以下 HTML 顯示了具有不同提示配置的登入連結:

<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>

模式三:程式設計與 OnRedirectToIdentityProvider

以下程式碼在重定向至身份提供者時,根據查詢參數與 Cookie 動態設定提示:

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;
        }
    };
});

應用案例

電子商務平台:

// Pre-fill returning customer email
loginHint = customerEmail

B2B 申請:

// Direct to customer's tenant
domainHint = customerDomain

多租戶 SaaS:

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

遵循最佳做法

應該做的事

1. 務必保留現有的事件處理程序。 在執行自訂邏輯前,先儲存並呼叫現有的處理器:

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

2. 使用相關識別碼進行追蹤。 將關聯 ID 附加到診斷用的令牌取得請求中。

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

3. 驗證自定義宣告。 在授權存取前,請確認自訂權利要求是否包含預期值:

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

4. 日誌自訂錯誤。 將自訂邏輯包裝在 try-catch 區塊中,並記錄錯誤:

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

5. 測試成功與失敗的路徑。 在測試中涵蓋所有認證情境:

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

禁忌事项

1。別跳過 Microsoft。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. 不要在生產環境中啟用個人識別資訊(PII)記錄:

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

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

3. 不要繞過代幣驗證:

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

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

4. 不要硬編碼敏感值:

//  Wrong
options.ClientSecret = "mysecret123";

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

5. 不要修改中介軟體中的認證:

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

排除常見問題

解決自訂功能未生效

檢查執行順序:

  1. AddMicrosoftIdentityWebApp / AddMicrosoftIdentityWebApi 設定預設值
  2. 你的 Configure 通話進行中
  3. PostConfigure 通話持續(如有)
  4. 使用選項

解決方案:如果PostConfigure呼叫沒有生效,請使用Configure,因為PostConfigure是在所有Configure呼叫之後執行的。

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

修正缺少的自訂聲明

如果沒有出現自訂聲明,請確認以下事項:

  1. 該OnTokenValidated處理器與現有處理器正確串接。
  2. 認證成功後,你的程式碼才會新增聲明。
  3. 聲明會新增到正確的 ClaimsIdentity。

以下程式碼會紀錄所有用於除錯的宣告:

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

修正事件未觸發

如果事件沒有觸發,請確認認證和授權中介件的註冊順序是否正確:

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