SageMaker+APIGateway+Lambdaで作るサーバレスアプリケーション~外伝 Cognito認証

目次
はじめに
こんにちは!スカイアーチHRソリューションズのnakaoです!
この記事は「SageMaker+APIGateway+Lambdaで作るサーバレスアプリケーション~③(完)」の続きです!
前回の記事でアプリケーションは完成しましたが、ユーザー認証機能も作りたい!と思っていました!
そのため外伝としてこの記事で、サービス連携させてみたいと思います!
アーキテクチャ
全体のアーキテクチャはこちらです!
ユーザー認証機能を作るために、Cognitoを使用しました。

Cognito導入にあたって、最終的にどちらのアーキテクチャを選ぶか、悩みました。
Amplifyの導入も検討しましたが、今回はCognitoのみで実現したかったため、見送りました。
①Lambda@Edgeを使用してCloudFrontにCognito認証を実装。
②ログイン前画面、ログイン後画面を作成し、フロント側にCognito認証処理を実装。
私がはじめてCognitoを触る、かつ、まずは簡単に実装したい!という思いもあり、①を選択しました。
①だと認証の仕組みをインフラ寄りに導入でき、新しく画面を作成する必要がありません。また、S3に置かれているリソースにCloudFront経由でダウンロードする前にCognitoの認証がかかります。コンテンツの管理という側面でも安全性は高いです。
②だと作成する画面数、フロント側の実装も多くなります。その分、画面側の自由度は大きいです。
また、認証のUIはCognitoで用意されているHostedUIを使用することにしました。
UIを自前で用意して①、②にそれぞれ導入することは可能ですが、今回は見送りました。
Cognitoとは
Cognitoは、AWSなどに構築した、Webアプリケーションやモバイルアプリケーションに認証・認可機能を提供するサービスです。代表的な機能としてはユーザープールとアイデンティティ(ID)プールです。
今回はユーザー認証を実装します。したがって、Cognitoのユーザープール機能のみを使用します。
詳細に関しては以下、公式ドキュメントを参照してください。
前提として
前提として、前回の記事「SageMaker+APIGateway+Lambdaで作るサーバレスアプリケーション~③(完)」で作成されるリソースをベースに加筆修正していきます。
前回の記事のリソースがなくてもCloudFront+S3構成であれば問題ありません。適宜読み替えて対応してください。

Cognito ユーザープールの作成とユーザーの作成まで
それではまず、Cognito ユーザープールの作成から始めていきましょう!
Cognito ユーザープールの作成
「Cognito」サービスを検索して、ユーザープールの作成を選択します。
サインインオプションはユーザー名にしておきましょう。
ユーザー名とパスワードでログインするようにします。

MFAなしで作成します。

セルフサービスのサインアップは許可しています。
ユーザーの登録は管理者のみ管理できるようにしたい場合は、「自己登録を有効化」のチェックを外すようにしてください。

CognitoデフォルトのEメールアドレスは、メッセージのカスタマイズができない、一日の送信数に制限があります。
今回は大量通知やカスタマイズ不要のため、Eメールプロバイダーは「CognitoでEメールを送信」を選択します。

Cognitoで用意されているHostedUIを使用するため、「CognitoのホストされたUIを使用」はチェックするようにしてください。
また、Cognitoドメインはリージョン内で一意である必要がありますが、任意のドメインを使用してください。
アプリケーションクライアントの「許可されているコールバックURL」には、連携したいCloudFrontのディストリビューションドメイン名を入力してください。認証後のユーザーのリダイレクト先をここで設定しています。

設定値に問題がなければ、Cognito ユーザープールを作成しましょう。
ユーザーの作成
次に、ユーザーの作成を行います。
先ほど作成したCognito ユーザープールを選択し、アプリケーションの統合タブから、アプリケーションクライアントを選択します。

ホストされたUIから「ホストされたUIを表示」を選択します。

Sign upを選択して、ユーザーの作成を行いましょう。


メールが届くので、codeを入力してユーザーの作成を完了させると、連携したコールバックURL(CloudFront)にリダイレクトします。


ユーザープールのユーザーを確認すると、先ほどUIから作成したユーザーが登録されていることを確認できます。

