Amazon Cognito をインターネットを介さずAWS通信網内だけで実現

今回は Amazon Cognito をどのようにインターネットを介さずAWS通信網内だけで実現することができるか解説したいと思います。

# 背景
セキュリティ要件がシビアな公共・金融関係の会社において、自社オンプレ環境から AWS 上にあるシステムを利用する際、途中経路がインターネット網ではなく閉域網であることが求められることが多いです。この場合、オンプレ拠点とAWS間を接続するために、一般的に DirectConnect または Site-to-Site VPN が利用されます。

例えば、Amazon S3 や Amazon DynamoDB など利用したいAWSサービスのエンドポイントがインターネット上にある場合でも、『VPCエンドポイント』を使用することで、オンプレ拠点からインターネットを経由せずにAWS S3 にアクセスすることができ、セキュアで効率的な通信を実現できます。

しかし、注意すべき点として、『VPCエンドポイント』が利用できないサービスもあります。例えば、WEBシステム認証に Amazon Cognito を使っている場合、Amazon Cognito はVPCエンドポイントに対応していないので、どうしてもクライアントからインターネット経由で認証データが流れてしまいます。



# (通常) インターネット経由で Amazon Cognito 利用
通常、一般公開されたWEBサービスでの Cognito認証フローは下記のようになります。

![一般的なCognito認証.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49353/94d44afa-943e-40dc-a924-992233d64d3c.png)


上図のように、インターネット経由でユーザがWEBサイトとにアクセスし、IDとパスワードを入力した認証結果としてIDトークンを受け取ります(①②③)。その後もページ遷移の度にIDトークンをヘッダーに含めたリクエストをインターネット経由で送信します(④)。
<br>

# プライベート接続経由で Amazon Cognito 利用
現時点で Cognito は直接VPCエンドポイントに対応していません。そのため、インターネット経由を避けてプライベート接続で Cognito を利用するには、API Gateway エンドポイントをプロクシとして設定し、Lambda関数を使用して Cognitoにアクセスする方法が考えられます。

![プライベート接続経由のCognito認証.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49353/8bd49cec-5f23-4399-92ad-3d6d80f939b8.png)


### ※重要なポイント① ルーティング
オンプレミス社内環境とAWS間は AWS DirectConnect (または Site-to-site VPNなど)でセキュアなプライベート接続経由を確保し、必ずインターネットを介さずに通信するようにルーティングしてください。

### ※重要なポイント② 認証時のセキュリティ強化
ここで Secret Manager を使ってセキュリティを強化してます。具体的には以下の手順で認証情報の保護を強化できます。

1\. Cognito にユーザプールにシークレットを使用したクライアントシークレットを有効化します。
2\.  シークレットの値は安全に取り扱える場所として Secret Manager に保存しておき、Lambda関数ではシークレットハッシュを計算してAPI呼び出し時のリクエストに追加します。
3\. API のクエリパラメータにシークレットハッシュが含まれない場合はブロックされます。

シークレットハッシュ値を計算するには「アプリのクライアントID」「アプリのクライアントシークレット」「Cognito ユーザプール内のユーザーのユーザー名」の情報が必要になります。
例えば Python で、クライアントシークレットをキーとして使用し、SHA256 ハッシュ関数を使用して HMAC ダイジェストの計算は次のように行います。

```
import sys, hmac, hashlib, base64
import boto3
from botocore.exceptions import ClientError

app_client_id = "<HIDDEN>" 
app_client_secret = "<HIDDEN>"

def get_secret_hash(username, app_client_id, app_client_secret):
    # メッセージとキーをバイト形式に変換
    message, key = (username + app_client_id).encode('utf-8'), app_client_secret.encode('utf-8')
    
    # シークレットハッシュの取得
    secret_hash = base64.b64encode(hmac.new(key, message, digestmod=hashlib.sha256).digest()).decode()
    return secret_hash

def lambda_handler(event, conntext):
    # Cognito ID プールのクライアントを作成
    client = boto3.client('cognito-idp', region_name=xxxxxx)
    
    # Secret Hashを生成
    secret_hash = get_secret_hash(username, app_client_id, app_client_secret)
    
    try:
        # ユーザー認証を開始
        response = client.initiate_auth(
            ClientId=app_client_id,
            AuthFlow='USER_PASSWORD_AUTH',  # 認証フロー指定（ユーザーパスワード認証）
            AuthParameters={
                'USERNAME': username,
                'PASSWORD': event['password'],  # イベントからパスワードを取得
                'SECRET_HASH': secret_hash
            }
        )

```

initiate_auth APIコールのレスポンス例
```
{    
    "ChallengeParameters": {},

    "AuthenticationResult": {
        "AccessToken": "<HIDDEN>",
        "ExpiresIn": 3600,
        "TokenType":
    "Bearer",

        "RefreshToken": "<HIDDEN>",
        "IdToken": "<HIDDEN>"
    }
}
```
### ※重要なポイント③ DNS名前解決
クライアントがAmplifyを使用する場合には、Private API Gateway の Public DNS 名を利用する必要があり、そのためには Public DNS 名を解決する経路(インターネット)が必要です。
一方、クライアントがAmplifyではなく、自分で開発したライブラリを使用する場合、Private API GatewayのPublic DNS名だけでなく、Private DNS名も利用できます。Private DNS名を使用するには、Route 53 Inbound Resolverの設定が必要です。

<br>
このような方法を採用することで、クライアントからの認証データがセキュアに保護され、インターネットを経由することなく、安全な認証フローが実現できます。


長くなっちゃうので今回ここまでにしておいて、次回は実際に試す方法についてもう少し詳しく書きたいと思います。





- - -
※この記事は以下の情報を参考にしました。
 - [Enriching Amazon Cognito features with an Amazon API Gateway proxy]
  https://aws.amazon.com/blogs/architecture/enriching-amazon-cognito-features-with-an-amazon-api-gateway-proxy/ 
 - [アプリクライアントによるアプリケーション固有の設定] 
 https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html )
 - [Invoke a private API]
  https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-private-api-test-invoke-url.html#apigateway-private-custom-domains-provider-invoke