Microsoft.Identity.Web을 사용하여 인증을 커스터마이즈합니다.

Microsoft. Identity.Web은 Microsoft Entra ID 통합하는 ASP.NET Core 애플리케이션에서 인증 및 권한 부여에 대한 보안 기본값을 제공합니다. 라이브러리의 기본 제공 보안 기능을 유지하면서 인증 동작의 여러 측면을 사용자 지정할 수 있습니다.

사용자 지정 가능한 영역 식별

영역 사용자 지정 옵션
Configuration 모든 MicrosoftIdentityOptions, OpenIdConnectOptions, JwtBearerOptions 속성
Events 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 여러 클래스에 매핑됩니다.

구성에서 이러한 클래스의 모든 속성을 사용할 수 있습니다.

패턴 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 구성(웹앱)

다음 코드는 웹앱에 대해 사용자 지정 OpenIdConnectOptions 하여 응답 유형을 설정하고, 범위를 추가하고, 쿠키 및 토큰 유효성 검사 설정을 구성합니다.

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)

다음 코드는 유효한 대상 그룹, 클레임 매핑 및 토큰 수명 유효성 검사를 설정하도록 웹 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
});

다음 코드는 보안 설정 및 만료 동작을 포함하여 앱에 대한 쿠키 정책 및 쿠키 인증 옵션을 구성합니다.

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

토큰 획득 사용자 지정

옵션을 전달하여 다운스트림 API를 호출할 때 토큰을 획득하는 IDownstreamApi방법을 사용자 지정할 수 있습니다.

사용자 지정 옵션과 함께 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 테넌트에 사용자를 안내하여 로그인 환경을 간소화합니다.

힌트 이해

힌트 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 공급자로 리디렉션하는 동안 쿼리 매개 변수 및 쿠키를 기반으로 힌트를 동적으로 설정합니다.

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를 사용합니다. 진단에 대한 토큰 획득 요청에 상관 관계 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. 옵션이 사용됩니다.

솔루션: 모든 Configure 호출 후에 PostConfigure가 실행되므로, Configure 호출이 적용되지 않는 경우에는 PostConfigure를 사용하십시오.

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