1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JWTを使用したトークンベース認証と、ASP.NET Coreにおける実践

Posted at

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の登録

エンドポイントアクセス時にAuthenticationHTTPヘッダに添付される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トークンを、リクエスト時のAuthorizationHTTPヘッダに付与してやれば良い。

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

ASP + JWT

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?