웹앱에서 다운스트림 API 호출

이 가이드에서는 Microsoft.Identity.Web를 사용하여 ASP.NET Core 및 OWIN 웹 애플리케이션에서 다운스트림 API를 호출하는 방법을 설명합니다. 웹앱에서는 로그인한 사용자를 대신하여 토큰을 획득하여 위임된 권한으로 API를 호출합니다.

토큰 흐름 이해

사용자가 웹 애플리케이션에 로그인하면 대신 다운스트림 API(Microsoft Graph, Azure 서비스 또는 사용자 지정 API)를 호출할 수 있습니다. Microsoft. Identity.Web은 토큰 획득, 캐싱 및 자동 새로 고침을 처리합니다.

사용자 토큰 흐름 검토

sequenceDiagram
    participant User as User Browser
    participant WebApp as Your Web App
    participant AzureAD as Microsoft Entra ID
    participant API as Downstream API

    User->>WebApp: 1. Access page requiring API data
    Note over WebApp: User already signed in
    WebApp->>AzureAD: 2. Request access token for API<br/>(using user's refresh token)
    AzureAD->>AzureAD: 3. Validate & check consent
    AzureAD->>WebApp: 4. Return access token
    Note over WebApp: Cache token
    WebApp->>API: 5. Call API with token
    API->>WebApp: 6. Return data
    WebApp->>User: 7. Render page with data

필수 조건 확인

시작하기 전에 환경이 다음 요구 사항을 충족하는지 확인합니다.

  • OpenID Connect 인증으로 구성된 웹앱
  • 사용자 로그인 작업
  • 구성된 API 권한으로 앱 등록
  • 사용자 동의 획득(또는 관리자 동의 부여)

ASP.NET Core 구현

1. 인증 및 토큰 획득 구성

인증 서비스를 추가하고 Program.cs 파일에서 토큰 획득을 가능하도록 설정합니다.

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

var builder = WebApplication.CreateBuilder(args);

// Add authentication with explicit scheme
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddInMemoryTokenCaches();

builder.Services.AddRazorPages()
    .AddMicrosoftIdentityUI();

builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = options.DefaultPolicy;
});

var app = builder.Build();

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

app.MapRazorPages();
app.Run();

2. appsettings.json 구성

Microsoft Entra ID 앱 등록 및 다운스트림 API 설정을 appsettings.json에서 정의합니다.

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "your-tenant-id",
    "ClientId": "your-client-id",
    "CallbackPath": "/signin-oidc",
    "SignedOutCallbackPath": "/signout-callback-oidc",
    "ClientCredentials": [
      {
        "SourceType": "ClientSecret",
        "ClientSecret": "your-client-secret"
      }
    ]
  },
  "DownstreamApis": {
    "GraphAPI": {
      "BaseUrl": "https://graph.microsoft.com/v1.0",
      "Scopes": ["user.read", "mail.read"]
    },
    "MyAPI": {
      "BaseUrl": "https://myapi.example.com",
      "Scopes": ["api://my-api-id/access_as_user"]
    }
  }
}

중요: 다운스트림 API를 호출하는 웹앱의 경우 로그인 구성 외에 클라이언트 자격 증명 (인증서 또는 비밀)이 필요합니다.

3. 다운스트림 API 지원 추가

다음 옵션 중 하나를 선택하여 다운스트림 API를 등록합니다.

옵션 A: 명명된 API 등록

다음 코드는 구성에서 여러 다운스트림 API를 등록합니다.

using Microsoft.Identity.Web;

// Register multiple downstream APIs
builder.Services.AddDownstreamApis(
    builder.Configuration.GetSection("DownstreamApis"));

옵션 B: Microsoft Graph 도우미 사용

다음 코드는 구성에서 Microsoft Graph SDK 클라이언트를 등록합니다.

// Install: Microsoft.Identity.Web.GraphServiceClient
builder.Services.AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApis:GraphAPI"));

4. 컨트롤러에서 다운스트림 API 호출

컨트롤러에 IDownstreamApi를 삽입하고 로그인한 사용자를 대신하여 API를 호출합니다.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Identity.Web;
using Microsoft.Identity.Abstractions;

[Authorize]
public class ProfileController : Controller
{
    private readonly IDownstreamApi _downstreamApi;
    private readonly ILogger<ProfileController> _logger;

    public ProfileController(
        IDownstreamApi downstreamApi,
        ILogger<ProfileController> logger)
    {
        _downstreamApi = downstreamApi;
        _logger = logger;
    }

