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

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リソースの制限など、まだまだできることは多そうですね!

私の記事が少しでも皆様のご参考になれば幸いです!

この記事をシェアする
著者:nakao
IoT、サーバーレスな開発に興味深々。AWSエンジニア。