web-dev-qa-db-fra.com

Authentification JWT pour API Web ASP.NET

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

Je vois un soutien 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? Est-il possible d'utiliser la configuration <authentication> de la même manière que j'utilise l'authentification par formulaire/Windows?

214
Amir Popovich

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

Maintenant, beaucoup de choses ont changé en matière de 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, pour ne pas nous perdre dans la jungle d’OWIN, Oauth2, Identité ASP.NET ... :).

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

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

En gros, 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éé et signé sur la base de l'en-tête et des revendications encodé 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:

enter image description here

Techniquement, JWT utilise une signature signée à partir d'en-têtes et revendique avec l'algorithme de sécurité spécifié dans les en-têtes (exemple: HMACSHA256). Par conséquent, le transfert de JWT sur HTTP est requis 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 le jeton JWT et comment le valider lorsque la demande arrive. C'est ça.

De retour à la démo, pour garder le jeton JWT léger, je ne stocke que username et expiration time dans JWT. Mais de cette façon, vous devez reconstruire une nouvelle identité locale (principal) pour ajouter des informations telles que: rôles .. si vous souhaitez effectuer une autorisation de rôle. Mais si vous souhaitez ajouter plus d'informations à 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;
    }
}

C'est une action naïve. en production, vous devez utiliser une demande POST ou un noeud final d'authentification de base pour fournir le jeton JWT.

Comment générer le jeton basé sur username?

Vous pouvez utiliser le package NuGet appelé System.IdentityModel.Tokens.Jwt de Microsoft pour générer le jeton, ou même un autre package si vous le souhaitez. Dans la démo, j'utilise HMACSHA256 avec 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 vient? Dans la démo, j'ai construit JwtAuthenticationAttribute qui hérite de IAuthenticationFilter (plus de détails sur le filtre d'authentification dans ici ).

Avec cet attribut, vous pouvez authentifier n'importe quelle action: il vous suffit de placer 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).

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)
        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 utilise la bibliothèque JWT (package NuGet ci-dessus) pour valider le jeton JWT, puis renvoyer ClaimsPrincipal. Vous pouvez effectuer davantage de validations, par exemple vérifier si un 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 ajouter davantage d'informations pour vérifier l'autorisation de rôle.

N'oubliez pas d'ajouter config.Filters.Add(new AuthorizeAttribute()); (autorisation par défaut) au niveau global 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

Placez 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

526
cuongle

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

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

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

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

Voici à quoi devrait ressembler Startup.cs:

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 de nos jours, comme vous pouvez le constater, cela ne diffère pas beaucoup de ce que nous avons là.

Cela m'a vraiment troublé au début, j'essayais d'implémenter des fournisseurs personnalisés, etc. Mais je ne m'attendais pas à ce que ce soit si simple. OWIN juste des rochers!

Une chose à mentionner - après avoir activé OWIN Startup NSWag, la bibliothèque ne fonctionne plus pour moi (par exemple, certains d'entre vous voudront peut-être générer automatiquement des proxies HTTP TypeScript pour Angular app).

La solution était également très simple: j'ai remplacé NSWag par Swashbuckle et je n'ai plus eu de problèmes.


Ok, partage maintenant le code ConfigHelper:

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 jeton JWT via l'en-tête Authorization , de sorte que le code TypeScript me cherche 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 des en-têtes - "Authorization": "Bearer " + localStorage.getItem('token')

7
Alex Herman

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

tout d'abord, vous devez exposer un noeud final qui renvoie un jeton JWT avec des revendications affecté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 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"])),
                 };
             });

maintenant, vous pouvez ajouter des politiques à vos services d'autorisation comme ceci:

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

ALTERNATIVEMENT, vous pouvez également (non nécessaire) remplir toutes vos revendications à partir de votre base de données, car celle-ci ne s'exécutera qu'une seule fois au démarrage de votre application et sera ajoutée à des stratégies telles que:

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

vous pouvez maintenant appliquer le filtre de stratégie à l’une des méthodes que vous souhaitez autoriser, 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

3
Zeeshan Adil

Je pense que vous devriez utiliser un serveur tiers pour prendre en charge le jeton JWT et il n’existe pas de prise en charge JWT prête à l'emploi dans Web API 2.

Cependant, il existe un projet OWIN permettant de prendre en charge certains formats de jetons signés (pas JWT). Il fonctionne comme un protocole réduit OAuth pour fournir une simple forme d'authentification à un site Web.

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

C'est plutôt long, mais la plupart des parties sont des détails avec les contrôleurs et l'identité ASP.NET dont vous n'avez peut-être pas besoin. Les plus importants sont

Étape 9: Ajout de la prise en charge de OAuth Génération de jetons de support

É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 expliquent comment connecter ce point de terminaison à la base de données, etc., et vous pouvez choisir les pièces dont vous avez besoin.

1
Ilya Chernomordik