    public async Task<IActionResult> Index()
    {
        try
        {
            // Call downstream API on behalf of user
            var userData = await _downstreamApi.GetForUserAsync<UserData>(
                "MyAPI",
                options => options.RelativePath = "api/profile");

            return View(userData);
        }
        catch (MicrosoftIdentityWebChallengeUserException ex)
        {
            // Incremental consent required
            // Redirect user to consent page
            return Challenge(
                new AuthenticationProperties
                {
                    RedirectUri = "/Profile"
                },
                OpenIdConnectDefaults.AuthenticationScheme);
        }
        catch (HttpRequestException ex)
        {
            _logger.LogError(ex, "Failed to call downstream API");
            return View("Error");
        }
    }
}

5. Razor 페이지에서 다운스트림 API 호출

Razor 페이지 모델에 IDownstreamApi를 삽입하고 API를 호출합니다.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Identity.Web;
using Microsoft.Identity.Abstractions;

[Authorize]
public class ProfileModel : PageModel
{
    private readonly IDownstreamApi _downstreamApi;

    public UserData UserData { get; set; }

    public ProfileModel(IDownstreamApi downstreamApi)
    {
        _downstreamApi = downstreamApi;
    }

    public async Task OnGetAsync()
    {
        try
        {
            UserData = await _downstreamApi.GetForUserAsync<UserData>(
                "MyAPI",
                options => options.RelativePath = "api/profile");
        }
        catch (MicrosoftIdentityWebChallengeUserException)
        {
            // Handle incremental consent
            // User will be redirected to consent page
            throw;
        }
    }
}

Microsoft Graph 호출

Microsoft Graph API 호출의 경우 전용 GraphServiceClient 사용합니다.

패키지 설치

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

dotnet add package Microsoft.Identity.Web.GraphServiceClient

시작 코드에서 Graph 클라이언트를 구성합니다.

// Startup configuration
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddMicrosoftGraph(options =>
    {
        options.Scopes = "user.read mail.read";
    })
    .AddInMemoryTokenCaches();

Graph API 호출

컨트롤러에 GraphServiceClient 삽입하여 Microsoft Graph 엔드포인트를 호출합니다.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Graph;

[Authorize]
{
    private readonly GraphServiceClient _graphClient;

    public HomeController(GraphServiceClient graphClient)
    {
        _graphClient = graphClient;
    }

    public async Task<IActionResult> Index()
    {
        // Get current user's profile
        var user = await _graphClient.Me.GetAsync();

        // Get user's emails
        var messages = await _graphClient.Me.Messages
            .GetAsync(config => config.QueryParameters.Top = 10);

        return View(new { User = user, Messages = messages });
    }
}

Microsoft Graph 통합에 대해 자세히 알아보세요


Azure SDK 클라이언트 호출

Azure 서비스를 호출하려면 MicrosoftIdentityTokenCredential 사용합니다.

패키지 설치

필요한 Azure SDK 패키지를 설치합니다.

dotnet add package Microsoft.Identity.Web.Azure
dotnet add package Azure.Storage.Blobs

Microsoft Entra 토큰 자격 증명을 시작 코드에 등록합니다.

using Microsoft.Identity.Web;

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

// Add Azure token credential
builder.Services.AddMicrosoftIdentityAzureTokenCredential();

Azure 서비스 액세스

토큰 자격 증명을 삽입하고 Azure SDK 클라이언트와 함께 사용합니다.

using Azure.Storage.Blobs;
using Microsoft.Identity.Web;

public class StorageController : Controller
{
    private readonly MicrosoftIdentityTokenCredential _credential;

    public StorageController(MicrosoftIdentityTokenCredential credential)
    {
        _credential = credential;
    }

    [Authorize]
    public async Task<IActionResult> ListBlobs()
    {
        var blobClient = new BlobServiceClient(
            new Uri("https://myaccount.blob.core.windows.net"),
            _credential);

        var container = blobClient.GetBlobContainerClient("mycontainer");
        var blobs = new List<string>();

        await foreach (var blob in container.GetBlobsAsync())
        {
            blobs.Add(blob.Name);
        }

        return View(blobs);
    }
}

Azure SDK 통합에 대해 자세히 알아보세요


IDownstreamApi를 사용하여 사용자 지정 API 호출

고유한 REST API IDownstreamApi 의 경우 구성 기반의 간단한 접근 방식을 제공합니다.

API 구성

에서 다운스트림 API 설정을 정의합니다 appsettings.json.

{
  "DownstreamApis": {
    "MyAPI": {
      "BaseUrl": "https://myapi.example.com",
      "Scopes": ["api://my-api-id/access_as_user"],
      "RequestAppToken": false
    }
  }
}

GET 요청 보내기

선택적 쿼리 매개 변수를 사용하여 다운스트림 API에서 데이터를 검색합니다.

