プログラマーのメモ書き

伊勢在住のプログラマーが気になることを気ままにメモったブログです

Cognito ユーザープールを単独で API GateWay と共に使う

前に書いた記事

Cognito ユーザープール使ってみました - プログラマーのメモ書き

では、Cognito ユーザープールを Cognito Identity Pool (Federated Identity) と一緒に使うようなことを書きました。

でも、よくよく考えてみると外部IDプロバイダで認証したIDを使う必要がないなら、Cognito ユーザープールのみを使って、AWSのサービスへは API Gateway 経由でアクセスするのが手っ取り早いのではないかと思い立ちました。

そこで、今回は、Cognito ユーザープールを単独で用いて、ユーザー認証後 API Gateway 経由で Lambda 関数を呼び出す方法を試してみました。 以下は、作業時のメモになります。

なお、Cognito ユーザープールの利用については前回の記事をベースにしているので、詳しくはそちらをご覧ください。

ユーザープールの作成~ログインまで

この部分は、前回の記事と同じなので、割愛します。 クライアント側のログイン処理とマイページの処理が少し変わるので、あとで説明したいと思います。

Lambda 関数の定義

ごく簡単な内容の Lambda関数を定義しておきます(Node.js で定義しています)。 処理内容は以下の通りです。

exports.handler = (event, context, callback) => {
    // TODO implement
    context.done(null, {message: "Hello API:" + event['email']});
};

event['email'] の部分ですが、次節で述べるように API Gateway で認証した情報を eventとして渡すことができ、それを出力しています。

API Gateway の定義

次に API Gateway を定義します。

  1. APIの作成を押します f:id:junichim:20170924152442p:plain
  2. API 名を適当につけます。 f:id:junichim:20170924152600p:plain
  3. 追加したAPIの『リソース』の『アクション』より『メソッドの作成』を選択し、GET を追加します。 f:id:junichim:20170924152811p:plain
  4. 『統合タイプ』でLambda関数を選択し、『Lambdaリージョン』で先ほどLamda関数を作成したリージョンを選択し、Lambda関数名も入力して、保存を押します。 f:id:junichim:20170924153045p:plain
  5. 『Lambda関数に権限を追加する』という確認ダイアログが出てくるので、OKを押します f:id:junichim:20170924153142p:plain
  6. 『アクション』より『CORSの有効化』を選択します
  7. 特に設定を変更する必要がなければデフォルト設定のまま『CORSを有効にして既存のCORSヘッダーを置換』ボタンを押します f:id:junichim:20170924153427p:plain
  8. 『アクション』より『APIのデプロイ』を選択して、この時点で一度デプロイしておきます。デプロイされるステージが未定義の場合は『新しいステージ』を選択し、ステージ名を適当に入れます(ここではprodとしました) f:id:junichim:20170924153704p:plain なお、 ステージエディターが立ち上がりますが、今回は特に設定を変更しません。

これで、 API Gateway が作成できました。

オーソライザー

次に、作成した API Gateway に対して、Cognito ユーザープールを使って、認可(認証に基づく認可)を得るために、オーソライザーを定義します。

  1. 作成したAPIを選択し、『オーソライザー』を選択します f:id:junichim:20170924154227p:plain
  2. 『新しいオーソライザーの作成』ボタンを押します
  3. 『名前』を適当に入力し、『タイプ』として『Cognito』を選択します f:id:junichim:20170924154249p:plain
  4. 『Cognitoユーザープール』として、作成済みのユーザープール名を入力します
  5. 『トークンのソース』として『Authorization』を入力します
  6. 『作成』をクリックします

これでオーソライザーが作成できました

メソッドの認可の設定

作成したオーソライザーを認可に使うため、メソッドの設定を変更します

  1. 『リソース』のGETメソッドを選択します f:id:junichim:20170924154834p:plain
  2. 『メソッドリクエスト』をクリックして、編集画面を開きます
  3. 『認証』(英文だと、Authorizationなので、本当は認可?)の編集アイコン(鉛筆のようなやつ)をクリックして、ドロップダウンリストより、作成したオーソライザーを選択します f:id:junichim:20170924154857p:plain なお、このとき、オーソライザーを作成済みにも関わらず、ドロップダウンリストに現れない場合は、ページをリロードすると表示されるようになりました(もっといい方法があれば教えてください)

本文マッピングテンプレート

最後に、Lambda関数を呼び出したユーザー(API Gatewayを呼び出したユーザー)の情報を伝えるための設定を行います

  1. 『リソース』でGETメソッドを選択します
  2. 『統合リクエスト』をクリックして編集状態にします f:id:junichim:20170924200946p:plain
  3. 『本文マッピングテンプレート』をクリックします f:id:junichim:20170924201108p:plain
  4. 『マッピングテンプレートの追加』をクリックして、Content-Typeとして application/json を入力します f:id:junichim:20170924201226p:plain
  5. パススルー動作の変更を行う旨の確認ダイアログが表示されるので、デフォルトの『はい、この統合を保護します』を選択します f:id:junichim:20170924201345p:plain
  6. テンプレートを定義します(内容は下記参照)
  7. 最後に『保存』ボタンを押せば、完了です

テンプレートの内容は、下記としました。

{
  "email": "$context.authorizer.claims.email",
  "sub" : "$context.authorizer.claims.sub"
}

テンプレートに指定できる項目については、

