Implementare l'autorizzazione nelle API Web con Microsoft. Identity.Web

In questo articolo si implementa l'autorizzazione nelle API Web ASP.NET Core usando Microsoft. Identity.Web. Verranno convalidati gli ambiti (autorizzazioni delegate) e le autorizzazioni dell'app (autorizzazioni dell'applicazione ) per controllare l'accesso alle risorse protette. Gli esempi utilizzano Microsoft Entra ID come provider di identità.

Comprendere i concetti relativi all'autorizzazione

In questa sezione vengono illustrate le differenze principali tra autenticazione e autorizzazione e viene descritto cosa Microsoft.Identity.Web convalida nei token di accesso.

Autenticazione e autorizzazione

Concetto Scopo Result
Autenticazione Verificare l'identità 401 Non autorizzato se ha esito negativo
Autorizzazione Verificare le autorizzazioni 403 Proibito se non sufficiente

Cosa viene convalidato

Quando un'API Web riceve un token di accesso, Microsoft. Identity.Web convalida:

  1. Firma del token : proviene da un'autorità attendibile?
  2. Destinatari dei token : è destinato a questa API?
  3. Scadenza del token : è ancora valida?
  4. Ambiti/Ruoli - L'app client e il soggetto (utente) hanno le giuste autorizzazioni?

Questa guida è incentrata sul numero 4: convalida degli ambiti e delle autorizzazioni dell'app.

Ambiti (autorizzazioni delegate)

Gli ambiti si applicano quando un utente delega l'autorizzazione a un'app per agire per loro conto(ad esempio, un'API Web chiamata per conto di un utente connesso).

Dettagli Value
Dichiarazione di token scp o scope (applicazione client); roles (utente)
Valori di esempio "access_as_user", "User.Read", "Files.ReadWrite"

Autorizzazioni per le app (autorizzazioni dell'applicazione)

Le autorizzazioni dell'app si applicano quando un'app chiama l'API Web come se stessa senza contesto utente, ad esempio un daemon o un servizio in background usando le credenziali client.

Dettagli Value
Dichiarazione del token roles
Valori di esempio "Mail.Read.All", "User.Read.All"

Convalidare gli ambiti con RequiredScope

L'attributo RequiredScope verifica che il token di accesso contenga almeno uno degli ambiti specificati. Usare questo attributo quando l'API gestisce solo le richieste delegate dall'utente.

Configurare la convalida dell'ambito

Seguire questa procedura per abilitare la convalida dell'ambito nell'API.

1. Abilitare l'autorizzazione nell'API:

Aggiungere servizi di autenticazione e autorizzazione alla pipeline dell'applicazione:

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

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));

builder.Services.AddAuthorization(); // Required for authorization

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization(); // Must be after UseAuthentication
app.MapControllers();

app.Run();

2. Proteggere controller o azioni:

Applicare gli [Authorize] attributi e [RequiredScope] al controller o alle singole azioni:

using Microsoft.AspNetCore.Authorization;
using Microsoft.Identity.Web.Resource;

[Authorize]
[RequiredScope("access_as_user")]
public class TodoListController : ControllerBase
{
    [HttpGet]
    public IActionResult GetTodos()
    {
        // Only accessible if token has "access_as_user" scope
        return Ok(new[] { "Todo 1", "Todo 2" });
    }
}

Applicare i modelli di ambito

Scegliere il modello più adatto alla gestione degli ambiti nell'applicazione.

Modello 1: Ambiti codificati in modo rigido

Usare questo modello quando gli ambiti sono fissi e noti in fase di sviluppo.

[Authorize]
[RequiredScope("access_as_user")]
public class TodoListController : ControllerBase
{
    // All actions require "access_as_user" scope
}

Per accettare uno qualsiasi di più ambiti, elencarli come parametri:

[Authorize]
[RequiredScope("read", "write", "admin")]
public class TodoListController : ControllerBase
{
    // Token must have "read" OR "write" OR "admin"
}

Modello 2: Ambiti dalla configurazione

Usare questo modello quando gli ambiti devono essere configurabili per ogni ambiente. Definire gli ambiti nel file di configurazione:

appsettings.json:

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "your-tenant-id",
    "ClientId": "your-api-client-id",
    "Scopes": "access_as_user read write"
  }
}

Fare riferimento alla chiave di configurazione nel controller:

