.NET Aspire 앱에 Microsoft Entra ID 인증 추가

이 가이드에서는 .NET AspireMicrosoft Entra ID 인증 및 권한 부여를 사용하여 분산 애플리케이션을 보호하는 방법을 보여줍니다. 다음을 다룹니다.

  1. Blazor Server 프런트 엔드 (MyService.Web): OpenID Connect 및 토큰 획득을 사용하여 사용자 로그인
  2. 보호된 API 백 엔드(MyService.ApiService): Microsoft.Identity.Web을 사용한 JWT 유효성 검사
  3. 엔드투엔드 흐름: Blazor는 액세스 토큰을 획득하고 Aspire 서비스 검색을 사용하여 보호된 API를 호출합니다.

이 가이드에서는 다음 명령을 사용하여 만든 Aspire 프로젝트를 시작했다고 가정합니다.

aspire new aspire-starter --name MyService

사전 요구 사항

  • .NET 9 SDK 이상
  • .NET Aspire CLI - Aspire CLI 설치을 참조하세요
  • Microsoft Entra 테넌트Microsoft Entra ID 앱 등록을 설정하려면 참조하세요.

팁 (조언)

Aspire에 처음 오셨나요? .NET Aspire 개요 참조하세요.

2단계 워크플로 이해

이 가이드는 2단계 접근 방식을 따릅니다.

단계 어떻게 되나요? 결과
1단계 자리 표시자 값을 사용하여 인증 코드 추가 앱 빌드하지만 실행되지 않음
2단계 Microsoft Entra 앱 등록 설정 실제 인증을 사용하여 앱 실행

Microsoft Entra ID 앱 등록

앱에서 사용자를 인증하려면 Microsoft Entra 두 개의 앱 등록이 필요합니다.

앱 등록 Purpose 핵심 구성
API (MyService.ApiService) 들어오는 토큰의 유효성을 검사합니다. 앱 ID URI, access_as_user 범위
웹앱 (MyService.Web) 사용자 로그인, 토큰 획득 리디렉션 URI, 클라이언트 암호, API 권한

앱 등록을 이미 구성한 경우 다음 값이 appsettings.json필요합니다.

  • TenantId - Microsoft Entra 테넌트 ID
  • API ClientId - API 앱 등록의 애플리케이션(클라이언트) ID
  • API 앱 ID URI - 일반적으로 api://<api-client-id> (AudiencesScopes에서 사용)
  • Web App ClientId - 웹앱 등록의 애플리케이션(클라이언트) ID
  • 클라이언트 암호 (또는 인증서) - 웹앱에 대한 자격 증명(appsettings.json아닌 사용자 비밀에 저장)
  • 범위 - 웹앱이 요청하는 범위(예 api://<api-client-id>/.default : api://<api-client-id>/access_as_user

1단계: API 등록

  1. Microsoft Entra 관리 센터>Identity>Applications>앱 등록으로 이동합니다.
  2. 새 등록선택합니다.
    • 이름: MyService.ApiService
    • 지원되는 계정 유형: 이 조직 디렉터리의 계정만(단일 테넌트)
    • 등록을 선택합니다.
  3. 애플리케이션 ID URI 옆에 있는 API 노출>로 이동하여 추가합니다.
    • 기본값(api://<client-id>)을 적용하거나 사용자 지정합니다.
    • 범위 추가를 선택합니다.
      • 범위 이름:access_as_user
      • 동의할 수 있는 사람: 관리자 및 사용자
      • 관리자 동의 표시 이름: MyService API 액세스
      • 관리자 동의 설명: 앱이 로그인한 사용자를 대신하여 MyService API에 액세스할 수 있도록 허용합니다.
      • 범위 추가를 선택합니다.
  4. 애플리케이션(클라이언트) ID를 복사합니다. 두 파일 모두 appsettings.json 에 필요합니다.

자세한 내용은 빠른 시작: 웹 API를 노출하도록 앱 구성을 참조하세요.

2단계: 웹앱 등록

  1. 앱 등록>새 등록로 이동합니다.
    • 이름: MyService.Web
    • 지원되는 계정 유형: 이 조직 디렉터리의 계정만
    • 리디렉션 URI: 을 선택하고 앱의 URL +를 입력합니다. /signin-oidc
      • 로컬 개발: 실제 포트를 확인하려면 https://localhost:7001/signin-oidc을(를) 확인하세요 launchSettings.json
    • 등록을 선택합니다.
  2. 인증>추가 URI로 이동하여 모든 개발 URL(원본launchSettings.json)을 추가합니다.
  3. 인증서 및 비밀>>로 이동합니다.
    • 설명 및 만료를 추가합니다.
    • 비밀 값을 즉시 복사하면 다시 표시되지 않습니다.
  4. API 권한으로 이동하여 내> 권한을 > 추가합니다.
    • MyService.ApiService를 선택합니다.
    • access_as_user 선택합니다>.
    • [테넌트]에 대한 관리자 동의 부여를 선택합니다(또는 사용자에게 처음 사용하라는 메시지가 표시됨).
  5. 웹앱의 애플리케이션(클라이언트) ID 를 복사합니다 appsettings.json.

메모

일부 조직에서는 클라이언트 비밀을 허용하지 않습니다. 대안은 인증서 자격 증명 또는 인증서 없는 인증을 참조하세요.

자세한 내용은 빠른 시작: 애플리케이션 등록을 참조하세요.

3단계: 구성 업데이트

앱 등록을 만든 후 appsettings.json 파일을 업데이트합니다.

API(MyService.ApiService/appsettings.json):

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "YOUR_TENANT_ID",
    "ClientId": "YOUR_API_CLIENT_ID",
    "Audiences": ["api://YOUR_API_CLIENT_ID"]
  }
}

