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:
- En-tête: format JSON codé en Base64
- Revendications: format JSON codé en Base64.
- 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:
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 username
et expiration time
dans 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.Jwt
par 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 arrive? Dans la démo que j'ai construite
JwtAuthenticationAttribute
qui 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