Odpowiedziałem na to pytanie: Jak zabezpieczyć interfejs API sieci Web ASP.NET 4 lata temu za pomocą HMAC.
Teraz wiele rzeczy zmieniło się pod względem bezpieczeństwa, szczególnie JWT zyskuje na popularności. W tym miejscu postaram się wyjaśnić, jak korzystać z JWT w najprostszy i podstawowy sposób, w jaki mogę, abyśmy nie zgubili się w dżungli OWIN, Oauth2, ASP.NET Identity ... :).
Jeśli nie znasz tokena JWT, musisz rzucić okiem na:
https://tools.ietf.org/html/rfc7519
Zasadniczo token JWT wygląda następująco:
<base64-encoded header>.<base64-encoded claims>.<base64-encoded signature>
Przykład:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1NzI0LCJleHAiOjE0Nzc1NjY5MjQsImlhdCI6MTQ3NzU2NTcyNH0.6MzD1VwA5AcOcajkFyKhLYybr3h13iZjDyHm9zysDFQ
Token JWT ma trzy sekcje:
- Nagłówek: format JSON zakodowany w Base64
- Roszczenia: Format JSON zakodowany w Base64.
- Podpis: utworzony i podpisany na podstawie nagłówka i oświadczeń zakodowanych w Base64.
Jeśli używasz witryny jwt.io z powyższym tokenem, możesz go zdekodować i zobaczyć jak poniżej:
Technicznie JWT używa podpisu podpisanego z nagłówków i oświadczeń za pomocą algorytmu bezpieczeństwa określonego w nagłówkach (przykład: HMACSHA256). Dlatego wymagane jest przesyłanie JWT przez HTTPs, jeśli przechowujesz poufne informacje w roszczeniach.
Teraz, aby korzystać z uwierzytelniania JWT, tak naprawdę nie potrzebujesz oprogramowania pośredniego OWIN, jeśli masz starszy system Web Api. Prosta koncepcja polega na tym, jak dostarczyć token JWT i jak sprawdzić poprawność tokena, gdy nadejdzie żądanie. Otóż to.
Powrót na demo, aby zachować JWT tokena lekka, tylko przechowywanie username
i expiration time
w JWT. Ale w ten sposób musisz odbudować nową tożsamość lokalną (podmiot główny), aby dodać więcej informacji, takich jak: role .. jeśli chcesz wykonać autoryzację roli. Ale jeśli chcesz dodać więcej informacji do JWT, to zależy od ciebie: jest bardzo elastyczny.
Zamiast używać oprogramowania pośredniego OWIN, możesz po prostu podać punkt końcowy tokena JWT, używając akcji z kontrolera:
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;
}
}
To naiwne działanie; w środowisku produkcyjnym należy użyć żądania POST lub punktu końcowego podstawowego uwierzytelniania, aby dostarczyć token JWT.
Jak wygenerować token na podstawie username
?
Możesz użyć pakietu NuGet wywoływanego System.IdentityModel.Tokens.Jwt
od Microsoft do wygenerowania tokena, a nawet innego pakietu, jeśli chcesz. W wersji demo używam HMACSHA256
z 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;
}
Punkt końcowy do dostarczenia tokenu JWT został zakończony. Jak sprawdzić poprawność JWT, gdy nadejdzie żądanie? W wersji demonstracyjnej, którą zbudowałem,
JwtAuthenticationAttribute
która dziedziczy IAuthenticationFilter
(więcej szczegółów na temat filtra uwierzytelniania tutaj ).
Za pomocą tego atrybutu możesz uwierzytelnić dowolne działanie: wystarczy umieścić ten atrybut na tym działaniu.
public class ValueController : ApiController
{
[JwtAuthentication]
public string Get()
{
return "value";
}
}
Możesz również użyć oprogramowania pośredniego OWIN lub DelegateHander, jeśli chcesz zweryfikować wszystkie przychodzące żądania dotyczące interfejsu WebAPI (nie dotyczy kontrolera ani działania)
Poniżej znajduje się podstawowa metoda z filtru uwierzytelniania:
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);
}
Przepływ pracy polega na użyciu biblioteki JWT (pakiet NuGet powyżej) do sprawdzenia poprawności tokena JWT, a następnie powrotu ClaimsPrincipal
. Możesz przeprowadzić większą weryfikację, np. Sprawdzić, czy użytkownik istnieje w systemie i dodać inne niestandardowe weryfikacje, jeśli chcesz. Kod sprawdzania poprawności tokenu JWT i odzyskania kwoty głównej:
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;
}
}
Jeśli token JWT jest sprawdzony, a nazwa główna jest zwracana, należy zbudować nową tożsamość lokalną i umieścić w niej więcej informacji, aby sprawdzić autoryzację roli.
Pamiętaj, aby dodać config.Filters.Add(new AuthorizeAttribute());
(domyślna autoryzacja) w zakresie globalnym, aby zapobiec anonimowym żądaniom do Twoich zasobów.
Możesz użyć Listonosza do przetestowania wersji demo:
Żeton żądania (naiwny, jak wspomniałem powyżej, tylko dla wersji demonstracyjnej):
GET http://localhost:{port}/api/token?username=cuong&password=1
Umieść token JWT w nagłówku autoryzowanego żądania, przykład:
GET http://localhost:{port}/api/value
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1MjU4LCJleHAiOjE0Nzc1NjY0NTgsImlhdCI6MTQ3NzU2NTI1OH0.dSwwufd4-gztkLpttZsZ1255oEzpWCJkayR_4yvNL1s
Demo znajduje się tutaj: https://github.com/cuongle/WebApi.Jwt