321 votes

Authentification JWT pour l'API Web ASP.NET

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

Je vois un support pour .NET Core et pour les 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 le module <authentication> configuration similaire à la façon dont j'utilise les formulaires/authentification Windows ?

747voto

Cuong Le Points 29324

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

Aujourd'hui, beaucoup de choses ont changé dans le domaine de la sécurité, notamment la popularité croissante de JWT. Dans cette réponse, je vais essayer d'expliquer comment utiliser JWT de la manière la plus simple et la plus basique possible, afin que nous ne nous perdions pas dans la jungle de OWIN, Oauth2, ASP.NET Identity... :)

Si vous ne connaissez pas les jetons JWT, vous devez y jeter un coup d'œil :

https://www.rfc-editor.org/rfc/rfc7519

En gros, un jeton JWT ressemble à ça :

<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 qui est encodé en Base64.
  2. Réclamations : Format JSON qui est codé en Base64.
  3. Signature : Créé et signé sur la base de l'en-tête et des revendications qui sont 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 :

A screenshot of jwt.io with the raw jwt source and the decoded JSON it represents

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

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

Dans le démo que j'ai créée (github) pour que le jeton JWT reste léger, je stocke seulement username y expiration time . Mais de cette façon, vous devez reconstruire une nouvelle identité locale (principal) pour ajouter plus d'informations comme les rôles, si vous voulez faire une autorisation de rôle, etc. Mais, si vous voulez ajouter plus d'informations dans JWT, c'est vous qui décidez : 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 une action de 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;
    }
}

Il s'agit d'une action naïve ; en production, vous devez utiliser une requête POST ou un point de terminaison d'authentification de base pour fournir le jeton JWT.

Comment générer le jeton en fonction de username ?

Vous pouvez utiliser le paquet NuGet appelé System.IdentityModel.Tokens.Jwt de Microsoft pour générer le jeton, ou même un autre paquet si vous le souhaitez. Dans la démo, j'utilise HMACSHA256 con 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 fait.

Comment valider le JWT lorsque la requête arrive ?

Dans le Démonstration J'ai construit JwtAuthenticationAttribute qui hérite de IAuthenticationFilter (plus de détails sur le filtre d'authentification dans aquí ).

Avec cet attribut, vous pouvez authentifier n'importe quelle action : il 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 requêtes entrantes pour votre WebAPI (non spécifique au contrôleur ou à l'action).

Vous trouverez ci-dessous la méthode de base 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 || !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 (paquet NuGet ci-dessus) pour valider le jeton JWT, puis à renvoyer l'information. ClaimsPrincipal . Vous pouvez effectuer d'autres validations, comme vérifier si l'utilisateur existe dans 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 ajouter des informations pour vérifier l'autorisation du rôle.

N'oubliez pas d'ajouter config.Filters.Add(new AuthorizeAttribute()); (autorisation par défaut) à l'échelle mondiale afin d'empêcher toute requête anonyme vers vos ressources.

Vous pouvez utiliser Postman pour tester le Démonstration :

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, exemple :

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

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1MjU4LCJleHAiOjE0Nzc1NjY0NTgsImlhdCI6MTQ3NzU2NTI1OH0.dSwwufd4-gztkLpttZsZ1255oEzpWCJkayR_4yvNL1s

La démo peut être trouvée ici : https://github.com/cuongle/WebApi.Jwt

8 votes

Bien expliqué par @Cuong Le mais j'aimerais ajouter quelque chose : Si vous utilisez OWIN, vérifiez le UseJwtBearerAuthentication disponible dans Microsoft.Owin.Security.Jwt. Vous pouvez utiliser ce middleware owin sur le WebAPI pour valider automatiquement chaque requête entrante. Utilisez la classe owin startup pour enregistrer le middleware.

1 votes

J'ai en fait mis en œuvre à peu près la même chose après avoir trouvé ceci : asp.net/web-api/overview/security/authentication-filters Votre réponse est excellente. +1 + accepter sur elle. Je n'ai qu'une seule question en suspens : quelle est la meilleure façon de faire un ping-pong (client-serveur) du jeton du porteur ? Mon client l'envoie - je passe par le filtre et j'obtiens soit une 401 soit une authentification valide, je fais mon travail et je veux mettre le jeton sur la réponse. Je peux le faire en modifiant l'en-tête de la réponse sur le filtre d'authentification, mais y a-t-il un meilleur moyen ?

