Amplify Reactで取得したトークンを使って、Swagger UIからAPIを実行してみた

Swagger UIをプチカスタマイズしてみました
2019.07.07

サーバーレス開発部改めCX事業本部の岩田です。 Amplify Reactを使ってCognitoユーザープールにサインインし、払い出されたトークンを使ってSwagger UIからAPIを実行できる環境を作ったので、構築手順等ご紹介します。

環境

今回利用した環境です

  • Node.js v8.10
  • Swagger UI 3.23.0
  • React 15.6.2
  • AWS Amplify 1.1.29
  • AWS Amplify React 2.3.9

概要

現在開発中の案件では以下のような環境でAPIの開発を行なっています。

  • API Gatewayの定義にはSwaggerを利用
  • Swagger UI × S3の静的ウェブサイトホスティングで上記Swaggerの定義を公開して顧客と共有
  • APIの認可にはCognitoユーザープールを利用

ちなみに、Swagger UIとはこんなツールです

見たことのある方も多いのではないでしょうか?

Swagger UIを利用することで、顧客側からも簡単にAPIをテストできるのですが、Cognitoによる認可を付けると、途端に面倒になります。APIを実行する前にサインインし、払い出されたトークンをAPIのリクエストヘッダーにセットする必要があります。

この辺りの手順をもっと楽チンにするべくCognitoとSwagger UIの連携について調べたところ、既にブログ化されていたのですが、今回は

  • 顧客側にCognitoのアプリクライアントIDを入力してもらう必要がある
  • Swaggerを使ってAPI GatewayにCognitoオーソライザーを定義する場合、type: oauth2 ではなくtype: apiKeyとして定義する必要があり、API Gatewayの定義をそのまま顧客と共有している今回の構成にマッチしない

という理由から採用を見送り、別のやり方を模索することにしました。 色々と考えた結果、Amplify Reactでサクっとサインイン画面を用意しつつ、サインイン後にSwagger UIの画面に遷移、以後のリクエストには払い出されたトークンを自動的に設定するという方式を選択しました。

API Gatewayの準備

では、ここから実際に環境を作っていきます。 あまり詳細まで解説しませんので、不明点があればこのあたりの記事も合わせてご参照下さい。

サンプルをもとにPetStoreAPIを作成し、Cognitoオーソライザーの設定を行います

今回はList all petsAPIを変更し、Cognitoオーソライザーで認可後、後続のLambdaに処理を引き渡すよう設定しました。LambdaはeventオブジェクトをJSONに変換して返却するよう実装しています。

ひと通り準備できたら、APIをデプロイしてSwaggerの定義をJSONで出力しておきます。

出力したJSONファイルは静的ウェブサイトホスティングを有効化したS3バケットにアップしておきます。 アップできたら、バックエンド側は準備完了です。

Swagger UIの環境作成

続いてフロント側を準備します。

まずはアプリのひな形作成とライブラリの導入です。

$ create-react-app myapp
$ cd myapp
$ npm install aws-amplify aws-amplify-react swagger-ui

準備できたらApp.jsを編集します

App.js

import React, {Component} from 'react';
import SwaggerUi, {presets} from 'swagger-ui';
import 'swagger-ui/dist/swagger-ui.css';
import Amplify, {Auth} from 'aws-amplify';
import {withAuthenticator } from 'aws-amplify-react';

Amplify.configure({
  Auth: {
      region: 'ap-northeast-1',
      userPoolId: '<CognitoのユーザープールID>',
      userPoolWebClientId: '<CognitoのアプリクライアントID>',
  }
});

