JWTを使用したトークンベース認証の実証。
れぽ
概論
JWTとは
JSON Web Token
JWTとは、JSONデータに対する暗号化・署名方法を定めたオープン標準(RFC7519)である。
JWT自体の詳細は割愛。
JWT自体は「認証のために策定されたもの」ではないのだが、
その性質からOAuthといったSSO技術のコア技術として使用されていたりする。
セッション VS トークン
JWTが特にセッションに勝っている点
認証情報の検証がJWT単体で完結する点。
セッション方式の場合、認証情報の有効性を検証するためにはセッションストアに記録されているIDの有効性との突合が必要になる。
一方のJWTは、JWT自身に付与されている署名を用いて有効性を検証するため、JWTのみで検証が自己完結する。
これにより、サービス間を疎結合に保ったまま、共通の認証基盤に依るSSOを実現することが出来る。
JWTが特にセッションに劣る点
JWTに無効化という概念はない。
Expires(有効期間)は規定されており、それによる無効化は可能だが、
セッションのように「ログアウト等の操作で、特定の認証情報を無効化する」という操作はできない。ステートレスな機構ゆえの現象。
そのため、不用意に有効期間の長いJWTを発行してしまうと、セッションハイジャックと同様の脅威に晒される。
やろうと思えば「JWTを無効化する」機構を実装すること自体はできるが、それをやるとJWTの最大の利点であるステートレス性を失うため元も子もない。
ASP.NET Core + JWT Bearer認証 実証
大筋、Microsoft公式のガイドラインに従っている。
JWT Bearerの登録
エンドポイントアクセス時にAuthentication
HTTPヘッダに添付されるJWTトークンを認証するサービスを登録する。
void ConfigureAuthentications(WebApplicationBuilder builder)
{
var services = builder.Services;
// ConfigureSettingsが先行していることを前提
var jwtSettings = services.BuildServiceProvider()
.GetRequiredService<IJwtSettings>();
_ = services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = jwtSettings.Issuer,
ValidAudience = jwtSettings.Audience,
IssuerSigningKey = jwtSettings.GetSymmetricSecurityKey(),
ValidateIssuer = true,
ValidateIssuerSigningKey = true,
ValidateAudience = true,
ValidateLifetime = true
};
});
}
今回はAPIサーバー自体がIssuer(発行者)になるので、署名に用いる鍵も自身で生成する。
認証サーバーが分かれるなら、認証サーバーの公開鍵をここに登録すれば良い。
public SymmetricSecurityKey GetSymmetricSecurityKey()
{
// キーを直接格納しているなら以下のように
//return new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Key));
// パスフレーズを登録させて、こちらでSHA256によるハッシュを行う方式
var passphraseBytes = Encoding.UTF8.GetBytes(Passphrase);
var keyBytes = SHA256.HashData(passphraseBytes);
return new SymmetricSecurityKey(keyBytes);
}
エンドポイントを要認証にする
要認証を表すシグネチャとして、[Authorize]
属性を使用する。
エンドポイント単体を要認証にする場合は以下のように。
[Authorize]
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
ApiController
クラス全体を要認証にしたり、
原則を要認証としたうえで、特定エンドポイントのみ認証不要にするなど、このあたりは柔軟に設定できる。
ここまでの実装で、当該のエンドポイントにそのままアクセスすると自動で401 Unauthorized
になる。
JWTトークンの発行
エンドポイント
一方で、JWTトークンを発行する側の実装。
Verification
部分についてはなんでもよい。
今回は単純なBasic認証でユーザ認証を行い、その結果に応じてJWTトークンを返却するエンドポイントとして実装をしている。
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginParameters parameters)
{
// Verification
var user = await userRepository.GetByIdAsync(parameters.Id);
var flatPassword = parameters.Password;
var verified = PasswordManager.VerifyPassword(flatPassword, user.Password);
if (verified)
{
var token = jwt.GenerateToken(user);
return Ok(new { token });
}
return Unauthorized();
}
JWTトークン生成
JWTトークン生成サービスはこんな感じ。
- Claimにユーザ情報とJTI(JWTの識別番号)を与えている
- Claimは暗号化されないので、機密情報は含めないこと
- Expiresを規定している
- 今回は1時間
- 署名鍵を登録している
- 今回は署名者も検証者も自分なので、署名鍵と検証鍵は共通鍵
public class JwtTokenService
{
private readonly IJwtSettings settings;
public JwtTokenService(IJwtSettings settings)
{
this.settings = settings;
}
public string GenerateToken(UserRecord user)
{
var credentials = new SigningCredentials(settings.GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256);
var claims = new Claim[]
{
new(JwtRegisteredClaimNames.Sub, user.Id),
new(JwtRegisteredClaimNames.Email, user.Email),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var token = new JwtSecurityToken(
issuer: settings.Issuer,
audience: settings.Audience,
claims: claims,
expires: GetExpires(),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
private static DateTime GetExpires(int expiresHours = 1) => DateTime.Now.AddHours(expiresHours);
}
JWTトークンを利用
あとは、上記のJWT取得エンドポイントから得たJWTトークンを、リクエスト時のAuthorization
HTTPヘッダに付与してやれば良い。
Authorization: Bearer <jwt-token>
TODO
ロールベース認証
軽い検証を行ったのみ。
こんな感じの属性でエンドポイントにアクセスできるロールを指定する。
[Authorize(Role = "Admin")]
ロールをValidatorに登録する流れは、まだふわふわしてるので割愛。
セッションとJWTを組み合わせた認証機構
セッションとJWTの互いの利点を組み合わせたハイブリッド認証機構。
クライアントのログイン状態管理にはセッション方式の認証を行い、
APIへのアクセスにはJWTトークンを要求する(つまり、認可になる)。
JWTトークンの発行の際にはセッションIDの有効性を検証する。
また、JWTのExpiresは十分に短い(1-15分程度)ものを設定する。
これにより、
- セッション方式の利点は享受しつつ
- APIのステートレス性を担保
- JWTの有効期間を最小にすることで、窃取への耐性を高める
一方で、
- 認証サーバーが単一障害点になる
- JWTが発行できなくなるとAPIも共倒れ
- JWT発行頻度が高くなるので結局認証サーバーへの負荷は高め
といった問題は残る。
NOTE
エンタープライズレベルの高いセキュリティ要件が求められるシナリオ向けに考えたもの。
正味、PJの規模次第ではオーバーエンジニアリング。
一方のセッション認証部分
ASP + Reditによるセッションストア管理がよいのかな~などと思っている段階。
LINKS
ABOUT JWT
- JWTについてまとめてみる
- OAuth 2.0のAccess TokenへのJSON Web Token(JSON Web Signature)の適用 - r-weblife
- JSON Web Token(JWT)のClaimについて · なるはやで いい感じの 動作確認