웹앱(MyService.Web/appsettings.json):

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "YOUR_TENANT_ID",
    "ClientId": "YOUR_WEB_CLIENT_ID",
    "CallbackPath": "/signin-oidc",
    "ClientCredentials": [
      { "SourceType": "ClientSecret" }
    ]
  },
  "WeatherApi": {
    "Scopes": ["api://YOUR_API_CLIENT_ID/.default"]
  }
}

비밀을 안전하게 저장합니다.

cd MyService.Web
dotnet user-secrets set "AzureAd:ClientCredentials:0:ClientSecret" "YOUR_SECRET_VALUE"
가치 찾을 위치
TenantId Microsoft Entra 관리 센터 > 개요 > 테넌트 ID
API ClientId 앱 등록 > MyService.ApiService > 애플리케이션(클라이언트) ID
Web ClientId 앱 등록 > MyService.Web > 애플리케이션(클라이언트) ID
Client Secret 2단계에서 생성됨(만들 때 즉시 복사)

메모

Aspire 시작 템플릿은 WeatherApiClient 클래스를 MyService.Web 프로젝트에 자동으로 만듭니다. 이 형식의 HttpClient는 이 가이드 전체에서 보호된 API 호출을 보여 주는 데 사용됩니다. 이 클래스는 템플릿의 일부인 직접 만들 필요가 없습니다.


빠르게 시작하세요

이 섹션에서는 인증을 추가하기 위한 압축된 참조를 제공합니다. 자세한 연습은 1부2부를 참조하세요.

API(MyService.ApiService)

Microsoft.Identity.Web NuGet 패키지를 설치합니다.

dotnet add package Microsoft.Identity.Web

appsettings.json에 Microsoft Entra 구성을 추가합니다.

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "<tenant-id>",
    "ClientId": "<api-client-id>",
    "Audiences": ["api://<api-client-id>"]
  }
}

Program.cs에서 인증 및 권한 부여를 등록하십시오.

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddAuthorization();
// ...
app.UseAuthentication();
app.UseAuthorization();
// ...
app.MapGet("/weatherforecast", () => { /* ... */ }).RequireAuthorization();

웹앱(MyService.Web)

Microsoft.Identity.Web NuGet 패키지를 설치합니다.

dotnet add package Microsoft.Identity.Web

