JWT es un
estándar RFC 7519 para
transmitir información con la identidad y claims de
un usuario de forma segura entre un cliente/servidor. Dicha información puede
ser verificada y confiable porque está firmada digitalmente.
Así en vez de
crear una sesión en el servidor y estar enviando cookies a los usuarios, se
utilizará un mecanismo de autenticación sin estado (la sesión del usuario nunca
se guarda en el proveedor de identidad o en el proveedor del servicio)
retornando un JSON Web Token via HTTP Header siempre y cuando el usuario
ingrese sus credenciales correctamente.
El usuario será
ahora el responsable de guardar el token que el servidor le envia en el proceso de autenticación a fin de acceder a una
ruta protegida del servicio Web Api, si el token está presente y es válido, el
proveedor del servicio otorga accesos a los recursos protegidos. Como los JWTs
contienen toda la información necesaria en sí misma, se reduce la necesidad de
consultar base de datos u otras fuentes de información múltiples veces.
Propiedades claim
estándar
El estándar
define los siguientes campos que pueden incluirse en los tokens JWT:
·
Issuer
(iss) - Identifica el proveedor de identidad que emitió el JWT.
·
Subject
(sub) - Identifica el objeto o usuario en nombre del cual fue emitido el JWT,
se puede usar para limitar su uso a ciertos casos.
·
Audience
(aud) - Identifica la audiencia o receptores para lo que el JWT fue emitido.
Cada servicio que recibe un JWT para su validación tiene que controlar la
audiencia a la que el JWT está destinado. Si el proveedor del servicio no se
encuentra presente en el campo aud, entonces el JWT tiene que ser
rechazado.
·
Expiration
Time (exp) - Una fecha que sirva para verificar si el JWT está vencido y
obligar al usuario a volver a autenticarse.
·
Not
Before (nbf) - Indica desde que momento se va a empezar a aceptar un JWT, EL
JWT no tiene que ser aceptado si el token es utilizando antes
de este tiempo.
·
Issued
At (iat) - Indica cuando fue creado el JWT.
·
JWT
ID (jti) - Un identificador único para cada JWT.
para el ejemplo lo primero será crear la contraseña a utilizar como secret key en el proceso de autenticación, se creó en la página
https://passwordsgenerator.net/ con una longitud de 128 tal como se muestra
se generó el password
+$3Vs@JmcdncM?@yBs$D_yFJ$TR5#pDbKwuX6787=k@6^GYN*x*#9hrEv9tBefaEG7e3XHh=Lw62$$hxN-g#artN8=Xqg&6Pt#W=yYU5!RT6B#VRcq4+v$L+rM4UEj25
la aplicación cliente que consumirá el servicio se encuentra en el puerto 55987 en IIS Express tal como se muestra en la figura, este numero de puerto se utilizara en los claims issuer y audience del JWT.
colocar en el controlador Empleado de la aplicación Web Api el atributo Authorize, de esta manera solamente los clientes que envíen el token podrán consumir el servicio
se utilizará la aplicación Web Api creada en
esta entrada y se agregaran:
el archivo Models.cs en el dentro de la carpeta Models.
el archivo LoginController dentro de la carpeta Controllers.
el archivo TokenValidationHandler en el directorio raíz, con lo que el Solution Explorer de la aplicación queda
en el archivo Models.cs se definen las clases para login de usuario y para respuesta ante login fallido
namespace WebApi2MVC.Models
{
public class Models
{}
public class LoginRequest{
public string Username { get; set; }
public string Password { get; set; }
}
public class LoginResponse{
public LoginResponse()
{
this.Token = "";
this.responseMsg = new HttpResponseMessage() { StatusCode = System.Net.HttpStatusCode.Unauthorized };
}
public string Token { get; set; }
public HttpResponseMessage responseMsg { get; set; }
}
}
Ahora debe agregarse el archivo TokenValidationHandler.cs en el cual se utilizará DelegatingHandler para capturar los mensajes HTTP enviados al servicio a fin de verificar que existe un token valido.
namespace WebApi2MVC
{
internal class TokenValidationHandler : DelegatingHandler
{
private static bool TryRetrieveToken(HttpRequestMessage request, out string token)
{
token = null;
IEnumerable authzHeaders;
if (!request.Headers.TryGetValues("Authorization", out authzHeaders) || authzHeaders.Count() > 1)
{
return false;
}
var bearerToken = authzHeaders.ElementAt(0);
token = bearerToken.StartsWith("Bearer ") ? bearerToken.Substring(7) : bearerToken;
return true;
}
protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
HttpStatusCode statusCode;
string token;
if (!TryRetrieveToken(request, out token))
{
statusCode = HttpStatusCode.Unauthorized;
return base.SendAsync(request, cancellationToken);
}
try
{
const string sec = "+$3Vs@JmcdncM?@yBs$D_yFJ$TR5#pDbKwuX6787=k@6^GYN*x*#9hrEv9tBefaEG7e3XHh=Lw62$$hxN-g#artN8=Xqg&6Pt#W=yYU5!RT6B#VRcq4+v$L+rM4UEj25";
var now = DateTime.UtcNow;
var securityKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(System.Text.Encoding.Default.GetBytes(sec));
SecurityToken securityToken;
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
TokenValidationParameters validationParameters = new TokenValidationParameters()
{
ValidAudience = "http://localhost:55987",
ValidIssuer = "http://localhost:55987",
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
LifetimeValidator = this.LifetimeValidator,
IssuerSigningKey = securityKey
};
Thread.CurrentPrincipal = handler.ValidateToken(token, validationParameters, out securityToken);
HttpContext.Current.User = handler.ValidateToken(token, validationParameters, out securityToken);
return base.SendAsync(request, cancellationToken);
}
catch (SecurityTokenValidationException)
{
statusCode = HttpStatusCode.Unauthorized;
}
catch (Exception)
{
statusCode = HttpStatusCode.InternalServerError;
}
return Task.Factory.StartNew(() => new HttpResponseMessage(statusCode) { });
}
public bool LifetimeValidator(DateTime? notBefore, DateTime? expires, SecurityToken securityToken, TokenValidationParameters validationParameters)
{
if (expires != null)
{
if (DateTime.UtcNow < expires) return true;
}
return false;
}
}
}
el archivo LoginController será el encargado de validar las credenciales, en caso de credenciales validas se creará el token y se enviará al cliente
namespace WebApi2MVC.Controllers
{
[RoutePrefix("api/login")]
public class LoginController : ApiController
{
[AllowAnonymous]
[HttpPost]
[Route("authenticate")]
public IHttpActionResult Authenticate(LoginRequest login)
{
var loginResponse = new LoginResponse { };
LoginRequest loginrequest = new LoginRequest { };
loginrequest.Username = login.Username.ToLower();
loginrequest.Password = login.Password;
IHttpActionResult response;
HttpResponseMessage responseMsg = new HttpResponseMessage();
bool isUsernamePasswordValid = false;
if (login != null)
isUsernamePasswordValid = loginrequest.Password == "pabletoreto.blogspot.com" ? true : false;
if (isUsernamePasswordValid)
{
string token = createToken(loginrequest.Username);
return Ok(token);
}
else
{
loginResponse.responseMsg.StatusCode = HttpStatusCode.Unauthorized;
response = ResponseMessage(loginResponse.responseMsg);
return response;
}
}
private string createToken(string username)
{
DateTime issuedAt = DateTime.UtcNow;
DateTime expires = DateTime.UtcNow.AddDays(7);
var tokenHandler = new JwtSecurityTokenHandler();
ClaimsIdentity claimsIdentity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, username)
,new Claim(ClaimTypes.Country, "El Salvador")
,new Claim(ClaimTypes.Gender, "Macho")
,new Claim(ClaimTypes.Surname, "pabletoreto")
,new Claim(ClaimTypes.Email, "pabletoreto@mail.com")
,new Claim(ClaimTypes.Role, "IT")
});
const string sec = "+$3Vs@JmcdncM?@yBs$D_yFJ$TR5#pDbKwuX6787=k@6^GYN*x*#9hrEv9tBefaEG7e3XHh=Lw62$$hxN-g#artN8=Xqg&6Pt#W=yYU5!RT6B#VRcq4+v$L+rM4UEj25";
var now = DateTime.UtcNow;
var securityKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(System.Text.Encoding.Default.GetBytes(sec));
var signingCredentials = new Microsoft.IdentityModel.Tokens.SigningCredentials(securityKey, Microsoft.IdentityModel.Tokens.SecurityAlgorithms.HmacSha256Signature);
var token =
(JwtSecurityToken)tokenHandler.CreateJwtSecurityToken(
issuer: "http://localhost:55987",
audience: "http://localhost:55987",
subject: claimsIdentity,
notBefore: issuedAt,
expires: expires,
signingCredentials: signingCredentials);
// claims personalizados
var jsonu = new { id = "pabletoreto el mero" };
token.Payload["user"] = jsonu;
var payload = new JwtPayload
{
{ "some ", "hello "},
{ "scope", "http://dummy.com/"},
};
var tokenString = tokenHandler.WriteToken(token);
return tokenString;
}
}
}
para finalizar se debe registrar la clase TokenValidationHandler en el WebApiConfig.cs
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
config.MessageHandlers.Add(new TokenValidationHandler());
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.EnableCors(new EnableCorsAttribute("*", "*", "GET,PUT,POST,DELETE"));
var appXmlType = config.Formatters.XmlFormatter.SupportedMediaTypes.FirstOrDefault(t => t.MediaType == "application/xml");
config.Formatters.XmlFormatter.SupportedMediaTypes.Remove(appXmlType);
}
}
ahora se realizarán las pruebas desde Postman, primero se realizará el login
la respuesta es una cadena alfanumérica generada desde el servicio y es enviada al cliente para autenticaciones futuras, evitando tener que enviar credenciales en cada invocación.
esta cadena consta de tres partes separadas por un punto, para decodificar la cadena se puede utilizar la página
https://jwt.io/
Ahora se realizará una peticion con el verbo GET para la cual deberemos enviar el token junto con la petición, este token se envía en el encabezamiento de Authorization utilizando el esquema Bearer, este esquema de autenticación fue creado como parte de OAuth 2.0 y en este caso implica darle acceso al portador de este token