Microsoft.Identity.Web 提供自訂驗證

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

識別可自訂區域

適用範圍 自訂選項
Configuration 所有 MicrosoftIdentityOptionsOpenIdConnectOptionsJwtBearerOptions 屬性
活動 OpenID Connect 事件(OnTokenValidatedOnRedirectToIdentityProvider等)
代幣獲取 相關性 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