追記: 2022/07/06(水)「技術的には可能だが、実装コストに見合うかは不明」というレベル感である旨を明記いたしました。
はじめに
こんにちは。アプリケーションサービス部の保田(ほだ)です。
というわけで今回は VPC Endpoint がサポートされていない Amazon Cognito を 力業 で閉域で利用することを考えます。
力業と申した通り、本文のやり方は 「技術的には可能だけど、閉域網であればオンプレミスの ID プロバイダーで認証した方が楽だよね」 といった類のものになります。
※注意※
※ 本記事は、あくまで AdminInitiateAuth API のみをプロキシする仕組みを用意しているだけであり、運用上はすべてのエンドポイントについて作り込みが必要です。
- 認証・認可エンドポイント
- API 一覧
要約
プライベート API Gateway をプロキシとして AWS サービスを呼び出すことにより、VPC 内から閉域で Cognito の API を叩く
ザックリですが、こういう構成になります。
プライベートサブネットの 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 への操作権限を渡します。
2. プライベート API Gateway をつくる
- マネジメントコンソール上で [API Gateway] を開きます。
- [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 です。
項目 | 設定値 | 補足 |
---|---|---|
統合タイプ | AWS サービス | |
AWS リージョン | ご利用のリージョン | 本記事では ap-northeast-1 としています。念のため。 |
AWS サービス | Cognito IDP | Cognito xxx と名のついたものがいくつかあるので注意 |
HTTP メソッド | POST | |
アクション | AdminInitiateAuth | AdminInitiateAuth |
実行ロール | 3 で作ったロール |
4. API のデプロイ
適当なステージにデプロイしてください。ここでは dev とします。
動作確認
適切な 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 を認証ありで使うことが出来そうです。
とはいえ、実用上は ※注意※
にも書いた通り準備が大掛かりになるため、あくまで 力業 というところでご理解いただければと思います。