appsettings.json에 Microsoft Entra 구성을 추가합니다.

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "<tenant-id>",
    "ClientId": "<web-client-id>",
    "CallbackPath": "/signin-oidc",
    "ClientCredentials": [{ "SourceType": "ClientSecret" }]
  },
  "WeatherApi": { "Scopes": ["api://<api-client-id>/.default"] }
}

다음에서 인증, 토큰 획득 및 다운스트림 API 클라이언트를 구성합니다.Program.cs

builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddInMemoryTokenCaches();

builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<BlazorAuthenticationChallengeHandler>();

builder.Services.AddHttpClient<WeatherApiClient>(client =>
    client.BaseAddress = new("https+http://apiservice"))
    .AddMicrosoftIdentityMessageHandler(builder.Configuration.GetSection("WeatherApi"));
// ...
app.UseAuthentication();
app.UseAuthorization();
app.MapGroup("/authentication").MapLoginAndLogout();

토큰을 MicrosoftIdentityMessageHandler 자동으로 획득 및 연결하고 동의 및 BlazorAuthenticationChallengeHandler 조건부 액세스 챌린지를 처리합니다.

중요합니다

로그인 버튼 UserInfo.razor을(를) 만드는 것을 잊지 마세요. 자세한 내용은 Blazor UI 구성 요소 추가를 참조하세요 .

메모

Microsoft.Identity.Web(v3.3.0 이상)에 포함된 기능 BlazorAuthenticationChallengeHandlerLoginLogoutEndpointRouteBuilderExtensions. 파일 복사가 필요하지 않습니다.


수정할 파일 식별

다음 표에서는 각 프로젝트에서 변경한 파일을 나열합니다.

프로젝트 파일 변경
ApiService Program.cs JWT 전달자 인증, 권한 부여 미들웨어
appsettings.json Microsoft Entra 구성
.csproj Microsoft.Identity.Web 추가
Program.cs OIDC 인증, 토큰 획득, BlazorAuthenticationChallengeHandler
appsettings.json Microsoft Entra 구성, 다운스트림 API 범위
.csproj Microsoft.Identity.Web 추가(v3.3.0 이상)
Components/UserInfo.razor 로그인 단추 UI(새 파일)
Components/Layout/MainLayout.razor UserInfo 구성 요소 포함
Components/Routes.razor 보호된 페이지에 대한 AuthorizeRouteView
API를 호출하는 페이지 ChallengeHandler를 사용하여 시도/catch

인증 흐름 이해

다음 다이어그램에서는 Blazor 프런트 엔드, Microsoft Entra 및 보호된 API가 상호 작용하는 방법을 보여 줍니다.

flowchart LR
  A[User Browser] -->|1 Login OIDC| B[Blazor Server<br/>MyService.Web]
  B -->|2 Redirect| C[Microsoft Entra ID]
  C -->|3 auth code| B
  B -->|4 exchange auth code| C
  C -->|5 tokens| B
  B -->|6 cookie + session| A
  B -->|7 HTTP + Bearer token| D[ASP.NET API<br/>MyService.ApiService<br/>Microsoft.Identity.Web]
  D -->|8 Validate JWT| C
  D -->|9 Weather data| B
  1. 사용자가 Blazor 앱에 방문 → 인증되지 않음 → "로그인" 버튼이 보입니다.
  2. 사용자가 로그인을 선택하면/authentication/login → OIDC 챌린지 → Microsoft Entra로 리디렉션됩니다.
  3. 사용자가 로그인함 → Microsoft Entra로 리디렉션됨 /signin-oidc → 쿠키가 설정됨.
  4. 사용자는 Blazor 호출 WeatherApiClient.GetAsync().
  5. MicrosoftIdentityMessageHandler 는 요청을 가로채고, 캐시에서 토큰을 획득하거나(또는 조용히 새로 고침하여), Authorization: Bearer <token> 헤더를 첨부합니다.
  6. API는 요청을 받습니다 → Microsoft. Identity.Web은 JWT → 데이터 반환의 유효성을 검사합니다.
  7. Blazor는 날씨 데이터를 렌더링합니다.

