Postman pre-requestスクリプトでOAuth2.0トークンをリフレッシュする方法 (Postman CLI / Newman / スケジュール実行でのAPIテスト自動化対策)

PostmanはAPIテストのための様々な便利機能を備えてますが、中でも、認証・認可のための機能は充実しています。例えば、OAuth2.0の自動トークンリフレッシュ機能があります。PostmanのAPIリクエスト設定の一部に「Authorization/認可」設定がありますが、ここでOAuth2.0トークンを取得し、有効期限が切れたら自動でリフレッシュする設定ができます。この自動トークンリフレッシュ機能については、下記の記事で紹介しています。

https://qiita.com/yokawasa/items/b4d89d983bfd23792788

本記事では、このトークンの自動リフレッシュを実現するための別解として、pre-requestスクリプトを利用する方法について解説します。なお、Pre-requestスクリプトの具体例として、AWS CognitoでのOAuth2.0におけるトークンリフレッシュ方法を紹介します。

# pre-requestスクリプトによるトークンリフレッシュの活用ケース

Postmanの自動トークンリフレッシュ機能は2023年2月にサポートが開始されました。それ以前は、OAuth2.0のトークンを自動リフレッシュのために、pre-requestスクリプト機能を活用して自前で組む必要がありました。では、自動トークンリフレッシュ機能がサポートされた今、このpre-requestスクリプトのアプローチは不要になったのでしょうか？ もちろんそのようなことはなく、このpre-requestスクリプトアプローチが必要または効果的なケースも存在します。

- **Postman CLI、Newman、スケジュール実行やPostmanモニターを使ったテスト自動化**
PostmanのGUI上で動作するリフレッシュ機能は、CLIツールである`Postman CLI`や`Newman`、また`スケジュール実行`や`Postmanモニター`の処理ではサポートされていません。この場合は、pre-requestスクリプトが有効な手段になります

- **柔軟なカスタマイズを要するテスト**
スクリプトを使えば特定の要件やビジネスロジックに基づいて、トークンのリフレッシュプロセスを柔軟に制御することができます。例えば、期限切れのトークンに対するAPIエラーを受け取った際のみトークンをリフレッシュするなどのカスタムロジックを実装できます

# Postmanのpre-requestスクリプトで新しいアクセストークンを取得する方法

Pre-requestスクリプトの具体例を紹介します。先述の通り、AWS CognitoでのOAuth2.0におけるトークンリフレッシュ方法を紹介します。

## AWS Cognito のRefresh Tokenの仕様

まずは、具体例を紹介する前に、サンプルとして利用するAWS CognitoのRefresh Tokenの仕様を簡単に紹介します。AWS Cognitoは、セキュアな認証フローを提供する人気の高いIDプロバイダーです。

AWS Cognitoのトークンリフレッシュには、以下の4つの情報が必要です。

- Client ID (OAuth2.0のクライアントID)
- Client Secret（使用する場合のみ。本サンプルでは使用しない）
- Refresh Token
- Token Endpoint（AWSのCognitoホスト名）

これらの情報を使って、次のようなリクエスト構造でアクセストークンを更新します。

> Postmanのリクエスト設定例
![Screenshot 2024-10-25 at 12.57.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103420/c888299f-9c5c-0c0d-7bba-d582686d3b4c.png)

:::note info
Postman CLI、スケジュール実行、Postmanモニターから実行する場合は、変数の初期値(Initial Value) にもデータをセットする必要があるのでご注意ください。