[Authorize]
[RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes")]
public class TodoListController : ControllerBase
{
    // Scopes read from configuration
}

Questo approccio consente di modificare gli ambiti senza ricompilare.

Modello 3: Ambiti a livello di azione

Usare questo modello quando diverse azioni richiedono autorizzazioni diverse. Applicare [RequiredScope] ai singoli metodi di azione:

[Authorize]
public class TodoListController : ControllerBase
{
    [HttpGet]
    [RequiredScope("read")]
    public IActionResult GetTodos()
    {
        return Ok(todos);
    }

    [HttpPost]
    [RequiredScope("write")]
    public IActionResult CreateTodo([FromBody] Todo todo)
    {
        // Only tokens with "write" scope can create
        return CreatedAtAction(nameof(GetTodos), todo);
    }

    [HttpDelete("{id}")]
    [RequiredScope("admin")]
    public IActionResult DeleteTodo(int id)
    {
        // Only tokens with "admin" scope can delete
        return NoContent();
    }
}

Informazioni sul flusso di convalida

Quando arriva una richiesta, il middleware lo elabora nell'ordine seguente:

  1. ASP.NET Core middleware di autenticazione convalida il token
  2. RequiredScopel'attributo verifica la presenza dell'attestazione scp o scope
  3. Se il token contiene almeno un ambito corrispondente, la richiesta procede.
  4. Se non viene trovato alcun ambito corrispondente, l'API restituisce una risposta 403 Accesso negato.

L'esempio seguente mostra una risposta di errore tipica:

{
  "error": "insufficient_scope",
  "error_description": "The token does not have the required scope 'access_as_user'."
}

Convalidare le autorizzazioni dell'app con RequiredScopeOrAppPermission

L'attributo RequiredScopeOrAppPermission convalida gli ambiti (delegati) o le autorizzazioni dell'app (applicazione). Usare questo attributo quando l'API serve app delegate dall'utente e app daemon/service dallo stesso endpoint.

Se l'API gestisce solo le richieste delegate dall'utente, usare RequiredScope invece .

Configurare la convalida dell'ambito o dell'autorizzazione dell'app

Applicare l'attributo per accettare uno dei due tipi di token:

using Microsoft.Identity.Web.Resource;

[Authorize]
[RequiredScopeOrAppPermission(
    AcceptedScope = new[] { "access_as_user" },
    AcceptedAppPermission = new[] { "TodoList.ReadWrite.All" }
)]
public class TodoListController : ControllerBase
{
    [HttpGet]
    public IActionResult GetTodos()
    {
        // Accessible with EITHER:
        // - User-delegated token with "access_as_user" scope, OR
        // - App-only token with "TodoList.ReadWrite.All" app permission
        return Ok(todos);
    }
}

Configurare le autorizzazioni dell'app dalle impostazioni

Archiviare gli ambiti e le autorizzazioni dell'app nella configurazione per modificarli senza ricompilare.

appsettings.json:

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "your-tenant-id",
    "ClientId": "your-api-client-id",
    "Scopes": "access_as_user",
    "AppPermissions": "TodoList.ReadWrite.All TodoList.Admin"
  }
}

Fare riferimento alle chiavi di configurazione nel controller:

[Authorize]
[RequiredScopeOrAppPermission(
    RequiredScopesConfigurationKey = "AzureAd:Scopes",
    RequiredAppPermissionsConfigurationKey = "AzureAd:AppPermissions"
)]
public class TodoListController : ControllerBase
{
    // Scopes and app permissions from configuration
}

Confrontare le differenze di dichiarazione del token

La tabella seguente illustra come le dichiarazioni differiscono tra i token con delega utente e solo per app.

Tipo di token Richiesta di rimborso Valore di esempio
Delegata dall'utente scp oppure scope "access_as_user User.Read"
Solo app roles ["TodoList.ReadWrite.All"]

L'esempio seguente mostra un token delegato dall'utente:

{
  "aud": "api://your-api-client-id",
  "iss": "https://login.microsoftonline.com/.../v2.0",
  "scp": "access_as_user",
  "sub": "user-object-id",
  ...
}

L'esempio seguente mostra un token solo per app:

{
  "aud": "api://your-api-client-id",
  "iss": "https://login.microsoftonline.com/.../v2.0",
  "roles": ["TodoList.ReadWrite.All"],
  "sub": "app-object-id",
  ...
}

Creare criteri di autorizzazione

Per scenari di autorizzazione complessi, usare ASP.NET Core criteri di autorizzazione. I criteri consentono di centralizzare le regole, combinare più requisiti e scrivere logica di autorizzazione testabile.

