Microsoft.Identity.Web で認証をカスタマイズする

Microsoft.Identity.Web は、Microsoft Entra ID と統合された ASP.NET Core アプリケーションにおいて、認証および承認のためのセキュリティ保護されたデフォルトを提供します。 ライブラリの組み込みのセキュリティ機能を維持しながら、認証動作のさまざまな側面をカスタマイズできます。

カスタマイズ可能な領域を特定する

面積 カスタマイズ オプション
Configuration すべての MicrosoftIdentityOptions、 OpenIdConnectOptions、 JwtBearerOptions プロパティ
イベント OpenID Connect イベント (OnTokenValidated、 OnRedirectToIdentityProviderなど)
トークンの取得 関連付け ID、追加のクエリ パラメーター
請求 カスタム要求を追加する ClaimsPrincipal
UI サインアウト ページ、リダイレクト動作
サインイン ログイン ヒント、ドメイン ヒント

カスタマイズ方法を選択する

次の表は、カスタマイズできる領域と、各領域でサポートされる内容をまとめたものです。

オプションをカスタマイズするには、次の 2 つの方法のいずれかを使用します。

  1. Configure<TOptions> - 使用する前にオプションを構成する
  2. PostConfigure<TOptions> - すべての Configure 呼び出しの後にオプションを構成します

実行順序:

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

認証オプションを構成する

このセクションでは、Microsoft.Identity.Webで使用されるさまざまな認証オプションクラスを設定する方法について説明します。

構成マッピングについて

"AzureAd"の appsettings.json セクションは、複数のクラスにマップされます。

これらのクラスの任意のプロパティを構成で使用できます。

パターン 1: MicrosoftIdentityOptions を構成する

次のコードでは、PII ログの有効化、クライアント機能の設定、トークン検証パラメーターの調整を行うために、 MicrosoftIdentityOptions をカスタマイズします。

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

パターン 2: OpenIdConnectOptions (Web アプリ) を構成する

次のコードでは、Web アプリの 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 (Web API) を構成する

次のコードでは、Web API の JwtBearerOptions をカスタマイズして、有効な対象ユーザー、要求マッピング、トークンの有効期間の検証を設定します。

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 Bearer 認証では、フックできるイベントが公開されます。 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"));
    };
});

一般的なイベント シナリオを適用する

トークン検証後にカスタム要求を追加する

次のコードでは、Web 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を呼び出して、Web アプリにカスタム要求を追加します。

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 ID プロバイダーに送信された承認要求にカスタム クエリ パラメーターを追加します。

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

サインアウトページをカスタマイズする

オプション 1: Razor ページをオーバーライドする

カスタム コンテンツを使用して 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>

オプション 2: カスタム ページにリダイレクトする

次のコードは、ユーザーを既定ではなくカスタムのサインアウト ページにリダイレクトします。

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 例
loginHint ユーザー名/電子メール フィールドを事前設定する "user@contoso.com"
domainHint 特定のテナント ログイン ページに直接移動する "contoso.com"

ヒント パターンを適用する

パターン 1: コントローラー ベース

次のコードは、標準サインイン、ログイン ヒント、ドメイン ヒント、またはその両方を使用したサインインに対するコントローラー アクションを示しています。

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

パターン 2: ビュー ベース

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

パターン 3: OnRedirectToIdentityProvider を使用したプログラム

次のコードは、ID プロバイダーへのリダイレクト中にクエリ パラメーターと 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;
        }
    };
});

利用事例

Eコマース プラットフォーム:

// 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 を使用します。 診断用のトークン取得要求に関連付け 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