class App extends Component {
  componentDidMount() {
    Auth.currentSession()
      .then(data => {
        const idToken = data.getIdToken().getJwtToken();
        SwaggerUi({
          dom_id: '#swaggerContainer',
          url: 'https://<Swaggerの定義をアップしたS3バケット名>.s3-ap-northeast-1.amazonaws.com/PetStore-dev-swagger.json',
          presets: [presets.apis],
          requestInterceptor: (req) =>{
            console.log(req);
            // S3の静的Webサイトホスティングに対してAuthorizationヘッダーでIDトークンを送るとエラーになるので
            // S3へのリクエストの時はリクエストを加工しない
            if (req.url.indexOf('s3-ap-northeast-1.amazonaws.com') !== -1){
              return req;
            }
            req.headers.Authorization = idToken; 
            return req;
          }
        });
    });
  }

  render() {
    return (
      <div className="App">
      <header className="App-header">
      </header>
      <div id="swaggerContainer" />
    </div>
    );
  }
}

export default withAuthenticator(App, true);

ポイントとなるのはcomponentDidMountの処理です。

    Auth.currentSession()
      .then(data => {
        const idToken = data.getIdToken().getJwtToken();

の部分で、Cognitoから払い出されたIDトークンを取得します。

次に以下のコードでSwagger UIのコンポーネントを作成します。

        SwaggerUi({
          dom_id: '#swaggerContainer',
          url: 'https://<Swaggerの定義をアップしたS3バケット名>.s3-ap-northeast-1.amazonaws.com/PetStore-dev-swagger.json',
          presets: [presets.apis],
          requestInterceptor: (req) =>{
            console.log(req);
            // S3の静的Webサイトホスティングに対してAuthorizationヘッダーでIDトークンを送るとエラーになるので
            // S3へのリクエストの時はリクエストを加工しない
            if (req.url.indexOf('s3-ap-northeast-1.amazonaws.com') === -1){
              req.headers.Authorization = idToken; 
            }
            return req;
          }
        });

requestInterceptorを実装することで、デフォルトのリクエストオブジェクトを加工することができます。ここではリクエストヘッダーのAuthorizationに先ほど取得したIDトークンを設定しています。 また、S3の静的Webサイトホスティングに対してAuthorizationヘッダーでIDトークンを送るとAuthorization header is invalid -- one and only one ' ' (space) requiredという403エラーが返ってくるので、S3向けにはAuthorizationヘッダーをセットしないように制御しています。

上記の実装だと、1時間後にCognitoのIDトークンの有効期限が切れるとリクエストが発行できなくなります。本来は

  requestInterceptor = (req)=>{
    return Auth.currentSession()
      .then(data => {
        // // S3の静的Webサイトホスティングに対してAuthorizationヘッダーでIDトークンを送るとエラーになるので
        // // S3へのリクエストの時はリクエストを加工しない
        if (req.url.indexOf('s3-ap-northeast-1.amazonaws.com') === -1){
          const idToken = data.getIdToken().getJwtToken();
          req.headers.Authorization = idToken
        }
      })
  }

のようにAmplifyにトークンを自動リフレッシュさせたいのですが、requestInterceptorにPromiseを返す関数を設定するとSwagger UIに表示されるCURLコマンドが正しく生成されないという不具合があるため、タイムアウトについては我慢しています。

https://github.com/swagger-api/swagger-ui/issues/4778

実装できたらnpm startして動かしてみましょう。

いつものサインイン画面が出てくるので、サインインすると、、、

Swagger UIの画面が表示されました!!

Cognitoオーソライザーを設定したList all petsのAPIを叩いてみます

リクエストヘッダーにAuthorizationが設定できていることが分かります

開発者コンソールにもリクエストの情報が出力されています

レスポンスもちゃんと返ってきてますね!!

まとめ

Swagger UI標準の Authorizeが表示されっぱなしになっているのが気になりますが、まあ一般公開するページではないので我慢することにします。

Swaggerのドキュメントによると

OIDC is currently not supported in Swagger Editor and Swagger UI. Please follow this issue for updates.

とのことなので、Swagger UIがOIDCにも対応して、もっと簡単かつ良い感じにCognitoと一緒に使えると良いですね!!