Benefit Descrizione
Logica centralizzata Definire le regole di autorizzazione una sola volta, riutilizzare ovunque
Componibile Combinare più requisiti (ambiti e attestazioni + logica personalizzata)
Verificabile Logica di autorizzazione unit test più semplice
Flessibile Requisiti personalizzati oltre la convalida dell'ambito

Modello 1: Definire un criterio con RequireScope

Definire politiche denominate che richiedono ambiti specifici, quindi riferirle nei controller.

using Microsoft.Identity.Web;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("TodoReadPolicy", policyBuilder =>
    {
        policyBuilder.RequireScope("read", "access_as_user");
    });

    options.AddPolicy("TodoWritePolicy", policyBuilder =>
    {
        policyBuilder.RequireScope("write", "admin");
    });
});

var app = builder.Build();

Applicare i criteri alle azioni del controller:

[Authorize]
public class TodoListController : ControllerBase
{
    [HttpGet]
    [Authorize(Policy = "TodoReadPolicy")]
    public IActionResult GetTodos()
    {
        return Ok(todos);
    }

    [HttpPost]
    [Authorize(Policy = "TodoWritePolicy")]
    public IActionResult CreateTodo([FromBody] Todo todo)
    {
        return CreatedAtAction(nameof(GetTodos), todo);
    }
}

Modello 2: Definire un criterio con ScopeAuthorizationRequirement

Usare ScopeAuthorizationRequirement per requisiti di ambito più espliciti:

using Microsoft.Identity.Web;
using Microsoft.Identity.Web.Resource;

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("CustomPolicy", policyBuilder =>
    {
        policyBuilder.AddRequirements(
            new ScopeAuthorizationRequirement(new[] { "access_as_user" })
        );
    });
});

Modello 3: Impostare un criterio predefinito

Impostare automaticamente un criterio predefinito che si applica a tutti gli [Authorize] attributi:

builder.Services.AddAuthorization(options =>
{
    var defaultPolicy = new AuthorizationPolicyBuilder()
        .RequireScope("access_as_user")
        .Build();

    options.DefaultPolicy = defaultPolicy;
});

Ogni [Authorize] attributo richiede ora l'ambito access_as_user :

[Authorize] // Automatically requires "access_as_user" scope
public class TodoListController : ControllerBase
{
    // All actions protected by default policy
}

Modello 4: Combinare più requisiti

Combinare i requisiti di ambito, ruolo e autenticazione in un singolo criterio:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminPolicy", policyBuilder =>
    {
        policyBuilder.RequireScope("admin");
        policyBuilder.RequireRole("Admin"); // Also check role claim
        policyBuilder.RequireAuthenticatedUser();
    });
});

Modello 5: Creare una policy dalla configurazione

Caricare gli scopi dalla configurazione per mantenere le policy specifiche per l'ambiente.

var requiredScopes = builder.Configuration["AzureAd:Scopes"]?.Split(' ');

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ApiAccessPolicy", policyBuilder =>
    {
        if (requiredScopes != null)
        {
            policyBuilder.RequireScope(requiredScopes);
        }
    });
});

Filtrare le richieste in base al tenant

Limitare l'accesso API ai token da specifici tenant di Microsoft Entra. Ciò è utile quando l'API multi-tenant deve accettare solo le richieste provenienti dai tenant dei clienti approvati.

Limitare l'accesso ai tenant consentiti

Definire un criterio che controlla l'attestazione ID tenant in base a un elenco di elementi consentiti:

builder.Services.AddAuthorization(options =>
{
    string[] allowedTenants =
    {
        "14c2f153-90a7-4689-9db7-9543bf084dad", // Contoso tenant
        "af8cc1a0-d2aa-4ca7-b829-00d361edb652", // Fabrikam tenant
        "979f4440-75dc-4664-b2e1-2cafa0ac67d1"  // Northwind tenant
    };

    options.AddPolicy("AllowedTenantsOnly", policyBuilder =>
    {
        policyBuilder.RequireClaim(
            "http://schemas.microsoft.com/identity/claims/tenantid",
            allowedTenants
        );
    });

    // Apply to all endpoints by default
    options.DefaultPolicy = options.GetPolicy("AllowedTenantsOnly");
});

Configurare il filtro dei tenant dalle impostazioni

Archiviare gli ID tenant consentiti nella configurazione per gestirli senza modifiche al codice.