// Simple GET
var data = await _downstreamApi.GetForUserAsync<MyData>(
    "MyAPI",
    options => options.RelativePath = "api/resource");

// GET with query parameters
var results = await _downstreamApi.GetForUserAsync<SearchResults>(
    "MyAPI",
    options =>
    {
        options.RelativePath = "api/search";
        options.QueryParameters = new Dictionary<string, string>
        {
            ["query"] = "test",
            ["limit"] = "10"
        };
    });

POST 요청 보내기

요청 본문을 게시하여 다운스트림 API에 새 리소스를 만듭니다.

var newItem = new CreateItemRequest
{
    Name = "New Item",
    Description = "Item description"
};

var created = await _downstreamApi.PostForUserAsync<CreateItemRequest, CreatedItem>(
    "MyAPI",
    newItem,
    options => options.RelativePath = "api/items");

PUT 및 DELETE 요청 보내기

다운스트림 API에서 리소스를 업데이트하거나 삭제합니다.

// PUT request
var updated = await _downstreamApi.PutForUserAsync<UpdateRequest, UpdatedItem>(
    "MyAPI",
    updateData,
    options => options.RelativePath = "api/items/123");

// DELETE request
await _downstreamApi.DeleteForUserAsync(
    "MyAPI",
    null,
    options => options.RelativePath = "api/items/123");

사용자 지정 API 호출에 대해 자세히 알아보기


IAuthorizationHeaderProvider 사용(고급)

HTTP 요청을 최대한 제어하려면 .를 사용합니다 IAuthorizationHeaderProvider.

HTTP 클라이언트 등록

다운스트림 API에 대해 명명된 HTTP 클라이언트를 등록합니다.

builder.Services.AddHttpClient("MyAPI", client =>
{
    client.BaseAddress = new Uri("https://myapi.example.com");
});

사용자 지정 HTTP 요청 만들기

사용자 지정 헤더 및 권한 부여를 사용하여 HTTP 요청을 빌드하고 보냅니다.

using Microsoft.Identity.Abstractions;

public class CustomApiService
{
    private readonly IAuthorizationHeaderProvider _authProvider;
    private readonly IHttpClientFactory _httpClientFactory;

    public CustomApiService(
        IAuthorizationHeaderProvider authProvider,
        IHttpClientFactory httpClientFactory)
    {
        _authProvider = authProvider;
        _httpClientFactory = httpClientFactory;
    }

    public async Task<MyData> GetDataAsync()
    {
        // Get authorization header
        var authHeader = await _authProvider.CreateAuthorizationHeaderForUserAsync(
            new[] { "api://my-api-id/access_as_user" });

        // Create HTTP request with custom logic
        var client = _httpClientFactory.CreateClient("MyAPI");
        var request = new HttpRequestMessage(HttpMethod.Get, "api/resource");
        request.Headers.Add("Authorization", authHeader);
        request.Headers.Add("X-Custom-Header", "custom-value");

        var response = await client.SendAsync(request);
        response.EnsureSuccessStatusCode();

        return await response.Content.ReadFromJsonAsync<MyData>();
    }
}

사용자 지정 HTTP 논리에 대해 자세히 알아보기


다운스트림 API를 호출할 때 애플리케이션은 사용자 상호 작용이 필요한 시나리오를 처리해야 할 수 있습니다. 이 문제는 다음 세 가지 주요 시나리오에서 발생합니다.

  1. 증분 동의 - 처음에 부여된 권한 이외의 추가 권한 요청
  2. 조건부 액세스 - MFA, 디바이스 규정 준수 또는 위치 정책과 같은 보안 요구 사항 충족
  3. 토큰 캐시 제거 - 애플리케이션 다시 시작 또는 캐시 만료 후 토큰 캐시 다시 채우기

Microsoft. Identity.Web은 필요한 최소한의 코드로 이러한 시나리오를 자동으로 처리합니다.

흐름 이해

Microsoft.Identity.Web은 사용자 상호 작용이 필요함을 감지하면 MicrosoftIdentityWebChallengeUserException 예외를 발생시킵니다. 프레임워크는 [AuthorizeForScopes] 속성 또는 Blazor에 대한 MicrosoftIdentityConsentAndConditionalAccessHandler 서비스를 통해 이 작업을 자동으로 처리합니다.

  1. 동의/인증을 위해 사용자를 Microsoft Entra ID 리디렉션합니다.
  2. 원래 요청 URL을 유지합니다.
  3. 흐름을 완료한 후 사용자를 원하는 대상으로 반환합니다.
  4. 새로 획득한 토큰을 캐시합니다.

필수 조건 확인

자동 동의 처리를 사용하도록 설정하려면 다음 구성을 포함해야 합니다 Program.cs .

builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration, "AzureAd")
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddDownstreamApi("MyAPI", builder.Configuration.GetSection("MyAPI"))
    .AddInMemoryTokenCaches();

// For MVC applications - enables the account controller
builder.Services.AddControllersWithViews()
    .AddMicrosoftIdentityUI();

// Ensure routes are mapped
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers(); // Required for AccountController

MVC 컨트롤러에서 [AuthorizeForScopes] 적용

컨트롤러 또는 컨트롤러 작업에 설정된 [AuthorizeForScopes] 특성은 추가 권한이 필요할 때 사용자에게 권한 승인을 요청하여 MicrosoftIdentityWebChallengeUserException를 자동으로 처리합니다.

범위를 인라인으로 선언하기

특성에서 직접 필요한 범위를 지정합니다.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Identity.Web;
using Microsoft.Identity.Abstractions;

[Authorize]
[AuthorizeForScopes(Scopes = new[] { "user.read" })]
public class ProfileController : Controller
{
    private readonly IDownstreamApi _downstreamApi;

    public ProfileController(IDownstreamApi downstreamApi)
    {
        _downstreamApi = downstreamApi;
    }

    public async Task<IActionResult> Index()
    {
        // AuthorizeForScopes automatically handles consent challenges
        var userData = await _downstreamApi.GetForUserAsync<UserData>(
            "MyAPI",
            options => options.RelativePath = "api/profile");

        return View(userData);
    }

    // Different action requires additional scopes
    [AuthorizeForScopes(Scopes = new[] { "user.read", "mail.read" })]
    public async Task<IActionResult> Emails()
    {
        var emails = await _downstreamApi.GetForUserAsync<EmailList>(
            "GraphAPI",
            options => options.RelativePath = "me/messages");

        return View(emails);
    }
}

appsettings에서 범위 구성

유지 관리 효율성 향상을 위해 범위를 저장합니다 appsettings.json .

appsettings.json:

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "common",
    "ClientId": "[Your-Client-ID]",
    "ClientCredentials": [
      {
        "SourceType": "ClientSecret",
        "ClientSecret": "[Your-Client-Secret]"
      }
    ]
  },
  "DownstreamApis": {
    "TodoList": {
      "BaseUrl": "https://localhost:5001",
      "Scopes": [ "api://[API-Client-ID]/access_as_user" ]
    },
    "GraphAPI": {
      "BaseUrl": "https://graph.microsoft.com/v1.0",
      "Scopes": [ "https://graph.microsoft.com/Mail.Read", "https://graph.microsoft.com/Mail.Send" ]
    }
  }
}

컨트롤러:

[Authorize]
[AuthorizeForScopes(ScopeKeySection = "DownstreamApis:TodoList:Scopes:0")]
public class TodoListController : Controller
{
    private readonly IDownstreamApi _downstreamApi;

    public TodoListController(IDownstreamApi downstreamApi)
    {
        _downstreamApi = downstreamApi;
    }

    public async Task<IActionResult> Index()
    {
        var todos = await _downstreamApi.GetForUserAsync<IEnumerable<TodoItem>>(
            "TodoList",
            options => options.RelativePath = "api/todolist");

        return View(todos);
    }

    [AuthorizeForScopes(ScopeKeySection = "DownstreamApis:GraphAPI:Scopes:0")]
    public async Task<IActionResult> EmailTodos()
    {
        // If user hasn't consented to Mail.Send, they'll be prompted
        await _downstreamApi.PostForUserAsync<EmailMessage, object>(
            "GraphAPI",
            new EmailMessage { /* ... */ },
            options => options.RelativePath = "me/sendMail");

        return RedirectToAction("Index");
    }
}

사용자 흐름을 사용하여 Microsoft Entra 외부 ID 구성

여러 사용자 흐름이 있는 B2C(외부 ID) 애플리케이션의 경우 특성에 사용자 흐름을 지정합니다.

[Authorize]
public class AccountController : Controller
{
    private const string SignUpSignInFlow = "b2c_1_susi";
    private const string EditProfileFlow = "b2c_1_edit_profile";
    private const string ResetPasswordFlow = "b2c_1_reset";

    [AuthorizeForScopes(
        ScopeKeySection = "DownstreamApis:TodoList:Scopes:0",
        UserFlow = SignUpSignInFlow)]
    public async Task<IActionResult> Index()
    {
        var data = await _downstreamApi.GetForUserAsync<UserData>(
            "TodoList",
            options => options.RelativePath = "api/data");

        return View(data);
    }

