Настройка проверки подлинности с помощью Microsoft. Identity.Web

Microsoft. Identity.Web предоставляет безопасные значения по умолчанию для проверки подлинности и авторизации в ASP.NET Core приложениях, которые интегрируются с Microsoft Entra ID. При сохранении встроенных функций безопасности библиотеки можно настроить множество аспектов поведения проверки подлинности.

Определение настраиваемых областей

Area Параметры настройки
Configuration Все MicrosoftIdentityOptions, OpenIdConnectOptions, JwtBearerOptions свойства
События События OpenID Connect (OnTokenValidated, OnRedirectToIdentityProviderи т. д.)
Получение токена Идентификаторы корреляции, дополнительные параметры запроса
Утверждения Добавление настраиваемых утверждений в ClaimsPrincipal
Пользовательский интерфейс Страницы выхода из системы, поведение при перенаправлении
Вход Подсказки для входа, указания домена

Выбор метода настройки

В следующей таблице перечислены области, которые можно настроить и какие области поддерживаются.

Используйте один из двух подходов для настройки параметров:

  1. Configure<TOptions> — настраивает параметры перед их применением
  2. PostConfigure<TOptions> — настраивает параметры после всех Configure вызовов

Порядок выполнения:

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

Настройка параметров проверки подлинности

В этом разделе показано, как настроить различные классы параметров проверки подлинности, которые Microsoft. Identity.Web используется.

Понимание сопоставления конфигурации

Раздел "AzureAd" в appsettings.json отображается на несколько классов.

Вы можете использовать любое свойство из этих классов в конфигурации.

Шаблон 1. Настройка 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();

Шаблон 2. Настройка 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 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"));
    };
});

Применение распространенных сценариев событий

Добавление пользовательских утверждений после проверки токена

Следующий код добавляет пользовательские утверждения в ClaimsPrincipal после проверки токена в веб-API. Он ищет отдел пользователя из базы данных и назначает роль для конкретного приложения на основе домена электронной почты:

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

Настройка получения токена

Вы можете настроить способ получения токенов при вызове API на более низком уровне, передав параметры в IDownstreamApi.

Использование IDownstreamApi с настраиваемыми параметрами

Следующий код передает идентификатор корреляции и дополнительные параметры запроса при получении токена через 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);
    }
}

Настройка пользовательского интерфейса

Вы можете контролировать, куда пользователи перенаправляются после входа и выхода, а также настроить опыт после выхода.

Перенаправление на определенную страницу после входа

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

Следующий код динамически задает указания на основе параметров запроса и файлов 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. Используйте идентификаторы корреляции для трассировки. Присоединение идентификатора корреляции к запросам на получение токенов для диагностики:

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