Ho risposto a questa domanda: come proteggere un'API Web ASP.NET 4 anni fa usando HMAC.
Ora, molte cose sono cambiate in termini di sicurezza, in particolare JWT sta diventando popolare. Qui, proverò a spiegare come usare JWT nel modo più semplice e semplice che posso, quindi non ci perderemo dalla giungla di OWIN, Oauth2, ASP.NET Identity ... :).
Se non conosci il token JWT, devi dare un'occhiata a:
https://tools.ietf.org/html/rfc7519
Fondamentalmente, un token JWT assomiglia a:
<base64-encoded header>.<base64-encoded claims>.<base64-encoded signature>
Esempio:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1NzI0LCJleHAiOjE0Nzc1NjY5MjQsImlhdCI6MTQ3NzU2NTcyNH0.6MzD1VwA5AcOcajkFyKhLYybr3h13iZjDyHm9zysDFQ
Un token JWT ha tre sezioni:
- Intestazione: formato JSON codificato in Base64
- Reclami: formato JSON codificato in Base64.
- Firma: creata e firmata in base all'intestazione e alle attestazioni codificate in Base64.
Se si utilizza il sito Web jwt.io con il token sopra, è possibile decodificare il token e vederlo come di seguito:
Tecnicamente, JWT utilizza la firma firmata dalle intestazioni e le attestazioni con l'algoritmo di sicurezza specificato nelle intestazioni (esempio: HMACSHA256). Pertanto, è necessario che JWT sia trasferito su HTTP se si memorizzano informazioni sensibili in attestazioni.
Ora, per utilizzare l'autenticazione JWT, non è necessario un middleware OWIN se si dispone di un sistema Web Api legacy. Il semplice concetto è come fornire il token JWT e come convalidare il token quando arriva la richiesta. Questo è tutto.
Tornando alla demo, per mantenere il token JWT leggero, conservo solo username
e expiration time
in JWT. Ma in questo modo, è necessario ricostruire la nuova identità locale (principale) per aggiungere ulteriori informazioni come: ruoli .. se si desidera eseguire l'autorizzazione del ruolo. Ma, se vuoi aggiungere ulteriori informazioni in JWT, dipende da te: è molto flessibile.
Invece di utilizzare il middleware OWIN, è possibile fornire semplicemente un endpoint token JWT utilizzando l'azione del controller:
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;
}
}
Questa è un'azione ingenua; in produzione è necessario utilizzare una richiesta POST o un endpoint di autenticazione di base per fornire il token JWT.
Come generare il token in base username
?
Puoi usare il pacchetto NuGet chiamato System.IdentityModel.Tokens.Jwt
da Microsoft per generare il token, o anche un altro pacchetto, se lo desideri. Nella demo, utilizzo 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;
}
L'endpoint per fornire il token JWT è terminato. Ora, come convalidare il JWT quando arriva la richiesta? Nella demo ho creato quello
JwtAuthenticationAttribute
che eredita da IAuthenticationFilter
(maggiori dettagli sul filtro di autenticazione qui ).
Con questo attributo, puoi autenticare qualsiasi azione: devi solo mettere questo attributo su quell'azione.
public class ValueController : ApiController
{
[JwtAuthentication]
public string Get()
{
return "value";
}
}
È inoltre possibile utilizzare il middleware OWIN o DelegateHander se si desidera convalidare tutte le richieste in arrivo per la propria API Web (non specifica del controller o dell'azione)
Di seguito è riportato il metodo principale dal filtro di autenticazione:
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);
}
Il flusso di lavoro è, utilizzando la libreria JWT (pacchetto NuGet sopra) per convalidare il token JWT e quindi tornare indietro ClaimsPrincipal
. Puoi eseguire più convalide come verificare se l'utente esiste sul tuo sistema e aggiungere altre convalide personalizzate se lo desideri. Il codice per convalidare il token JWT e recuperare l'entità:
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 il token JWT è convalidato e l'entità viene restituita, è necessario creare una nuova identità locale e inserire ulteriori informazioni per verificare l'autorizzazione del ruolo.
Ricorda di aggiungere config.Filters.Add(new AuthorizeAttribute());
(autorizzazione predefinita) a livello globale al fine di prevenire qualsiasi richiesta anonima alle tue risorse.
Puoi usare Postman per testare la demo:
Token di richiesta (ingenuo come ho detto sopra, solo per la demo):
GET http://localhost:{port}/api/token?username=cuong&password=1
Inserisci il token JWT nell'intestazione per una richiesta autorizzata, ad esempio:
GET http://localhost:{port}/api/value
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1MjU4LCJleHAiOjE0Nzc1NjY0NTgsImlhdCI6MTQ3NzU2NTI1OH0.dSwwufd4-gztkLpttZsZ1255oEzpWCJkayR_4yvNL1s
La demo è disponibile qui: https://github.com/cuongle/WebApi.Jwt