[RS256] JWTでRSA秘密鍵を環境変数で処理したい [Javascript]

使ってますか、JSON Web Token。

わが Salesforce部隊は、外部からの接続においてOAuth認証(認可)したい場合にいちいちログイン画面を開かずに、固定された接続専用のユーザを利用してシステム連携をしたい場合があります。もちろんそうではなくとも、JWTを利用したいケースはいろいろあります。最近はJWTの虜と言っても申し分ありません。

さて、JWTを公開鍵認証方式で利用する場合について考えましょう。`jwt.sign`でアクセストークンを作る場合の「鍵」は「秘密鍵」になります。この秘密鍵、基本原則的には`server.key(pem)`などの秘密鍵ファイルを元にシて、アクセストークンを生成します。しかし、Githubなどでソースコードを保管する場合に、この秘密鍵のファイルも含めてあげないといけないので少々厄介です。できることならば、環境変数などで設定したいの心。

しかし、環境変数に秘密鍵のファイルの内容をそのまま書いても利用できません。それは次の２つの理由があります。

1. 改行コードも秘密鍵の一部
2. よく利用される `jsonwebtoken` npmパッケージでは、秘密鍵をバイナリとして扱う

このように、環境変数に定義して再利用するには、そのままではいけません。これらに対処が必要です。

# 秘密鍵の改行コードを維持したまま環境変数化する

やり方は次の２ステップです。

1. 秘密鍵を一行化して環境変数化
2. 環境変数の内容に改行を付与する

## 1. 秘密鍵を一行化して環境変数化

何らかの方法で準備された秘密鍵に対して、`cat cert/server.key | sed -e :loop -e 'N; $!b loop' -e 's/\n/\\n/g'` コマンドをうちます(Macの場合)。要は改行コードを`\n`という文字に置き換えます。そして改行コード自体を排除して１行化します。

試しに実行するとこんな感じです(もう利用していない秘密鍵なので、悪さには使えません)。

