LoginSignup
8
14

More than 5 years have passed since last update.

C# 任意のペイロードを含んだJWT(JSON Web Token)を生成する

Last updated at Posted at 2019-03-24

このドキュメントの内容

System.IdentityModel.Tokens.Jwt を使って JWT (JSON Web Token) を生成する方法を説明します。
System.IdentityModel.Tokens.Jwt のバージョンは 5.4.0 です。

トークンの生成

トークンプロバイダー

今回定義しているのはトークン生成メソッドのみです。

ITokenProvider
/// <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);
}

クレーム情報

トークンに与えるクレーム情報のうち、呼び出し元から指定する項目を定義しています。

IClaim
/// <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 を用いています。

MsJwtProvider
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;
    }
}

トークンの検証

トークンバリデーター

今回定義しているのはトークン文字列検証メソッドのみです。
検証すると同時に、戻り値としてトークンから取り出したクレーム情報とペイロードを返します。

ITokenValidator
/// <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 を用いています。

MsJwtValidator
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 プロパティが定義されており、上記と同じ内容の情報を取得することができました。但し、常にこの型が返されるのか、その他の型が返されることがあるのかは分かりません。

8
14
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
8
14