.NETでJWTのエンコードとデコード

# 経緯

.NETでJWT(Json Web Token)を扱おうとしたら日本語の情報が少なかったりPEMがそのまま読み込めなかったりで思いのほか苦労したのでまとめておく

# 前提

.NET Framework 4.5以上

鍵は平文で扱ってるけどローカルに秘密鍵を置くときはkey containerとか使うといいらしいけどここでは省略

JWSについてしか書いてないけどJWEも大体似たような流れだと思うのでこれも省略

# 準備

.NETでJWTを扱うためのライブラリは何種類かあるけどMicrosoft製のものがあるのでそれを使う

nugetに`System.IdentityModel.Tokens.Jwt`ってパッケージがあるのでそれを追加しておく（最新版は.NET4.5.1以上が対象なので注意）

# 共通鍵を使う方法

鍵周りの処理が簡単なので割と楽

## トークンの生成

```csharp
using System;
using System.Text;

class Program
{
    // issuerがGHKENのJWTを生成する
    static void Main(string[] args)
    {
        // 共通鍵を用意
        var keyString = "hogehogehogehoge";
        // トークン操作用のクラスを用意
        var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
        // 共通鍵なのでSymmetricSecurityKeyクラスを使う
        // 引数は鍵のバイト配列
        var key = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(Encoding.UTF8.GetBytes(keyString));
        // 署名情報クラスを生成
        // 共通鍵を使うのでアルゴリズムはHS256使っとけばいいはず
        var credentials = new Microsoft.IdentityModel.Tokens.SigningCredentials(key, "HS256");
        // トークンの詳細情報クラス？を生成
        var descriptor = new Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor
        {
            Issuer = "GHKEN",
            SigningCredentials = credentials,
        };
        // トークンの生成
        //SecurityTokenDescriptor使わずにhandler.CreateJwtSecurityToken("GHKEN", null, null, null, null, null, credentials)でもOK
        var token = handler.CreateJwtSecurityToken(descriptor);
        // トークンの文字列表現を取得
        var tokenString = handler.WriteToken(token);
        // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0ODc4MjQ3MTQsImV4cCI6MTQ4NzgyODMxNCwiaWF0IjoxNDg3ODI0NzE0LCJpc3MiOiJHSEtFTiJ9.PJ-5KzFq7n2hBiJnoZMli0XajaJPNup0BztIO9QlDFY
        Console.WriteLine(tokenString);
        Console.Read();
    }
}
```

## トークンのパース

```csharp
using System;
using System.Text;

 class Program
{
    // 共通鍵で署名されたトークンを検証する
    // トークンの内容は
    // aud: 空
    // iss: "GHKEN"
    // exp: 期限切れ
    static void Main(string[] args)
    {
        // 鍵
        var keyString = "hogehogehogehoge"; ;
        var key = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(Encoding.UTF8.GetBytes(keyString));
        // トークン操作用のクラス
        var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
        // トークンの文字列表現
        var tokenString = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0ODc4MjUxMjMsImV4cCI6MTQ4NzgyODcyMywiaWF0IjoxNDg3ODI1MTIzLCJpc3MiOiJHSEtFTiJ9.AJFdztPP3GOBBjtiJeHc6wvy5Z3idQW2yGw9yCd6_wc";
        // トークン検証用のパラメータを用意
        // Audience, Issuer, Lifetimeに関してはデフォルトで検証が有効になっている
        // audが空でexpが期限切れなのでValidateAudienceとValidateLifetimeはfalseにしておく
        var validationParams = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
        {
            ValidateAudience = false,
            ValidIssuer = "GHKEN",
            ValidateLifetime = false,
            IssuerSigningKey = key,
        };
        try
        {
            Microsoft.IdentityModel.Tokens.SecurityToken token;
            // 第三引数にSecurityToken型の変数を参照で渡しておくと、検証済みのトークンが出力される
            handler.ValidateToken(tokenString, validationParams, out token);
            Console.WriteLine(token.Issuer);
        }
        catch (Exception e)
        {
            // ValidateTokenで検証に失敗した場合はここにやってくる
            Console.WriteLine("トークンが無効です: " + e.Message);
        }
        Console.Read();
    }
}
```

# 非対称鍵を使う方法

RSAの鍵を使う前提

PEM形式の鍵がそのまま使えないのでちょっと面倒

鍵をXML形式にしてあげれば簡単に読み込めるようになる（方法は後述）

## トークンの生成