솔루션 구조 검토

Aspire 시작 템플릿은 다음 프로젝트 레이아웃을 만듭니다.

MyService/
├── MyService.AppHost/           # Aspire orchestration
├── MyService.ApiService/        # Protected API (Microsoft.Identity.Web)
├── MyService.Web/               # Blazor Server (Microsoft.Identity.Web)
├── MyService.ServiceDefaults/   # Shared defaults
└── MyService.Tests/             # Tests

1부: Microsoft.Identity.Web을 사용하여 API 백엔드를 보호합니다.

이 섹션에서는 Microsoft Entra 발급한 JWT 전달자 토큰의 유효성을 검사하도록 API 프로젝트를 구성합니다.

Microsoft.Identity.Web 패키지를 추가합니다.

다음 명령을 실행하여 Microsoft.Identity.Web NuGet 패키지를 설치합니다.

cd MyService.ApiService
dotnet add package Microsoft.Identity.Web

Microsoft Entra 설정 구성

MyService.ApiService/appsettings.json에 Microsoft Entra 구성을 추가합니다.

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "<your-tenant-id>",
    "ClientId": "<your-api-client-id>",
    "Audiences": [
      "api://<your-api-client-id>"
    ]
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

키 속성:

  • ClientId: Microsoft Entra API 앱 등록 ID
  • TenantId: Microsoft Entra 테넌트 ID 또는 다중 테넌트에 대한 "organizations" 또는 모든 Microsoft 계정에 대한 "common"
  • Audiences: 유효한 토큰 대상 그룹(일반적으로 앱 ID URI)

API Program.cs 업데이트

JWT 전달자 인증을 MyService.ApiService/Program.cs 추가하고 엔드포인트를 보호하려면 다음 코드로 내용을 바꿉니다.

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();

// Add Microsoft.Identity.Web JWT Bearer authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));

builder.Services.AddProblemDetails();
builder.Services.AddOpenApi();
builder.Services.AddAuthorization();

var app = builder.Build();

app.UseExceptionHandler();
app.UseAuthentication();
app.UseAuthorization();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

string[] summaries = ["Freezing", "Bracing", "Chilly", "Cool", "Mild",
    "Warm", "Balmy", "Hot", "Sweltering", "Scorching"];

app.MapGet("/", () =>
    "API service is running. Navigate to /weatherforecast to see sample data.");

app.MapGet("/weatherforecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast")
.RequireAuthorization();

app.MapDefaultEndpoints();
app.Run();

record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

주요 변경 내용:

  • AddMicrosoftIdentityWebApi를 사용하여 JWT Bearer 인증을 등록하십시오
  • 추가 app.UseAuthentication()app.UseAuthorization() 미들웨어
  • 보호된 엔드포인트에 적용 .RequireAuthorization()

보호된 API 테스트

API가 인증되지 않은 요청을 거부하고 유효한 토큰을 허용하는지 확인합니다.

토큰 없이 요청을 보냅니다.

curl https://localhost:<PORT>/weatherforecast
# Expected: 401 Unauthorized

유효한 토큰을 사용하여 요청을 보냅니다.

curl -H "Authorization: Bearer <TOKEN>" https://localhost:<PORT>/weatherforecast
# Expected: 200 OK with weather data

2부: 인증을 위해 Blazor 프런트 엔드 구성

Blazor Server 앱은 Microsoft.Identity.Web을 사용하여:

  • OIDC를 사용하여 사용자 로그인
  • API를 호출하는 액세스 토큰 획득
  • 나가는 HTTP 요청에 토큰 연결

Microsoft.Identity.Web 패키지를 추가합니다.

다음 명령을 실행하여 Microsoft.Identity.Web NuGet 패키지를 설치하십시오.

cd MyService.Web
dotnet add package Microsoft.Identity.Web

Microsoft Entra 설정 구성