    [AuthorizeForScopes(
        Scopes = new[] { "openid", "offline_access" },
        UserFlow = EditProfileFlow)]
    public async Task<IActionResult> EditProfile()
    {
        // This triggers the B2C edit profile flow
        return RedirectToAction("Index");
    }
}

Razor 페이지에서 [AuthorizeForScopes] 적용

[AuthorizeForScopes]을(를) 페이지 모델 클래스에 적용합니다.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Identity.Web;
using Microsoft.Identity.Abstractions;

[Authorize]
[AuthorizeForScopes(ScopeKeySection = "DownstreamApis:MyAPI:Scopes:0")]
public class IndexModel : PageModel
{
    private readonly IDownstreamApi _downstreamApi;

    public UserData UserData { get; set; }

    public IndexModel(IDownstreamApi downstreamApi)
    {
        _downstreamApi = downstreamApi;
    }

    public async Task OnGetAsync()
    {
        // Automatically handles consent challenges
        UserData = await _downstreamApi.GetForUserAsync<UserData>(
            "MyAPI",
            options => options.RelativePath = "api/profile");
    }
}

Blazor Server 애플리케이션에는 MicrosoftIdentityConsentAndConditionalAccessHandler 서비스를 사용하여 명시적 예외 처리가 필요합니다.

Program.cs 구성

시작 코드에서 Blazor Server에 대한 동의 처리기를 등록합니다.

builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration, "AzureAd")
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddDownstreamApis("TodoList", builder.Configuration.GetSection("DownstreamApis"))
    .AddInMemoryTokenCaches();

// Register the consent handler for Blazor
builder.Services.AddServerSideBlazor()
    .AddMicrosoftIdentityConsentHandler();

Blazor 구성 요소 만들기

API 호출을 try-catch 블록으로 래핑하고 동의 챌린지를 처리하는 데 ConsentHandler.HandleException()를 사용하십시오.

@page "/todolist"
@using Microsoft.Identity.Web
@using Microsoft.Identity.Abstractions
@using MyApp.Models

@inject MicrosoftIdentityConsentAndConditionalAccessHandler ConsentHandler
@inject IDownstreamApi DownstreamApi

<h3>My Todo List</h3>

@if (todos == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <ul>
        @foreach (var todo in todos)
        {
            <li>@todo.Title</li>
        }
    </ul>
}

@code {
    private IEnumerable<TodoItem> todos;

    protected override async Task OnInitializedAsync()
    {
        await LoadTodosAsync();
    }

    [AuthorizeForScopes(ScopeKeySection = "DownstreamApis:TodoList:Scopes:0")]
    private async Task LoadTodosAsync()
    {
        try
        {
            todos = await DownstreamApi.GetForUserAsync<IEnumerable<TodoItem>>(
                "TodoList",
                options => options.RelativePath = "api/todolist");
        }
        catch (Exception ex)
        {
            // Handles MicrosoftIdentityWebChallengeUserException
            // and initiates user consent/authentication flow
            ConsentHandler.HandleException(ex);
        }
    }

    private async Task AddTodoAsync(string title)
    {
        try
        {
            await DownstreamApi.PostForUserAsync<TodoItem, TodoItem>(
                "TodoList",
                new TodoItem { Title = title },
                options => options.RelativePath = "api/todolist");

            await LoadTodosAsync();
        }
        catch (Exception ex)
        {
            ConsentHandler.HandleException(ex);
        }
    }
}

수동으로 예외 처리(고급)

사용자 지정 동의 흐름 논리가 필요한 경우, MicrosoftIdentityWebChallengeUserException를 명시적으로 처리합니다.

[Authorize]
public class AdvancedController : Controller
{
    private readonly IDownstreamApi _downstreamApi;
    private readonly ILogger<AdvancedController> _logger;

    public AdvancedController(
        IDownstreamApi downstreamApi,
        ILogger<AdvancedController> logger)
    {
        _downstreamApi = downstreamApi;
        _logger = logger;
    }

    public async Task<IActionResult> SendEmail()
    {
        try
        {
            await _downstreamApi.PostForUserAsync<EmailMessage, object>(
                "GraphAPI",
                new EmailMessage
                {
                    Subject = "Test",
                    Body = "Test message"
                },
                options => options.RelativePath = "me/sendMail");

            return RedirectToAction("Success");
        }
        catch (MicrosoftIdentityWebChallengeUserException ex)
        {
            // Log the consent requirement
            _logger.LogWarning(
                "Consent required for scopes: {Scopes}. Challenging user.",
                string.Join(", ", ex.Scopes));

            // Custom properties for redirect
            var properties = new AuthenticationProperties
            {
                RedirectUri = Url.Action("SendEmail", "Advanced"),
            };

            // Add custom state if needed
            properties.Items["consent_attempt"] = "1";

            return Challenge(properties, OpenIdConnectDefaults.AuthenticationScheme);
        }
        catch (HttpRequestException ex)
        {
            _logger.LogError(ex, "Failed to send email");
            return View("Error");
        }
    }
}

