Autenticazione JWT per l'API Web ASP.NET


264

Sto cercando di supportare il token bearer JWT (token Web JSON) nella mia applicazione API Web e mi sto perdendo.

Vedo il supporto per .NET Core e per le applicazioni OWIN.
Attualmente sto ospitando la mia applicazione in IIS.

Come posso ottenere questo modulo di autenticazione nella mia applicazione? È possibile utilizzare la <authentication>configurazione in modo simile al modo in cui utilizzo i moduli / l'autenticazione di Windows?

Risposte:


611

Ho risposto a questa domanda: come proteggere un'API Web ASP.NET 4 anni fa usando HMAC.

Ora, molte cose sono cambiate in termini di sicurezza, in particolare JWT sta diventando popolare. Qui, proverò a spiegare come usare JWT nel modo più semplice e semplice che posso, quindi non ci perderemo dalla giungla di OWIN, Oauth2, ASP.NET Identity ... :).

Se non conosci il token JWT, devi dare un'occhiata a:

https://tools.ietf.org/html/rfc7519

Fondamentalmente, un token JWT assomiglia a:

<base64-encoded header>.<base64-encoded claims>.<base64-encoded signature>

Esempio:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1NzI0LCJleHAiOjE0Nzc1NjY5MjQsImlhdCI6MTQ3NzU2NTcyNH0.6MzD1VwA5AcOcajkFyKhLYybr3h13iZjDyHm9zysDFQ

Un token JWT ha tre sezioni:

  1. Intestazione: formato JSON codificato in Base64
  2. Reclami: formato JSON codificato in Base64.
  3. Firma: creata e firmata in base all'intestazione e alle attestazioni codificate in Base64.

Se si utilizza il sito Web jwt.io con il token sopra, è possibile decodificare il token e vederlo come di seguito:

inserisci qui la descrizione dell'immagine

Tecnicamente, JWT utilizza la firma firmata dalle intestazioni e le attestazioni con l'algoritmo di sicurezza specificato nelle intestazioni (esempio: HMACSHA256). Pertanto, è necessario che JWT sia trasferito su HTTP se si memorizzano informazioni sensibili in attestazioni.

Ora, per utilizzare l'autenticazione JWT, non è necessario un middleware OWIN se si dispone di un sistema Web Api legacy. Il semplice concetto è come fornire il token JWT e come convalidare il token quando arriva la richiesta. Questo è tutto.

Tornando alla demo, per mantenere il token JWT leggero, conservo solo usernamee expiration timein JWT. Ma in questo modo, è necessario ricostruire la nuova identità locale (principale) per aggiungere ulteriori informazioni come: ruoli .. se si desidera eseguire l'autorizzazione del ruolo. Ma, se vuoi aggiungere ulteriori informazioni in JWT, dipende da te: è molto flessibile.

Invece di utilizzare il middleware OWIN, è possibile fornire semplicemente un endpoint token JWT utilizzando l'azione del controller:

public class TokenController : ApiController
{
    // This is naive endpoint for demo, it should use Basic authentication
    // to provide token or POST request
    [AllowAnonymous]
    public string Get(string username, string password)
    {
        if (CheckUser(username, password))
        {
            return JwtManager.GenerateToken(username);
        }

        throw new HttpResponseException(HttpStatusCode.Unauthorized);
    }

    public bool CheckUser(string username, string password)
    {
        // should check in the database
        return true;
    }
}

Questa è un'azione ingenua; in produzione è necessario utilizzare una richiesta POST o un endpoint di autenticazione di base per fornire il token JWT.

Come generare il token in base username?

Puoi usare il pacchetto NuGet chiamato System.IdentityModel.Tokens.Jwtda Microsoft per generare il token, o anche un altro pacchetto, se lo desideri. Nella demo, utilizzo HMACSHA256con SymmetricKey:

/// <summary>
/// Use the below code to generate symmetric Secret Key
///     var hmac = new HMACSHA256();
///     var key = Convert.ToBase64String(hmac.Key);
/// </summary>
private const string Secret = "db3OIsj+BXE9NZDy0t8W3TcNekrF+2d/1sFnWG4HnV8TZY30iTOdtVWJG8abWvB1GlOgJuQZdcF2Luqm/hccMw==";

