はじめに
これまで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外の認証プラットフォームを使っているとき
参考
- https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html
- https://dev.classmethod.jp/articles/lambda-authorizer/
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関数を実行するようにしています。
- API Gatewayの設定
- フロントの実装
やってみる
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
このidToken
をAuthorization
キーのバリューとしてヘッダーに持たせることで、リクエストが可能になります。
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をたたくところのみ抜粋
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制限を外しても、セキュリティを担保することができたのではないかと思います!めでたし!