Microsoft Entra 구성 및 다운스트림 API 범위를 MyService.Web/appsettings.json에 추가합니다.

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "<your-tenant>.onmicrosoft.com",
    "TenantId": "<tenant-guid>",
    "ClientId":  "<web-app-client-id>",
    "CallbackPath": "/signin-oidc",
    "ClientCredentials": [
      {
        "SourceType": "ClientSecret",
        "ClientSecret": "<your-client-secret>"
      }
    ]
  },
  "WeatherApi": {
    "Scopes": [ "api://<api-client-id>/.default" ]
  },
  "Logging": {
    "LogLevel":  {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

구성 세부 정보:

  • ClientId: 웹앱 등록 ID(API ID 아님)
  • ClientCredentials: 토큰을 획득할 웹앱의 자격 증명입니다. 여러 자격 증명 형식을 지원합니다. 프로덕션 준비 옵션에 대한 자격 증명 개요 를 참조하세요.
  • Scopes: API의 앱 ID URI를 /.default 접미사와 일치시켜야 합니다.

경고

프로덕션의 경우 클라이언트 비밀 대신 인증서 또는 관리 ID를 사용합니다. 권장되는 방법은 인증서 없는 인증 을 참조하세요.

웹앱 Program.cs 업데이트

OIDC 인증, 토큰 획득 및 다운스트림 API 클라이언트를 구성하려면 다음 코드로 내용을 바꿉니다 MyService.Web/Program.cs .

using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Identity.Abstractions;
using Microsoft.Identity.Web;
using MyService.Web;
using MyService.Web.Components;

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();

// Authentication + Microsoft Identity Web
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddInMemoryTokenCaches();

builder.Services.AddCascadingAuthenticationState();

// Blazor components
builder.Services.AddRazorComponents().AddInteractiveServerComponents();

// Blazor authentication challenge handler for incremental consent and Conditional Access
builder.Services.AddScoped<BlazorAuthenticationChallengeHandler>();

builder.Services.AddOutputCache();

// Downstream API client with MicrosoftIdentityMessageHandler
builder.Services.AddHttpClient<WeatherApiClient>(client =>
{
    // Aspire service discovery: resolves "apiservice" at runtime
    client.BaseAddress = new("https+http://apiservice");
})
.AddMicrosoftIdentityMessageHandler(builder.Configuration.GetSection("WeatherApi"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
app.UseOutputCache();

app.MapStaticAssets();
app.MapRazorComponents<App>()
   .AddInteractiveServerRenderMode();

// Login/Logout endpoints with incremental consent support
app.MapGroup("/authentication").MapLoginAndLogout();

app.MapDefaultEndpoints();
app.Run();

핵심 사항:

  • AddMicrosoftIdentityWebApp: OIDC 인증 구성
  • EnableTokenAcquisitionToCallDownstreamApi: 다운스트림 API에 대한 토큰 획득을 사용하도록 설정
  • AddScoped<BlazorAuthenticationChallengeHandler>: Blazor Server에서 증분 동의 및 조건부 액세스를 처리합니다.
  • AddMicrosoftIdentityMessageHandler: 전달자 토큰을 HttpClient 요청에 자동으로 연결
  • https+http://apiservice: Aspire 서비스 검색은 이를 실제 API URL로 해결합니다.
  • 미들웨어 순서: UseAuthentication()UseAuthorization() → 엔드포인트

확장은 AddMicrosoftIdentityMessageHandler 다음과 같은 여러 구성 패턴을 지원합니다.

옵션 1: appsettings.json 구성(이전에 표시)

.AddMicrosoftIdentityMessageHandler(builder.Configuration.GetSection("WeatherApi"));

옵션 2: 작업 대리자를 사용하여 인라인 구성

.AddMicrosoftIdentityMessageHandler(options =>
{
    options.Scopes.Add("api://<api-client-id>/.default");
});

옵션 3: 요청별 구성(매개 변수 없는)

.AddMicrosoftIdentityMessageHandler();

// Then in your service, configure per-request:
var request = new HttpRequestMessage(HttpMethod.Get, "/weatherforecast")
    .WithAuthenticationOptions(options =>
    {
        options.Scopes.Add("api://<api-client-id>/.default");
    });
var response = await _httpClient.SendAsync(request);

Blazor UI 구성 요소 추가

중요합니다

이 단계는 자주 잊혀집니다. UserInfo 구성 요소가 없으면 사용자는 로그인할 방법이 없습니다.

BlazorAuthenticationChallengeHandlerLoginLogoutEndpointRouteBuilderExtensionsMicrosoft.Identity.Web v3.3.0+에 포함되어 있습니다. 패키지를 참조하면 자동으로 사용할 수 있습니다. 파일 복사가 필요하지 않습니다.

만들기 MyService.Web/Components/UserInfo.razor:

@using Microsoft.AspNetCore.Components.Authorization

<AuthorizeView>
    <Authorized>
        <span class="nav-item">Hello, @context.User.Identity?.Name</span>
        <form action="/authentication/logout" method="post" class="nav-item">
            <AntiforgeryToken />
            <input type="hidden" name="returnUrl" value="/" />
            <button type="submit" class="btn btn-link nav-link">Logout</button>
        </form>
    </Authorized>
    <NotAuthorized>
        <a href="/authentication/login?returnUrl=/" class="nav-link">Login</a>
    </NotAuthorized>
</AuthorizeView>

레이아웃에 추가: 다음을 포함합니다<UserInfo />.MainLayout.razor

@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <div class="top-row px-4">
            <UserInfo />
        </div>

        <article class="content px-4">
            @Body
        </article>
    </main>
</div>

AuthorizeRouteView에 대한 Routes.razor 업데이트

다음 RouteView에서 AuthorizeRouteView을/를 Components/Routes.razor으로 바꾸기.

@using Microsoft.AspNetCore.Components.Authorization

<Router AppAssembly="typeof(Program).Assembly">
    <Found Context="routeData">
        <AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
            <NotAuthorized>
                <p>You are not authorized to view this page.</p>
                <a href="/authentication/login">Login</a>
            </NotAuthorized>
        </AuthorizeRouteView>
        <FocusOnNavigate RouteData="routeData" Selector="h1" />
    </Found>
</Router>

API를 호출하는 페이지의 예외 처리

Blazor Server에는 조건부 액세스 및 동의에 대한 명시적 예외 처리가 필요합니다. 다운스트림 API를 호출하는 모든 페이지에서 MicrosoftIdentityWebChallengeUserException을(를) 처리해야 하며, 이는 앱이 미리 인증되지 않았거나 Program.cs에서 모든 범위를 미리 요청하지 않은 경우에 적용됩니다.

다음 Weather.razor 예제에서는 적절한 예외 처리를 보여 줍니다.

@page "/weather"
@attribute [Authorize]

@using Microsoft.AspNetCore.Authorization
@using Microsoft.Identity.Web

@inject WeatherApiClient WeatherApi
@inject BlazorAuthenticationChallengeHandler ChallengeHandler

<PageTitle>Weather</PageTitle>

<h1>Weather</h1>

@if (!string.IsNullOrEmpty(errorMessage))
{
    <div class="alert alert-warning">@errorMessage</div>
}
else if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[]? forecasts;
    private string? errorMessage;

    protected override async Task OnInitializedAsync()
    {
        if (!await ChallengeHandler.IsAuthenticatedAsync())
        {
            await ChallengeHandler.ChallengeUserWithConfiguredScopesAsync("WeatherApi:Scopes");
            return;
        }

        try
        {
            forecasts = await WeatherApi.GetWeatherAsync();
        }
        catch (Exception ex)
        {
            // Handle incremental consent / Conditional Access
            if (!await ChallengeHandler.HandleExceptionAsync(ex))
            {
                errorMessage = $"Error loading weather data: {ex.Message}";
            }
        }
    }
}

패턴은 다음과 같이 작동합니다.

  1. IsAuthenticatedAsync() 는 API를 호출하기 전에 사용자가 로그인했는지 확인합니다.
  2. HandleExceptionAsync() 예외를 잡습니다 MicrosoftIdentityWebChallengeUserException ( 또는 InnerException).
  3. 챌린지 예외인 경우 사용자는 필요한 클레임 또는 범위로 다시 인증하도록 리디렉션됩니다.
  4. 챌린지 예외 HandleExceptionAsync가 아닌 경우, false을 반환하여 오류를 직접 처리할 수 있도록 합니다.

사용자 비밀에 클라이언트 비밀 저장

.NET Secret Manager를 사용하여 개발 중에 클라이언트 비밀을 안전하게 저장합니다.

주의

비밀을 소스 제어에 커밋하지 마세요.

사용자 비밀을 초기화하고 클라이언트 암호를 저장합니다.

cd MyService.Web
dotnet user-secrets init
dotnet user-secrets set "AzureAd:ClientCredentials:0:ClientSecret" "<your-client-secret>"

그런 다음, 하드 코딩된 비밀을 제거하도록 업데이트 appsettings.json 합니다.

{
  "AzureAd": {
    "ClientCredentials": [
      {
        "SourceType": "ClientSecret"
      }
    ]
  }
}

Microsoft. Identity.Web은 여러 자격 증명 형식을 지원합니다. 프로덕션의 경우 자격 증명 개요를 참조하세요.


구현 확인

이 검사 목록을 사용하여 필요한 모든 단계를 완료되었는지 확인합니다.

API 프로젝트

  • [ ] Microsoft.Identity.Web 패키지 추가됨
  • appsettings.json를 섹션 AzureAd으로 업데이트함
  • [ ] Program.csAddMicrosoftIdentityWebApi로 업데이트
  • [ ] 보호된 엔드포인트에 추가 .RequireAuthorization()

Web/Blazor 프로젝트

  • [ ] Microsoft.Identity.Web 패키지 추가됨(v3.3.0 이상)
  • [ ] appsettings.jsonAzureAdWeatherApi 섹션으로 업데이트되었습니다.
  • [ ] OIDC, 토큰 획득으로 업데이트 Program.cs
  • [ ] 추가됨 AddScoped<BlazorAuthenticationChallengeHandler>()
  • [ ] 생성됨 Components/UserInfo.razor (로그인 단추)
  • [ ] 포함하도록 업데이트됨 MainLayout.razor<UserInfo />
  • [ ] Routes.razorAuthorizeRouteView로 업데이트
  • [ ] API를 호출하는 모든 페이지에서 try/catch가 ChallengeHandler 추가됨
  • [ ] 사용자 비밀에 저장된 클라이언트 암호

확인

  • [ ] dotnet build 성공
  • [ ] Microsoft Entra 관리 센터에서 생성된 앱 등록
  • [ ] appsettings.json 실제 GUID(자리 표시자 없음)가 있습니다.

테스트 및 문제 해결

구현을 완료한 후 애플리케이션을 실행하고 엔드 투 엔드 인증 흐름을 확인합니다.

애플리케이션 실행

Aspire AppHost를 시작하여 웹 및 API 프로젝트를 모두 시작합니다.

# From solution root
dotnet restore
dotnet build

# Launch AppHost (starts both Web and API)
dotnet run --project .\MyService.AppHost\MyService.AppHost.csproj

인증 흐름 테스트

  1. Blazor Web UI → 브라우저를 엽니다(URL에 대한 Aspire 대시보드 확인).
  2. Login → Microsoft Entra 로그인을 선택합니다.
  3. 날씨 페이지로 이동합니다.
  4. 보호된 API에서 날씨 데이터 로드를 확인합니다.

일반적인 문제 해결

다음 표에는 빈번한 문제 및 해당 솔루션이 나와 있습니다.

Issue 해결 방법
API 호출에서 401 appsettings.json의 범위가 API의 앱 ID URI와 일치하는지 확인
OIDC 리디렉션 실패 Microsoft Entra 리디렉션 URI에 /signin-oidc를 추가하십시오
토큰이 연결되지 않음 AddMicrosoftIdentityMessageHandler에서 HttpClient가 호출되었는지 확인하세요.
서비스 검색 실패 두 프로젝트의 AppHost.cs 참조가 확인되었고, 실행 중입니다.
AADSTS65001 관리자 동의 필요 - Microsoft Entra 관리 센터 동의 부여
로그인 버튼 없음 UserInfo.razor가 존재하고 MainLayout.razor에 포함되어 있는지 확인하십시오.
동의 루프 모든 API 호출 페이지에서 HandleExceptionAsync와 함께 try/catch가 있는지 확인합니다.

MSAL 로깅 활성화

인증 문제를 해결할 때 자세한 MSAL 로깅을 사용하도록 설정하여 토큰 획득 세부 정보를 확인합니다. 다음 로그 수준을 appsettings.json에 추가합니다.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.Identity": "Debug",
      "Microsoft.IdentityModel": "Debug"
    }
  }
}