조건부 액세스 시나리오 처리

조건부 액세스 정책에는 추가 인증 요소가 필요할 수 있습니다. 처리는 증분 동의와 동일합니다.

[Authorize]
[AuthorizeForScopes(ScopeKeySection = "DownstreamApis:SecureAPI:Scopes:0")]
public class SecureDataController : Controller
{
    private readonly IDownstreamApi _downstreamApi;

    public SecureDataController(IDownstreamApi downstreamApi)
    {
        _downstreamApi = downstreamApi;
    }

    public async Task<IActionResult> Index()
    {
        // If conditional access requires MFA, AuthorizeForScopes
        // automatically challenges the user
        var sensitiveData = await _downstreamApi.GetForUserAsync<SensitiveData>(
            "SecureAPI",
            options => options.RelativePath = "api/sensitive");

        return View(sensitiveData);
    }
}

일반적인 조건부 액세스 트리거:

  • 다중 요소 인증(MFA)
  • 준수 디바이스 요구 사항
  • 신뢰할 수 있는 네트워크 위치
  • 사용 약관 동의
  • 암호 변경 요구 사항

모범 사례 준수

동의 및 조건부 액세스 처리를 구현할 때 이러한 권장 사항을 적용합니다.

사용 [AuthorizeForScopes] - MVC 컨트롤러와 Razor Pages에 가장 쉬운 접근 방법

구성에서 범위 저장 - 다음에서 범위를 참조하려면 ScopeKeySection = "DownstreamApis:ApiName:Scopes:0"을(를) 사용하십시오 appsettings.json

컨트롤러 수준에서 적용 - 컨트롤러에서 기본 범위 설정, 특정 작업 재정의

Blazor에서 예외 처리 - 항상 try-catch로 API 호출을 래핑하고 ConsentHandler.HandleException()을(를) 사용하세요.

예외를 다시 던지는 것을 허용 - MicrosoftIdentityWebChallengeUserException를 catch하는 경우, [AuthorizeForScopes]가 처리할 수 있도록 다시 던지십시오

조건부 액세스 테스트 - 앱이 MFA 및 기타 CA 정책을 올바르게 처리하는지 확인

예외를 표시하지 않음 - 다시 throw하지 않고 Catch하면 동의 흐름이 중단됩니다.

응답을 무기한 캐시하지 마세요 . 토큰이 만료됩니다. 다시 인증을 위한 디자인


정적 권한(관리자 동의)

앱 등록 중에 모든 권한이 요청되고 테넌트 관리자가 동의합니다.

장점:

  • 사용자에게 동의 프롬프트가 표시되지 않습니다.
  • Microsoft 자사 앱에 필수 항목
  • 더 간단한 사용자 환경

단점:

  • 테넌트 관리자 참여 필요
  • 처음부터 과도하게 권한 부여됨
  • 다중 테넌트 시나리오에 대한 유연성이 떨어지다

구성:

// Request all pre-approved scopes for Microsoft Graph
var scopes = new[] { "https://graph.microsoft.com/.default" };

var userData = await _downstreamApi.GetForUserAsync<UserData>(
    "GraphAPI",
    options =>
    {
        options.RelativePath = "me";
        options.Scopes = scopes; // Use .default scope
    });

증분 동의(동적)

런타임 중에 필요에 따라 사용 권한이 요청됩니다.

장점:

  • 보안 향상(최소 권한 원칙)
  • 사용자가 실제로 사용하는 것에 동의
  • 다중 테넌트 앱에 대해 작동

단점:

  • 동의 프롬프트로 사용자가 중단될 수 있음
  • 처리 필요 MicrosoftIdentityWebChallengeUserException

추천: 다중 테넌트 애플리케이션에는 증분 동의를 사용하고, 관리자 동의가 보장되는 자사 엔터프라이즈 앱에는 정적 사용 권한을 사용하세요.


토큰 캐싱 구성

Microsoft. Identity.Web은 성능을 향상시키고 Microsoft Entra 대한 호출을 줄이기 위해 토큰을 캐시합니다.

메모리 내 캐시 사용(기본값)

개발 또는 단일 서버 시나리오에 대한 메모리 내 토큰 캐시를 추가합니다.

builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddInMemoryTokenCaches(); // In-memory cache

다음 용도로 사용합니다.

  • 발달
  • 단일 서버 배포
  • 소규모 사용자 기반

Limitations:

  • 인스턴스 간에 공유되지 않음
  • 앱 다시 시작 중 손실됨
  • 사용자와 함께 메모리 사용량 증가