6 votes

@AmirPopovich Vous n'avez pas besoin de mettre le jeton sur la réponse, le jeton doit être stocké quelque part ailleurs sur le côté client, pour le web, vous pouvez le mettre dans le stockage local, chaque fois que vous envoyez une requête HTTP, mettez le jeton sur le header.

21voto

Alex Herman Points 662

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

Pour cela, j'utilise OWIN Startup.cs et Microsoft.Owin.Security.Jwt bibliothèque.

Pour que l'application atteigne Startup.cs nous devons modifier Web.config :

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

Voici comment Startup.cs devrait 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 ASP.NET Core aujourd'hui, donc comme vous pouvez le voir, cela ne diffère pas beaucoup de ce que nous avons ici.

Cela m'a d'abord laissé perplexe, j'essayais d'implémenter des fournisseurs personnalisés, etc. Mais je ne m'attendais pas à ce que ce soit si simple. OWIN Tout simplement génial !

Juste une chose à mentionner - après avoir activé OWIN Startup NSWag a cessé de fonctionner pour moi (par exemple, certains d'entre vous pourraient vouloir générer automatiquement des proxies HTTP en script de type pour les applications Angular).

La solution était également très simple - j'ai remplacé NSWag con Swashbuckle et n'a pas eu d'autres problèmes.


Ok, maintenant partageons ConfigHelper code :

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é le Token JWT via Autorisation donc le code typographique se présente pour moi 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')

0 votes

I replaced NSWag with Swashbuckle and didn't have any further issues. Swashbuckle est-il capable de générer des fichiers typescript ou est-ce quelque chose que vous avez ajouté vous-même ?

0 votes

@crush swashbucle est une bibliothèque backend fournissant json, comme la bibliothèque nuget nswag mais en mieux. Afin de produire un fichier typescript, vous devez toujours utiliser le paquet nswag de npm.

0 votes

C'est vrai, j'ai déjà swashbuckle dans mon projet depuis un certain temps, il me semblait que vous suggériez qu'il puisse générer les modèles TypeScript à la place de nswag. Je ne suis pas un fan de nswag... c'est lourd. J'ai créé ma propre conversion C#->TypeScript qui est reliée à Swashbuckle - génère les fichiers comme un processus post-build, et les publie dans un flux npm pour nos projets. Je voulais juste m'assurer que je n'avais pas négligé un projet Swashbuckle qui faisait déjà la même chose.

14voto

Zeeshan Adil Points 968

Voici une mise en œuvre très minimale et sécurisée d'une authentification basée sur des 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 les 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 l'authentification à vos services dans votre ConfigureServices à l'intérieur de votre startup.cs pour ajouter l'authentification JWT comme 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 politiques à vos services d'autorisation comme ceci :

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

ALTERNATIVEMENT Vous pouvez également (ce n'est pas nécessaire) alimenter toutes vos demandes à partir de votre base de données, car cette opération ne sera exécutée qu'une seule fois au démarrage de votre application, et les ajouter aux polices comme ceci :

  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 politique sur n'importe quelle méthode que vous voulez être autorisé 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 vous aidera

8voto

Ron Newcomb Points 975

Dans mon cas, le JWT est créé par une API distincte, de sorte qu'il suffit à ASP.NET de le décoder et de le valider. Contrairement à la réponse acceptée, nous utilisons le RSA qui est un algorithme non symétrique, donc le SymmetricSecurityKey La classe 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;
        }
    }

5voto

Ilya Chernomordik Points 419

Je pense que vous devez utiliser un serveur tiers pour prendre en charge le jeton JWT et il n'y a pas de prise en charge JWT immédiate dans l'API WEB 2.

Il existe toutefois un projet OWIN qui prend en charge un certain format de jeton signé (pas JWT). Il fonctionne comme un protocole OAuth réduit pour fournir une forme simple d'authentification pour un site web.

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

Il est assez long, mais la plupart des parties sont des détails sur les contrôleurs et l'identité ASP.NET dont vous n'aurez peut-être pas besoin du tout. Les plus importants sont

Étape 9 : Ajouter le support pour la génération de jetons OAuth Bearer Tokens

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

Vous pouvez y lire comment configurer un point de terminaison (par exemple "/token") auquel vous pouvez accéder depuis le frontend (et des détails sur le format de la requête).

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 parties dont vous avez besoin.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X