Respondí esta pregunta: Cómo asegurar una API web ASP.NET hace 4 años usando HMAC.
Ahora, muchas cosas cambiaron en seguridad, especialmente JWT se está volviendo popular. Aquí trataré de explicar cómo usar JWT de la manera más simple y básica que pueda, para que no nos perdamos de la selva de OWIN, Oauth2, ASP.NET Identity ... :).
Si no conoce el token JWT, debe echar un vistazo a:
https://tools.ietf.org/html/rfc7519
Básicamente, un token JWT se ve así:
<base64-encoded header>.<base64-encoded claims>.<base64-encoded signature>
Ejemplo:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1NzI0LCJleHAiOjE0Nzc1NjY5MjQsImlhdCI6MTQ3NzU2NTcyNH0.6MzD1VwA5AcOcajkFyKhLYybr3h13iZjDyHm9zysDFQ
Un token JWT tiene tres secciones:
- Encabezado: formato JSON que está codificado en Base64
- Reclamaciones: formato JSON que está codificado en Base64.
- Firma: Creado y firmado basado en Encabezado y Reclamaciones que está codificado en Base64.
Si usa el sitio web jwt.io con el token anterior, puede decodificar el token y verlo como a continuación:
Técnicamente, JWT utiliza una firma que está firmada desde encabezados y notificaciones con algoritmo de seguridad especificado en los encabezados (ejemplo: HMACSHA256). Por lo tanto, se requiere que JWT se transfiera a través de HTTP si almacena información confidencial en las notificaciones.
Ahora, para usar la autenticación JWT, realmente no necesita un middleware OWIN si tiene un sistema Web Api heredado. El concepto simple es cómo proporcionar el token JWT y cómo validar el token cuando llega la solicitud. Eso es.
De vuelta a la demostración, para mantener el token JWT ligero, solo almaceno username
y expiration time
en JWT. Pero de esta manera, debe reconstruir una nueva identidad local (principal) para agregar más información como: roles ... si desea hacer la autorización de roles. Pero, si desea agregar más información a JWT, depende de usted: es muy flexible.
En lugar de usar el middleware OWIN, simplemente puede proporcionar un punto final de token JWT mediante la acción del controlador:
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;
}
}
Esta es una acción ingenua; en producción, debe usar una solicitud POST o un punto final de autenticación básica para proporcionar el token JWT.
¿Cómo generar el token basado en username
?
Puede usar el paquete NuGet llamado System.IdentityModel.Tokens.Jwt
desde Microsoft para generar el token, o incluso otro paquete si lo desea. En la demostración, uso 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;
}
El punto final para proporcionar el token JWT está hecho. Ahora, ¿cómo validar el JWT cuando llega la solicitud? En la demostración que he creado,
JwtAuthenticationAttribute
que hereda de IAuthenticationFilter
(más detalles sobre el filtro de autenticación aquí ).
Con este atributo, puede autenticar cualquier acción: solo tiene que poner este atributo en esa acción.
public class ValueController : ApiController
{
[JwtAuthentication]
public string Get()
{
return "value";
}
}
También puede usar el middleware OWIN o DelegateHander si desea validar todas las solicitudes entrantes para su WebAPI (no específico para el Controlador o la acción)
A continuación se muestra el método principal del filtro de autenticación:
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);
}
El flujo de trabajo es usar la biblioteca JWT (paquete NuGet arriba) para validar el token JWT y luego regresar ClaimsPrincipal
. Puede realizar más validaciones, como verificar si el usuario existe en su sistema y agregar otras validaciones personalizadas si lo desea. El código para validar el token JWT y recuperar el 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 se valida el token JWT y se devuelve el principal, debe crear una nueva identidad local y agregar más información para verificar la autorización de roles.
Recuerde agregar config.Filters.Add(new AuthorizeAttribute());
(autorización predeterminada) en el ámbito global para evitar cualquier solicitud anónima a sus recursos.
Puede usar Postman para probar la demostración:
Solicitar token (ingenuo como mencioné anteriormente, solo para demostración):
GET http://localhost:{port}/api/token?username=cuong&password=1
Coloque el token JWT en el encabezado de la solicitud autorizada, por ejemplo:
GET http://localhost:{port}/api/value
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1MjU4LCJleHAiOjE0Nzc1NjY0NTgsImlhdCI6MTQ3NzU2NTI1OH0.dSwwufd4-gztkLpttZsZ1255oEzpWCJkayR_4yvNL1s
La demostración se incluye aquí: https://github.com/cuongle/WebApi.Jwt