JWT Bearer Authentication
ライブラリ
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
ライブラリは.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必須要件
主要リンク
- NuGet Package
- Microsoft Docs - JWT Bearer
- ASP.NET Core Authentication
- JWT.io
- Azure Active Directory
- OpenID Connect
書き方の例
基本的なセットアップ
<!-- パッケージインストール -->
<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;
}
};
});