JWT Bearer Authentication

認証JWTBearerASP.NET Core.NETミドルウェアOpenID Connect

ライブラリ

JWT Bearer Authentication

概要

JWT Bearer Authenticationは、ASP.NET CoreアプリケーションでJSON Web Token(JWT)ベースの認証を実装するためのMicrosoftの公式ミドルウェアライブラリです。

詳細

JWT Bearer Authenticationは、Microsoft.AspNetCore.Authentication.JwtBearerパッケージとして提供される、ASP.NET CoreおよびAzure環境でのJWTベースの認証を支援するミドルウェアライブラリです。このライブラリは、OpenID Connect Bearer トークンの受信を可能にし、APIやWebサービスのセキュアな認証を実現します。

主要な機能として、JWTトークンの自動検証、複数の暗号化アルゴリズムのサポート、OpenID Connectプロトコルとの統合、カスタム認証イベントハンドリング、Azureクラウドサービスとの完全統合があります。特に、Authority URIを使用してトークンの署名検証に必要な公開キーの自動取得・検証機能により、企業レベルのセキュリティ要件を満たします。

JwtBearerHandlerは、AuthenticationHandlerを継承し、HandleAuthenticateAsync()メソッドをオーバーライドしてJWT処理を行います。このハンドラーは、JSON Web Tokenの逆シリアル化、検証、適切なAuthenticateResultとAuthenticationTicketの作成を担当します。

ライブラリは.NET Core 3.0以降および.NET Standard 2.1と互換性があり、シームレスなASP.NET Coreアプリケーション統合、セキュアなAPIとWebサービスの認証、柔軟なトークン検証パラメータ設定、高いセキュリティ基準への準拠を特徴としています。

メリット・デメリット

メリット

  • Microsoft公式サポート: 公式ライブラリによる信頼性とサポート
  • ASP.NET Core統合: フレームワークとの完全統合
  • 自動トークン検証: JWT署名の自動検証とセキュリティチェック
  • OpenID Connect対応: 標準プロトコルとの完全互換性
  • Azure統合: Azure Active Directoryとのシームレス連携
  • 柔軟な設定: 豊富な設定オプションとカスタマイズ性
  • 高パフォーマンス: 最適化されたミドルウェア実装

デメリット

  • .NET限定: .NET/ASP.NET Core環境でのみ利用可能
  • 学習コスト: JWT仕様とASP.NET Core認証システムの理解が必要
  • 設定の複雑さ: 高度なセキュリティ設定時の複雑性
  • デバッグ困難: 認証エラーの原因特定が困難な場合がある
  • Microsoft依存: Microsoftエコシステムへの依存
  • HTTPS必須: 本番環境でのHTTPS必須要件

主要リンク

書き方の例

基本的なセットアップ

<!-- パッケージインストール -->
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.0.3" />
# CLI経由でのインストール
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package System.IdentityModel.Tokens.Jwt

基本的なJWT設定(Program.cs)

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

// JWT認証の設定
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SecretKey"])
            ),
            ClockSkew = TimeSpan.Zero // トークンの時刻スキューを無効化
        };
    });

builder.Services.AddAuthorization();
builder.Services.AddControllers();

var app = builder.Build();

// 認証ミドルウェアの追加(順序が重要)
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();
app.Run();

appsettings.json設定

{
  "Jwt": {
    "SecretKey": "your-super-secret-key-that-is-at-least-256-bits-long",
    "Issuer": "your-app-issuer",
    "Audience": "your-app-audience",
    "ExpirationInMinutes": 60
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}

JWT生成サービス

using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

public interface IJwtTokenService
{
    string GenerateToken(string userId, string email, IList<string> roles);
    ClaimsPrincipal ValidateToken(string token);
}

public class JwtTokenService : IJwtTokenService
{
    private readonly IConfiguration _configuration;
    private readonly SymmetricSecurityKey _securityKey;

    public JwtTokenService(IConfiguration configuration)
    {
        _configuration = configuration;
        _securityKey = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(_configuration["Jwt:SecretKey"])
        );
    }

    public string GenerateToken(string userId, string email, IList<string> roles)
    {
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.NameIdentifier, userId),
            new Claim(ClaimTypes.Email, email),
            new Claim(JwtRegisteredClaimNames.Sub, userId),
            new Claim(JwtRegisteredClaimNames.Email, email),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            new Claim(JwtRegisteredClaimNames.Iat, 
                new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds().ToString(), 
                ClaimValueTypes.Integer64)
        };

        // ロールをクレームに追加
        foreach (var role in roles)
        {
            claims.Add(new Claim(ClaimTypes.Role, role));
        }

        var credentials = new SigningCredentials(_securityKey, SecurityAlgorithms.HmacSha256);
        var expiration = DateTime.UtcNow.AddMinutes(
            int.Parse(_configuration["Jwt:ExpirationInMinutes"])
        );

        var token = new JwtSecurityToken(
            issuer: _configuration["Jwt:Issuer"],
            audience: _configuration["Jwt:Audience"],
            claims: claims,
            expires: expiration,
            signingCredentials: credentials
        );

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

    public ClaimsPrincipal ValidateToken(string token)
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var validationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = _configuration["Jwt:Issuer"],
            ValidAudience = _configuration["Jwt:Audience"],
            IssuerSigningKey = _securityKey,
            ClockSkew = TimeSpan.Zero
        };

        SecurityToken validatedToken;
        var principal = tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
        return principal;
    }
}

