JWTの仕組みを使って、ゲスト用のアクセストークンを発行する

この記事はシスコの有志による Webex Advent Calendar 2021 の 23 日目として投稿しています。
Webex Advent Calendar 2021: https://qiita.com/advent-calendar/2021/webex

## はじめに
今回は JWT(JSON Web Token)の仕組みを使って、Webex ゲスト用アクセストークンを作ってみたので紹介します。今回はJavascriptで、ゲスト用アクセストークンを発行しました。

## JWT(JSON Web Token) の概要
そもそもJWTとは、何なのか？について、説明していきます。
JSON Web Token（ジェイソン・ウェブ・トークン）は、JSONデータに署名や暗号化を施す方法を定めたオープン標準 (RFC 7519) である。略称はJWT。
JWTでは、トークン内に任意の情報（クレーム）を保持することが可能であり、例えばサーバーはクライアントに対して「管理者としてログイン済」という情報を含んだトークンを生成することができる。クライアントはそのトークンを、自身が管理者としてログイン済であることの証明に使用することができる。トークンは当事者の一方（通常はサーバー）または両方（もう一方は公開鍵を提供する）の秘密鍵により署名されており、発行されたトークンが正規のものか確認することができる。
JWTのトークンはコンパクトな設計となっており、またURLセーフであり、特にウェブブラウザでシングルサインオン (SSO) を行う場合に使いやすくなっている。トークンには一般的に認証プロバイダ（英語版）やサービス・プロバイダが認証したユーザー識別情報が格納される他、各々のサービスで必要な情報も格納される。
とWikiでは記載されています。詳しくは下記リンクを参考にしてください。
https://ja.wikipedia.org/wiki/JSON_Web_Token

## JWT の構造
JWTのトークンは以下の3つの要素から構成されています。

|要素|構成|備考|
|---|---|---|
|ヘッダー|	{ <br>  　"alg" : "HS256", <br>  　"typ" : "JWT" <br> }|	署名生成に使用したアルゴリズムを格納する。 <br> 左記のHS256は、このトークンがHMAC-SHA256で署名されていることを示す。 <br> 署名アルゴリズムとしては、SHA-256を使用したHMAC (HS256) や、SHA-256を使用したRSA署名 (RS256) がよく用いられる。JSON Web Algorithms (JWA, RFC 7518) では認証や暗号化用のさらに多くのアルゴリズムが提示されている。|
|ペイロード|	{ <br>  　"sub" : "admin", <br>  　"name" : 1422779638, <br>  　"iss" : 1422779638, <br>  　"exp" : 1422779638 <br> }|	認証情報などのクレームを格納する。JWTの仕様では、トークンに一般的に含まれる7つの標準フィールドが定義されている。また用途に応じた独自のカスタムフィールドを含むこともできる。 <br> ゲスト用の、アクセストークンを発行する場合は、ゲスト識別用のID (sub) 、ゲスト表示名 (name) 、Webex 開発者サイトで作成した Guest Issuer ID (iss) と、このJWT の有効期限 UNIX 時間の整数値 (exp) を格納している。|
|署名|	HMAC-SHA256( <br>  　base64urlEncoding(header) + '.' + 　base64urlEncoding(payload), <br>  　secret <br> )|	トークン検証用の署名。署名は、ヘッダーとペイロードをBase64urlエンコーディングしてピリオドで結合したものから生成する。署名はヘッダーで指定された暗号化アルゴリズムにより生成される。左記はHMAC-SHA256形式でのコード例である。 <br> Base64url方式は、Base64を元に特殊記号やパディングの扱いを変えたものである。|

これら3つの要素は、Base64urlエンコーディングされた上で、ピリオドにより結合される。以下にコード例を示しています。

```javascript
const token = base64urlEncoding(header) + '.' + base64urlEncoding(payload) + '.' + base64urlEncoding(signature)
```

JWTの仕組みを使って、ゲスト用のアクセストークンを発行する場合は、秘密鍵 "&lt;Guest Issuer Shared Secret&gt;" を bytes 列にパースする必要がある。以下は JWT のサンプルです。