public static string GenerateToken(string username, int expireMinutes = 20)
{
    var symmetricKey = Convert.FromBase64String(Secret);
    var tokenHandler = new JwtSecurityTokenHandler();

    var now = DateTime.UtcNow;
    var tokenDescriptor = new SecurityTokenDescriptor
    {
        Subject = new ClaimsIdentity(new[]
        {
            new Claim(ClaimTypes.Name, username)
        }),

        Expires = now.AddMinutes(Convert.ToInt32(expireMinutes)),

        SigningCredentials = new SigningCredentials(
            new SymmetricSecurityKey(symmetricKey), 
            SecurityAlgorithms.HmacSha256Signature)
    };

    var stoken = tokenHandler.CreateToken(tokenDescriptor);
    var token = tokenHandler.WriteToken(stoken);

    return token;
}

L'endpoint per fornire il token JWT è terminato. Ora, come convalidare il JWT quando arriva la richiesta? Nella demo ho creato quello JwtAuthenticationAttributeche eredita da IAuthenticationFilter(maggiori dettagli sul filtro di autenticazione qui ).

Con questo attributo, puoi autenticare qualsiasi azione: devi solo mettere questo attributo su quell'azione.

public class ValueController : ApiController
{
    [JwtAuthentication]
    public string Get()
    {
        return "value";
    }
}

È inoltre possibile utilizzare il middleware OWIN o DelegateHander se si desidera convalidare tutte le richieste in arrivo per la propria API Web (non specifica del controller o dell'azione)

Di seguito è riportato il metodo principale dal filtro di autenticazione:

private static bool ValidateToken(string token, out string username)
{
    username = null;

    var simplePrinciple = JwtManager.GetPrincipal(token);
    var identity = simplePrinciple.Identity as ClaimsIdentity;

    if (identity == null)
        return false;

    if (!identity.IsAuthenticated)
        return false;

    var usernameClaim = identity.FindFirst(ClaimTypes.Name);
    username = usernameClaim?.Value;

    if (string.IsNullOrEmpty(username))
       return false;

    // More validate to check whether username exists in system

    return true;
}

protected Task<IPrincipal> AuthenticateJwtToken(string token)
{
    string username;

    if (ValidateToken(token, out username))
    {
        // based on username to get more information from database 
        // in order to build local identity
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Name, username)
            // Add more claims if needed: Roles, ...
        };

        var identity = new ClaimsIdentity(claims, "Jwt");
        IPrincipal user = new ClaimsPrincipal(identity);

        return Task.FromResult(user);
    }

    return Task.FromResult<IPrincipal>(null);
}

Il flusso di lavoro è, utilizzando la libreria JWT (pacchetto NuGet sopra) per convalidare il token JWT e quindi tornare indietro ClaimsPrincipal. Puoi eseguire più convalide come verificare se l'utente esiste sul tuo sistema e aggiungere altre convalide personalizzate se lo desideri. Il codice per convalidare il token JWT e recuperare l'entità:

public static ClaimsPrincipal GetPrincipal(string token)
{
    try
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var jwtToken = tokenHandler.ReadToken(token) as JwtSecurityToken;

        if (jwtToken == null)
            return null;

        var symmetricKey = Convert.FromBase64String(Secret);

        var validationParameters = new TokenValidationParameters()
        {
            RequireExpirationTime = true,
            ValidateIssuer = false,
            ValidateAudience = false,
            IssuerSigningKey = new SymmetricSecurityKey(symmetricKey)
        };

        SecurityToken securityToken;
        var principal = tokenHandler.ValidateToken(token, validationParameters, out securityToken);

        return principal;
    }
    catch (Exception)
    {
        //should write log
        return null;
    }
}

Se il token JWT è convalidato e l'entità viene restituita, è necessario creare una nuova identità locale e inserire ulteriori informazioni per verificare l'autorizzazione del ruolo.

Ricorda di aggiungere config.Filters.Add(new AuthorizeAttribute());(autorizzazione predefinita) a livello globale al fine di prevenire qualsiasi richiesta anonima alle tue risorse.

Puoi usare Postman per testare la demo:

Token di richiesta (ingenuo come ho detto sopra, solo per la demo):

GET http://localhost:{port}/api/token?username=cuong&password=1

Inserisci il token JWT nell'intestazione per una richiesta autorizzata, ad esempio:

GET http://localhost:{port}/api/value

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1MjU4LCJleHAiOjE0Nzc1NjY0NTgsImlhdCI6MTQ3NzU2NTI1OH0.dSwwufd4-gztkLpttZsZ1255oEzpWCJkayR_4yvNL1s

