Respondi a esta pergunta: Como proteger uma API da Web do ASP.NET há 4 anos usando o HMAC.
Agora, muitas coisas mudaram em segurança, especialmente o JWT está ficando popular. Aqui, tentarei explicar como usar o JWT da maneira mais simples e básica possível, para que não nos percam da selva de OWIN, Oauth2, ASP.NET Identity ... :).
Se você não conhece o token JWT, precisa dar uma olhada em:
https://tools.ietf.org/html/rfc7519
Basicamente, um token JWT se parece com:
<base64-encoded header>.<base64-encoded claims>.<base64-encoded signature>
Exemplo:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1NzI0LCJleHAiOjE0Nzc1NjY5MjQsImlhdCI6MTQ3NzU2NTcyNH0.6MzD1VwA5AcOcajkFyKhLYybr3h13iZjDyHm9zysDFQ
Um token JWT possui três seções:
- Cabeçalho: formato JSON codificado em Base64
- Reivindicações: formato JSON codificado em Base64.
- Assinatura: criada e assinada com base no cabeçalho e nas reivindicações, codificado em Base64.
Se você usar o site jwt.io com o token acima, poderá decodificar o token e vê-lo como abaixo:
Tecnicamente, o JWT usa assinatura assinada a partir de cabeçalhos e declarações com o algoritmo de segurança especificado nos cabeçalhos (exemplo: HMACSHA256). Portanto, é necessário que o JWT seja transferido por HTTPs se você armazenar informações confidenciais nas declarações.
Agora, para usar a autenticação JWT, você realmente não precisa de um middleware OWIN se tiver um sistema Web Api herdado. O conceito simples é como fornecer o token JWT e como validar o token quando a solicitação chegar. É isso aí.
De volta à demonstração, para manter o token JWT leve, eu apenas armazeno username
eexpiration time
no JWT. Porém, dessa maneira, é necessário recriar a nova identidade local (principal) para adicionar mais informações como: functions .. se você deseja autorizar a função. Mas, se você deseja adicionar mais informações ao JWT, você decide: é muito flexível.
Em vez de usar o middleware OWIN, você pode simplesmente fornecer um ponto de extremidade do token JWT usando a ação do 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 é uma ação ingênua; na produção, você deve usar uma solicitação POST ou um terminal de autenticação básica para fornecer o token JWT.
Como gerar o token com base username
?
Você pode usar o pacote NuGet chamado System.IdentityModel.Tokens.Jwt
da Microsoft para gerar o token ou até outro pacote, se desejar. Na demonstração, eu uso HMACSHA256
com 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;
}
O terminal para fornecer o token JWT está pronto. Agora, como validar o JWT quando a solicitação chega? Na demonstração que construí,
JwtAuthenticationAttribute
que herda de IAuthenticationFilter
(mais detalhes sobre o filtro de autenticação aqui ).
Com esse atributo, você pode autenticar qualquer ação: basta colocar esse atributo nessa ação.
public class ValueController : ApiController
{
[JwtAuthentication]
public string Get()
{
return "value";
}
}
Você também pode usar o middleware OWIN ou o DelegateHander se desejar validar todas as solicitações recebidas pela sua WebAPI (não específica ao Controller ou ação)
Abaixo está o método principal do filtro de autenticação:
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);
}
O fluxo de trabalho é, usando a biblioteca JWT (pacote NuGet acima) para validar o token JWT e depois retornar ClaimsPrincipal
. Você pode executar mais validações, como verificar se o usuário existe no seu sistema e adicionar outras validações personalizadas, se desejar. O código para validar o token JWT e recuperar o 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;
}
}
Se o token JWT for validado e o principal for retornado, você deverá construir uma nova identidade local e colocar mais informações para verificar a autorização da função.
Lembre-se de adicionar config.Filters.Add(new AuthorizeAttribute());
(autorização padrão) no escopo global para impedir qualquer solicitação anônima aos seus recursos.
Você pode usar o Postman para testar a demonstração:
Token de solicitação (ingênuo como mencionei acima, apenas para demonstração):
GET http://localhost:{port}/api/token?username=cuong&password=1
Coloque o token JWT no cabeçalho da solicitação autorizada, exemplo:
GET http://localhost:{port}/api/value
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1MjU4LCJleHAiOjE0Nzc1NjY0NTgsImlhdCI6MTQ3NzU2NTI1OH0.dSwwufd4-gztkLpttZsZ1255oEzpWCJkayR_4yvNL1s
A demonstração é apresentada aqui: https://github.com/cuongle/WebApi.Jwt