VPC Endpoint サポートされてないけど閉域から Cognito で認証したい!

記事タイトルとURLをコピーする

はじめに

こんにちは。アプリケーションサービス部の保田(ほだ)です。

最近オートミールを主食として食べるようになったのですが、和洋中どんな料理にも派生できて保存も楽、調理も数分とダイエット云々とか関係なしに便利です。

というわけで今回は VPC Endpoint がサポートされていない Amazon Cognito を何とかして閉域で利用することを考えます。

要約

プライベート API Gateway をプロキシとして AWS サービスを呼び出すことにより、VPC 内から閉域で Cognito の API を叩く

ザックリですが、こういう構成になります。

f:id:swx-kazuma-hoda:20211018095758p:plain
EC2 から Cognito にたどり着きたい

プライベートサブネットの EC2 から(API Gateway の)VPCエンドポイントを経由してプライベート API から Cognito へ至ります。

手順

ベースのアイデアは以下のドキュメントです。

0. 事前準備

以下のリソースは作成済みであるとして説明します。

  • API Gateway の VPC エンドポイント
  • Cognito ユーザープール
  • 上記ユーザープールに属するユーザー

1. API にアタッチするロールを作成する

これから作成する API Gateway には Cognito の API を実行してもらわないといけないので、そのための権限を付与した IAM ロールを作成します。

  • マネジメントコンソール上で [IAM] を開き、 [ロール] → [ロールの作成] を選択して IAM ロールの作成画面を開きます。
  • ユースケースの選択は [API Gateway] を選択し [次のステップ: アクセス権限] を押下します。
  • アタッチできるポリシーとしては AmazonAPIGatewayPushToCloudWatchLogs しか表示されないので、一旦はそのまま [次のステップ: タグ] へ進みます。
  • タグは必要に応じて付与し、最後にロールの名前を apigateway-cognito として [ロールの作成] を押下します。
  • ロールが作成されたら、改めて AWS 管理ポリシー AmazonCognitoPowerUser をアタッチして Cognito への操作権限を渡します。

f:id:swx-kazuma-hoda:20211016172351p:plain
作ったロール

2. プライベート API Gateway をつくる

  • マネジメントコンソール上で [API Gateway] を開きます。

f:id:swx-kazuma-hoda:20211016172526p:plain
実は後からでも変えられる

  • [API を作成] を選択し、 API タイプとして [REST API プライベート] を選択します。
  • 名前は適当に cognito-proxy とします。
  • 作成画面で VPC エンドポイントを指定する欄がありますが、ここはスルーで良いです。
  • リソースポリシーを設定します。こちらのドキュメント を参考に、次のようなポリシーを設定することになります。
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:ap-northeast-1:012345678901:xxxxxxxxxx/*" // xxxxxxxxxx は作成した API のID
        },
        {
            "Effect": "Deny",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:ap-northeast-1:012345678901:xxxxxxxxxx/*", // xxxxxxxxxx は作成した API のID
            "Condition": {
                "StringNotEquals": {
                    "aws:SourceVpce": "vpce-xxxxxxxxxx" // この VPC エンドポイントを介さない接続は拒否する
                }
            }
        }
    ]
}

3. ログイン用の API を作成する

  • [リソース] → [リソースの作成] からリソース(URLにおけるパス)の作成画面へ遷移します。
  • リソース名は admin-initiate-auth とします。
  • 作成したら、そのリソースに対してメソッドを追加します。 POST メソッドを選択します。
  • 統合リクエストの中身はそれぞれ以下のように設定します。表中に書いてない項目はすべてデフォルトで OK です。

f:id:swx-kazuma-hoda:20211016172807p:plain

項目 設定値 補足
統合タイプ AWS サービス
AWS リージョン ご利用のリージョン 本記事では ap-northeast-1 としています。念のため。
AWS サービス Cognito IDP Cognito xxx と名のついたものがいくつかあるので注意
HTTP メソッド POST
アクション AdminInitiateAuth AdminInitiateAuth
実行ロール 3 で作ったロール

4. API のデプロイ

適当なステージにデプロイしてください。ここでは dev とします。

f:id:swx-kazuma-hoda:20211016172725p:plain
デプロイ!

動作確認

適切な VPC 内からこの API を叩いてみます。

裏で実行する Cognito の API は AdminInitiateAuth ですので、この Request シンタックスに従えば良いということになります。

curl コマンドを使った場合はこうなります。(※戻り値は適当に整形しています。

$ curl -X POST -v https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/admin-initiate-auth \
    -d '{"AuthParameters": {"USERNAME": "test-user","PASSWORD": "P@ssword01"},"ClientId": "22k5ngbgh0pl2vhuxxxxxxxxxx","UserPoolId": "ap-northeast-1_xxxxxx","AuthFlow": "ADMIN_NO_SRP_AUTH"}'

{
    "AuthenticationResult": {
        "AccessToken": "eyJraWQiOiJxTT(長いので中略)N67-75aKg3M86OGyrA",
        "ExpiresIn": 3600,
        "IdToken": "eyJraWQiOiJcL05aZ(長いので中略)eFfXymllilVSQvKrg",
        "RefreshToken": "eyJjdHkiOiJKV1Qi(長いので中略)MYj1vEvhhUnl0Iy2rWw",
        "TokenType": 'Bearer'
    },
    "ChallengeParameters": {}
}

トークンが返ってきますね!

もう少し見やすくしたいので requests を使った例も記載します。

import requests
 
url = 'https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/admin-initiate-auth'
 
data={
    "AuthParameters": {
        "USERNAME": "test-user",
        "PASSWORD": "P@ssword01"
    },
    "ClientId": "22k5ngbgh0pl2vhuxxxxxxxxxx",
    "UserPoolId": "ap-northeast-1_xxxxxx",
    "AuthFlow": "ADMIN_NO_SRP_AUTH"
}
 
r = requests.post(url, json=data)
 
res = r.json()
 
print(res)

実行した際の戻り値としては先ほどと同じです。

{
    'AuthenticationResult': {
        'AccessToken': 'eyJraWQiOiJxTT(長いので中略)N67-75aKg3M86OGyrA',
        'ExpiresIn': 3600,
        'IdToken': 'eyJraWQiOiJcL05aZ(長いので中略)eFfXymllilVSQvKrg',
        'RefreshToken': 'eyJjdHkiOiJKV1Qi(長いので中略)MYj1vEvhhUnl0Iy2rWw',
        'TokenType': 'Bearer'
    },
    'ChallengeParameters': {}
}

これで閉域網からでも Cognito ユーザープールのユーザーでログインできますね。

ちなみに、サラッと書いた r = requests.post(url, json=data) ですが、次と等価です。

import requests
import json
 
r = requests.post(
    url,
    data=json.dumps(data)
)

便利ですね。

さいごに

プライベート API に対しても Cognito オーソライザー は使えますので、上記のようにログインさえしてしまえば後は良い感じにプライベート API を認証ありで使うことが出来そうです。