Cognito ユーザープールの作成とユーザーの作成に関しては以上です。
Lambda@Edgeの作成からデプロイまで
次にLambda@Edgeを作成して、CloudFrontと連携させます。
Cognitoで認証されているユーザーのみ、CloudFrontにアクセスできるようにします。
それではLambda@Edgeの方にCognitoのトークンを検証するコードを記述していきたいところですが、、、既に便利なパーケージがAWSからawslabsという場所に公開されています。
今回はawslabsで公開されているcognito-at-edgeというパッケージを使用して、CognitoとCloudFrontを連携させたいと思います。
Lambda@Edgeを作成する前に、CloudFront、Cognito、Lambda@Edge 間のやり取りについて簡単に紹介しますと、
①ユーザーがCloudFrontに対してURLアクセス。
②CloudFrontがリクエストを受信した後(ビューアーリクエスト)、Lambda@Edgeがトリガーされる。
③Lambda@Edgeで認証情報を確認(今回はcognito-at-edgeというパッケージを使用)。
④認証情報が正しければ、CloudFrontの指定されたオリジンにパススルーする。認証情報が正しくなければ、ログインページへリダイレクトする。
ということをしているようです。
詳細に関しては以下、AWS公式のブログ記事に記載してあります。
Lambda@Edgeの作成まで
前置きが長くなりましたが、それではLambda@Edgeを作成していきましょう。
cognito-at-edgeのリポジトリに記載してある記述の通りに作成していきます。
今回はローカル環境で作成します。
まず、node.jsをpcにインストールしていない場合は、インストールしておきましょう。npmを使用します。
node --version
v18.17.1
npm --version
9.6.7
空のフォルダを作成し、フォルダ直下でcognito-at-edgeをローカルインストールします。
空のファイルも作成しておきましょう。
mkdir cognito-edge
cd cognito-edge
npm install congnito-at-edge
touch index.js
フォルダ構成は以下のようになっていると思います。
index.jsを修正しましょう。

regionにはCognito ユーザープールを作成したリージョンを書き換えます。
userPoolDomainはhttps:// 以降のドメインで書き換えてください。
Lambda@Edgeでは環境変数を設定できません。
今回は値をハードコーディングしますが、これを回避したい場合はSystem Managerのパラメータストアを使用することで解決できます。

const { Authenticator } = require('cognito-at-edge');
const authenticator = new Authenticator({
// Replace these parameter values with those of your own environment
region: 'ap-northeast-1', // user pool region
userPoolId: 'xxxxxxxxxxxxxxxxxxxx', // user pool ID
userPoolAppId: 'xxxxxxxxxxxxxxxxxxxx', // user pool app client ID
userPoolDomain: 'xxxxxxxxxxxxxxxxxxxx', // user pool domain
});
exports.handler = async (request) => authenticator.handle(request);
フォルダ直下でzip化します。
zip -r cognito-edge.zip *
Lambda@Edgeのデプロイまで
次にLambda@Edgeをデプロイしていきます。
「Lambda」サービスを検索して、リージョンをバージニア北部に変更します。
Lambda@Edgeをデプロイできるのはバージニア北部のみです。
関数を作成します。ランタイムはローカルで確認したNode.jsのランタイムと合わせましょう。

準備したzipファイルをアップロードします。

CloudFrontトリガーを使用するためにLambdaの実行ロールを修正します。
ロール名を選択してください。

信頼関係タブから信頼ポリシーを編集して下記のように修正してください。

Lambda@Edgeへのデプロイを行います。

CloudFront eventは「Viewer request」時に発生するよう修正します。

Cognito認証 動作確認
それでは、連携させているCloudFrontのディストリビューションドメインにアクセスして動作確認してみましょう。
ユーザー作成した際のトークンが残ってる可能性があるので、シークレットウィンドウなどで確認しましょう。
アクセスすると、Cognito認証画面へリダイレクトされます。
先ほど作成したユーザーでログインしましょう。

ログインに成功して、オリジンコンテンツが取得できてますね!
デベロッパーツールのアプリケーションタブのCookiesを確認すると、CognitoのIDトークンなどが取得できていることも確認できます!
ここで取得したIDトークンなどを渡してユーザーごとに、APIGatewayなどのAWSリソースに制限をかけて使用するなど、活用方法はたくさんありそうですね!

