Cognito の Managed Login を ALB と連携する

# はじめに

Amazon Cognito で Managed Login 機能を 2024 年 11 月に提供を開始しました。今までの Hosted UI が進化してリッチな見た目になり、日本語対応が可能で、MFA やパスキーの登録や認証機能が付属されるようになり、アプリケーション側の実装負担を削減できる、といったうれしさがあります。

Hosted UI のログイン画面

![image-20241222125148384.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/18232827-33ef-23dc-787a-4fc37ef644e2.png)



Managed Login のログイン画面

![image-20241222125155590.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/913fd82d-93c6-74ff-d5c1-43917df51325.png)



今回は、ALB と Cognito の Managed Login を連携する設定方法を紹介します。

# 構成図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/0c270c3b-e75d-cffc-0516-ad620e272e89.png)


# 事前設定

テーマとは外れるので、以下の内容は記事の中では触れません。

- EC2 インスタンスで Nginx と Flask を動かす
- ALB と EC2 を紐づける
- ACM と連携して HTTPS を構成



# Cognito User Pool の作成

Cognito User Pool を作成します。

![image-20241221202333562.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/6f64b559-5050-7699-4909-a8357f57f576.png)



名前などは適当に指定します。

![image-20241221204759228.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/6441ab1e-c166-c6f4-faf0-1448cc467c09.png)









# Callback URL の指定

作成した Cognito Userpool で、Callback URL を指定します。これは、Cognito がログイン処理を行ったあとに表示する Web サイトの URL です。ALB に指定しているドメインを指定します。

Cognito Userpool の画面の中で、作成した App Client を開きます。

![image-20241221205103792.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/50b1c98f-61ce-0a56-621e-0408d91de462.png)



Login pages のタブから Edit を押します。

![image-20241221210327951.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/48e025c2-f9f6-8267-a817-fac81463b16c.png)



エンドユーザーに提供する Web サイトの URL である、ALB のドメイン名を指定します。ドメインの末尾に `/oauth2/idpresponse` を付与します。これは、ALB 側と Cognito 連携する際に指定する、固定されているパスです。

> https://cognito-nginx01.sugiaws.tokyo/oauth2/idpresponse

![image-20241221211649415.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/04006db2-8d2d-5094-6993-2d29d4c96ada.png)



# Custom Domain の指定

任意の設定項目になりますが、Cognito のログイン URL を独自のドメインで指定できます。組織内で Managed Login を利用する場合には、独自のドメインを利用したいことが多いと思います。

Domain のメニューから、Create custom domain を選択します。

![image-20241221212411509.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/564ffe41-b713-7fd2-9825-88912949dbd6.png)



エンドユーザーに提供したい Web サイトの URL が、以下の URL だとします。

- https://cognito-nginx01.sugiaws.tokyo



今回の手順では、Cognito の Custom Domain は、`auth` サブドメインを追加するものとします。

- https://auth.cognito-nginx01.sugiaws.tokyo



なので、以下のパラメーターで指定をします。ACM の証明書設定が別途設定しておく必要があります。

![image-20241221215756227.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/343c8d28-d44d-7b10-11c2-60a1f66ce62b.png)



Create ボタンを押すと、以下の画面に変わります。CNAME レコードを作成するように、という指示があります。

![image-20241221215942412.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/764ec317-d52b-24df-c271-1b60a61b316d.png)



以下のように auth サブドメインの大して、 CNAME レコードを作成します。以下の画像は、Route 53 の画面ですが、独自の DNS サーバーに合わせて変更ください。

![image-20241221220246904.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/030ff816-e11f-f967-e0ab-9edecf222a7f.png)



# ALB と Cognito の連携設定

ALB と Cognito を連携する設定を入れていきます。Listener のルールを指定します。

![image-20241221220752311.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/2b6954a6-9e0a-c5cf-6946-9ca466b3bcd9.png)



Authentication から、Cognito の連携設定を入れていきます。

![image-20241221221313605.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/f1026896-a6a8-96c7-7668-c38ab7a2d13a.png)



# 動作確認

動作確認をしていきます。以下の URL にアクセスします。

https://cognito-nginx01.sugiaws.tokyo/

![image-20241221221924297.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/b9c8c719-732d-6c9c-e965-3c69c4c30768.png)



Cognito の Managed Login の画面が自動的に開かれます。**Create an account** を選択します。 

![image-20241221221942300.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/72aef6f3-7970-8e19-563a-73ba4727f542.png)



自分自身のメールアドレスやパスワードなどを指定します。

![image-20241221222042008.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/72fa4b6f-61f0-2b03-eb99-86e7540055fe.png)



