Serverless Architectures on AWS ~Cognito + API Gateway①~

こんにちは。katoです。

久しぶりのブログ投稿になります。

 

AWSサービスを利用したサーバレスアーキテクチャとして、翻訳機能付きのコミュニケーションサイトを構築していきたいと思います。

 

概要

 

今回構築するシステムは以下のようになります。

 

Cognitoを利用したログイン機能付きのサーバレスサイトとなります。

 

コミュニケーションサイトということで、API Gatewayで投稿機能を設け、Amazon Translateを利用した翻訳機能を実装します。

また、言語学習等の用途でも利用可能なように、Amazon Pollyを利用したテキストの読み上げ機能も併せて実装していきます。

 

未完成(未実装)の部分も御座いますが、最終的には以下のようなサイトが出来上がります。

 

 

①ログイン機能の実装(Cognito関連)

②サイト機能の実装(APIGateway&Lambda関連)

 

の2回に分けて説明していきたいと思います。

 

今回は、Cognitoを利用したログイン機能実装をご説明していきたいと思います。

Cognitoユーザの発行やアクティベーションといった内容を中心に、サイトTOPページの表示まで進めていきます。

 

手順

 

Cognito

 

まずは、Cognitoの設定を行いたいと思います。

Cognitoでは、ユーザプールの作成とIDプールの作成を行います。

 

AWSマネジメントコンソールのCognitoサービスページから、ユーザプールの作成を行います。

プール名を適宜入力したら、「ステップに従って設定する」を選択します。

 

サインイン(ログイン)に必要となる認証情報および属性を選択します。

この設定は後から変更できないので、ご注意ください。

 

ここでは、「Eメールアドレスおよび電話番号」での認証とし、Eメールアドレスを利用した認証を採用する形にします(ログインにEメールアドレスを利用する形)。

 

また、今回は属性として「name」を選択します。

ここで選択した属性は、get_user等のAPI実行時にUserAttributesとして値の参照が可能となります。

今回は、投稿サイトということで、アカウント名としてこの属性を利用しております。

 

 

次にサイドメニューの「アプリクライアント」を選択し、「アプリクライアントの追加」をクリックします。

今回は以下の2点を設定します。

 

・クライアントシークレットを作成:無効

・ユーザー名パスワードベースの認証を有効にする (ALLOW_USER_PASSWORD_AUTH):有効

 

 

「アプリクライアントの作成」をクリックし、作成されたアプリクライアントIDを控えておきます。

 

その他の設定は環境に合わせて適宜設定してください。

 

すべての設定が完了したら、サイドメニューの「確認」を選択し、「プールの作成」をクリックします。

 

ユーザプールの作成が完了したら、「プールID」と「プールARN」を控え、IDプールの作成に移ります。

Cognitoサービスページの上部に表示されている「フェデレーティッドアイデンティティ」を選択し、「新しいIDプールの作成」をクリックします。

 

ここでは以下の設定でプールの作成を行います。

 

・認証されていない ID に対してアクセスを有効にする:有効

・認証プロバイダー:Cognito

・ユーザプールID:先ほど作成したCognitoユーザプールIDを指定

・アプリクライアントID:先ほど作成したCognitoアプリクライアントIDを指定

 

IDプールの作成が完了すると、サンプルコードと合わせて、IDプールのIDが表示されますので、これも控えておきます。

 

 

以上でCognitoの設定は完了となります。

 

Lambda

 

続いて、Lambda関数の作成を行います。

今回、Cognitoのログイン処理で利用するLambda関数は以下の3つとなります。

 

・サインアップ(アカウント作成)用

アクティベーション

・ログイン用

 

なお、ログイン用のLambda関数は、サイト機能の実装(投稿取得等)が含まれますので、今回はログイン処理のみ行う、サンプルのLambdaを記載させていただきます。

 



サインアップ(アカウント作成)用
import json
import boto3
import os

cognito = boto3.client('cognito-idp')

class ExtendException(Exception):
    def __init__(self, status, message):
        self.status = status
        self.message = message

    def __str__(self):
        response = {
            "status": self.status,
            "message": self.message
        }
        return json.dumps(response)

