Cognitoを使ってAPI Gatewayのアクセス認証をしてみた


はじめに

これまでS3のバケットポリシーとAPI Gatewayのリソースポリシーでアクセス元のIP制限をかけていたため、許可されているIPアドレスからでないと、アクセスし、APIをたたけないようになっていました。しかし、今回訳あってIP制限を外すことになってしまい、APIを誰でも叩き放題になってしまったので、解決策を考えました。

方法検討

API Gatewayで使えるアクセスを認証

APIのアクセスを認証する方法を調べたところ以下の3つが出てきました。

  • Cognito
  • Lambdaオーソライザー
  • IAM認証

Cognito

Cognitoユーザープールで認証時にユーザープールトークンが発行され、そのトークンを使用して認証する方法です。

わたしが思うユースケース
  • ユーザー認証にCognitoを使用しているとき

Lambdaオーソライザー

API Gatewayを叩いた時に、認証用のLambda関数を呼び、認証が通れば、実行したいAPI(今回だとLambda関数)が実行されるようになるという方法です。

わたしが思うユースケース
  • Auth0などのCognito外の認証プラットフォームを使っているとき

参考

IAM認証

APIの実行権限を付与したIAMユーザーを作成し、IAMユーザーのアクセスキー、シークレットキーを使ってAWS Signature V4 署名を作成し認証する方法です。
ユーザーにIAMロールが付与されていれば、それも使用することができます。

わたしが思うユースケース
  • サーバーからAPIをたたくとき(EC2などIAMロールが使えるときはIAMロールを利用)
  • CognitoのグループでIAMロールを付与しているとき

参考

現在の構成図

アプリケーション部分の構成図は下記の通りです。

ユーザー認証部分はCognito + Amplifyフレームワークで構築しています。構築の基本部分については「【React】ユーザー認証をCognito + Amplifyで構築してみた」の構築準備編構築完成編をご覧ください。
そして、アプリケーション部分はLambda + RDS Proxy + RDSで実装しています。この構築方法については「祝GA‼︎【Go】Lambda + RDS 接続にRDS Proxyを使ってみた」をご覧ください。

結論

現状、Cognitoユーザープールを使ってユーザー認証をしているので、API Gatewayのアクセス認証にもCognitoを使うことにしました!

手順

既存の構成にAPIのアクセス認証をつけていくので、Cognitoユーザープールを使ってのユーザー認証、API Gatewayを使ってLambdaを実行する部分については既に構築できていることを前提として、下記の流れで進めていきます。ただ、今回はDB操作は行わず、メッセージを送り、メッセージをそのまま返すLambda関数を実行するようにしています。

  1. API Gatewayの設定
  2. フロントの実装

やってみる

1. API Gatewayの設定

まず、API Gatewayのオーソライザーを作成していきます。
API Gatewayのコンソールから、[オーソライザー]を開きます。

新規でオーソライザーを作成します。
名前、タイプ、Cognitoユーザープール、トークンのソースを入力し、作成ボタンをクリックします。トークンのソースAuthorizationはリクエストのヘッダーとしてトークンを送るときに使います。

次に、作成したオーソライザーはメソッド単位で設定していきます。つまり、複数メソッドがある場合はそれぞれに設定しないとトークンなしでAPI Gatewayを叩けてしまうので注意です。

次のように、[リソース]→[オーソライザーを設定したいメソッド]→[メソッドリクエスト]を開きます。

許可の部分に先ほど作ったcognito-authorizerを設定します。選択肢に出てこない場合はリロードなどすると選択肢に出てきます!

そして最後にデプロイします!
これでオーソライザーの設定は完了です。

2. フロントの実装

取得したユーザープールトークンをヘッダーにつけてAPI Gatewayをたたく処理を実装します。

axiosのインストール

API Gatewayを叩くのにaxiosを使うために、プロジェクトにaxiosを追加します。

$ yarn add axios

ソースコード

認証時に必要なトークンは下記の方法で取得可能です。

const user = Auth.currentAuthenticatedUser()
const idToken = user.signInUserSession.idToken.jwtToken

このidTokenAuthorizationキーのバリューとしてヘッダーに持たせることで、リクエストが可能になります。

App.js
import React from "react";
import Amplify, {Auth} from 'aws-amplify';
import awsconfig from './aws-exports';
import {withAuthenticator} from "@aws-amplify/ui-react";
import axios from "axios";
import "./App.css"

Amplify.configure(awsconfig);

function App() {
    const API_URL = "<API Gatewayで取得したURL>"
    const [message, setMessage] = React.useState("");
    const [response, setResponse] = React.useState("");

    const handleChange = event => {
        setMessage(event.target.value);
    };

    const handleSubmit = async(event) => {
        const user = await Auth.currentAuthenticatedUser()
        const idToken = user.signInUserSession.idToken.jwtToken
        const headers = {headers: {"Authorization": idToken}};
        axios.post(API_URL, {message: message}, headers)
            .then((response) => {
                if(response.data.message === message){
                    setResponse(response.data.message);
                } else {
                    throw Error(response.data.errorMessage)
                }
            }).catch((response) => {
                alert("登録に失敗しました。もう一度送信してください。");
                console.log(response);
        });
        event.preventDefault();
    }

    return (
        <fieldset>
            <form onSubmit={handleSubmit}>
                <label >
                    <input type="text" value={message} onChange={handleChange} />
                </label>
                <input type="submit" value="送信" />
            </form>
            <div>{response}</div>
        </fieldset>
    );
}

export default withAuthenticator(App);

実行結果

ヘッダーあり

入力欄の下に、Lambdaから返ってきたメッセージが表示されるようになっています。入力した値がLambdaを介して返ってきています!

ヘッダーなし

ちなみに、ヘッダーにidトークンを付けずに実行してみました。

※ API Gatewayをたたくところのみ抜粋

App.js
        axios.post(API_ADD_URL, {message: message})
            .then((response) => {
                if(response.data.message === message){
                    setResponse(response.data.message);
                } else {
                    throw Error(response.data.errorMessage)
                }
            }).catch((response) => {
                alert("送信に失敗しました。もう一度送信してください。");
                console.log(response);
        });

ソースを上記のように変更し、実行すると・・

エラーが出て、Lambdaが実行できないことがわかりました!

おわりに

無事に、API Gatewayにアクセス認証をつけることができました!今回はもともとCognitoユーザープールを使ってユーザー認証をやっていたので、Cognitoのオーソライザーを使って簡単に設定することができました。既存のシステムの構成によってこれでIP制限を外しても、セキュリティを担保することができたのではないかと思います!めでたし!

参考