appsettings.json:

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "ClientId": "your-api-client-id",
    "AllowedTenants": [
      "14c2f153-90a7-4689-9db7-9543bf084dad",
      "af8cc1a0-d2aa-4ca7-b829-00d361edb652"
    ]
  }
}

Leggere l'elenco dei tenant e creare la politica all'avvio:

var allowedTenants = builder.Configuration.GetSection("AzureAd:AllowedTenants")
    .Get<string[]>();

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AllowedTenantsOnly", policyBuilder =>
    {
        policyBuilder.RequireClaim(
            "http://schemas.microsoft.com/identity/claims/tenantid",
            allowedTenants ?? Array.Empty<string>()
        );
    });
});

Combinare gli ambiti con il filtraggio dei tenant

Creare criteri che richiedono sia un ambito valido che un tenant approvato:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("SecureApiAccess", policyBuilder =>
    {
        // Require specific scope
        policyBuilder.RequireScope("access_as_user");

        // AND require specific tenant
        policyBuilder.RequireClaim(
            "http://schemas.microsoft.com/identity/claims/tenantid",
            allowedTenants
        );
    });
});

Seguire le migliori pratiche

Applicare queste raccomandazioni per creare logica di autorizzazione sicura e gestibile.

Cose da fare

1. Associa sempre [Authorize] alla convalida dell'ambito:

[Authorize] // Authentication
[RequiredScope("access_as_user")] // Authorization
public class MyController : ControllerBase { }

2. Usare la configurazione per ambiti specifici dell'ambiente:

[RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes")]

3. Applicare privilegi minimi:

[HttpGet]
[RequiredScope("read")] // Only read permission needed

[HttpPost]
[RequiredScope("write")] // Write permission for modifications

4. Usare i criteri per l'autorizzazione complessa:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policy =>
    {
        policy.RequireScope("admin");
        policy.RequireClaim("department", "IT");
    });
});

5. Abilitare risposte di errore dettagliate nello sviluppo:

if (builder.Environment.IsDevelopment())
{
    Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
}

Cose da non fare

1. Non ignorare [Authorize] quando si usa RequiredScope:

//  Wrong - RequiredScope won't work without [Authorize]
[RequiredScope("access_as_user")]
public class MyController : ControllerBase { }

//  Correct
[Authorize]
[RequiredScope("access_as_user")]
public class MyController : ControllerBase { }

2. Non utilizzare gli ID tenant codificati in modo rigido nell'ambiente di produzione:

//  Wrong
policyBuilder.RequireClaim("tid", "14c2f153-90a7-4689-9db7-9543bf084dad");

//  Better - use configuration
var tenants = Configuration.GetSection("AllowedTenants").Get<string[]>();
policyBuilder.RequireClaim("tid", tenants);

3. Non confondere gli ambiti con i ruoli:

//  Wrong - This checks roles claim, not scopes
[RequiredScope("Admin")] // "Admin" is typically a role, not a scope

//  Correct
[RequiredScope("access_as_user")] // Scope
[Authorize(Roles = "Admin")] // Role

4. Non esporre informazioni sull'ambito sensibile nei messaggi di errore di produzione:

Configurare i livelli di registrazione e la gestione degli errori appropriati per gli ambienti di produzione.


Risolvere i problemi di autorizzazione

Usare le indicazioni seguenti per diagnosticare i problemi di autorizzazione comuni.

403 Accesso vietato - Ambito mancante

Errore: L'API restituisce 403 anche con un token valido.

Diagnosi:

  1. Decodificare il token in jwt.ms.
  2. Controllare l'attestazione scp o scope.
  3. Verificare che il valore corrisponda all'attributo RequiredScope .

Soluzione:

  • Assicurarsi che l'app client richieda l'ambito corretto durante l'acquisizione del token.
  • Verificare che l'ambito sia esposto nella registrazione dell'applicazione API in Microsoft Entra.
  • Concedere il consenso amministratore, se necessario.

RequiredScope non funziona

Sintomo: L'attributo sembra essere ignorato.

Controllare:

  1. L'attributo [Authorize] è stato aggiunto?
  2. Viene app.UseAuthorization() chiamato dopo app.UseAuthentication()?
  3. È services.AddAuthorization() registrato?

Chiave di configurazione non trovata

Errore: La convalida dell'ambito fallisce silenziosamente.

Controllare:

{
  "AzureAd": {
    "Scopes": "access_as_user" // Matches RequiredScopesConfigurationKey
  }
}

Verificare che il percorso di configurazione corrisponda esattamente.