def lambda_handler(event, context):
    print (event)
    jsondata = {}
    data = event['body']
    datas = data.split("&")
    for num in range(len(datas)):
        param = datas[num].split('=')
        jsondata[param[0]] = param[1]
    print(jsondata)
    
    try:
        check_name = "name=\""+ jsondata['name'] + "\""
        check_param = cognito.list_users(
            UserPoolId=os.environ['PoolId'],
            Filter=check_name
        )
        if len(check_param['Users']) != 0:
            print (check_param)
            raise Exception("Unauthorized (UserName already used.)")
        
        sign_up = cognito.sign_up(
            ClientId=os.environ['ClientId'],
            Username=jsondata['email'],
            Password=jsondata['password'],
            UserAttributes=[
                {
                    'Name': 'name',
                    'Value': jsondata['name']
                }
            ]
        )
        url = os.environ['url'] + "/activation/index.html"
        return {'location': url}
    except Exception as e:
        print (e)
        raise ExtendException(401, "Unauthorized")

サインアップ用のLambdaに関しましては、formの入力内容に基づき、sign_upのAPIを実行している単純なものとなります。

1点、今回のシステム向けに調整している点として、list_usersを利用し、入力されたname値(アカウント名)を最初にチェックしております。

機能実装の項で詳細は説明しますが、今回、投稿内容や翻訳コンテンツをS3バケットに保存しますが、この際、アカウント名でprefixを切っておりますので、システムでアカウント名が一意になるように、アカウント名のチェックを行っております。

既に存在するアカウント名が指定された場合、errorレスポンスを返すような形を採用しております。

Cognitoではサインアップ後に、アクティベーションが必要となりますので、API Gatewayへのreturnとして、アクティベーションページのURLを返しています。



アクティベーション

import json
import boto3
import os

cognito = boto3.client('cognito-idp')

class ExtendException(Exception):
    def __init__(self, status, message):
        self.status = status
        self.message = message

    def __str__(self):
        response = {
            "status": self.status,
            "message": self.message
        }
        return json.dumps(response)

def lambda_handler(event, context):
    print (event)
    jsondata = {}
    data = event['body']
    datas = data.split("&")
    for num in range(len(datas)):
        param = datas[num].split('=')
        jsondata[param[0]] = param[1]
    print(jsondata)
    
    try:
        activate = cognito.confirm_sign_up(
            ClientId=os.environ['ClientId'],
            Username=jsondata['email'],
            ConfirmationCode=jsondata['key']
        )

        return {'location': os.environ['url']}
    except Exception as e:
        print (e)
        raise ExtendException(401, "Unauthorized")

アクティベーション用のLambdaは、formの入力内容に基づき、confirm_sign_upでアカウントのアクティベーション(認証)を行います。

アクティベーションが成功すると、ログインページにリダイレクトするように、returnでログインページのURLを返しております。



ログイン用

import json
import boto3
import os

cognito = boto3.client('cognito-idp')

class ExtendException(Exception):
    def __init__(self, status, message):
        self.status = status
        self.message = message

    def __str__(self):
        response = {
            "status": self.status,
            "message": self.message
        }
        return json.dumps(response)

def lambda_handler(event, context):
    jsondata = {}
    data = event['body']
    datas = data.split("&")
    for num in range(len(datas)):
        param = datas[num].split('=')
        jsondata[param[0]] = param[1]

    try:
        auth = cognito.initiate_auth(
            AuthFlow='USER_PASSWORD_AUTH',
            ClientId=os.environ['ClientId'],
            AuthParameters={
                'USERNAME': jsondata['email'],
                'PASSWORD': jsondata['password']
            }
        )
        user = cognito.get_user(
            AccessToken=auth['AuthenticationResult']['AccessToken']
        )
        username = ""
        for user_param in user['UserAttributes']:
            if user_param['Name'] == 'name':
                username = user_param['Value']
        if username == "":
            username = 'unknown'
        return {
            'body': json.dumps({
                'user': username,
                'AccessToken': auth['AuthenticationResult']['AccessToken']
            })
        }

    except Exception as e:
        print (e)
        raise ExtendException(401, "Unauthorized")

ログイン用のLambdaは、initiate_authを利用し、アクセストークンを取得した後、取得したアクセストークンでget_userを実施し、formに入力された認証情報の正当性をチェックしております。

認証情報が間違っている場合や、アクセストークンが無効の場合、APIがerrorを返すので、ログインに失敗するような形となります。

本システムでは、ログイン以降のページは、S3等でページを用意せず、動的にページ(HTML)生成しますので、API Gatewayに対して、ページ生成にあたって必要となる情報をreturnで返します。

