このドキュメントの内容
System.IdentityModel.Tokens.Jwt
を使って JWT (JSON Web Token) を生成する方法を説明します。
System.IdentityModel.Tokens.Jwt
のバージョンは 5.4.0 です。
トークンの生成
トークンプロバイダー
今回定義しているのはトークン生成メソッドのみです。
/// <summary>
/// トークンプロバイダー。
/// </summary>
/// <typeparam name="TPayload">ペイロードの型</typeparam>
public interface ITokenProvider<TPayload>
{
/// <summary>
/// トークンを生成します。
/// </summary>
/// <param name="claim">クレーム</param>
/// <param name="payload">ペイロード</param>
/// <param name="expiration">有効期限</param>
/// <returns>トークン</returns>
string CreateToken(IClaim claim, TPayload payload, DateTimeOffset expiration);
}
クレーム情報
トークンに与えるクレーム情報のうち、呼び出し元から指定する項目を定義しています。
/// <summary>
/// クレーム情報。
/// </summary>
public interface IClaim
{
/// <summary>
/// IDを取得します。
/// </summary>
string ID { get; }
/// <summary>
/// 利用者を取得します。
/// </summary>
string Audience { get; }
}
/// <summary>
/// クレーム情報。
/// </summary>
public class MsJwtClaim : IClaim
{
/// <summary>
/// IDを取得または設定します。
/// </summary>
public string ID { get; set; }
/// <summary>
/// 利用者を取得または設定します。
/// </summary>
public string Audience { get; set; }
}
トークンプロバイダーの実装
前述の ITokenProvider<TPayload>
インターフェースを実装する、System.IdentityModel.Tokens.Jwt
を用いたトークンプロバイダーです。
ペイロードは JSON 文字列にして指定します。JSON シリアライザには JSON.net
を用いています。
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using Newtonsoft.Json;
/// <summary>
/// トークンプロバイダー。
/// </summary>
/// <typeparam name="TPayload">ペイロードの型</typeparam>
public class MsJwtProvider<TPayload> : ITokenProvider<TPayload>
{
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="key">RSA秘密鍵</param>
public MsJwtProvider(RsaSecurityKey key)
{
m_Key = key;
}
private readonly RsaSecurityKey m_Key;
private readonly JwtSecurityTokenHandler m_TokenHandler = new JwtSecurityTokenHandler();
/// <summary>
/// 発行者を取得または設定します。
/// </summary>
public string Issuer { get; set; }
/// <summary>
/// トークンを生成します。
/// </summary>
/// <param name="claim">クレーム</param>
/// <param name="payload">ペイロード</param>
/// <param name="expiration">有効期限</param>
/// <returns>トークン</returns>
public string CreateToken(IClaim claim, TPayload payload, DateTimeOffset expiration)
{
var credentials = new SigningCredentials(m_Key, "RS256");
var descriptor = new SecurityTokenDescriptor { };
descriptor.SigningCredentials = credentials;
descriptor.Issuer = Issuer;
descriptor.Audience = claim.Audience;
descriptor.Expires = expiration.UtcDateTime;
descriptor.NotBefore = DateTime.UtcNow.AddSeconds(-5);
descriptor.IssuedAt = DateTime.UtcNow;
// 定義済でない項目をクレームに含めるには、Claims ではなく Subject に格納します。
//descriptor.Claims = new Dictionary<string, object> {
// { "userpayload", JsonConvert.SerializeObject(payload) }
// { "jti", claim.ID }
//};
descriptor.Subject = new ClaimsIdentity(new Claim[] {
new Claim("userpayload", JsonConvert.SerializeObject(payload)),
new Claim("jti", claim.ID)
});
var token = m_TokenHandler.CreateJwtSecurityToken(descriptor);
var tokenString = m_TokenHandler.WriteToken(token);
return tokenString;
}
}
トークンの検証
トークンバリデーター
今回定義しているのはトークン文字列検証メソッドのみです。
検証すると同時に、戻り値としてトークンから取り出したクレーム情報とペイロードを返します。
/// <summary>
/// トークンバリデーター。
/// </summary>
/// <typeparam name="TPayload">ペイロードの型</typeparam>
public interface ITokenValidator<TPayload>
{
/// <summary>
/// 指定されたトークンを検証します。
/// </summary>
/// <param name="token">トークン文字列</param>
/// <param name="claim">トークンから取り出したクレーム情報</param>
/// <param name="payload">トークンから取り出したペイロード</param>
/// <param name="tokenState">トークンの状態</param>
/// <param name="errorMessage">エラーメッセージ</param>
/// <returns>トークンが妥当であるかどうか</returns>
bool ValidateToken(string token
, out IClaim claim
, out TPayload payload
, out TokenState tokenState
, out string errorMessage
);
}
トークンの状態
検証したトークンの状態を表す列挙体です。
/// <summary>
/// トークンの状態。
/// </summary>
public enum TokenState
{
/// <summary>
/// 不明
/// </summary>
Unknown = 0,
/// <summary>
/// 妥当
/// </summary>
Valid,
/// <summary>
/// 不正
/// </summary>
Invalid,
/// <summary>
/// まだ有効でない
/// </summary>
NotBefore,
/// <summary>
/// 有効期限切れ
/// </summary>
Expired,
}
トークンバリデーターの実装
前述の ITokenValidator<TPayload>
インターフェースを実装する、System.IdentityModel.Tokens.Jwt
を用いたトークンバリデーターです。
ペイロードは JSON 文字列から復元します。JSON シリアライザには JSON.net
を用いています。
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using Newtonsoft.Json;
/// <summary>
/// トークンバリデーター。
/// </summary>
/// <typeparam name="TPayload">ペイロードの型</typeparam>
public class MsJwtValidator<TPayload> : ITokenValidator<TPayload>
{
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="key">RSA公開鍵</param>
internal MsJwtValidator(RsaSecurityKey key)
{
m_Key = key;
m_LifetimeValidator = new LifetimeValidator((nbf, exp, token, parameter) =>
{
return ValidateLifetime(nbf, exp, out TokenState state, out string message);
}
);
}
private readonly RsaSecurityKey m_Key;
private readonly JwtSecurityTokenHandler m_TokenHandler = new JwtSecurityTokenHandler();
private readonly LifetimeValidator m_LifetimeValidator;
/// <summary>
/// 妥当と見なす発行者を取得または設定します。
/// </summary>
public string[] ValidIssuers { get; set; }
/// <summary>
/// 妥当と見なす利用者を取得または設定します。
/// </summary>
public string[] ValidAudiences { get; set; }
/// <summary>
/// 指定されたトークンを検証します。
/// </summary>
/// <param name="token">トークン文字列</param>
/// <param name="claim">トークンから取り出したクレーム情報</param>
/// <param name="payload">トークンから取り出したペイロード</param>
/// <param name="tokenState">トークンの状態</param>
/// <param name="errorMessage">エラーメッセージ</param>
/// <returns>トークンが妥当であるかどうか</returns>
public bool ValidateToken(string tokenString
, out IClaim claim
, out TPayload payload
, out TokenState tokenState
, out string errorMessage
)
{
TokenValidationParameters parameters = new TokenValidationParameters();
parameters.IssuerSigningKey = m_Key;
parameters.ValidateIssuerSigningKey = true;
// 発行者を検証するかどうか
if (ValidIssuers != null && ValidIssuers.Length > 0)
{
if (ValidIssuers.Length == 1) { parameters.ValidIssuer = ValidIssuers.First(); }
else { parameters.ValidIssuers = ValidIssuers; }
parameters.ValidateIssuer = true;
}
else
{
parameters.ValidateIssuer = false;
}
// 利用者を検証するかどうか
if (ValidAudiences != null && ValidAudiences.Length > 0)
{
if (ValidAudiences.Length == 1) { parameters.ValidAudience = ValidAudiences.First(); }
else { parameters.ValidAudiences = ValidAudiences; }
parameters.ValidateAudience = true;
}
else
{
parameters.ValidateAudience = false;
}
// 有効期限を検証するかどうか
parameters.ValidateLifetime = true;
parameters.LifetimeValidator = m_LifetimeValidator;
try
{
ClaimsPrincipal claims = m_TokenHandler.ValidateToken(tokenString, parameters, out SecurityToken token);
// クレーム情報
claim = new MsJwtClaim()
{
ID = GetClaim(claims, "jti),
Audience = GetClaim(claims, "aud")
};
// ペイロード
string payloadJson = claims.FindFirst("userpayload")?.Value;
if (string.IsNullOrEmpty(payloadJson))
{
payload = default(TPayload);
}
else
{
payload = JsonConvert.DeserializeObject<TPayload>(payloadJson);
}
tokenState = TokenState.Valid;
errorMessage = null;
return true;
}
catch (SecurityTokenInvalidLifetimeException ex)
{
// 有効期限が不正
claim = null;
payload = default(TPayload);
if (ValidateLifetime(ex.NotBefore, ex.Expires, out tokenState, out errorMessage))
{
// NotBefore でも Expires でもない
tokenState = TokenState.Invalid;
errorMessage = "The token is invalid.";
}
return false;
}
catch (SecurityTokenInvalidIssuerException)
{
// 発行者が不正
claim = null;
payload = default(TPayload);
tokenState = TokenState.Invalid;
errorMessage = "The issuer is invalid.";
return false;
}
catch (SecurityTokenInvalidAudienceException)
{
// 利用者が不正
claim = null;
payload = default(TPayload);
tokenState = TokenState.Invalid;
errorMessage = "The audience is invalid.";
return false;
}
}
/// <summary>
/// 指定されたクレームの値を取得します。
/// </summary>
/// <param name="claims">クレーム情報</param>
/// <param name="key">キー</param>
/// <returns></returns>
private string GetClaim(ClaimsPrincipal claims, string key)
{
foreach (Claim claim in claims.Claims)
{
if (string.Compare(claim.Type, key, true) == 0)
{
return claim.Value;
}
}
return null;
}
/// <summary>
/// 有効期限を検証します。
/// </summary>
/// <param name="nbf">トークンが有効になる日時</param>
/// <param name="exp">トークンの有効期限</param>
/// <param name="state">トークンの状態</param>
/// <param name="message">エラーメッセージ</param>
/// <returns>妥当であるかどうか</returns>
private bool ValidateLifetime(DateTime? nbf, DateTime? exp, out TokenState state, out string message)
{
DateTime now = DateTime.Now.ToUniversalTime();
if (exp.HasValue && exp.Value < now)
{
state = TokenState.Expired;
message = "The token is expired.";
return false;
}
if (nbf.HasValue && nbf.Value > now)
{
state = TokenState.NotBefore;
message = "The token is not yet valid.";
return false;
}
state = TokenState.Valid;
message = null;
return true;
}
}
トークンの検証時に得られる内容
ClaimsPrincipal claims = m_TokenHandler.ValidateToken(tokenString, parameters, out SecurityToken token);
上記のトークン検証メソッドで得られる内容を簡単に説明します。
戻り値 claims の Claims プロパティにはクレームがコレクションとして格納されています。トークン生成時に Subjects として与えたクレームもここに格納されます。今回指定したペイロードの型には Role と Permission というプロパティが定義されています。その値が格納されていることが分かります。
[0] {userpayload: {"user":{"Role":2,"Permissions":1}}}
[1] {jti: ff28ffc3-6709-4de2-a914-e5aa88594fd8}
[2] {nbf: 1553426924}
[3] {exp: 1553427228}
[4] {iat: 1553426929}
[5] {iss: testIssuer}
[6] {aud: testAudience}
今回の検証では out 引数 token からもクレーム情報を取得することはできました。
SecurityToken は基底型であり、実際に返された型は System.IdentityModel.Tokens.Jwt.JwtSecurityToken でした。この型には Claims プロパティが定義されており、上記と同じ内容の情報を取得することができました。但し、常にこの型が返されるのか、その他の型が返されることがあるのかは分かりません。