認証コントローラー

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private readonly IJwtTokenService _jwtTokenService;
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly SignInManager<ApplicationUser> _signInManager;

    public AuthController(
        IJwtTokenService jwtTokenService,
        UserManager<ApplicationUser> userManager,
        SignInManager<ApplicationUser> signInManager)
    {
        _jwtTokenService = jwtTokenService;
        _userManager = userManager;
        _signInManager = signInManager;
    }

    [HttpPost("login")]
    public async Task<IActionResult> Login([FromBody] LoginRequest request)
    {
        var user = await _userManager.FindByEmailAsync(request.Email);
        if (user == null)
        {
            return Unauthorized(new { message = "無効な認証情報です" });
        }

        var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, false);
        if (!result.Succeeded)
        {
            return Unauthorized(new { message = "無効な認証情報です" });
        }

        var roles = await _userManager.GetRolesAsync(user);
        var token = _jwtTokenService.GenerateToken(user.Id, user.Email, roles);

        return Ok(new LoginResponse
        {
            Token = token,
            ExpiresAt = DateTime.UtcNow.AddMinutes(60),
            User = new UserInfo
            {
                Id = user.Id,
                Email = user.Email,
                Name = user.UserName,
                Roles = roles
            }
        });
    }

    [HttpPost("refresh")]
    [Authorize]
    public async Task<IActionResult> RefreshToken()
    {
        var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        var user = await _userManager.FindByIdAsync(userId);
        
        if (user == null)
        {
            return Unauthorized();
        }

        var roles = await _userManager.GetRolesAsync(user);
        var newToken = _jwtTokenService.GenerateToken(user.Id, user.Email, roles);

        return Ok(new { token = newToken, expiresAt = DateTime.UtcNow.AddMinutes(60) });
    }

    [HttpGet("me")]
    [Authorize]
    public async Task<IActionResult> GetCurrentUser()
    {
        var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        var user = await _userManager.FindByIdAsync(userId);
        
        if (user == null)
        {
            return NotFound();
        }

        var roles = await _userManager.GetRolesAsync(user);
        
        return Ok(new UserInfo
        {
            Id = user.Id,
            Email = user.Email,
            Name = user.UserName,
            Roles = roles
        });
    }
}

public class LoginRequest
{
    public string Email { get; set; }
    public string Password { get; set; }
}

public class LoginResponse
{
    public string Token { get; set; }
    public DateTime ExpiresAt { get; set; }
    public UserInfo User { get; set; }
}

public class UserInfo
{
    public string Id { get; set; }
    public string Email { get; set; }
    public string Name { get; set; }
    public IList<string> Roles { get; set; }
}

保護されたAPIコントローラー

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;

[ApiController]
[Route("api/[controller]")]
[Authorize] // JWT認証が必要
public class ProductsController : ControllerBase
{
    [HttpGet]
    public IActionResult GetProducts()
    {
        var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        var userEmail = User.FindFirst(ClaimTypes.Email)?.Value;
        
        // ユーザー固有のデータ取得ロジック
        var products = GetUserProducts(userId);
        
        return Ok(products);
    }

    [HttpPost]
    [Authorize(Roles = "Admin,Manager")] // 特定ロールが必要
    public IActionResult CreateProduct([FromBody] ProductCreateRequest request)
    {
        var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        
        // 製品作成ロジック
        var product = CreateNewProduct(request, userId);
        
        return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
    }

    [HttpGet("{id}")]
    public IActionResult GetProduct(int id)
    {
        var product = GetProductById(id);
        if (product == null)
        {
            return NotFound();
        }

        return Ok(product);
    }