API Gateway マッピングテンプレートとアクセスのログ記録の変数リファレンス - Amazon API Gateway

などを参照してください。

なお、本文マッピングテンプレートの『リクエスト本文のパススルー』オプションに関するの詳しい説明については、下記記事がわかりやすかったです。

dev.classmethod.jp

クライアント側の処理

前回記事のクライアント側の処理と大きくは変わらないのですが、ログイン画面で承認された後の動作を若干変更し、マイページにAPI Gateway 呼び出し用のボタンと結果の表示を行い処理を追加します。

ログイン

今回は、 Cognito ユーザープール単独で用いる(Cognito Identity Pool を用いない)ので、ログイン後、Identity Pool と統合する処理が不要になります。 具体的には、

upsample.login = function() {
    var username = $('#inputUserName').val();
    var password = $('#inputPassword').val();
    if (!username | !password) { return false; }

    var authenticationData = {
        Username: username,
        Password: password
    };
    var authenticationDetails = new AWSCognito.CognitoIdentityServiceProvider.AuthenticationDetails(authenticationData);

    var userData = {
        Username: username,
        Pool: upsample.UserPool
    };

    var message_text;
    var cognitoUser = new AWSCognito.CognitoIdentityServiceProvider.CognitoUser(userData);
    cognitoUser.authenticateUser(authenticationDetails, {
        onSuccess: function(result) {
            console.log('access token + ' + result.getAccessToken().getJwtToken());

            $(location).attr('href', 'mypage.html');
        },

        onFailure: function(err) {
            alert(err);
        }
    });

}

となります。 また、htmlファイルにおいても、AWS SDK を読み込む必要がなくなっています。

マイページでのAPI Gateway の呼び出し

マイページのhtmlファイルで API Gateway を呼び出すためのボタンを追加しておきます。

ボタンが押された際に、API Gateway へアクセスして、その結果を表示します。

upsample.apiCalling = function() {

    var cognitoUser = upsample.UserPool.getCurrentUser();
    if (cognitoUser != null) {
        cognitoUser.getSession(function (err, sessionResult) {
            if (sessionResult) {
                var idToken = sessionResult.getIdToken().getJwtToken();

                $.ajax(
                    "https://API Gateway のURL",
                    {
                        type: 'GET',
                        contentType: 'application/json',
                        headers: {
                            Authorization: idToken
                        },
                        async: false,
                        cache: false
                    }
                )
                .done(function(data) {
                    $('#api_result').text('result: ' + JSON.stringify(data));
                })
                .fail(function() {
                    console.log("failed to call api");
                });
            }
        });
    }
}

呼び出し時は、 Authorization ヘッダーに Idトークンを指定すればOKです。 認証が切れた状態だと、ステータスコード 401 でレスポンスが返ってきます。

実験

では、実際にブラウザからアクセスしてみます。 ログイン画面を表示後、ログインするとマイページが表示されます。

f:id:junichim:20170924204848p:plain

ここで、『API実行』ボタンを押すと、 API Gateway 経由で Lambda が呼ばれ、ログイン時のユーザーのメールアドレスを含んだメッセージが返されます。

f:id:junichim:20170924204752p:plain

問題なく表示されていますね。

コンソールから試し

必要というわけでもないのですが、curlでも試してみます。

ログインを行っていない状態で

curl --include https://xxxx.execute-api.ap-northeast-1.amazonaws.com/ステージ名

とすると 401 が返ってきます。

次にブラウザでログイン後、ディベロッパーツールでIdトークンの値を取得して、curlで呼び出すと

curl --include https://xxxx.execute-api.ap-northeast-1.amazonaws.com/ステージ名 -H 'Authorization: Idトークンの値'

問題がなければ、

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 23
Connection: keep-alive
Date: Sat, 23 Sep 2017 14:04:50 GMT
x-amzn-RequestId: 29d9022d-a068-11e7-a3a1-ad50f52c7d3e
Access-Control-Allow-Origin: *
X-Amzn-Trace-Id: sampled=0;root=1-59c66a01-cdeca4ab25f31bc912c89815
X-Cache: Miss from cloudfront
Via: 1.1 e20489ede5d5a153bd489790cc8e71ab.cloudfront.net (CloudFront)
X-Amz-Cf-Id: awZjLU21X6F0SBrmnSQlezZPMap4Mxv2Arvoxg6TLYy5yLVFfggsbA==

{"message":"Hello API:test@example.com"}

のようなヘッダが返ってきます。 やはり、問題なさそうですね。

はまったところ

実は、最初、 API Gateway のドキュメント

Amazon Cognito ユーザープールをオーソライザーとして使用して REST API へのアクセスを制御する - Amazon API Gateway

を参考にして、API Gateway のオーソライザーの設定でトークンのソースとして

method.request.header.Authorization

としていたのですが、このままでは 401 が返ってきてAPI Gateway にアクセスできません。

単に、

Authorization

のみでよかったのでした。 でも、これがわかるまで、半日ぐらいかかってしまったので、一応書いておきます。

参考

以下の記事を参考にさせていただきました。

Cognito User Poolsの機能と使い所 - たれぱんのびぼーろく

http://www.h4a.jp/detail/25148

API GatewayでCognito UserPools Authorizerを使う - Qiita

AWS Cognito の認証情報を API Gateway + Lambda で受け取りたい - Qiita

また、今回の一連のソースは Github にアップしてあるので、気になる方はご参考までどうぞ。

github.com