Autenticación JWT para ASP.NET Web API
Estoy intentando admitir el token de portador JWT (JSON Web Token) en mi aplicación API web y me estoy perdiendo.
Veo soporte para .NET Core y aplicaciones OWIN.
Actualmente estoy alojando mi aplicación en IIS.
¿Cómo puedo lograr este módulo de autenticación en mi aplicación? ¿Hay alguna forma de utilizar la <authentication>
configuración similar a la forma en que uso los formularios/autenticación de Windows?
Respondí esta pregunta: Cómo proteger una API web ASP.NET hace 4 años usando HMAC.
Ahora, muchas cosas han cambiado en materia de seguridad, especialmente porque JWT se está volviendo popular. En esta respuesta, intentaré explicar cómo usar JWT de la manera más simple y básica que pueda, para no perdernos en la jungla de OWIN, Oauth2, ASP.NET Identity, etc.
Si no conoce los tokens JWT, debe consultar:
https://www.rfc-editor.org/rfc/rfc7519
Básicamente, un token JWT se ve así:
<base64-encoded header>.<base64-encoded claims>.<base64-encoded signature>
Ejemplo:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1NzI0LCJleHAiOjE0Nzc1NjY5MjQsImlhdCI6MTQ3NzU2NTcyNH0.6MzD1VwA 5AcOcajkFyKhLYybr3h13iZjDyHm9zysDFQ
Un token JWT tiene tres secciones:
- Encabezado: formato JSON codificado en Base64
- Reclamaciones: formato JSON codificado en Base64.
- Firma: creada y firmada según el encabezado y los reclamos codificados en Base64.
Si utiliza el sitio web jwt.io con el token anterior, puede decodificar el token y verlo como se muestra a continuación:
Técnicamente, JWT utiliza una firma que se firma a partir de encabezados y reclamos con un algoritmo de seguridad especificado en los encabezados (ejemplo: HMACSHA256). Por lo tanto, JWT debe transferirse a través de HTTP si almacena información confidencial en sus reclamos.
Ahora, para utilizar 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 validarlo cuando llega la solicitud. Eso es todo.
En la demostración que creé (github) , para mantener el token JWT liviano, solo almaceno username
y expiration time
. Pero de esta manera, debe reconstruir una nueva identidad local (principal) para agregar más información como roles, si desea realizar autorización de roles, etc. Pero, si desea agregar más información a JWT, depende de usted: es muy flexible.
En lugar de utilizar middleware OWIN, simplemente puede proporcionar un punto final de token JWT mediante una acción de 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;
}
}
Ésta es una acción ingenua; en producción, debe utilizar una solicitud POST o un punto final de autenticación básica para proporcionar el token JWT.
¿Cómo generar el token en función de username
?
Puede utilizar 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á listo.
¿Cómo validar el JWT cuando llega la solicitud?
En la demostración , he creado
JwtAuthenticationAttribute
el que hereda IAuthenticationFilter
(más detalles sobre el filtro de autenticación aquí ) .
Con este atributo puedes autenticar cualquier acción: sólo tienes que poner este atributo en esa acción.
public class ValueController : ApiController
{
[JwtAuthentication]
public string Get()
{
return "value";
}
}
También puede utilizar el middleware OWIN o DelegateHander si desea validar todas las solicitudes entrantes para su WebAPI (no específicas del 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 || !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 consiste en utilizar la biblioteca JWT (paquete NuGet anterior) 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 capital:
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 poner más información en ella para verificar la autorización del rol.
Recuerde agregar config.Filters.Add(new AuthorizeAttribute());
(autorización predeterminada) en el ámbito global para evitar cualquier solicitud anónima a sus recursos.
Puede utilizar 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 para la solicitud autorizada, ejemplo:
GET http://localhost:{port}/api/value
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1MjU4LCJleHAiOjE0Nzc1NjY0NTgsImlhdCI6MTQ3NzU2NTI1OH0.dSwwufd4-gztkLpttZsZ1255oEzpWCJkayR_4yvNL1s
La demostración se puede encontrar aquí: https://github.com/cuongle/WebApi.Jwt
Logré lograrlo con el mínimo esfuerzo (tan simple como con ASP.NET Core).
Para eso utilizo Startup.cs
el archivo y Microsoft.Owin.Security.Jwt
la biblioteca OWIN.
Para que la aplicación funcione Startup.cs
debemos modificar Web.config
:
<configuration>
<appSettings>
<add key="owin:AutomaticAppStartup" value="true" />
...
Así es como Startup.cs
debería verse:
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
}
});
}
}
}
Muchos de ustedes usan ASP.NET Core hoy en día, por lo que, como pueden ver, no difiere mucho de lo que tenemos allí.
Realmente me dejó perplejo al principio, estaba tratando de implementar proveedores personalizados, etc. Pero no esperaba que fuera tan simple. OWIN
¡solo rocas!
Solo una cosa para mencionar: después de habilitar OWIN Startup, NSWag
la biblioteca dejó de funcionar para mí (por ejemplo, es posible que algunos de ustedes quieran generar automáticamente servidores proxy HTTP mecanografiados para la aplicación Angular).
La solución también fue muy simple: lo reemplacé NSWag
y Swashbuckle
no tuve más problemas.
Ok, ahora compartiendo ConfigHelper
código:
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;
}
}
Otro aspecto importante: envié el token JWT a través del encabezado de Autorización , por lo que el código mecanografiado me busca de la siguiente manera:
(el siguiente código es generado por 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')
})
};
Ver parte de encabezados -"Authorization": "Bearer " + localStorage.getItem('token')
A continuación se muestra una implementación mínima y segura de una autenticación basada en notificaciones utilizando el token JWT en una API web ASP.NET Core.
En primer lugar, debe exponer un punto final que devuelva un token JWT con notificaciones asignadas a un usuario:
/// <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");
}
ahora necesita agregar autenticación a sus servicios en ConfigureServices
su startup.cs para agregar la autenticación JWT como su servicio de autenticación predeterminado de esta manera:
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"])),
};
});
ahora puede agregar políticas a sus servicios de autorización de esta manera:
services.AddAuthorization(options =>
{
options.AddPolicy("YourPolicyNameHere",
policy => policy.RequireClaim("YourClaimNameHere"));
});
ALTERNATIVAMENTE , también puede (no es necesario) completar todos sus reclamos desde su base de datos, ya que esto solo se ejecutará una vez al iniciar la aplicación y agregarlos a políticas como esta:
services.AddAuthorization(async options =>
{
var ClaimList = await claimApplication.GetList(applicationClaim);
foreach (var item in ClaimList)
{
options.AddPolicy(item.ClaimCode, policy => policy.RequireClaim(item.ClaimCode));
}
});
ahora puedes poner el filtro de Política en cualquiera de los métodos que quieras que estén autorizados de esta manera:
[HttpPost("update")]
[Authorize(Policy = "ACC_UP")]
public async Task<IActionResult> Update([FromBody] UserRequestVM requestVm, CancellationToken ct)
{
//your logic goes here
}
Espero que esto ayude