```csharp
using System;

class Program
{
    static void Main(string[] args)
    {
        // 秘密鍵を用意
        var keyString = "<RSAKeyValue><Modulus>yT12/iqZLNcrnTTFGy3NMuCjo6wJNLuG5j5L2yM6iX7CT5sWVq2BuXtdbq6PFuOIkzwJ+5Sng+qthAX5qHnuxRMI+QITe1qP+k0pOtK/EVtuedz6zdu2+Sp24CvGIMt1y8yMeOBXrRZTZzxpH9VsSq9kA/ylHKuWRfWLHysIqsdO0Tgf9eLwNAhRr6vpkvsAwvJnreIdWr/7aTrt9vq3EIJI3NYHV7/zqbZ7mKS1GbvJkAMbrQkYJ45hhEBUdYE45V8Dhkb9NTlExIcrar3vqsXSOVjQvuiGN4HsYmqPGUw26P9F7DrPyM4eQksb+PRMdkPW4dTjIRj9X3OIBHXrBw==</Modulus><Exponent>AQAB</Exponent><P>8Qw9p6A+11Tu6Dsl6+ndb7qiQP3u4cE5JMDRuq71A11XiEKU9K+1j5O26TtcJaJUCeH01RCKvMa/hNp2G7NqPnjxpRQU06Vj+bvJono7YTHcScC4Apa8cSsFQ62Iu2jpoHIkEz/5j7EdkToyFpC4opxbcHANPc9lXwfjIJTyieE=</P><Q>1bkXNBVazXVSGaP2DXVSSme9uXF5DmiEdKbpqRY6hlW+wIUBOG3RStkPC5ah62+3ObAooehVveR+kJOmSl2qLYvSaqV/DPkTyRyFOpTlpOSpLBsRvzPMoA7BFweXiy3YIbDsSr7S1qC1JgoMK4Htz742tDXLBUM32SWZr9OFoec=</Q><DP>aE8rvwYRK42NdOFjn5ssP9U7sXQxk2/SEp1+JJLhY/tYjZaCbwA6SU9ar8MINSDxzPUCxdDKuLYo2ozO313cc/xSVWVDPfMsOD2TG8RZPc4dzayf9D7WfQJo3MiTisXzk4LRKaNdk1jJura8RheKTpPq3dUfZcgBzgXTu5249wE=</DP><DQ>E8JP9d2/jl05YOt6tRXSrNRYgwuNoJpjHJHN6ncGpCLLRutFCJ2Giv/0VyLvB2BFtUynBQkA3FSCqwUri5aLRDi4FGoGjAF/JcnAO4FGle8aANzj0CSO14FlsqZeCV0MrVi5D9QClBs5hDHLnD4f6WPxlMmgYnUrdaT3R30rzqM=</DQ><InverseQ>dSfitpkpXxGrKbPA4HxVtSZU71tWOMbvIjYKy8cYTw+/EsQ7LW84Q1I8WDrbB7m/Zj67EufC2n1VNaP+x9dOCXpud+R/48piD2bp5JDCv5wUSs7xsjPsx8o1ScrHaXOeySQ486HTLji4RaqiiD1I46fF6NV1ZKRmOSUmMInxDDM=</InverseQ><D>DqjBkEY+HjwWWz9K1G4Dsp8WjIetq/+1FfSXxgDM9NMdCHt9pxbAimhoJ/XjSoGMo10ORRtREJT5ytI8m382W3jFgI4cKTIxpsQUKsrLTFJiu9HTG0fUDlZ/jljh9+WaURw3Z17AREWKEc0ew0jiuJYKLRgsVuhQ7Au09LJH0VjOTj9h62Trb2srbz/s+XjnTi8cch6oSBeqV/2YbYQla9bAMswR84fRRNUonDPrYvwC5rnhw5Xp0vJueHZpmTsruXjQJasue/Tgp/p6CsZlZX1CvTX8muSROyJ8vCjbG1dGplx+3Jbca+RoXj1FajdlmfrZxvDiH+v4M2mLenuDgQ==</D></RSAKeyValue>";
        // RSAを使うのでRsaSecurityKeyを使う
        var rsa = new System.Security.Cryptography.RSACryptoServiceProvider();
        Microsoft.IdentityModel.Tokens.RsaSecurityKey key = null;
        try
        {
            rsa.FromXmlString(keyString);
            key = new Microsoft.IdentityModel.Tokens.RsaSecurityKey(rsa);
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
        }
        // トークン操作用のクラスを用意
        var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();

        // 署名情報クラスを生成
        // 非対称鍵を使うのでアルゴリズムはRS256使っとけばいいはず
        var credentials = new Microsoft.IdentityModel.Tokens.SigningCredentials(key, "RS256");
        // トークンの詳細情報クラス？を生成
        var descriptor = new Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor
        {
            Issuer = "GHKEN",
            SigningCredentials = credentials,
        };
        // トークンの生成
        //SecurityTokenDescriptor使わずにhandler.CreateJwtSecurityToken("GHKEN", null, null, null, null, null, credentials)でもOK
        var token = handler.CreateJwtSecurityToken(descriptor);
        // トークンの文字列表現を取得
        var tokenString = handler.WriteToken(token);
        Console.WriteLine(tokenString);
        Console.Read();
    }
}
```

