Condividi tramite


Chiamata di API downstream dalle app Web

Questa guida illustra come chiamare le API downstream da applicazioni Web ASP.NET Core e OWIN usando Microsoft. Identity.Web. Nelle app Web si acquisiscono i token per conto dell'utente connesso per chiamare le API con autorizzazioni delegate.

Informazioni generali

Quando un utente accede all'applicazione Web, è possibile chiamare api downstream (Microsoft Graph, servizi Azure o API personalizzate) per loro conto. Microsoft. Identity.Web gestisce l'acquisizione dei token, la memorizzazione nella cache e l'aggiornamento automatico.

Flusso del token utente

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

Prerequisiti

  • App Web configurata con l'autenticazione OpenID Connect
  • Accesso utente funzionante
  • Registrazione dell'app con autorizzazioni API configurate
  • Consenso utente ottenuto (o consenso amministratore concesso)

implementazione ASP.NET Core

1. Configurare l'autenticazione e l'acquisizione di token

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. Configurare 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"]
    }
  }
}

Importante: Per le app Web che chiamano LE API downstream, sono necessarie credenziali client (certificato o segreto) oltre alla configurazione di accesso.

3. Aggiungere il supporto dell'API downstream

Opzione A: Registrare API denominate

using Microsoft.Identity.Web;

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

Option B: Usare Microsoft Graph Helper

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

4. Chiamare l'API downstream dal controller

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. Chiamare l'API downstream dalla pagina Razor

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

Uso di Microsoft Graph

Per le chiamate a Microsoft API Graph, usare il GraphServiceClient dedicato.

Setup

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

Utilizzo

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

[Authorize]
public class HomeController : Controller
{
    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 });
    }
}

Altre informazioni sull'integrazione Microsoft Graph


Uso di client Azure SDK

Per chiamare i servizi di Azure, usare MicrosoftIdentityTokenCredential:

Setup

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

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

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

Utilizzo

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

Altre informazioni sull'integrazione Azure SDK


Uso di API personalizzate con IDownstreamApi

Per le PROPRIE API REST, IDownstreamApi offre un approccio semplice basato sulla configurazione:

Configurazione

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

Utilizzo - Richiesta GET

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

Utilizzo - Richiesta POST

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

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

Utilizzo - PUT e DELETE

// 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");

Altre informazioni sulle chiamate API personalizzate


Uso di IAuthorizationHeaderProvider (avanzato)

Per il controllo massimo sulle richieste HTTP, usare IAuthorizationHeaderProvider:

Setup

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

Utilizzo

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

Altre informazioni sulla logica HTTP personalizzata


Quando si chiamano API downstream, l'applicazione potrebbe dover gestire scenari in cui è necessaria l'interazione dell'utente. Questo avviene in tre scenari principali:

  1. Consenso incrementale : richiesta di autorizzazioni aggiuntive oltre a quanto inizialmente concesso
  2. Accesso condizionale - Soddisfare i requisiti di sicurezza, ad esempio MFA, conformità dei dispositivi o criteri di posizione
  3. Rimozione della cache dei token : ripopolamento della cache dei token dopo il riavvio o la scadenza della cache dell'applicazione

Microsoft. Identity.Web offre la gestione automatica di questi scenari con codice minimo necessario.

Informazioni sul flusso

Quando Microsoft. Identity.Web rileva che l'interazione dell'utente è necessaria, genera un MicrosoftIdentityWebChallengeUserException. Il framework gestisce automaticamente questa operazione tramite l'attributo [AuthorizeForScopes] o il MicrosoftIdentityConsentAndConditionalAccessHandler servizio (per Blazor), che:

  1. Reindirizza l'utente a Microsoft Entra ID per il consenso o l'autenticazione
  2. Mantiene l'URL della richiesta originale
  3. Restituisce l'utente alla destinazione desiderata dopo aver completato il flusso
  4. Memorizza nella cache i token appena acquisiti

Prerequisiti

