1. GHKEN

    Posted

    GHKEN
Changes in title
+.NETでJWTのエンコードとデコード
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,242 @@
+# 経緯
+
+.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も待ってます)