```bash
$ cat cert/server.key | sed -e :loop -e 'N; $!b loop' -e 's/\n/\\n/g'
-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA3VIkZoGFoT7AH1IJQxYPtjK3HWlnUF0Dq1AoIZwH8sB+zFsS\naB517XHJsi4OkSiu83O2TB1JoM8U60msR3ix6wL8lLSA7Ggu/zxx0iiR3kkgBYq7\nIhLMMfaSA0nl3lK31FcHBwQMscptoicESwpfxgHVWm0GcMV1k1W8bcenlRy/Aybb\n7c6sRUnN4n1UuXOFgEfQWtVxyAofbEkKRDxPN/Sh1SF3LPWVZSNswy6wkKh5C1lq\n7vz88FhXuu012ZK84qqSrXzCmL8TDsl5qbkbjvCt35VSMT5COkQPZ9NVS0ibkvFj\n/CaXNd1mqvZI9klz7anM6utbwxFBdHVwTtAhHwIDAQABAoIBAQCgckdPRMCyl8JC\nMn/icaDsTjHwEZTDbtsVG+QsEWi1tJV34uIiN0r421AEa11GIL9MYOucnHLfMKES\nvfM2USpynFSuHXmFaTYA9TnyyFSPWGXtfpiRaB0+b2mFFtKdbUw1lO3USTxGu+Dk\n9/Il0JyD+JpKltVfddb5++LBl0dHUhmI1BbdShbVP9dbtZO37oxkjTn/9bTucBO6\nnA3Zs5II7SoEogjPd0pUfggM4Mxfes1tMk5wq3hEbW8Y1Bmby+qNurWYmMiCfCkC\nNghUlRiuaYXWPCaVqQrJvSvdUZ4yUZTbfgjStUQk8Z/2nAmS4KwYsAhlbmv6IpX0\n9ay8kYgxAoGBAPASZl5/CKVPiA857rbkc4FT1HlJ0grSbBv3hLOSm15PifmvzNrN\n4/wKTZ/y6ynlF0lUSOwH92B+R4yo0r9oXHxmzuos3JgcUi84jkkiq1RwsYseKoev\nGmuFPZnTtE8hzcTsvUE8PcYi8cXYVlAO8mFF1JAORkghrrQo/YLv+nj5AoGBAOwB\nQhFToh/xgHqy+9FBKA9SQwP5P1YEoPiCSYRWvkKCh5Ni1jpD5pjCBetcIHPmyGUI\nFrBRZvMjJVdTJ4QXVsB0wqTxzwV6bvj6bdjEJguCFt+kHxZi6W7s+g7HhbMHBfjo\nPKiAwkqrOwu379+QNK9zds8CdzyhvjHDFsz7AsjXAoGADqjw+2BChOXAZz2gaCa3\nLvSRiv6JBwQmeea5gcW4GyA8SrUDi3D7NQ7kAppw5dQJgf7VnSQ3ZPsRH3PHusyC\nqU4V4JLwvZEtK5kGh0zIrZVcIiSrcDGvKVWvl08oOZTU3eue+vbUxt6naO93BdiD\n0JDVFB7rB8iWxIWkRXSmFPECgYEA68kF/NGVtFxPhEa1l4aFQ2loUtv+Dy5otF9W\nm8UeKMzILtQcO/ICvLN7vn04XxM/OtEt+dIaDOgcMnZ9kFbQ8U61+J0tu8dqf42T\nmXG+oNjDiYQrGu6PUaeo3IMybH6j1N4RXDfn5TnVsAuAt9cXDANLu942yni90HGc\nogZV7dkCgYBs8MtkD08LjEVPhhcXgc4geck7trHt/MnoV2ikItO0lKnZzZ0a0t7j\n3F0bvQ11AFplmXmcFyy286yan1oEEaRvmGMju+xoru8K+QUvG2Kwre6dJuu4YoZv\nYzP49HIOJ/XmDzjjdMz9+J8iOZKZqoiGGldGVXyn7ksJtv1SYXzoqg==\n-----END RSA PRIVATE KEY-----
```

これで１行化できました。これを環境変数にセットします。`.env` を利用している場合には、ファイルに書き込んであげればよいし、Herokuを利用する場合には、`Settings`の`Config Vars`に追記ですね。

## 2. 環境変数の内容に改行を付与する

さて、これだけではすみません。プログラム側でこの環境変数を読み込んだ後、改行コードをもとに戻して上げる必要があります(Ω ΩΩ< な、なんだってー!!)。

大げさな話でもなく、`const secret_key = process.env.PRIVATE_KEY.replace(/\\n/g, '\n');` などとしてあげれば良いだけです(PRIVATE_KEYは環境変数名)。

# 秘密鍵をバイナリ化して署名する

RSAの秘密鍵・公開鍵を利用する場合、それらはバイナリとして扱われていなければなりません。

別に難しいことでもなく、Javascriptの場合 `Buffer` にしてあげれば良いだけです。標準で文字列をバッファ化する `Buffer.from` がありますので、これを利用して秘密鍵の文字列をバイナリ化します。`const secret = Buffer.from(secret_key)` なんかで良いでしょう。これでバイナリ化された秘密鍵を元に、`jsonwebtoken` パッケージの `sign`メソッドで署名すれば、JWTトークンの完成です。

`iss`や`aud`といった必須パラメータも環境変数化することで、より安全なソースコードになります。それらも踏まえて、トークンを作成するコードを準備すると、結果、次のようになります。

```javascript:jwt.js
const jwt = require('jsonwebtoken');
const claim = {
  iss: process.env.ISSUER,
  aud: process.env.AUDIENCE,
  sub: process.env.SUBJECT,
  exp: Math.floor(Date.now() / 1000) + (3 * 60) // 3分
};
const secret = Buffer.from(process.env.PRIVATE_KEY.replace(/\\n/g, '\n'));
const token = jwt.sign(claim, secret, { algorithm: 'RS256' });
```