指定したメールアドレスに Code が届きます。

![image-20241221222110277.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/d73cb1db-0146-c445-28e2-98a8ed8e92c6.png)



Code を入力します。

![image-20241221222123939.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/71280ea4-e860-4884-de9c-16e55d73d51f.png)





ログインされました。Nginx が稼働しているサーバーが受け取る、Request Header を表示させています。

![image-20241222225918125.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/a07521cf-9855-acd3-55b4-7af100b2e708.png)



# EC2 インスタンスが受け取る JWT について

ALB と Cognito を連携しているとき、ALB から  EC2 インスタンスへ送信される HTTP Request の Request Header の `X-Amzn-Oidc-Data`  に JWT (JSON Web Token) が付与されている。これを利用して正常な JWT の検証をしながら、ユーザーのメールアドレスなどを確認可能

![image-20241223140138877.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/65ffda6a-7398-d5bb-78e7-15360f8bcb90.png)







以下が[こちらのサイトをつかって](https://jwt.io/) JWT を decode したときの例。アクセスしたユーザーのメールアドレスを確認ができるので、アプリケーション側で認可処理を実装可能。

![image-20241223135650852.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/91106e3e-3299-7982-2b18-f74326919610.png)





# 付録 : Cookie

ALB と Cognito を連携したあと、ユーザーが Managed Login を開いて、ログイン成功した場合、自動的にブラウザ側に Cookie が保存される。この Cookie を削除すると、ログアウトされている状態として扱われる。

![image-20241221222404589.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/22289b25-76bf-d5b1-881c-87ed855a2768.png)





# 付録 : Nginx と Flask の導入方法

以下に簡単にメモをする。

Nginx をインストール

```
sudo dnf install nginx
```



起動

```
sudo systemctl restart nginx
sudo systemctl enable nginx
```



必要なパッケージのインストール

```
sudo dnf install python3-pip
pip3 install flask gunicorn
```



アプリケーションディレクトリの作成

```
mkdir ~/flaskapp
cd ~/flaskapp
```



アプリケーションコードの作成

```
cat << EOF > app.py
import json
import base64
from flask import Flask, request, redirect, make_response
from datetime import datetime

app = Flask(__name__)

COGNITO_DOMAIN = "https://auth.cognito-nginx01.sugiaws.tokyo"
CLIENT_ID = "5ehctf8ikmlvo1grqk7id5v4a2"
LOGOUT_URI = "https://cognito-nginx01.sugiaws.tokyo"

def decode_jwt(token):
    # JWTのペイロード部分（2番目の部分）を取得
    payload = token.split('.')[1]
    # Base64デコード（パディングの調整が必要な場合がある）
    payload += '=' * ((4 - len(payload) % 4) % 4)
    decoded = base64.b64decode(payload)
    # JSONとしてパース
    return json.loads(decoded)

@app.route('/logout')
def logout():
    logout_url = f"{COGNITO_DOMAIN}/logout?client_id={CLIENT_ID}&logout_uri={LOGOUT_URI}"
    response = make_response(redirect(logout_url))
    
    # Delete ALB auth cookies by setting them to empty and expired
    response.set_cookie('AWSELBAuthSessionCookie-0', '', expires=0, domain=None, path='/', httponly=True)
    response.set_cookie('AWSELBAuthSessionCookie-1', '', expires=0, domain=None, path='/', httponly=True)

    return response

@app.route('/')
def home():
    headers = dict(request.headers)
    headers.pop('X-Forwarded-For', None)
    
    formatted_headers = '\n'.join([f"{key}: {value}" for key, value in headers.items()])
    
    # JWTからメールアドレスを抽出
    jwt_data = headers.get('X-Amzn-Oidc-Data')
    email = "Not found"
    if jwt_data:
        try:
            decoded = decode_jwt(jwt_data)
            email = decoded.get('email', 'Email not found in JWT')
        except Exception as e:
            email = f"Error decoding JWT: {str(e)}"

    return f"""
        <h1>Request Headers</h1>
        <pre>{formatted_headers}</pre>
        <h2>Email from JWT</h2>
        <p>{email}</p>
        <a href="/logout">Logout</a>
    """

if __name__ == '__main__':
    app.run()

EOF
```



Gunicorn サービスファイルの作成

```bash
sudo tee /etc/systemd/system/flaskapp.service << EOF
[Unit]
Description=Gunicorn instance to serve flask application
After=network.target

[Service]
User=ec2-user
Group=nginx
WorkingDirectory=/home/ec2-user/flaskapp
Environment="PATH=/home/ec2-user/.local/bin"
ExecStart=/home/ec2-user/.local/bin/gunicorn --workers 3 --bind unix:flaskapp.sock -m 777 app:app

[Install]
WantedBy=multi-user.target
EOF
```



Nginx の設定ファイル作成

```
sudo tee /etc/nginx/conf.d/flaskapp.conf << EOF
server {
    listen 80;
    server_name _;

    location / {
        proxy_pass http://unix:/home/ec2-user/flaskapp/flaskapp.sock;
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;
    }
}
EOF
```



ec2-user を nginx グループに追加

```bash
sudo usermod -a -G nginx ec2-user
```



flaskapp ディレクトリに nginx ユーザーがアクセスできるようにする

```bash
sudo chmod 755 /home/ec2-user
sudo chmod 755 /home/ec2-user/flaskapp
```



Gunicorn サービスの起動

```bash
sudo systemctl daemon-reload
sudo systemctl restart flaskapp
sudo systemctl enable flaskapp
```



status

```
$ sudo systemctl status flaskapp
● flaskapp.service - Gunicorn instance to serve flask application
     Loaded: loaded (/etc/systemd/system/flaskapp.service; disabled; preset: disabled)
     Active: active (running) since Sun 2024-12-22 00:28:08 UTC; 4s ago
   Main PID: 62726 (gunicorn)
      Tasks: 4 (limit: 9346)
     Memory: 52.4M
        CPU: 495ms
     CGroup: /system.slice/flaskapp.service
             ├─62726 /usr/bin/python3 /home/ec2-user/.local/bin/gunicorn --workers 3 --bind unix:flaskapp.sock -m 007 app:app
             ├─62728 /usr/bin/python3 /home/ec2-user/.local/bin/gunicorn --workers 3 --bind unix:flaskapp.sock -m 007 app:app
             ├─62729 /usr/bin/python3 /home/ec2-user/.local/bin/gunicorn --workers 3 --bind unix:flaskapp.sock -m 007 app:app
             └─62730 /usr/bin/python3 /home/ec2-user/.local/bin/gunicorn --workers 3 --bind unix:flaskapp.sock -m 007 app:app

Dec 22 00:28:08 ip-10-0-1-135.ap-northeast-1.compute.internal systemd[1]: Started flaskapp.service - Gunicorn instance to serve flask application.
Dec 22 00:28:08 ip-10-0-1-135.ap-northeast-1.compute.internal gunicorn[62726]: [2024-12-22 00:28:08 +0000] [62726] [INFO] Starting gunicorn 23.0.0
Dec 22 00:28:08 ip-10-0-1-135.ap-northeast-1.compute.internal gunicorn[62726]: [2024-12-22 00:28:08 +0000] [62726] [INFO] Listening at: unix:flaskapp.sock (62726)
Dec 22 00:28:08 ip-10-0-1-135.ap-northeast-1.compute.internal gunicorn[62726]: [2024-12-22 00:28:08 +0000] [62726] [INFO] Using worker: sync
Dec 22 00:28:08 ip-10-0-1-135.ap-northeast-1.compute.internal gunicorn[62728]: [2024-12-22 00:28:08 +0000] [62728] [INFO] Booting worker with pid: 62728
Dec 22 00:28:08 ip-10-0-1-135.ap-northeast-1.compute.internal gunicorn[62729]: [2024-12-22 00:28:08 +0000] [62729] [INFO] Booting worker with pid: 62729
Dec 22 00:28:08 ip-10-0-1-135.ap-northeast-1.compute.internal gunicorn[62730]: [2024-12-22 00:28:08 +0000] [62730] [INFO] Booting worker with pid: 62730
```



Nginx の再起動

```
sudo systemctl restart nginx
```

# 付録 : 日本語の表示について
https://qiita.com/sugimount-a/items/f044a523cfca057ee146

# 付録 : セルフサインアップの停止

Managed Login は、デフォルトでユーザー側のセルフアインアップ機能が提供されているので、これを停止することが可能。

![image-20241222124601110.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/6647e8a2-9790-1c89-e9e6-93bbf7878665.png)



![image-20241222124616344.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/f86161ed-0824-499c-be7f-13c56ca317ad.png)



無くなっている

![image-20241222124647108.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/a6260d98-704b-9372-4c26-1f65520c3f85.png)





# 検証を通じてわかったこと

- ALB と Cognito を連携して Managed Login を利用したログイン画面を提供可能
- Managed Login で認証成功したとき、ブラウザ側に Cookie が保存される。この Cookie の名前は、デフォルトで `AWSELBAuthSessionCookie` となっているが、任意の名前を指定可能