경고

매우 자세한 정보가 될 수 있으므로 프로덕션 환경에서 디버그 로깅을 사용하지 않도록 설정합니다.

토큰 검사

토큰 문제를 디버그하려면 jwt.ms JWT를 디코딩하고 다음을 확인합니다.

  • aud (대상 그룹): API의 클라이언트 ID 또는 앱 ID URI와 일치
  • iss (발급자): 귀하의 테넌트와 일치(https://login.microsoftonline.com/<tenant-id>/v2.0)
  • scp (범위): 필요한 범위를 포함합니다.
  • exp (만료): 토큰이 만료되지 않았습니다.

일반적인 시나리오 살펴보기

다음 섹션에서는 추가 사용 사례에 대한 기본 구현을 확장하는 방법을 보여 줍니다.

Blazor 페이지 보호

인증이 필요한 페이지에 [Authorize] 특성을 추가합니다.

@page "/weather"
@attribute [Authorize]

또는 다음에서 권한 부여 정책을 정의합니다.Program.cs

// Program.cs
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));
});
@attribute [Authorize(Policy = "AdminOnly")]

API에서 범위 유효성 검사

API가 다음을 연결하여 특정 범위의 토큰만 허용하는지 확인합니다.RequireScope

app.MapGet("/weatherforecast", () =>
{
    // ... implementation
})
.RequireAuthorization()
.RequireScope("access_as_user");