La demo è disponibile qui: https://github.com/cuongle/WebApi.Jwt


5
Ben spiegato da @Cuong Le, ma mi piace aggiungere altro: Se stai usando OWIN controlla UseJwtBearerAuthentication disponibile in Microsoft.Owin.Security.Jwt puoi usare questo middleware owin sul WebAPI per validare automaticamente ogni richiesta in arrivo. utilizzare la classe di avvio owin per registrare il middleware
Jek,

5
@AmirPopovich Non è necessario impostare il token sulla risposta, il token deve essere archiviato da qualche altra parte sul lato client, per il web, è possibile inserire nella memoria locale, ogni volta che si invia una richiesta HTTP, inserire il token nell'intestazione.
cuongle

7
Wow, questa è la spiegazione più semplice che ho visto da molto tempo. +100 se potessi
gyozo kudor

4
@Homam: Mi dispiace per questa risposta tardiva, il modo migliore per generare è: varhmac = new HMACSHA256();var key = Convert.ToBase64String(hmac.Key);
cuongle

4
Chiunque utilizzi il codice demo dal repository di CuongLe noterà che esiste un bug in cui le richieste senza intestazione di autorizzazione non vengono gestite, il che significa che qualsiasi query senza una può passare (un endpoint non così sicuro!). C'è una richiesta pull da @magicleon per risolvere questo problema qui: github.com/cuongle/WebApi.Jwt/pull/4
Chucky

11

Sono riuscito a raggiungerlo con il minimo sforzo (semplice come con ASP.NET Core).

Per questo uso il Startup.csfile e la Microsoft.Owin.Security.Jwtlibreria OWIN .

Affinché l'app colpisca, Startup.csdobbiamo modificare Web.config:

<configuration>
  <appSettings>
    <add key="owin:AutomaticAppStartup" value="true" />
    ...

Ecco come Startup.csdovrebbe apparire:

using MyApp.Helpers;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Jwt;
using Owin;

[assembly: OwinStartup(typeof(MyApp.App_Start.Startup))]

namespace MyApp.App_Start
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.UseJwtBearerAuthentication(
                new JwtBearerAuthenticationOptions
                {
                    AuthenticationMode = AuthenticationMode.Active,
                    TokenValidationParameters = new TokenValidationParameters()
                    {
                        ValidAudience = ConfigHelper.GetAudience(),
                        ValidIssuer = ConfigHelper.GetIssuer(),
                        IssuerSigningKey = ConfigHelper.GetSymmetricSecurityKey(),
                        ValidateLifetime = true,
                        ValidateIssuerSigningKey = true
                    }
                });
        }
    }
}

Molti di voi usano ASP.NET Core al giorno d'oggi, quindi come potete vedere non differisce molto da quello che abbiamo lì.

In primo luogo mi ha lasciato perplesso, stavo cercando di implementare provider personalizzati, ecc. Ma non mi aspettavo che fosse così semplice. OWINsolo rocce!