初期値と現在値の詳細については、[こちらページ](https://qiita.com/yokawasa/items/05913df60aea07395903)の「初期値(Initial Value)と現在値(Current Value)」を参照ください。
:::


> curlのリクエスト設定例
```bash
curl --location 'https://<AWS-Cognitoのホスト名>/oauth2/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=<クライアントID>' \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode 'refresh_token=<リフレッシュトークン>'
```

リクエストを実行すると次のような結果が取得されます。

```json
{
    "access_token": "<リフレッシュされたアクセストークン>",
    "expires_in": 3600,
    "token_type": "Bearer"
}
```

なお、AWS Cognitoのトークンリフレッシュの仕組みについて、具体的には次のページが参考になります。

https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-the-refresh-token.html


AWS Cognitoのアクセストークンは`JWT(JSON Web Token) 形式`です。トークンの中には、エンドポイントや、クライアントID、スコープ、有効期限などさまざまなクライアントに関する情報が含まれています。アクセストークンの内容については下記のページが参考になります。

https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-the-access-token.html

参考までに、実際のアクセストークンを[jwt.io](https://jwt.io/)で見てみると次のようになっています。

![Screenshot 2024-10-25 at 13.40.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103420/654f9d3f-1536-013b-802d-8ba70f72c7cc.png)


# Pre-requestスクリプト例

## 事前設定

AWS CognitoのトークンをリフレッシュするためのPostman pre-requestスクリプトの例を紹介します。

まず、このスクリプトでは、事前に環境に必要な情報を格納していることを前提としています。次の情報を格納しています。

- token_endpoint
- client_id
- client_secret (このサンプルでは使っていない)
- access_token
- refresh_token

これはPostmanでの環境変数設定の画面イメージです。

![Screenshot 2024-10-25 at 13.48.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103420/f1691e33-4fc3-c333-0474-f7af9229cb82.png)

なお、Postmanの環境変数を含め、変数についてはこちらのページに網羅的にまとめています。

https://qiita.com/yokawasa/items/05913df60aea07395903

## サンプルスクリプト

```javascript

// アクセストークンが有効かどうかをチェックする関数
// <<アクセストークンがJWT形式であること前提>>
// 通常、OAuth 2.0のAccess TokenにはJWT (JSON Web Token) が使われていることが多いので、
// JWTのペイロード部分から有効期限 (exp クレーム) を取得して確認できる
function isAccessTokenValid(access_token) {
    if (!access_token) {
        console.log('No Access Token found.');
        return false;
    }
    // Access Tokenのペイロード部分をデコードするために、Base64URLエンコードをBase64に変換
    const tokenParts = access_token.split('.');
    // JWTは3つの部分に分かれており、真ん中の部分（ペイロード）をBase64URLからBase64に変換してデコード
    if (tokenParts.length != 3) {
         console.log('Invalid JWT token format.');
        return false;
    } 
    const atob = require('atob') 
    const base64Url = tokenParts[1];
    const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
    const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
        return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
    }).join(''));
    // ペイロードをJSONとしてパースし、exp クレーム（有効期限）を取得
    const payload = JSON.parse(jsonPayload);
    const expirationTime = payload.exp;
    if (!expirationTime) {
        console.log('No expiration time found in token.');
        return false;
    }
    // exp の値はUNIXタイムスタンプ（秒単位）なので、現在の時刻と比較して有効期限を算出
    const now = Math.floor(Date.now() / 1000);
    return (expirationTime - now) > 0
}

// アクセストークンを変数から取得する。ここでは環境変数を活用
const refreshToken = pm.environment.get("refresh_token");
const clientId = pm.environment.get("client_id");
// const clientSecret = pm.environment.get("client_secret");
const tokenEndpoint = pm.environment.get("token_endpoint");
const accessToken = pm.environment.get("access_token"); 

const isValid = isAccessTokenValid(accessToken);
console.log(`Access Token is valid: ${isValid}`);

// アクセストークンの有効期限が切れている場合にのみトークンリフレッシュ
if (accessToken && !isValid) {
    pm.sendRequest({
        url: tokenEndpoint,
        method: 'POST',
        header: 'Content-Type: application/x-www-form-urlencoded',
        body: {
            mode: 'urlencoded',
            urlencoded: [
                { key: "client_id", value: clientId, disabled: false },
                { key: "grant_type", value: "refresh_token", disabled: false },
                { key: "refresh_token", value: refreshToken, disabled: false }
            ]
        }
    }, function (err, res) {
        var jsonData = res.json();
        if(jsonData.access_token) {
            // リフレッシュされたアクセストークンで環境変数を更新
            console.log('Access Token is updated!');
            pm.environment.set('access_token', jsonData.access_token);
        }
    });
}
```

このスクリプトでは`pm.sendRequest`関数を使用して、AWS CognitoのトークンエンドポイントにPOSTリクエストを送信し、リフレッシュトークンから新しいアクセストークンを取得します。取得したアクセストークンは環境変数access_tokenに保存され、後続のAPIリクエストで使用されます。

なお、有効期限の判断は、isAccessTokenValid関数で行っています。この関数に、環境から取得したアクセストークンを引数として与えると、関数内で、JWT形式のJWTのペイロード部分から有効期限 (exp クレーム) を取得して、現在時間と比較して有効期限が切れているかどうかを判断します。通常、OAuth 2.0のAccess TokenにはJWT が使われていることが多いので、その場合、この関数は汎用的に活用できます。

## リクエスト実行とその結果

それでは、上記で紹介したサンプルスクリプトを、AWS CognitoのOAuth2.0認可が必要なAPIリクエストで試してみます。

まずは、APIリクエストの認可タブで、AuthTypeにBearerトークンを選び、値として環境変数に設定されているアクセストークンを設定します。

![Screenshot 2024-10-25 at 13.58.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103420/a0346cf2-8a31-44ea-0ac0-ddbdcae19d2f.png)

次に、紹介したスクリプトをpre-requestスクリプトとして登録します。

![Screenshot 2024-10-25 at 13.56.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103420/f990db81-0b28-e2b3-f444-248eacf101fb.png)

最後に、リクエストを実行します。もし有効期限が切れていたら、アクセストークンの環境変数がアップデートされます。
次のイメージは、アクセストークンの有効期限が切れていた場合に、出力されるPostmanコンソールログの例です。まず、トークンリフレッシュ用のエンドポイントにリクエストが送信され、新しいアクセストークンを取得し、それを元にリソース取得用のエンドポイントにリクエストが送信されていることが分かるかと思います。

![Screenshot 2024-10-25 at 14.08.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103420/e7f4f866-35ff-71e5-f7b2-ce42f1cd4f10.png)

以上になります。何かお気づきの点があれば、コメントいただければと思います。