```javascript
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRJbkFzIjoiYWRtaW4iLCJpYXQiOjE0MjI3Nzk2Mzh9.gzSraSYS8EXBxLN_oWnFSRgCzcmJmMjLiuyu5CSpyHI
```

JWT の構造に関して、wiki から抜粋しています。
https://ja.wikipedia.org/wiki/JSON_Web_Token

## 動作確認環境
•	Node.js 17.1.0
•	MacOS 12.0.1

## 免責事項
スクリプトの実行は自己責任でお願いします。本スクリプトを実行することにより生じるいかなる問題に関しましても、筆者は一切責任を負いません。

## 準備
JWTを発行するため、最低限下記の情報が必要です。
1.	Webex Guest Issuer の Guest Issuer ID
2.	Webex Guest Issuer の Shared Secret
Webex Guest Issuer 作成は、下記サイトから作成してください。
https://developer.webex.com/my-apps/new/guest-issuer
Webex Guest Issuer を作成したら、Guest Issuer ID と Shared Secret が発行されるので、大切に保管してください。特にShared Secret は発行した直後にしか確認することができません。Shared Secret を失念してしまった場合は、再度 Regenerate the shared secret で再発行が可能です。
https://developer.webex.com/my-apps

## スクリプトの説明
ここでは、実際に使う下記のスクリプトについて説明します。
1.	jwt.js

### jwt.js
これがメインのスクリプトになります。

```javascript
// Base64URL Section
function base64url(source) {
  // Encode in classical base64
  encodedSource = CryptoJS.enc.Base64.stringify(source);

  // Remove padding equal characters
  encodedSource = encodedSource.replace(/=+$/, '');

  // Replace characters according to base64url specifications
  encodedSource = encodedSource.replace(/\+/g, '-');
  encodedSource = encodedSource.replace(/\//g, '_');

  return encodedSource;
}

// Signed Token Section
function createSignedToken() {
  var header = {
    "alg": "HS256",
    "typ": "JWT"
  };
  
  var stringifiedHeader = CryptoJS.enc.Utf8.parse(JSON.stringify(header));
  var encodedHeader = base64url(stringifiedHeader);
  
  var data = {
    "sub": "GUEST",
    "name": "ゲスト",
    "iss": "<Guest Issuer ID>",
    "exp": Math.floor(Date.now() / 1000) + 90
  };
  
  var stringifiedData = CryptoJS.enc.Utf8.parse(JSON.stringify(data));
  var encodedData = base64url(stringifiedData);
  
  var token = encodedHeader + "." + encodedData;  
  
  var secret = CryptoJS.enc.Base64.parse("<Guest Issuer Shared Secret>");
  
  var signature = CryptoJS.HmacSHA256(token, secret);
  signature = base64url(signature);
  
  return token + "." + signature;
}

// Access Token Section
function initializeWebexTokenVersion() {
  const signedToken = createSignedToken();
  return new Promise(() => {
    fetch('https://webexapis.com/v1/jwt/login', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer ' + signedToken,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({})
    })
    .then(response => {
      return response.json();
    })
  });
}
```

基本は Javascript で JWT を作成し、Webex API に作成した JWT を送信してアクセストークンを取得するだけのシンプルなシェルスクリプトです。

## スクリプトの配置
上記のスクリプトを、実行スクリプトと同じ階層に配置します。例えば、Browser SDK の browser-single-party-call を使用しているのであれば、browser-single-party-call 直下に配置します。
browser-single-party-call
┣app.js
┣index.html
┗jwt.js

index.html に crypto-js.min.js と 先ほど作成した jwt.js を読み込みます。

```xml
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/crypto-js.min.js"></script>
<script src="jwt.js"></script>
```

## スクリプトの解説
まず、JWT を作成するために、header を登録します。

```javascript
  var header = {
    "alg": "HS256",
    "typ": "JWT"
  };
```

alg は、暗号化のアルゴリズムです。今回は一般的な HS256 を採用しています。
typ は、どの種類のトークンを作成するかです。今回は JWT なので、JWT と記載します。

次に、先ほど登録した JSON 形式の header を文字列にパースします。

```javascript
  var stringifiedHeader = CryptoJS.enc.Utf8.parse(JSON.stringify(header));
  var encodedHeader = base64url(stringifiedHeader);
```