Solo una cosa da menzionare: dopo aver abilitato la NSWaglibreria di avvio OWIN ha smesso di funzionare per me (ad esempio, alcuni di voi potrebbero voler generare automaticamente proxy HTTP dattiloscritti per l'app Angular).

Anche la soluzione è stata molto semplice: ho sostituito NSWagcon Swashbucklee non ho avuto ulteriori problemi.


Ok, ora condividi il ConfigHelpercodice:

public class ConfigHelper
{
    public static string GetIssuer()
    {
        string result = System.Configuration.ConfigurationManager.AppSettings["Issuer"];
        return result;
    }

    public static string GetAudience()
    {
        string result = System.Configuration.ConfigurationManager.AppSettings["Audience"];
        return result;
    }

    public static SigningCredentials GetSigningCredentials()
    {
        var result = new SigningCredentials(GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256);
        return result;
    }

    public static string GetSecurityKey()
    {
        string result = System.Configuration.ConfigurationManager.AppSettings["SecurityKey"];
        return result;
    }

    public static byte[] GetSymmetricSecurityKeyAsBytes()
    {
        var issuerSigningKey = GetSecurityKey();
        byte[] data = Encoding.UTF8.GetBytes(issuerSigningKey);
        return data;
    }

    public static SymmetricSecurityKey GetSymmetricSecurityKey()
    {
        byte[] data = GetSymmetricSecurityKeyAsBytes();
        var result = new SymmetricSecurityKey(data);
        return result;
    }

    public static string GetCorsOrigins()
    {
        string result = System.Configuration.ConfigurationManager.AppSettings["CorsOrigins"];
        return result;
    }
}

Un altro aspetto importante: ho inviato il token JWT tramite l' intestazione di autorizzazione , quindi il codice dattiloscritto mi cerca come segue:

(il codice seguente è generato da NSWag )

@Injectable()
export class TeamsServiceProxy {
    private http: HttpClient;
    private baseUrl: string;
    protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;

    constructor(@Inject(HttpClient) http: HttpClient, @Optional() @Inject(API_BASE_URL) baseUrl?: string) {
        this.http = http;
        this.baseUrl = baseUrl ? baseUrl : "https://localhost:44384";
    }

    add(input: TeamDto | null): Observable<boolean> {
        let url_ = this.baseUrl + "/api/Teams/Add";
        url_ = url_.replace(/[?&]$/, "");

        const content_ = JSON.stringify(input);

        let options_ : any = {
            body: content_,
            observe: "response",
            responseType: "blob",
            headers: new HttpHeaders({
                "Content-Type": "application/json", 
                "Accept": "application/json",
                "Authorization": "Bearer " + localStorage.getItem('token')
            })
        };

Vedi parte delle intestazioni - "Authorization": "Bearer " + localStorage.getItem('token')


I replaced NSWag with Swashbuckle and didn't have any further issues.Swashbuckle ha la capacità di generare file dattiloscritti o è qualcosa che hai aggiunto tu stesso?
schiaccia

@crush swashbucle è una libreria di backend che fornisce json, come la libreria nuget nswag solo meglio. Per produrre un file dattiloscritto dovresti comunque usare il pacchetto nswag da npm.
Alex Herman,

Bene, ho già avuto un colpo di spada nel mio progetto da un po 'di tempo, sembrava che tu stia suggerendo che potrebbe generare i modelli TypeScript invece di nswag. Non sono un fan di nswag ... è pesante. Ho creato la mia conversione C # -> TypeScript che è agganciata a Swashbuckle - genera i file come processo post-build e li pubblica su un feed npm per i nostri progetti. Volevo solo assicurarmi di non aver trascurato un progetto Swashbuckle che stava già facendo la stessa cosa.
schiaccia il

8

Ecco un'implementazione molto minimale e sicura di un'autenticazione basata su attestazioni utilizzando il token JWT in un'API Web ASP.NET Core.

prima di tutto, è necessario esporre un endpoint che restituisce un token JWT con attestazioni assegnate a un utente:

 /// <summary>
        /// Login provides API to verify user and returns authentication token.
        /// API Path:  api/account/login
        /// </summary>
        /// <param name="paramUser">Username and Password</param>
        /// <returns>{Token: [Token] }</returns>
        [HttpPost("login")]
        [AllowAnonymous]
        public async Task<IActionResult> Login([FromBody] UserRequestVM paramUser, CancellationToken ct)
        {

            var result = await UserApplication.PasswordSignInAsync(paramUser.Email, paramUser.Password, false, lockoutOnFailure: false);

            if (result.Succeeded)
            {
                UserRequestVM request = new UserRequestVM();
                request.Email = paramUser.Email;


                ApplicationUser UserDetails = await this.GetUserByEmail(request);
                List<ApplicationClaim> UserClaims = await this.ClaimApplication.GetListByUser(UserDetails);

                var Claims = new ClaimsIdentity(new Claim[]
                                {
                                    new Claim(JwtRegisteredClaimNames.Sub, paramUser.Email.ToString()),
                                    new Claim(UserId, UserDetails.UserId.ToString())
                                });


                //Adding UserClaims to JWT claims
                foreach (var item in UserClaims)
                {
                    Claims.AddClaim(new Claim(item.ClaimCode, string.Empty));
                }

                var tokenHandler = new JwtSecurityTokenHandler();
                  // this information will be retrived from you Configuration
                //I have injected Configuration provider service into my controller
                var encryptionkey = Configuration["Jwt:Encryptionkey"];
                var key = Encoding.ASCII.GetBytes(encryptionkey);
                var tokenDescriptor = new SecurityTokenDescriptor
                {
                    Issuer = Configuration["Jwt:Issuer"],
                    Subject = Claims,

                // this information will be retrived from you Configuration
                //I have injected Configuration provider service into my controller
                    Expires = DateTime.UtcNow.AddMinutes(Convert.ToDouble(Configuration["Jwt:ExpiryTimeInMinutes"])),

                    //algorithm to sign the token
                    SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)

                };

                var token = tokenHandler.CreateToken(tokenDescriptor);
                var tokenString = tokenHandler.WriteToken(token);

                return Ok(new
                {
                    token = tokenString
                });
            }

            return BadRequest("Wrong Username or password");
        }

ora devi aggiungere l'autenticazione ai tuoi servizi ConfigureServicesall'interno di startup.cs per aggiungere l'autenticazione JWT come servizio di autenticazione predefinito come questo:

services.AddAuthentication(x =>
            {
                x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
             .AddJwtBearer(cfg =>
             {
                 cfg.RequireHttpsMetadata = false;
                 cfg.SaveToken = true;
                 cfg.TokenValidationParameters = new TokenValidationParameters()
                 {
                     //ValidateIssuerSigningKey = true,
                     IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JWT:Encryptionkey"])),
                     ValidateAudience = false,
                     ValidateLifetime = true,
                     ValidIssuer = configuration["Jwt:Issuer"],
                     //ValidAudience = Configuration["Jwt:Audience"],
                     //IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Key"])),
                 };
             });

ora puoi aggiungere criteri ai tuoi servizi di autorizzazione come questo:

services.AddAuthorization(options =>
            {
                options.AddPolicy("YourPolicyNameHere",
                                policy => policy.RequireClaim("YourClaimNameHere"));
            });

INOLTRE , puoi anche (non necessario) popolare tutti i tuoi reclami dal tuo database in quanto questo verrà eseguito solo una volta all'avvio dell'applicazione e aggiungerli a criteri come questo:

  services.AddAuthorization(async options =>
            {
                var ClaimList = await claimApplication.GetList(applicationClaim);
                foreach (var item in ClaimList)
                {                        
                    options.AddPolicy(item.ClaimCode, policy => policy.RequireClaim(item.ClaimCode));                       
                }
            });

ora puoi inserire il filtro Policy su uno dei metodi che desideri autorizzare in questo modo:

 [HttpPost("update")]
        [Authorize(Policy = "ACC_UP")]
        public async Task<IActionResult> Update([FromBody] UserRequestVM requestVm, CancellationToken ct)
        {
//your logic goes here
}

Spero che questo ti aiuti


3

Penso che dovresti usare un server party 3d per supportare il token JWT e non esiste un supporto JWT pronto all'uso nell'API WEB 2.

Tuttavia, esiste un progetto OWIN per supportare alcuni formati di token firmati (non JWT). Funziona come un protocollo OAuth ridotto per fornire solo una semplice forma di autenticazione per un sito web.

Puoi leggere di più al riguardo, ad esempio qui .

È piuttosto lungo, ma la maggior parte delle parti sono dettagli con controller e identità ASP.NET di cui potresti non aver bisogno. I più importanti sono

Passaggio 9: aggiungere il supporto per la generazione di token Bearer OAuth

Passaggio 12: test dell'API back-end

Qui puoi leggere come impostare l'endpoint (ad es. "/ Token") a cui puoi accedere dal frontend (e dettagli sul formato della richiesta).

Altri passaggi forniscono dettagli su come connettere tale endpoint al database, ecc. Ed è possibile scegliere le parti richieste.


2

Nel mio caso il JWT è creato da un'API separata, quindi ASP.NET deve solo decodificarlo e convalidarlo. Contrariamente alla risposta accettata, stiamo usando RSA che è un algoritmo non simmetrico, quindi la SymmetricSecurityKeyclasse sopra menzionata non funzionerà.

Ecco il risultato.

using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Threading;
using System.Threading.Tasks;

    public static async Task<JwtSecurityToken> VerifyAndDecodeJwt(string accessToken)
    {
        try
        {
            var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>($"{securityApiOrigin}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
            var openIdConfig = await configurationManager.GetConfigurationAsync(CancellationToken.None);
            var validationParameters = new TokenValidationParameters()
            {
                ValidateLifetime = true,
                ValidateAudience = false,
                ValidateIssuer = false,
                RequireSignedTokens = true,
                IssuerSigningKeys = openIdConfig.SigningKeys,
            };
            new JwtSecurityTokenHandler().ValidateToken(accessToken, validationParameters, out var validToken);
            // threw on invalid, so...
            return validToken as JwtSecurityToken;
        }
        catch (Exception ex)
        {
            logger.Info(ex.Message);
            return null;
        }
    }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.