本記事では、ログインの確認までを実施するので、returnとしてアカウント名を返して、API Gatewayでログインユーザのアカウント名を動的に表示するような仕組みを組んでいきます。

各Lambda関数に関しましては、環境変数でアプリクライアントID等を指定していますので、適宜設定を行います。

また、後述のAPI Gatewayの統合レスポンスでエラーレスポンスの設定を行いますので、エラー時のStatusCode等も適宜設定します。



API Gateway

Lambdaの作成が完了したら、API Gatewayを設定していきます。

ログインまでの処理で必要になるのは、以下の3つのリソースとなります。

・/signup

・/activate

・/login

今回のシステムでは、formの入力内容に基づき、処理を行いますので、各リソースにPOSTメソッドを定義していきます。


signup

サインアップ用のLambdaを指定し、POSTメソッドを作成したら、「統合リクエスト」からマッピングテンプレートの設定を行います。

「application/x-www-form-urlencoded」のContent-Typeでマッピングテンプレートを追加し、テンプレート本文に以下指定します。


{
  "body": $util.urlDecode($input.json("$"))
}

これは、formの入力値をLambdaに連携する設定となります。

上記の設定に基づき、「event['body']」で入力値の参照がLambdaで可能になります。

次に「メソッドレスポンス」から以下の2つのステータスコードを設定します。

ステータスコード:302

レスポンスヘッダー:Location

ステータスコード:401

レスポンスヘッダー:なし

次に「統合レスポンス」から、各ステータスコードに応じた処理を設定します。

メソッドレスポンスのステータス:302

Lambdaのエラーの正規表現:指定なし

コンテンツの処理:パススルー

レスポンスヘッダー:Location

マッピングの値:integration.response.body.location

メソッドレスポンスのステータス:401

Lambdaのエラーの正規表現:.*"status": 401.*

コンテンツの処理:パススルー

Content-Type:application/json

マッピングテンプレート:$input.path('$.errorMessage')

レスポンスの設定は、Lambdaの返り値に応じて、リダイレクトまたは、エラーメッセージの表示を行うための設定となります。

activate

アクティベートに関しては、signupと同じ内容で、「統合レスポンス」、「メソッドレスポンス」、「統合レスポンス」の設定を行います。

処理としては、signupと同じで、form入力値のLambda連携、returnに基づくレスポンス設定となります。

login

loginも「統合リクエスト」の設定は他の2つと同じものになります。

メソッドレスポンスは、200と401を設定します。

401の設定は他2つと同様になりますが、2ooのメソッドレスポンスは以下の内容で設定します。

ステータスコード:200

ヘッダー:なし

コンテンツタイプ:text/html

モデル:Empty

「統合レスポンス」も同様に200のステータスコードようの設定を追加します。

メソッドレスポンスのステータス:200

Lambdaのエラーの正規表現:指定なし

コンテンツの処理:パススルー

Content-Type:application/json

マッピングの値:

#set($params = $util.parseJson($input.path('$.body')))
<!DOCTYPE html>
<html lang=&quot;en&quot;>
  <head>
    <meta charset=&quot;UTF-8&quot; />
    <title>TOP</title>
  </head>
  <body>
    <div id=&quot;TOP&quot;>
        <h1>Community TOP</h1>
        <p>Welcome $params.user</p>
    </div>
  </body>
</html>

loginはreturnに応じて、動的にHTMLを生成し、表示する必要があるので、上記のような設定が必要となります。

以上でAPI Gatewayの設定は完了となります。APIのデプロイを行い、エンドポイントURLを控えておいてください。

S3 & CloudFront

ここまで設定できたら、後はS3にhtmlファイルを配置し、CloudFront経由で公開するのみとなります。

S3やCloudFrontは特殊な設定は行っておらず、一般的なものとなりますので、省略させていただき、S3に配置するHTMLファイルのみ記載させていただきます。