この秘密鍵が正しいかどうかは`verify`して上げればよいです。`jwt.verify(token, public_key, options)` となりますので、これを含めたソースコードは次のようになります。

```javascript:jwt.js
const jwt = require('jsonwebtoken');
const claim = {
  iss: process.env.ISSUER,
  aud: process.env.AUDIENCE,
  sub: process.env.SUBJECT,
  exp: Math.floor(Date.now() / 1000) + (3 * 60) // 3分
};
const secret = Buffer.from(process.env.PRIVATE_KEY.replace(/\\n/g, '\n'));
const public = Buffer.from(process.env.PUBLIC_KEY.replace(/\\n/g, '\n'));
const token = jwt.sign(claim, secret, { algorithm: 'RS256' });

claim.algorithm = ['RS256'];
jwt.verify(token, public, claim, (err, decorded) => {
  if (err) {
    console.err(err);
  }
  console.log(decorded);
});
```

例えば、環境変数を次のようにセットした場合。

```bash
PRIVATE_KEY='-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA3VIkZoGFoT7AH1IJQxYPtjK3HWlnUF0Dq1AoIZwH8sB+zFsS\naB517XHJsi4OkSiu83O2TB1JoM8U60msR3ix6wL8lLSA7Ggu/zxx0iiR3kkgBYq7\nIhLMMfaSA0nl3lK31FcHBwQMscptoicESwpfxgHVWm0GcMV1k1W8bcenlRy/Aybb\n7c6sRUnN4n1UuXOFgEfQWtVxyAofbEkKRDxPN/Sh1SF3LPWVZSNswy6wkKh5C1lq\n7vz88FhXuu012ZK84qqSrXzCmL8TDsl5qbkbjvCt35VSMT5COkQPZ9NVS0ibkvFj\n/CaXNd1mqvZI9klz7anM6utbwxFBdHVwTtAhHwIDAQABAoIBAQCgckdPRMCyl8JC\nMn/icaDsTjHwEZTDbtsVG+QsEWi1tJV34uIiN0r421AEa11GIL9MYOucnHLfMKES\nvfM2USpynFSuHXmFaTYA9TnyyFSPWGXtfpiRaB0+b2mFFtKdbUw1lO3USTxGu+Dk\n9/Il0JyD+JpKltVfddb5++LBl0dHUhmI1BbdShbVP9dbtZO37oxkjTn/9bTucBO6\nnA3Zs5II7SoEogjPd0pUfggM4Mxfes1tMk5wq3hEbW8Y1Bmby+qNurWYmMiCfCkC\nNghUlRiuaYXWPCaVqQrJvSvdUZ4yUZTbfgjStUQk8Z/2nAmS4KwYsAhlbmv6IpX0\n9ay8kYgxAoGBAPASZl5/CKVPiA857rbkc4FT1HlJ0grSbBv3hLOSm15PifmvzNrN\n4/wKTZ/y6ynlF0lUSOwH92B+R4yo0r9oXHxmzuos3JgcUi84jkkiq1RwsYseKoev\nGmuFPZnTtE8hzcTsvUE8PcYi8cXYVlAO8mFF1JAORkghrrQo/YLv+nj5AoGBAOwB\nQhFToh/xgHqy+9FBKA9SQwP5P1YEoPiCSYRWvkKCh5Ni1jpD5pjCBetcIHPmyGUI\nFrBRZvMjJVdTJ4QXVsB0wqTxzwV6bvj6bdjEJguCFt+kHxZi6W7s+g7HhbMHBfjo\nPKiAwkqrOwu379+QNK9zds8CdzyhvjHDFsz7AsjXAoGADqjw+2BChOXAZz2gaCa3\nLvSRiv6JBwQmeea5gcW4GyA8SrUDi3D7NQ7kAppw5dQJgf7VnSQ3ZPsRH3PHusyC\nqU4V4JLwvZEtK5kGh0zIrZVcIiSrcDGvKVWvl08oOZTU3eue+vbUxt6naO93BdiD\n0JDVFB7rB8iWxIWkRXSmFPECgYEA68kF/NGVtFxPhEa1l4aFQ2loUtv+Dy5otF9W\nm8UeKMzILtQcO/ICvLN7vn04XxM/OtEt+dIaDOgcMnZ9kFbQ8U61+J0tu8dqf42T\nmXG+oNjDiYQrGu6PUaeo3IMybH6j1N4RXDfn5TnVsAuAt9cXDANLu942yni90HGc\nogZV7dkCgYBs8MtkD08LjEVPhhcXgc4geck7trHt/MnoV2ikItO0lKnZzZ0a0t7j\n3F0bvQ11AFplmXmcFyy286yan1oEEaRvmGMju+xoru8K+QUvG2Kwre6dJuu4YoZv\nYzP49HIOJ/XmDzjjdMz9+J8iOZKZqoiGGldGVXyn7ksJtv1SYXzoqg==\n-----END RSA PRIVATE KEY-----'
PUBLIC_KEY='-----BEGIN CERTIFICATE-----\nMIIDwjCCAqoCCQDaWkj3iVN+5DANBgkqhkiG9w0BAQsFADCBojELMAkGA1UEBhMC\nSlAxDjAMBgNVBAgMBVRva3lvMRAwDgYDVQQHDAdDaGl5b2RhMRgwFgYDVQQKDA9T\nYWxlc2ZvcmNlLmRlbW8xEjAQBgNVBAsMCURldmVsb3BlcjEfMB0GA1UEAwwWZGVt\nby1jZGMuaGVyb2t1YXBwLmNvbTEiMCAGCSqGSIb3DQEJARYTdGFiZUBzYWxlc2Zv\ncmNlLmNvbTAeFw0xOTAyMjAwMTM0MDhaFw0yMDAyMjAwMTM0MDhaMIGiMQswCQYD\nVQQGEwJKUDEOMAwGA1UECAwFVG9reW8xEDAOBgNVBAcMB0NoaXlvZGExGDAWBgNV\nBAoMD1NhbGVzZm9yY2UuZGVtbzESMBAGA1UECwwJRGV2ZWxvcGVyMR8wHQYDVQQD\nDBZkZW1vLWNkYy5oZXJva3VhcHAuY29tMSIwIAYJKoZIhvcNAQkBFhN0YWJlQHNh\nbGVzZm9yY2UuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3VIk\nZoGFoT7AH1IJQxYPtjK3HWlnUF0Dq1AoIZwH8sB+zFsSaB517XHJsi4OkSiu83O2\nTB1JoM8U60msR3ix6wL8lLSA7Ggu/zxx0iiR3kkgBYq7IhLMMfaSA0nl3lK31FcH\nBwQMscptoicESwpfxgHVWm0GcMV1k1W8bcenlRy/Aybb7c6sRUnN4n1UuXOFgEfQ\nWtVxyAofbEkKRDxPN/Sh1SF3LPWVZSNswy6wkKh5C1lq7vz88FhXuu012ZK84qqS\nrXzCmL8TDsl5qbkbjvCt35VSMT5COkQPZ9NVS0ibkvFj/CaXNd1mqvZI9klz7anM\n6utbwxFBdHVwTtAhHwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQA/emMvo+Q7f5yt\nhYSt11psUFCHcgNgS/pxV5JkhuxOoGKG9NgjptZZtqYtr/NqI6QIVF5HBkWc/I33\naevf8xCFrISLBjGuldKOSJ25FwEboh/UyqBfBm+uviC8c4btOAC7HpqRRVLSRg25\n2Xj/FzXAZyP7ThNOnI/3/fV4gTlnGYHasDP2nLlHsB1sLFOs8h5a0LZGCzrNYO9B\n5klZ7GoCxnYzYjrck3UiL0HOUXO7mwGqyc8b9tyH/xGjlJK9r/FJWFVxG1o7HGL2\nu1lgd4FKWtWeDxk7cM0QnZ2XOo522UH5hfOENCHA5Cx2QunZdQgI3o/NyprYKBoR\n8TX3Slll\n-----END CERTIFICATE-----'
ISSUER='ISSUER'
AUDIENCE='AUDIENCE'
SUBJECT='sho@oshiire.to'
```

