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 :
- En-tête : Format JSON qui est encodé en Base64.
- Réclamations : Format JSON qui est codé en Base64.
- 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 :
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