Per abilitare la gestione automatica del consenso, assicurarsi che Program.cs includa:

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

Controller MVC : uso di [AuthorizeForScopes]

L'attributo [AuthorizeForScopes] , impostato su controller o azioni del controller, gestisce MicrosoftIdentityWebChallengeUserException automaticamente sfidando l'utente quando sono necessarie autorizzazioni aggiuntive.

Ambiti dichiarativi

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

ambiti basati su configurazione

Archiviare gli ambiti in appsettings.json per una migliore manutenibilità:

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" ]
    }
  }
}

Controller:

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

Azure AD B2C con i flussi degli utenti

Per le applicazioni B2C con più flussi utente:

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

Pagine Razor - utilizzo di [AuthorizeForScopes]

Applica [AuthorizeForScopes] alla classe del modello di pagina:

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 : uso di MicrosoftIdentityConsentAndConditionalAccessHandler

Le applicazioni Blazor Server richiedono la gestione esplicita delle eccezioni usando il MicrosoftIdentityConsentAndConditionalAccessHandler servizio.

configurazione di Program.cs

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

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

Componente Blazor

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

Gestione manuale delle eccezioni (avanzata)

Se è necessaria una logica del flusso di consenso personalizzata, gestire MicrosoftIdentityWebChallengeUserException in modo esplicito:

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

Scenari di accesso condizionale

I criteri di accesso condizionale possono richiedere fattori di autenticazione aggiuntivi. La gestione è identica al consenso incrementale:

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

Trigger di accesso condizionale comuni:

  • Multi-Factor Authentication (MFA)
  • Requisito del dispositivo conforme
  • Posizione di rete attendibile
  • Accettazione delle condizioni per l'utilizzo
  • Requisito di modifica della password

Procedure consigliate

Utilizzare [AuthorizeForScopes] - Approccio più semplice per controller MVC e Razor Pages

Archiviare gli ambiti nella configurazione : usare ScopeKeySection = "DownstreamApis:ApiName:Scopes:0" per fare riferimento agli ambiti in appsettings.json

Applica a livello di controller : impostare gli ambiti predefiniti nel controller, eseguire l'override su azioni specifiche

Gestire le eccezioni in Blazor - Eseguire sempre il wrapping delle chiamate API con try-catch e usare ConsentHandler.HandleException()

Consentire di generare nuovamente le eccezioni : se si intercetta MicrosoftIdentityWebChallengeUserException, generarla nuovamente in modo [AuthorizeForScopes] da poterla elaborare

Testare l'accesso condizionale : verificare che l'app gestisca correttamente mfa e altri criteri della CA

Non sopprimere le eccezioni - Intercettare senza rilanciare rompe il flusso di consenso

Non memorizzare nella cache le risposte per un periodo illimitato : i token scadono; progettare per la ripetizione dell'autenticazione


Autorizzazioni statiche (consenso amministratore)

Tutte le autorizzazioni vengono richieste durante la registrazione dell'app e concesse da un amministratore tenant:

Vantaggi:

  • Gli utenti non visualizzano mai richieste di consenso
  • Obbligatorio per le app Microsoft proprietarie
  • Esperienza utente più semplice

Svantaggi:

  • Richiede il coinvolgimento dell'amministratore tenant
  • Privilegi elevati fin dall'inizio
  • Meno flessibile per scenari multi-tenant

Configuration:

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

Consenso incrementale (dinamico)

Le autorizzazioni vengono richieste in base alle esigenze durante il runtime:

Vantaggi:

  • Maggiore sicurezza (principio dei privilegi minimi)
  • Gli utenti acconsentono a ciò che usano effettivamente
  • Funziona per le app multi-tenant

Svantaggi:

  • Gli utenti possono essere interrotti con richieste di consenso
  • Richiede la gestione MicrosoftIdentityWebChallengeUserException

Raccomandazione: Usare il consenso incrementale per le applicazioni multi-tenant; usare le autorizzazioni statiche per le app aziendali proprietarie in cui è garantito il consenso dell'amministratore