## トークンのパース

```csharp
using System;

class Program
{
    static void Main(string[] args)
    {
        // 公開鍵を用意
        var keyString = "<RSAKeyValue><Modulus>yT12/iqZLNcrnTTFGy3NMuCjo6wJNLuG5j5L2yM6iX7CT5sWVq2BuXtdbq6PFuOIkzwJ+5Sng+qthAX5qHnuxRMI+QITe1qP+k0pOtK/EVtuedz6zdu2+Sp24CvGIMt1y8yMeOBXrRZTZzxpH9VsSq9kA/ylHKuWRfWLHysIqsdO0Tgf9eLwNAhRr6vpkvsAwvJnreIdWr/7aTrt9vq3EIJI3NYHV7/zqbZ7mKS1GbvJkAMbrQkYJ45hhEBUdYE45V8Dhkb9NTlExIcrar3vqsXSOVjQvuiGN4HsYmqPGUw26P9F7DrPyM4eQksb+PRMdkPW4dTjIRj9X3OIBHXrBw==</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>"; ;
        // RSAを使うのでRsaSecurityKeyを使う
        var rsa = new System.Security.Cryptography.RSACryptoServiceProvider();
        Microsoft.IdentityModel.Tokens.RsaSecurityKey key = null;
        try
        {
            rsa.FromXmlString(keyString);
            key = new Microsoft.IdentityModel.Tokens.RsaSecurityKey(rsa);
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
        }
        // トークン操作用のクラス
        var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
        // トークンの文字列表現
        var tokenString = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0ODgyNjgyNDgsImV4cCI6MTQ4ODI3MTg0OCwiaWF0IjoxNDg4MjY4MjQ4LCJpc3MiOiJHSEtFTiJ9.qgyYG-q8-aDdjabt-Wp3dn3wNVIu8WGP2n8Mnv_AxrFY98Abmb96M_SP3dnZI3mDKk5NC3QYKf42cbvu20DbAAdiawAVclLMXYBgKZJqHc-5Wkq7PsGA9ECoVE2KLzKGisqHFrZUm-kv51gdCegPsANm0ukdp5CWAy26Em1og02WG9--q0peGOWgYjtE5V2sM8b861QtAsWUtUSKs6kf_r9c5bcvN2xFS4_iw5luVY0u4dSjdeaaeIOjMqLCpZaelleTAubyEdoJ89J9vz6gj6ghzYe9dvND_mlUYpfiperSceSR8eKLPtwsno0zn7DaYYqcMI5uERqUtj2YKWcIgg";
        // トークン検証用のパラメータを用意
        // Audience, Issuer, Lifetimeに関してはデフォルトで検証が有効になっている
        // 今回発行したトークンの内容
        // Audience: 空なので検証スキップ
        // Issuer: "GHKEN"
        // Lifetime: 期限切れなので検証スキップ
        var validationParams = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
        {
            ValidateAudience = false,
            ValidIssuer = "GHKEN",
            ValidateLifetime = false,
            IssuerSigningKey = key,
        };
        try
        {
            Microsoft.IdentityModel.Tokens.SecurityToken token;
            // 第三引数にSecurityToken型の変数を参照で渡しておくと、検証済みのトークンが出力される
            handler.ValidateToken(tokenString, validationParams, out token);
            Console.WriteLine(token.Issuer);
        }
        catch (Exception e)
        {
            // ValidateTokenで検証に失敗した場合はここにやってくる
            Console.WriteLine("トークンが無効です: " + e.Message);
        }
        Console.Read();
    }
}
```

# PEM使えない問題

.NETではPEMの鍵を直接読み込むことができないので`openssl genrsa`とかで作った鍵を使おうとするとめんどくさい

鍵のパラメータを解析してXML形式で書くと読み込めるようになるけど変換が面倒

オンラインにツールがあるけどさすがに秘密鍵をオンラインのツールで変換したくない

のでとりあえずPEM -> XMLにしてくれるgemを作ってみました（動作確認はしたけどテストとかまだ書いてない）

https://github.com/ghken/pem2xml


```bash
gem install pem2xml
pem2xml key.pem
```

↑で標準出力にXMLが出るようになってるので使ってみてください

(PRも待ってます）