    [HttpDelete("{id}")]
    [Authorize(Roles = "Admin")] // 管理者のみ
    public IActionResult DeleteProduct(int id)
    {
        var success = DeleteProductById(id);
        if (!success)
        {
            return NotFound();
        }

        return NoContent();
    }

    private List<Product> GetUserProducts(string userId)
    {
        // 実装例
        return new List<Product>();
    }

    private Product CreateNewProduct(ProductCreateRequest request, string userId)
    {
        // 実装例
        return new Product { Id = 1, Name = request.Name };
    }

    private Product GetProductById(int id)
    {
        // 実装例
        return new Product { Id = id, Name = "Sample Product" };
    }

    private bool DeleteProductById(int id)
    {
        // 実装例
        return true;
    }
}

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
}

public class ProductCreateRequest
{
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
}

Azure AD統合

// Azure AD統合のためのProgram.cs設定
using Microsoft.AspNetCore.Authentication.JwtBearer;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "https://login.microsoftonline.com/{tenant-id}";
        options.Audience = builder.Configuration["AzureAd:ClientId"];
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ClockSkew = TimeSpan.Zero
        };

        // カスタムイベントハンドリング
        options.Events = new JwtBearerEvents
        {
            OnTokenValidated = context =>
            {
                // トークン検証後の処理
                var claimsIdentity = context.Principal.Identity as ClaimsIdentity;
                var userId = claimsIdentity?.FindFirst("sub")?.Value;
                
                // カスタムクレームの追加など
                return Task.CompletedTask;
            },
            OnAuthenticationFailed = context =>
            {
                // 認証失敗時の処理
                context.Response.StatusCode = 401;
                context.Response.ContentType = "application/json";
                
                var response = new { message = "認証に失敗しました" };
                return context.Response.WriteAsync(System.Text.Json.JsonSerializer.Serialize(response));
            },
            OnChallenge = context =>
            {
                // 認証チャレンジ時の処理
                context.HandleResponse();
                context.Response.StatusCode = 401;
                context.Response.ContentType = "application/json";
                
                var response = new { message = "認証が必要です" };
                return context.Response.WriteAsync(System.Text.Json.JsonSerializer.Serialize(response));
            }
        };
    });

カスタム認証ミドルウェア

public class CustomJwtMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IJwtTokenService _jwtTokenService;

    public CustomJwtMiddleware(RequestDelegate next, IJwtTokenService jwtTokenService)
    {
        _next = next;
        _jwtTokenService = jwtTokenService;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var token = ExtractTokenFromHeader(context.Request);
        
        if (!string.IsNullOrEmpty(token))
        {
            try
            {
                var principal = _jwtTokenService.ValidateToken(token);
                context.User = principal;
            }
            catch (Exception ex)
            {
                // ログ記録
                // トークンが無効でも処理を続行(匿名アクセス許可)
            }
        }

        await _next(context);
    }

    private string ExtractTokenFromHeader(HttpRequest request)
    {
        var authHeader = request.Headers["Authorization"].FirstOrDefault();
        if (authHeader != null && authHeader.StartsWith("Bearer "))
        {
            return authHeader.Substring("Bearer ".Length).Trim();
        }
        return null;
    }
}

// ミドルウェアの登録
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseMiddleware<CustomJwtMiddleware>();
    // 他のミドルウェア設定
}

高度なトークン検証設定

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            // 基本検証パラメータ
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            
            // 発行者とオーディエンスの設定
            ValidIssuer = configuration["Jwt:Issuer"],
            ValidAudience = configuration["Jwt:Audience"],
            
            // 署名キーの設定
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(configuration["Jwt:SecretKey"])
            ),
            
            // 高度な設定
            ClockSkew = TimeSpan.Zero, // 時刻スキューを無効化
            RequireExpirationTime = true, // 有効期限必須
            RequireSignedTokens = true, // 署名必須
            
            // カスタム検証
            LifetimeValidator = (notBefore, expires, token, validationParameters) =>
            {
                return expires != null && expires > DateTime.UtcNow;
            },
            
            // クレーム変換
            NameClaimType = ClaimTypes.NameIdentifier,
            RoleClaimType = ClaimTypes.Role
        };

        // HTTPSリダイレクト設定(本番環境)
        options.RequireHttpsMetadata = !env.IsDevelopment();
        
        // カスタムトークン取得
        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                // クッキーからトークンを取得する場合
                var token = context.Request.Cookies["access_token"];
                if (!string.IsNullOrEmpty(token))
                {
                    context.Token = token;
                }
                return Task.CompletedTask;
            }
        };
    });