Słyszeliście o JSON Web Token-ach? Zapewne tak. W sieci pełno jest artykułów o wadach, zaletach, wykorzystaniu w implementacjach OAuth2 czy OIDC. Czy to znaczy, że napisano o nich wszystko, co się da? Przemilczę odpowiedź i dorzucę własną, mam nadzieje, że przydatną, cegiełkę.
Trochę z własnych obserwacji, jednak podejrzewam, że bliskich prawdzie, zakładam, że JWT to dzisiejszy standard zabezpieczania API. Pomimo, iż np. ze specyfikacji PASETO zaczyna powoli wyrastać coś ciekawego, jeszcze długa droga przed twórcami. JWT nie posiada prawdziwej konkurencji. Dziś przedstawie jak w kilku prostych krokach dodać generowanie JWT w aplikacji opartej o .NET Core Identity.
Konfiguracja
Musimy stworzyć trzy klucze w pliku appsettings.json
. Będą to odpowiednio:
JwtKey
- klucz szyfrowania podpisów sygnatury JWT;JwtIssuer
- nazwa serwera, przekazujący informacje o wystawiającym;JwtExpireDays
- czas ważności tokena (pamiętajcie - w aplikacjach produkcyjnych tokeny powinny być wystawiane na jak najkrótszy czas);
Nasz obiekt przechowywany w formacie JSON będzie wyglądał następująco:
{
"JwtKey": "SOME_RANDOM_KEY_DO_NOT_SHARE",
"JwtIssuer": "http://yourdomain.com",
"JwtExpireDays": 30,
}
Jak zawsze, dla takich struktur, możemy stworzyć prostą klasę, która posłuży nam za obiekt POCO.
public class JwtConfig
{
public string JwtKey { get; set; }
public string JwtIssuer { get; set; }
public int JwtExpireDays { get; set; }
}
Pozwala to w prosty sposób zbindować dane za pomocą API IServiceCollection w pliku Startup.cs
.
services.Configure<JwtConfig>(Configuration);
Tworzymy serwis
Teraz, kiedy mamy gotową konfigurację modułu do tworzenia tokenów JWT, możemy przejść do napisania naszego serwisu. Będzie on bardzo prosty. Jego jedyną odpowiedzialnością będzie generowanie nowego tokena. Nazwijmy go zagadkowo TokenService
.
Zacznijmy od zdefiniowania jego interfejsu abyśmy mogli go potem zarejestrować w kontenerze zależności:
public interface ITokenService
{
Task<string> GenerateNewToken(ApplicationUser user);
}
Serwis będzie posiadał tylko jedną metodę, mającą za zadanie tylko i wyłącznie wygenerować token JWT. Takie tam Single Responsibility Principle do kwadratu.
Czas na ...implementacje (a co myśleliście, kakałko? Jest miliard stopni, nawet ja nie jestem aż takim fanatykiem)*. Na początku pobieramy wcześniej zaimplementowany obiekt POCO.
Następnie przechodzimy do deklaracji naszej tablicy claims
. Oczywiście, mamy możliwość użycia kilku wcześniej zdefiniowanych propertis-ów. Są to wymagane i najczęściej używane claims-y
takie jak Sub
, Jti
, czy niestandardowy, ale bardzo często używany Name
.
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, user.Email),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.NameIdentifier, user.Id)
};
Kolejny krok to stworzenie credentiali. Do tego wykorzystamy wcześniej zdefiniowany klucz. Użyjemy do tego algorytmu HmacSha256.
Implementacje czegoś takiego zajmie nam dosłownie dwie linijki.
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtConfig.JwtKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
Teraz generujemy token i zwracamy jako wynik. Cała implementacja naszego serwisu wygląda następująco:
public class TokenService : ITokenService
{
private readonly JwtConfig _jwtConfig;
public TokenService(IOptions<JwtConfig> jwtConfig)
{
_jwtConfig = jwtConfig.Value;
}
public async Task<string> GenerateNewToken(ApplicationUser user)
{
if (user is null) throw new ArgumentNullException();
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, user.Email),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.NameIdentifier, user.Id)
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtConfig.JwtKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var expires = DateTime.Now.AddDays(Convert.ToDouble(_jwtConfig.JwtExpireDays));
var token = new JwtSecurityToken(
_jwtConfig.JwtIssuer,
_jwtConfig.JwtIssuer,
claims,
expires: expires,
signingCredentials: creds
)
{
Payload =
{
["MyPayloadKey"] = "MyPayloadValue"
}
};
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
Dodajemy middleware
Ostatnim krokiem jest zdefiniowanie middleware-a, który będzie sprawdzał poprawność wystawionych tokenów JWT. Jako, że to trochę bardziej skomplikowany kod niż tylko if (tokenIsValid()) to polecam wykorzystanie extensionMethod
do rozszerzenia klasy IServiceCollection
. Pozwala to utrzymać trochę porządku w pliku Startup.cs
.
public static class JwtAuthorizationExtension
{
public static void AddJwtAuthorization(this IServiceCollection services)
{
var serviceProvider = services.BuildServiceProvider();
var configuration = serviceProvider.GetService<IOptions<JwtConfig>>().Value;
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); // => remove default claims
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = configuration.JwtIssuer,
ValidAudience = configuration.JwtIssuer,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration.JwtKey)),
ClockSkew = TimeSpan.Zero // remove delay of token when expire
};
});
}
}
Teraz wystarczy że w metodzie ConfigureServices
wywołamy metodę
services.AddJwtAuthorization();
Podsumowanie
W ten sposób otrzymaliśmy serwis, którym możemy generować tokeny JWT. Dodatkowo, pokazałem wam jak można skonfigurować uwierzytelnienie.
Jak zwykle, dzięki za przeczytanie tego artykułu.
Do Następnego!
Cześć
*Jestem świadom, że można pić kakałko na zimno. Uznaje to za barbarzyństwo i nie planuje zniżyć się do takich pseudo praktyk.