프로덕션 배포를 위해 Redis 또는 SQL Server 같은 분산 캐시를 구성합니다.

// Install: Microsoft.Identity.Web.TokenCache

// Redis
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration["Redis:ConnectionString"];
    options.InstanceName = "MyApp_";
});

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

// SQL Server
builder.Services.AddDistributedSqlServerCache(options =>
{
    options.ConnectionString = builder.Configuration["SqlCache:ConnectionString"];
    options.SchemaName = "dbo";
    options.TableName = "TokenCache";
});

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

다음 용도로 사용합니다.

  • 다중 서버 배포(부하 분산)
  • 고가용성 시나리오
  • 대규모 사용자 기반
  • 재시작 후에도 지속적인 캐시

토큰 획득 실패 처리

예외를 catch

다음 코드에서는 가장 일반적인 토큰 획득 예외를 catch하고 처리하는 방법을 보여 줍니다.

try
{
    var data = await _downstreamApi.GetForUserAsync<MyData>(
        "MyAPI",
        options => options.RelativePath = "api/resource");
}
catch (MicrosoftIdentityWebChallengeUserException ex)
{
    // User needs to consent or reauthenticate
    _logger.LogWarning($"User consent required: {ex.Message}");
    return Challenge(new AuthenticationProperties { RedirectUri = Request.Path });
}
catch (MsalUiRequiredException ex)
{
    // User interaction required (sign-in again, MFA, etc.)
    _logger.LogWarning($"User interaction required: {ex.Message}");
    return Challenge(OpenIdConnectDefaults.AuthenticationScheme);
}
catch (MsalServiceException ex)
{
    // Service error (Microsoft Entra ID unavailable, etc.)
    _logger.LogError(ex, "Microsoft Entra ID service error");
    return StatusCode(503, "Authentication service temporarily unavailable");
}
catch (HttpRequestException ex)
{
    // Downstream API unreachable
    _logger.LogError(ex, "Downstream API call failed");
    return StatusCode(503, "Downstream service unavailable");
}

점진적 성능 저하 구현

다운스트림 API에서 선택적 데이터를 로드하고 호출이 실패하면 기본값으로 대체합니다.

public async Task<IActionResult> Dashboard()
{
    var model = new DashboardModel();

    // Try to load optional data from downstream API
    try
    {
        model.EnrichedData = await _downstreamApi.GetForUserAsync<EnrichedData>(
            "MyAPI",
            options => options.RelativePath = "api/enriched");
    }
    catch (Exception ex)
    {
        _logger.LogWarning(ex, "Failed to load enriched data, using defaults");
        model.EnrichedData = new EnrichedData { /* defaults */ };
    }

    return View(model);
}

OWIN 구현(.NET Framework)

.NET Framework의 OWIN 기반 웹 애플리케이션의 경우 다음 단계를 수행합니다.

1. 패키지 설치

필요한 NuGet 패키지를 설치합니다.

Install-Package Microsoft.Identity.Web.OWIN
Install-Package Microsoft.Owin.Host.SystemWeb

2. 시작 구성

OWIN 시작 클래스에서 Microsoft Entra 인증 및 토큰 획득을 구성합니다.

using Microsoft.Identity.Web;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Owin;

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

        app.UseCookieAuthentication(new CookieAuthenticationOptions());

        app.AddMicrosoftIdentityWebApp(
            Configuration,
            configSectionName: "AzureAd",
            openIdConnectScheme: "OpenIdConnect",
            cookieScheme: CookieAuthenticationDefaults.AuthenticationType,
            subscribeToOpenIdConnectMiddlewareDiagnosticsEvents: true);

        app.EnableTokenAcquisitionToCallDownstreamApi();
        app.AddDistributedTokenCaches();
    }
}

3. 다운스트림 API 호출

토큰을 획득하고 MVC 컨트롤러에서 다운스트림 API를 호출합니다.

using Microsoft.Identity.Web;
using System.Threading.Tasks;
using System.Web.Mvc;

[Authorize]
public class ProfileController : Controller
{
    public async Task<ActionResult> Index()
    {
        var downstreamApi = TokenAcquirerFactory.GetDefaultInstance()
            .GetTokenAcquirer()
            .GetDownstreamApi();

        var userData = await downstreamApi.GetForUserAsync<UserData>(
            "MyAPI",
            options => options.RelativePath = "api/profile");

        return View(userData);
    }
}

Note: OWIN 지원에는 ASP.NET Core 몇 가지 차이점이 있습니다. 자세한 내용은 OWIN 설명서를 참조하세요.


보안 모범 사례 따르기

범위 관리