その後、base64url 形式にエンコードします。この処理は繰り返し利用するので、関数化しています。

```javascript
// Base64URL Section
function base64url(source) {
  // Encode in classical base64
  encodedSource = CryptoJS.enc.Base64.stringify(source);

  // Remove padding equal characters
  encodedSource = encodedSource.replace(/=+$/, '');

  // Replace characters according to base64url specifications
  encodedSource = encodedSource.replace(/\+/g, '-');
  encodedSource = encodedSource.replace(/\//g, '_');

  return encodedSource;
}
```

今度は、data(payload) 部分を登録します。

```javascript
  var data = {
    "sub": "GUEST",
    "name": "ゲスト",
    "iss": "<Guest Issuer ID>",
    "exp": Math.floor(Date.now() / 1000) + 90
  };
```

sub は、ゲスト用のID。英数文字とハイフンのみ使用できます。 sub の値を使い回すと JWT が上書きされてしまうので、sub はユニークなものにしておきましょう。
name は、ゲスト表示名。英数文字の他、日本語も使用できます。
iss は、Webex 開発者サイトで作成した、Guest Issuer ID です。
exp は、この JWT の有効期限。UNIX 時間の整数値になります。ゲスト用のアクセストークンを発行するための JWT の有効期限なので、90 秒や 180 秒と短い時間が良いです。

header と同様、このdata を文字列にパースし、base64url 形式にエンコードします。

```javascript
  var stringifiedData = CryptoJS.enc.Utf8.parse(JSON.stringify(data));
  var encodedData = base64url(stringifiedData);
```

エンコードされた header と data を連結します。token という名前にしています。

```javascript
  var token = encodedHeader + "." + encodedData;  
```

次に、JWT の署名部分を作成していきます。ここで、ハマりポイントですが、Webex 開発者サイトで作成した、Guest Issuer Shared Secret は既に、Base64 にエンコードされているので、パースして bytes 列に変換してあげる必要があります。

```javascript
  var secret = CryptoJS.enc.Base64.parse("<Guest Issuer Shared Secret>");
```

bytes 列への変換に、CryptoJS.enc.Base64.parse() 関数を使用しています。

先ほど、連結した token と bytes 列にパースした secret で、署名 signature を作成します。暗号化アルゴリズムは header で登録した HS256 です。CryptoJS を使用する場合は、HmacSHA256() 関数で暗号化します。

```javascript
  var signature = CryptoJS.HmacSHA256(token, secret);
  signature = base64url(signature);
```

暗号化した signature を base64url 形式にエンコードします。

再び token と 先ほど作成した signature を連結して JWT の完成です。

```javascript
  return token + "." + signature;
```

最後に、作成した JWT で、ゲスト用アクセストークンを作成していきましょう。

```javascript
// Access Token Section
function initializeWebexTokenVersion() {
  const signedToken = createSignedToken();
  return new Promise(() => {
    fetch('https://webexapis.com/v1/jwt/login', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer ' + signedToken,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({})
    })
    .then(response => {
      return response.json();
    })
  });
}
```

先ほど作成した、JWT を signedToken 変数に格納しています。その値を headers の Authorization に、Bearer のスキーマを用いて、https://webexapis.com/v1/jwt/login に POST リクエストします。その際の body は空で送信します。最後にREST API で問い合わせをするため、Javascript の場合、非同期処理を挟む必要があります。非同期処理は Promise()、リクエストに fetch()、レスポンスの結果を Promise() の完了後、then() で受け取っています。

## 動作確認
実際に Webex Browser SDK のサンプルで試してみると、下図赤枠のように Guest Issuer 用のアクセストークンが取得できていることが確認できます。(セキュリティー上全部お見せしていません)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2383689/935e0479-54be-aa73-c9b4-ef711fee51c2.png)
 
## 最後に
今回は、JWT の仕組みを使って、ゲスト用のアクセストークンを発行しました。スクリプトの解説で、ハマりポイントとして説明しましたが、Webex 開発者サイトで作成した Guest Issuer の Shared Secret は、既に Base64 でエンコードされているため、署名として使用する場合は、bytes 列にパースしてあげる必要があります。
それでは、皆様、楽しい年末年始をお過ごしください。