Authentification JWT pour l'API Web ASP.NET

264

J'essaie de prendre en charge le jeton de porteur JWT (jeton Web JSON) dans mon application API Web et je me perds.

Je vois la prise en charge de .NET Core et des applications OWIN.
J'héberge actuellement mon application dans IIS.

Comment puis-je réaliser ce module d'authentification dans mon application? Existe-t-il un moyen d'utiliser la <authentication>configuration de la même manière que j'utilise l'authentification par formulaire / Windows?

Amir Popovich
la source

Réponses:

611

J'ai répondu à cette question: Comment sécuriser une API Web ASP.NET il y a 4 ans en utilisant HMAC.

Maintenant, beaucoup de choses ont changé dans la sécurité, en particulier JWT devient populaire. Ici, je vais essayer d'expliquer comment utiliser JWT de la manière la plus simple et la plus simple possible, afin que nous ne nous perdions pas de la jungle de OWIN, Oauth2, ASP.NET Identity ... :).

Si vous ne connaissez pas le jeton JWT, vous devez jeter un œil à:

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

Fondamentalement, un jeton JWT ressemble à:

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

Exemple:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1NzI0LCJleHAiOjE0Nzc1NjY5MjQsImlhdCI6MTQ3NzU2NTcyNH0.6MzD1VwA5AcOcajkFyKhLYybr3h13iZjDyHm9zysDFQ

Un jeton JWT comporte trois sections:

  1. En-tête: format JSON codé en Base64
  2. Revendications: format JSON codé en Base64.
  3. Signature: créée et signée sur la base de l'en-tête et des revendications codées en Base64.

Si vous utilisez le site Web jwt.io avec le jeton ci-dessus, vous pouvez décoder le jeton et le voir comme ci-dessous:

entrez la description de l'image ici

Techniquement, JWT utilise une signature qui est signée à partir des en-têtes et des revendications avec un algorithme de sécurité spécifié dans les en-têtes (exemple: HMACSHA256). Par conséquent, JWT doit être transféré via HTTP si vous stockez des informations sensibles dans des revendications.

Maintenant, pour utiliser l'authentification JWT, vous n'avez pas vraiment besoin d'un middleware OWIN si vous avez un système Web Api hérité. Le concept simple est de savoir comment fournir un jeton JWT et comment valider le jeton lorsque la demande arrive. C'est tout.

Retour à la démo, pour garder le token JWT léger, je stocke uniquement usernameet expiration timedans JWT. Mais de cette façon, vous devez reconstruire une nouvelle identité locale (principal) pour ajouter plus d'informations comme: rôles .. si vous voulez faire une autorisation de rôle. Mais, si vous souhaitez ajouter plus d'informations dans JWT, c'est à vous de décider: c'est très flexible.

Au lieu d'utiliser le middleware OWIN, vous pouvez simplement fournir un point de terminaison de jeton JWT en utilisant l'action du contrôleur:

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

Ceci est une action naïve; en production, vous devez utiliser une demande POST ou un point de terminaison d'authentification de base pour fournir le jeton JWT.

Comment générer le jeton sur la base de username?

Vous pouvez utiliser le package NuGet appelé System.IdentityModel.Tokens.Jwtpar Microsoft pour générer le jeton, ou même un autre package si vous le souhaitez. Dans la démo, j'utilise HMACSHA256avec 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;
}

Le point de terminaison pour fournir le jeton JWT est terminé. Maintenant, comment valider le JWT lorsque la demande arrive? Dans la démo que j'ai construite JwtAuthenticationAttributequi hérite de IAuthenticationFilter(plus de détails sur le filtre d'authentification ici ).

Avec cet attribut, vous pouvez authentifier n'importe quelle action: il vous suffit de mettre cet attribut sur cette action.

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