Lambda@Edgeの作成からデプロイまでに関しては以上です。
静的ウェブコンテンツ(Vue.js)の修正
せっかくですから、フロントエンド側も少し修正してみましょう。
CookieStorageを利用して、Cognito認証ログインしているユーザーのユーザー名を取り出して表示してみましょう。
ここからは前提として「SageMaker+APIGateway+Lambdaで作るサーバレスアプリケーション~③(完)」の記事で作成したVue プロジェクトをベースに改良していきます。
amazon-cognito-identity-jsをインストールする
今回は、amazon-cognito-identity-jsというjsライブラリを使用します。

カレントディレクトリをプロジェクト直下にして、amazon-cognito-identity-jsをローカルインストールしてください。
npm install amazon-cognito-identity-js
ライブラリの説明の中に、CookieStorageから取得する方法が記載してあります。
こちらをソースコードに追記します。

<template>
<div id="app">
<h2>ようこそ、{{ currentUser }}さん</h2>
<p><input type="file" accept="image/*" v-on:change="onFileChange"></p>
<div v-if="image">
<img :src="image" alt="image">
<p><button v-on:click="uploadImage">アップロード</button></p>
</div>
<h2>{{ result_sagemaker_endpoint }}</h2>
</div>
</template>
<script>
import axios from 'axios';
import {
CognitoUserPool,
CookieStorage,
} from 'amazon-cognito-identity-js';
const url ='API Gateway URL';
const UserPoolId = 'cognito user pool id';
const ClientId = 'cognito user pool appclient id';
const CookieDomain = "cookie storage domain";
var poolData = {
UserPoolId : UserPoolId,
ClientId : ClientId,
Storage: new CookieStorage({domain: CookieDomain})
};
var userPool = new CognitoUserPool(poolData);
var cognitoUser = userPool.getCurrentUser();
export default {
data: () => ({
currentUser: cognitoUser.username,
image: '',
selectedFile: '',
result_sagemaker_endpoint: '',
}),
methods: {
onFileChange: function(event) {
let files = event.target.files;
if (!files.length) {
return;
}
this.previewImage(files[0]);
},
previewImage: function(file) {
let reader = new FileReader();
// ファイル読み込み完了後の処理
reader.onload = e => {
this.image = e.target.result;
this.selectedFile = file;
};
reader.readAsDataURL(file);
},
uploadImage: async function() {
let config = {
headers: {
'content-type': 'image/png'
}
};
try {
let res = await axios.post(url, this.selectedFile, config)
if (res.status == 200) {
console.log(res);
console.log(`SUCCESS! HTTP Status: ${res.status}`)
this.result_sagemaker_endpoint = res.data;
}
} catch (error) {
console.log(error);
let error_res = error.response;
console.log(`ERROR! HTTP Status: ${error_res.status}`);
console.log(`ERROR! message: ${error_res.data}`);
this.result_sagemaker_endpoint = error_res.data;
}
}
}
}
</script>
<style>
</style>
ビルド、デプロイ
ビルドして、S3にデプロイしましょう。
前回同様プロジェクト直下で、以下コマンドを実行します。
npm run build
distフォルダ以下をS3に配置してください。
CloudFrontのキャッシュが残ってる場合があるので、CloudFrontのキャッシュもクリアしておきましょう。

ブラウザからリクエスト、レスポンスの確認
さあ、とうとうここまできましたね!
CloudFrontのディストリビューションドメインに、再度アクセスしてみましょう!!!
シークレットウィンドウを開きなおしたので、cookieなどはクリアされています。
認証情報が保存されていないので、再度認証画面にリダイレクトしました。

ユーザー名とパスワードを入力して、アクセス成功できました!
CookieStorageからusernameを取得して画面に表示できていますね!
画像の推論もやってみましょう。

推論させたい画像ファイルを選択して、アップロードします。

推論に成功しました!Cognito認証情報も画面上で保持できていますね!

おわりに
お疲れ様でした!Cognitoを用いたユーザー認証の仕組みを簡単に導入できましたね!
今回はインフラ寄りのアーキテクチャを追加しましたが、フロント寄りのCognito実装や、認証で使用したHostedUIのカスタマイズや自前実装、IDプールを使用したAWSリソースの制限など、まだまだできることは多そうですね!
私の記事が少しでも皆様のご参考になれば幸いです!