zukucode
主にWEB関連の情報を技術メモとして発信しています。

ASP.NET jwtのログイン認証を実装する

ASP.NETのSPAプロジェクトにjwtのログイン認証を実装する方法を紹介します。

基本的にはASP.NET SPAサイトにcookieベースのログイン認証を実装するで紹介したcookieベースの方法と同じです。

ライブラリの追加

Microsoft.AspNetCore.Authentication.JwtBearerのライブラリが必要になるので、以下のURLを参考にプロジェクトにライブラリを追加します。

参考URL:https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.JwtBearer

Startup.csの修正

Startup.csに設定を追記します。

ValidIssuerValidAudienceはサイトのURLを設定しておくと良いかと思いますがlocalhostなどでも構いません。

IssuerSigningKeyには秘密鍵を設定しますので、実際は設定ファイルなどから読み込むようにしておくと良いかと思います。

デフォルトの認証をjwtにするためには23行目の設定ではなく25行目のように設定する必要があります。(23行目の設定方法だとcookie認証が有効になる)

Startup.cs(一部抜粋)
public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
    {
        var jwtConfig = new JwtConfig();
        Configuration.Bind("Jwt", jwtConfig);
        options.TokenValidationParameters = new TokenValidationParameters()
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = "https://zukucode.com",
            ValidAudience = "https://zukucode.com",
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("1234567890abcdefg")),
            ClockSkew = TimeSpan.Zero
        };
    });

    services.AddControllers(options =>
    {
        // すべてのアクセスに対してcookie認証保護を適用する
        options.Filters.Add(new AuthorizeFilter(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()));
        // すべてのアクセスに対してjwtの認証保護を適用する
        options.Filters.Add(new AuthorizeFilter(new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme).RequireAuthenticatedUser().Build()));
    })


public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IAntiforgery antiforgery)
{
    app.UseRouting();

    // app.UseRouting();の下に追加
    app.UseAuthentication();
    app.UseAuthorization();

認証トークンの発行

ログイン時に以下のように認証トークンを発行して、クライアントにレスポンスします。

issueraudienceなどはStartup.csで設定したものと同じ値を使用します。

public string GenerateToken(string userid)
{
    var claims = new[] {
        // 必要な認証情報を追加する
        new Claim(ClaimTypes.Name, userid)
    };

    var token = new JwtSecurityToken(
        "https://zukucode.com", // issuer
        "https://zukucode.com", // audience
        claims,
        expires: DateTime.Now.AddSeconds(10000), // 有効期限
        signingCredentials: new SigningCredentials("1234567890abcdefg", SecurityAlgorithms.HmacSha256) // Startup.csで設定したものと同じ値を設定
    );

    return new JwtSecurityTokenHandler().WriteToken(token);
}

動作確認

cookie認証の場合と同じ手順になります。

プロジェクトを作成した際の雛形として作成されたWeatherForecastControllerで動作確認を行います。

ここではAPIとしてajaxでアクセスするため、Routeの設定箇所に以下のようにapiを追加します。

こうすると、api/WeatherForecastでアクセスできるようになります。

    [ApiController]
    [Route("[controller]")]
    [Route("api/[controller]")]
    public class WeatherForecastController : ControllerBase

クライアント側で、ajaxのリクエストを投げます。

ここではajaxのライブラリにaxiosを使用します。

const response = axios.get('/api/WeatherForecast');

未認証の状態でアクセスをすると401(Unauthorized)のエラーになることが確認できます。

認証保護の除外

例えばログイン時のAPIなど、未認証の状態でもアクセスしたい場合があります。

その場合は対象のメソッドにAllowAnonymousを設定します。

using Microsoft.AspNetCore.Authorization;


    [HttpGet]
    [AllowAnonymous]
    public IEnumerable<WeatherForecast> Get()

AllowAnonymousを設定後にアクセスするとデータが取得できることを確認できます。

ログイン/ログアウト処理

認証用のコントローラーを作成し、以下のようにログイン処理とログアウト処理を実装します。

「認証判定処理」の部分は実際にはデータベースなどでIDやパスワードを使って判定することになると思います。

AuthController.cs
[ApiController]
[Route("api/[controller]/[action]")]
public class AuthController : ControllerBase
{

    public class LoginRequest
    {
        public string login_id { get; set; }
        public string password { get; set; }
    }

    [HttpPost]
    [AllowAnonymous]
    public async Task<IActionResult> Login(LoginRequest request)
    {
        if (認証判定処理(request.login_id, request.password) == false)
        {
            return BadRequest("ユーザー名またはパスワードが違います。");
        }

        return Ok(GenerateToken(login_id)); // 認証トークンをレスポンスする
    }
}

クライアント側からは以下のようにログイン処理をコールします。

const response = await axios.post('/api/auth/Login', { login_id: 入力したログインID, password: 入力したパスワード });

ログイン処理に成功した場合、認証トークンがレスポンスされます。

次回以降のリクエストにはこの認証トークンをリクエストヘッダに含める必要があります。

axiosの場合は以下のように設定します。

axios.interceptors.request.use((request) => {
    request.headers['Authorization'] = `Bearer ${token}`;
    return request;
});

画面を再描画したときにログイン状態を保持したい場合は、認証トークンをlocalstorageなどに保存しておく必要があります。

セキュリティを考慮して、localstorageの代わりにhttponlyのcookieに保持したい場合は設定を少し変更する必要があります。

jwtをcookieで保持する方法はASP.NET jwtの認証トークンをcookieで保持する方法で紹介しています。


関連記事