signup.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset=&quot;UTF-8&quot;>
    <title>Sign Up</title>
  </head>
  <body>
    <div id=&quot;signup&quot;>
      <h1>Sign Up</h1>
      <form method='post' action='https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/login/signup'>
        <span style=&quot;display: inline-block; width: 150px;&quot;>User ID (Email)</span>
        <input type=&quot;email&quot; name=&quot;email&quot; required>
        <br/>
        <span style=&quot;display: inline-block; width: 150px;&quot;>User Name</span>
        <input type=&quot;text&quot; name=&quot;name&quot; required pattern=&quot;^[a-zA-Z0-9!#?%_@-]+&quot;  title=&quot;only use a-z, A-Z, 0-9, !#?%_@-&quot;>
        <br/>
        <span style=&quot;display: inline-block; width: 150px;&quot;>Password</span>
        <input type=&quot;password&quot; name=&quot;password&quot; required pattern=&quot;^[a-zA-Z0-9!#?%_@]+&quot; minlength=&quot;8&quot; title=&quot;min length = 8, only use a-z, A-Z, 0-9, !#?%_@&quot;>
        <br/><br/>
        <input type=&quot;submit&quot; value=&quot;Create Account&quot;>
      </form>
    </div>
    <div id=&quot;Login&quot;>
      </br><input type=&quot;button&quot; onclick=&quot;location.href='https://xxxxxxxxxx.cloudfront.net/'&quot;value=&quot;Back to Login Page&quot;>
    </div>
  </body>
</html>

activation/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset=&quot;UTF-8&quot;>
    <title>Activation</title>
  </head>
  <body>
    <div id=&quot;Activation&quot;>
      <h1>Activation</h1>
      <form method='post' action='https://xxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/login/activate'>
        <span style=&quot;display: inline-block; width: 300px;&quot;>User ID(Email)</span>
        <input type=&quot;text&quot; name=&quot;email&quot;>
        <br/>
        <span style=&quot;display: inline-block; width: 300px;&quot;>Activation Key (verification code)</span>
        <input type=&quot;password&quot; name=&quot;key&quot;>
        <br/><br/>
        <input type=&quot;submit&quot; value=&quot;Activation&quot;>
      </form>
    </div>
  </body>
</html>

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset=&quot;UTF-8&quot;>
    <title>Login</title>
  </head>
  <body>
    <div id=&quot;Login&quot;>
      <h1>Login</h1>
      <form method='post' action='https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/login/blog'>
        <span style=&quot;display: inline-block; width: 150px;&quot;>User ID(Email)</span>
        <input type=&quot;email&quot; name=&quot;email&quot; required>
        <br/>
        <span style=&quot;display: inline-block; width: 150px;&quot;>Password</span>
        <input type=&quot;password&quot; name=&quot;password&quot; required pattern=&quot;^[a-zA-Z0-9!#?%_@]+&quot; minlength=&quot;8&quot; title=&quot;min length = 8, only use a-z, A-Z, 0-9, !#?%_@&quot;>
        <br/>
        <input type=&quot;submit&quot; value=&quot;Login&quot;>
      </form>
    </div>
    <div id=&quot;signup&quot;>
      <h1>SignUp</h1>
      <input type=&quot;button&quot; onclick=&quot;location.href='https://xxxxxxxxxx.cloudfront.net/signup.html'&quot;value=&quot;SignUp&quot;>
    </div>
  </body>
</html>

長くなりましたが、以上で設定は完了となります。

動作確認

それでは動作の確認を行っていきます。

CloudFrontのエンドポイントにアクセスすると、ログインページが表示されます。

「SignUp」をクリックし、アカウント発行ページに移動します。

認証情報を入力し、「CreateAccount」をクリックします。

指定したメールアドレスに検証コードが届きますので、遷移先のアクティベーションページでコードを入力し、「Activation」をクリックします。

ログインページで発行したアカウントの認証情報を入力し、「Login」をクリックします。

サイトのTOPページとして、アカウント名が表示されていれば正常な動作となります。

なお、この際、URLがCloudFrontのエンドポイントからAPI Gatewayのエンドポイントに変更されていることが確認できるかと思います。

今回のシステムでは、ログイン以降のページはAPI Gatewayによって動的に生成されますので、ドメインやURLの扱いには注意が必要となります(ログイン後のURLに直接アクセスしてもエラーとなります)。

まとめ

CognitoとAPI Gatewayを利用したサーバレスのログインサイト構築に関して、ご紹介いたしました。

Cognitoを利用することで、簡単にサーバレスで会員向けサイトなどの仕組みを構築することが可能となります。

次回は、より複雑なサイト機能の実装として、Amazon TranslateやPollyの組み込み、API Gatewayマッピングテンプレートなどに関して、ご説明していきたいと思います。

xp-cloud.jp

このブログの著者