API 권한을 요청할 때 최소 권한 원칙을 적용합니다.

Do:

  • 필요한 범위만 요청
  • 고급 기능에 대해 증분 동의 사용
  • 앱에서 필요한 범위 문서화

Don't:

  • 불필요한 범위를 선불로 요청
  • 근거 없이 관리자 전용 범위 요청
  • 모든 스코프가 부여된다고 가정합니다.

토큰을 안전하게 처리

애플리케이션에서 액세스 토큰을 보호하려면 다음 지침을 따릅니다.

Do:

  • Microsoft.Identity.Web이 토큰을 관리하도록 허용하세요
  • 프로덕션 환경에서 분산 캐시 사용
  • 토큰 획득 실패를 정상적으로 처리

Don't:

  • 직접 토큰 저장
  • 로그 액세스 토큰
  • 클라이언트 쪽 코드에 토큰 보내기

오류를 처리하십시오.

인증 및 API 호출 실패에 대한 강력한 오류 처리를 구현합니다.

Do:

  • 동의 예외를 포착하여 처리
  • 사용자에게 명확한 오류 메시지 제공
  • 디버깅에 대한 로그 오류

Don't:

  • 사용자에게 토큰 오류 노출
  • 알림 없이 조용히 API 호출 실패
  • 인증 예외 무시

일반적인 문제 해결

자주 발생하는 인증 오류에 대한 솔루션을 검토하세요.

문제: "AADSTS65001: 사용자 또는 관리자가 동의하지 않았습니다."

원인: 사용자가 필요한 범위에 동의하지 않았습니다.

Solution:

catch (MicrosoftIdentityWebChallengeUserException ex)
{
    // Redirect to consent page
    return Challenge(
        new AuthenticationProperties { RedirectUri = Request.Path },
        OpenIdConnectDefaults.AuthenticationScheme);
}

문제: "AADSTS50076: 다단계 인증 필요"

원인: 사용자는 MFA를 완료해야 합니다.

Solution:

catch (MsalUiRequiredException)
{
    // Redirect user to sign in with MFA
    return Challenge(OpenIdConnectDefaults.AuthenticationScheme);
}

문제: 앱 다시 시작에서 토큰이 유지되지 않음

원인: 메모리 내 캐시 사용.

Solution: 분산 캐시(Redis, SQL Server 또는 Cosmos DB)로 전환합니다.

문제: 다운스트림 API에서 401 권한 없음

가능한 원인:

  • 잘못된 범위 요청됨
  • 앱 등록에 API 권한이 부여되지 않음
  • 토큰 만료됨

Solution:

  1. appsettings.json 범위가 API 요구 사항과 일치하는지 확인
  2. 앱 등록에 API 권한이 있는지 확인
  3. 토큰이 캐시되고 새로 고쳐지는지 확인

자세한 진단을 위해서는 상관 ID, 토큰 캐시 디버깅 및 포괄적인 문제 해결 패턴이 포함된 로깅 및 진단 가이드를 참조하세요.


성능 최적화

토큰 캐싱 전략 계획

배포 토폴로지와 일치하는 캐싱 전략을 선택합니다.

  • 다중 서버 배포에 분산 캐시 사용
  • 적절한 캐시 만료 구성
  • 캐시 성능 모니터하기

토큰 요청 최소화

Microsoft. Identity.Web은 토큰을 자동으로 캐시합니다. 다음 예제의 두 호출은 모두 동일한 캐시된 토큰을 다시 사용합니다.

// Bad: Multiple token acquisitions
var profile = await _downstreamApi.GetForUserAsync<Profile>(
    "API",
    options => options.RelativePath = "profile");
var settings = await _downstreamApi.GetForUserAsync<Settings>(
    "API",
    options => options.RelativePath = "settings");

// Good: Single token, multiple calls (token is cached)
// Both calls use the same cached token
var profile = await _downstreamApi.GetForUserAsync<Profile>(
    "API",
    options => options.RelativePath = "profile");
var settings = await _downstreamApi.GetForUserAsync<Settings>(
    "API",
    options => options.RelativePath = "settings");

병렬 API 호출

여러 다운스트림 API를 동시에 호출하여 전체 대기 시간을 줄입니다.

// Call multiple APIs in parallel
var profileTask = _downstreamApi.GetForUserAsync<Profile>(
    "API1",
    options => options.RelativePath = "profile");
var settingsTask = _downstreamApi.GetForUserAsync<Settings>(
    "API2",
    options => options.RelativePath = "settings");

await Task.WhenAll(profileTask, settingsTask);

var profile = profileTask.Result;
var settings = settingsTask.Result;

관련 시나리오에 대한 추가 지침을 찾습니다.