Microsoft. Identity.Web은 Microsoft Entra ID 통합하는 ASP.NET Core 애플리케이션에서 인증 및 권한 부여에 대한 보안 기본값을 제공합니다. 라이브러리의 기본 제공 보안 기능을 유지하면서 인증 동작의 여러 측면을 사용자 지정할 수 있습니다.
사용자 지정 가능한 영역 식별
| 영역 | 사용자 지정 옵션 |
|---|---|
| Configuration | 모든 MicrosoftIdentityOptions, OpenIdConnectOptions, JwtBearerOptions 속성 |
| Events | OpenID Connect 이벤트(OnTokenValidated등 OnRedirectToIdentityProvider) |
| 토큰 획득 | 상관 관계 ID, 추가 쿼리 매개 변수 |
| 클레임 | 에 사용자 지정 클레임 추가 ClaimsPrincipal |
| UI (사용자 인터페이스) | 로그아웃 페이지, 리디렉션 동작 |
| 로그인 | 로그인 힌트, 도메인 힌트 |
사용자 지정 방법 선택
다음 표에는 사용자 지정할 수 있는 영역과 각 영역이 지원하는 항목이 요약되어 있습니다.
다음 두 가지 방법 중 하나를 사용하여 옵션을 사용자 지정합니다.
-
Configure<TOptions>- 옵션을 사용하기 전에 구성합니다. -
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
});
패턴 4: 쿠키 옵션 구성
다음 코드는 보안 설정 및 만료 동작을 포함하여 앱에 대한 쿠키 정책 및 쿠키 인증 옵션을 구성합니다.
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!
});
일반적인 문제 해결
사용자 지정이 적용되지 않는 문제 해결
실행 순서 확인:
-
AddMicrosoftIdentityWebApp/AddMicrosoftIdentityWebApi기본값 설정 -
Configure통화가 정상적으로 작동합니다 -
PostConfigure호출 실행(있는 경우) - 옵션이 사용됩니다.
솔루션: 모든 Configure 호출 후에 PostConfigure가 실행되므로, Configure 호출이 적용되지 않는 경우에는 PostConfigure를 사용하십시오.
services.PostConfigure<OpenIdConnectOptions>(
OpenIdConnectDefaults.AuthenticationScheme,
options => { /* your changes */ }
);
누락된 사용자 지정 클레임 수정
사용자 지정 클레임이 표시되지 않는 경우 다음을 확인합니다.
-
OnTokenValidated처리기가 기존 처리기와 올바르게 연결됩니다. - 코드에서 클레임을 추가하기 전에 인증이 성공합니다.
- 클레임이 올바른
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