앱 전용 토큰 사용(서비스 간)

사용자 컨텍스트가 없는 데몬 시나리오 또는 서비스 간 호출의 경우, RequestAppToken을(를) true로 설정합니다.

builder.Services.AddHttpClient<WeatherApiClient>(client =>
{
    client.BaseAddress = new("https+http://apiservice");
})
.AddMicrosoftIdentityMessageHandler(options =>
{
    options.Scopes.Add("api://<api-client-id>/.default");
    options.RequestAppToken = true;
});

프로덕션에 인증서 없는 자격 증명 사용

Azure 프로덕션 배포의 경우 클라이언트 비밀 대신 관리 ID를 사용합니다. 섹션을 ClientCredentials 다음과 같이 구성합니다.

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "<tenant-guid>",
    "ClientId":  "<web-app-client-id>",
    "ClientCredentials": [
      {
        "SourceType": "SignedAssertionFromManagedIdentity",
        "ManagedIdentityClientId": "<user-assigned-mi-client-id>"
      }
    ]
  }
}

자세한 내용은 인증서 없는 인증을 참조하세요.

API에서 하위 API를 대리로 호출하기

API가 사용자를 대신하여 다른 다운스트림 API를 호출해야 하는 경우 Program.cs에서 대리자 토큰 획득 기능을 활성화합니다.

// MyService.ApiService/Program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddInMemoryTokenCaches();

builder.Services.AddDownstreamApi("GraphApi", builder.Configuration.GetSection("GraphApi"));

다운스트림 API 구성을 다음으로 추가합니다 appsettings.json.

{
  "GraphApi": {
    "BaseUrl": "https://graph.microsoft.com/v1.0",
    "Scopes": [ "User.Read" ]
  }
}

그런 다음, 엔드포인트에서 다운스트림 API를 호출합니다.

{
    var user = await downstreamApi.GetForUserAsync<JsonElement>("GraphApi", "me");
    return user;
}).RequireAuthorization();

자세한 내용은 다운스트림 API 호출을 참조하세요.