Vous pouvez également utiliser le middleware OWIN ou DelegateHander si vous souhaitez valider toutes les demandes entrantes pour votre WebAPI (non spécifique au contrôleur ou à l'action)

Voici la méthode principale du filtre d'authentification:

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

Le flux de travail consiste à utiliser la bibliothèque JWT (package NuGet ci-dessus) pour valider le jeton JWT, puis revenir ClaimsPrincipal. Vous pouvez effectuer plus de validation comme vérifier si l'utilisateur existe sur votre système et ajouter d'autres validations personnalisées si vous le souhaitez. Le code pour valider le jeton JWT et récupérer le principal:

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

Si le jeton JWT est validé et que le principal est renvoyé, vous devez créer une nouvelle identité locale et y mettre plus d'informations pour vérifier l'autorisation de rôle.

N'oubliez pas d'ajouter config.Filters.Add(new AuthorizeAttribute());(autorisation par défaut) à portée globale afin d'empêcher toute demande anonyme à vos ressources.

Vous pouvez utiliser Postman pour tester la démo:

Jeton de demande (naïf comme je l'ai mentionné ci-dessus, juste pour la démo):

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

Mettez le jeton JWT dans l'en-tête de la demande autorisée, par exemple:

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

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1MjU4LCJleHAiOjE0Nzc1NjY0NTgsImlhdCI6MTQ3NzU2NTI1OH0.dSwwufd4-gztkLpttZsZ1255oEzpWCJkayR_4yvNL1s

La démo est mise ici: https://github.com/cuongle/WebApi.Jwt

cuongle
la source
5
Bien expliqué par @Cuong Le mais j'aime en ajouter plus: si vous utilisez OWIN, vérifiez UseJwtBearerAuthentication disponible dans Microsoft.Owin.Security.Jwt, vous pouvez utiliser ce middleware owin sur WebAPI pour valider automatiquement chaque demande entrante. utiliser la classe de démarrage owin pour enregistrer le middleware
Jek
5
@AmirPopovich Vous n'avez pas besoin de définir de jeton sur la réponse, le jeton doit être stocké ailleurs sur le côté client, pour le Web, vous pouvez placer dans le stockage local, chaque fois que vous envoyez une demande HTTP, placez le jeton dans l'en-tête.
cuongle
7
Wow, c'est l'explication la plus simple que j'ai vue depuis longtemps. +100 si je pouvais
gyozo kudor
4
@Homam: Désolé pour cette réponse tardive, la meilleure façon de générer est: varhmac = new HMACSHA256();var key = Convert.ToBase64String(hmac.Key);
cuongle
4
Quiconque utilise le code de démonstration du repo de CuongLe remarquera qu'il y a un bogue où les demandes sans en-tête d'autorisation ne sont pas traitées, ce qui signifie que toute requête sans que l'on puisse y accéder (pas un point de terminaison si sûr!). Il y a une requête pull de @magicleon pour résoudre ce problème ici: github.com/cuongle/WebApi.Jwt/pull/4
Chucky
11

J'ai réussi à y parvenir avec un minimum d'effort (tout aussi simple qu'avec ASP.NET Core).

Pour cela, j'utilise le Startup.csfichier et la Microsoft.Owin.Security.Jwtbibliothèque OWIN .

Pour que l'application frappe, Startup.csnous devons modifier Web.config:

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

Voici à quoi Startup.csdevrait ressembler:

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

Beaucoup d'entre vous utilisent aujourd'hui ASP.NET Core, donc comme vous pouvez le voir, cela ne diffère pas beaucoup de ce que nous avons là-bas.

Cela m'a vraiment rendu perplexe en premier, j'essayais d'implémenter des fournisseurs personnalisés, etc. Mais je ne m'attendais pas à ce que ce soit aussi simple. OWINjuste des rochers!

Juste une chose à mentionner - après avoir activé la NSWagbibliothèque de démarrage OWIN a cessé de fonctionner pour moi (par exemple, certains d'entre vous voudront peut-être générer automatiquement des proxys HTTP tapuscript pour l'application Angular).

La solution est également très simple - je l' ai remplacé NSWagpar Swashbuckleet n'a pas eu d'autres problèmes.


Ok, partage maintenant le ConfigHelpercode:

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 autre aspect important - j'ai envoyé un jeton JWT via l'en- tête d' autorisation , donc le code dactylographié me ressemble comme suit:

(le code ci-dessous est généré par 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')
            })
        };

Voir la partie en-têtes - "Authorization": "Bearer " + localStorage.getItem('token')

Alex Herman
la source
I replaced NSWag with Swashbuckle and didn't have any further issues.Swashbuckle a-t-il la capacité de générer des fichiers dactylographiés ou est-ce quelque chose que vous y avez ajouté vous-même?
écraser le
@crush swashbucle est une bibliothèque backend fournissant json, comme la bibliothèque nuget nswag, mais en mieux. Afin de produire un fichier dactylographié, vous devez toujours utiliser le package nswag de npm.
Alex Herman
Bon, j'ai déjà du swashbuckle dans mon projet depuis un certain temps, il semblait que vous suggériez qu'il pourrait générer les modèles TypeScript au lieu de nswag. Je ne suis pas fan de nswag ... c'est lourd. J'ai créé ma propre conversion C # -> TypeScript qui est connectée à Swashbuckle - génère les fichiers en tant que processus de post-construction et les publie dans un flux npm pour nos projets. Je voulais juste m'assurer que je n'avais pas oublié un projet Swashbuckle qui faisait déjà la même chose.
écraser le
8

Voici une implémentation très minimale et sécurisée d'une authentification basée sur les revendications utilisant un jeton JWT dans une API Web ASP.NET Core.

tout d'abord, vous devez exposer un point de terminaison qui renvoie un jeton JWT avec des revendications attribuées à un utilisateur:

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

Maintenant , vous devez ajouter une authentification à vos services dans votre ConfigureServicesintérieur de votre startup.cs d'ajouter l' authentification JWT en tant que votre service d'authentification par défaut comme ceci:

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

vous pouvez maintenant ajouter des stratégies à vos services d'autorisation comme ceci:

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

ALTERNATIVEMENT , vous pouvez également (pas nécessaire) remplir toutes vos revendications à partir de votre base de données car cela ne s'exécutera qu'une seule fois au démarrage de votre application et les ajoutera à des politiques comme celle-ci:

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

maintenant, vous pouvez mettre le filtre de stratégie sur l'une des méthodes que vous souhaitez être autorisées comme ceci:

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

J'espère que cela t'aides

Zeeshan Adil
la source
3

Je pense que vous devriez utiliser un serveur tiers pour prendre en charge le jeton JWT et il n'y a pas de support JWT prêt à l'emploi dans l'API WEB 2.

Cependant, il existe un projet OWIN pour prendre en charge un certain format de jeton signé (pas JWT). Il fonctionne comme un protocole OAuth réduit pour fournir simplement une forme simple d'authentification pour un site Web.

Vous pouvez en savoir plus à ce sujet, par exemple ici .

C'est assez long, mais la plupart des parties sont des détails avec des contrôleurs et une identité ASP.NET dont vous pourriez ne pas avoir besoin du tout. Les plus importants sont

Étape 9: ajouter la prise en charge de la génération de jetons OAuth Bearer

Étape 12: Test de l'API back-end

Là, vous pouvez lire comment configurer le point de terminaison (par exemple "/ token") auquel vous pouvez accéder depuis le frontend (et des détails sur le format de la demande).

D'autres étapes fournissent des détails sur la façon de connecter ce point de terminaison à la base de données, etc. et vous pouvez choisir les pièces dont vous avez besoin.

Ilya Chernomordik
la source
2

Dans mon cas, le JWT est créé par une API distincte, donc ASP.NET n'a qu'à le décoder et le valider. Contrairement à la réponse acceptée, nous utilisons RSA qui est un algorithme non symétrique, donc la SymmetricSecurityKeyclasse mentionnée ci-dessus ne fonctionnera pas.

Voici le résultat.

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;
        }
    }
Ron Newcomb
la source