結果は、次のとおりになります。

```javascript
{ iss: 'ISSUER',
  aud: 'AUDIENCE',
  sub: 'sho@oshiire.to',
  exp: 1550706538,
  iat: 1550706358 }
```

正しくVerifyされていれば、このようにclaimの内容がDecordの結果として返されてきています。

# まとめ

RSA公開鍵方式で作成された鍵ファイルを環境変数で利用したい場合には、次の２ステップが必要。

秘密鍵を `cat cert/server.key | sed -e :loop -e 'N; $!b loop' -e 's/\n/\\n/g'` で一行化して環境変数に登録する。

アクセストークンを作成するときには、改行コードを復帰してバイナリ化してから署名する。

```javascript
const secret = Buffer.from(process.env.PRIVATE_KEY.replace(/\\n/g, '\n'));
const token = jwt.sign(claim, secret, { algorithm: 'RS256' });
```

# おまけ

`jsonwebtoken` の npm の使い方を見ていたら、`base64`化するのが、作法みたいにも見えますので、その場合の使い方についても書き記しておきます。そうか、`base64`か...。

## 環境変数を一行化

```bash
$ base64 ./cert/server.key
LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBM1ZJa1pvR0ZvVDdBSDFJSlF4WVB0akszSFdsblVGMERxMUFvSVp3SDhzQit6RnNTCmFCNTE3WEhKc2k0T2tTaXU4M08yVEIxSm9NOFU2MG1zUjNpeDZ3TDhsTFNBN0dndS96eHgwaWlSM2trZ0JZcTcKSWhMTU1mYVNBMG5sM2xLMzFGY0hCd1FNc2NwdG9pY0VTd3BmeGdIVldtMEdjTVYxazFXOGJjZW5sUnkvQXliYgo3YzZzUlVuTjRuMVV1WE9GZ0VmUVd0Vnh5QW9mYkVrS1JEeFBOL1NoMVNGM0xQV1ZaU05zd3k2d2tLaDVDMWxxCjd2ejg4RmhYdXUwMTJaSzg0cXFTclh6Q21MOFREc2w1cWJrYmp2Q3QzNVZTTVQ1Q09rUVBaOU5WUzBpYmt2RmoKL0NhWE5kMW1xdlpJOWtsejdhbk02dXRid3hGQmRIVndUdEFoSHdJREFRQUJBb0lCQVFDZ2NrZFBSTUN5bDhKQwpNbi9pY2FEc1RqSHdFWlREYnRzVkcrUXNFV2kxdEpWMzR1SWlOMHI0MjFBRWExMUdJTDlNWU91Y25ITGZNS0VTCnZmTTJVU3B5bkZTdUhYbUZhVFlBOVRueXlGU1BXR1h0ZnBpUmFCMCtiMm1GRnRLZGJVdzFsTzNVU1R4R3UrRGsKOS9JbDBKeUQrSnBLbHRWZmRkYjUrK0xCbDBkSFVobUkxQmJkU2hiVlA5ZGJ0Wk8zN294a2pUbi85YlR1Y0JPNgpuQTNaczVJSTdTb0VvZ2pQZDBwVWZnZ000TXhmZXMxdE1rNXdxM2hFYlc4WTFCbWJ5K3FOdXJXWW1NaUNmQ2tDCk5naFVsUml1YVlYV1BDYVZxUXJKdlN2ZFVaNHlVWlRiZmdqU3RVUWs4Wi8ybkFtUzRLd1lzQWhsYm12NklwWDAKOWF5OGtZZ3hBb0dCQVBBU1psNS9DS1ZQaUE4NTdyYmtjNEZUMUhsSjBnclNiQnYzaExPU20xNVBpZm12ek5yTgo0L3dLVFoveTZ5bmxGMGxVU093SDkyQitSNHlvMHI5b1hIeG16dW9zM0pnY1VpODRqa2tpcTFSd3NZc2VLb2V2CkdtdUZQWm5UdEU4aHpjVHN2VUU4UGNZaThjWFlWbEFPOG1GRjFKQU9Sa2docnJRby9ZTHYrbmo1QW9HQkFPd0IKUWhGVG9oL3hnSHF5KzlGQktBOVNRd1A1UDFZRW9QaUNTWVJXdmtLQ2g1TmkxanBENXBqQ0JldGNJSFBteUdVSQpGckJSWnZNakpWZFRKNFFYVnNCMHdxVHh6d1Y2YnZqNmJkakVKZ3VDRnQra0h4Wmk2VzdzK2c3SGhiTUhCZmpvClBLaUF3a3FyT3d1Mzc5K1FOSzl6ZHM4Q2R6eWh2akhERnN6N0FzalhBb0dBRHFqdysyQkNoT1hBWnoyZ2FDYTMKTHZTUml2NkpCd1FtZWVhNWdjVzRHeUE4U3JVRGkzRDdOUTdrQXBwdzVkUUpnZjdWblNRM1pQc1JIM1BIdXN5QwpxVTRWNEpMd3ZaRXRLNWtHaDB6SXJaVmNJaVNyY0RHdktWV3ZsMDhvT1pUVTNldWUrdmJVeHQ2bmFPOTNCZGlECjBKRFZGQjdyQjhpV3hJV2tSWFNtRlBFQ2dZRUE2OGtGL05HVnRGeFBoRWExbDRhRlEybG9VdHYrRHk1b3RGOVcKbThVZUtNeklMdFFjTy9JQ3ZMTjd2bjA0WHhNL090RXQrZElhRE9nY01uWjlrRmJROFU2MStKMHR1OGRxZjQyVAptWEcrb05qRGlZUXJHdTZQVWFlbzNJTXliSDZqMU40UlhEZm41VG5Wc0F1QXQ5Y1hEQU5MdTk0MnluaTkwSEdjCm9nWlY3ZGtDZ1lCczhNdGtEMDhMakVWUGhoY1hnYzRnZWNrN3RySHQvTW5vVjJpa0l0TzBsS25aelowYTB0N2oKM0YwYnZRMTFBRnBsbVhtY0Z5eTI4NnlhbjFvRUVhUnZtR01qdSt4b3J1OEsrUVV2RzJLd3JlNmRKdXU0WW9adgpZelA0OUhJT0ovWG1EempqZE16OStKOGlPWktacW9pR0dsZEdWWHluN2tzSnR2MVNZWHpvcWc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=
```

## バイナリ化する際に`base64`を指定する

```javascript:jwt.js
const jwt = require('jsonwebtoken');
const claim = {
  iss: process.env.ISSUER,
  aud: process.env.AUDIENCE,
  sub: process.env.SUBJECT,
  exp: Math.floor(Date.now() / 1000) + (3 * 60) // 3分
};
const secret = Buffer.from(process.env.SECRET, 'base64');
const public = Buffer.from(process.env.PUBLIC, 'base64');
const token = jwt.sign(claim, secret, { algorithm: 'RS256' });

claim.algorithm = ['RS256'];
jwt.verify(token, public, claim, (err, decorded) => {
  if (err) {
    console.err(err);
  }
  console.log(decorded);
});
```

おまけが本編じゃないのかな...。