Memorizzazione nella cache dei token

Microsoft. Identity.Web memorizza nella cache i token per migliorare le prestazioni e ridurre le chiamate a Microsoft Entra ID.

cache In-Memory (impostazione predefinita)

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

Usare per:

  • Sviluppo
  • Distribuzioni a server singolo
  • Base utenti di piccole dimensioni

Limitations:

  • Non condiviso tra istanze
  • Perso al riavvio dell'app
  • L'utilizzo della memoria aumenta con gli utenti
// 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();

Usare per:

  • Distribuzioni multiserver (bilanciamento del carico)
  • Scenari di disponibilità elevata
  • Base utenti di grandi dimensioni
  • Cache persistente tra riavvii

Gestione degli errori di acquisizione dei token

Eccezioni comuni

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

Degradazione controllata

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

Implementazione di OWIN (.NET Framework)

Per le applicazioni Web basate su OWIN in .NET Framework:

1. Installare i pacchetti

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

2. Configurare l'avvio

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. Chiamare l'API downstream

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: supporto OWIN presenta alcune differenze rispetto a ASP.NET Core. Per informazioni dettagliate, vedere la documentazione di OWIN .


Procedure consigliate per la sicurezza

Gestione dell'ambito

Do:

  • Richiedere solo gli ambiti di autorizzazione necessari
  • Usare il consenso incrementale per le funzionalità avanzate
  • Documentare gli ambiti necessari nell'app

Don't:

  • Richiedere autorizzazioni non necessarie anticipatamente
  • Richiedere autorizzazioni solo amministratore senza fornire una giustificazione
  • Si supponga che tutti gli ambiti vengano concessi

Gestione dei token

Do:

  • Lasciare Microsoft. Identity.Web gestisce i token
  • Usare la cache distribuita nell'ambiente di produzione
  • Gestire gli errori di acquisizione dei token normalmente

Don't:

  • Archiviare i token manualmente
  • Token di accesso ai log
  • Inviare token al codice lato client

Gestione degli errori

Do:

  • Rilevare e gestire le eccezioni di consenso
  • Fornire messaggi di errore chiari agli utenti
  • Errori di log per il debug

Don't:

  • Esporre gli errori del token agli utenti
  • Consenti alle chiamate API di fallire senza notifiche visibili per l'utente
  • Ignorare le eccezioni di autenticazione

Risoluzione dei problemi

Problema: "AADSTS65001: l'utente o l'amministratore non ha acconsentito"

Causa: L'utente non ha acconsentito agli ambiti obbligatori.

Soluzione:

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

Problema: "AADSTS50076: Autenticazione a più fattori richiesta"

Causa: L'utente deve completare l'autenticazione a più fattori (MFA).

Soluzione:

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

Problema: i token non vengono mantenuti tra i riavvii dell'app

Causa: Uso della cache in memoria.

Solution: Passare alla cache distribuita (Redis, SQL Server o Cosmos DB).

Problema: 401 Non autorizzato dall'API downstream

Possibili cause:

  • Ambiti non corretti richiesti
  • Autorizzazione API non concessa nella registrazione dell'app
  • Token scaduto

Soluzione:

  1. Verificare che gli scopi in appsettings.json soddisfino i requisiti dell'API.
  2. Verificare che la registrazione dell'app disponga delle autorizzazioni API
  3. Verificare che i token vengano memorizzati nella cache e aggiornati

Per una diagnostica dettagliata: Vedere La Guida alla registrazione e alla diagnostica per gli ID di correlazione, il debug della cache dei token e i modelli di risoluzione dei problemi completi.


Considerazioni sulle prestazioni

Strategia di memorizzazione nella cache dei token

  • Usare la cache distribuita per le distribuzioni multiserver
  • Configurare la scadenza della cache appropriata
  • Monitorare le prestazioni della cache

Ridurre al minimo le richieste di token

// 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");

Chiamate API parallele

// 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;