Boto3でAmazon CognitoのOIDC Providerを作成する


[Amazon CognitoとSlackで無理矢理OpenID Connect - Qiita](https://qiita.com/dbgso/items/0f6fd08b6184eb4bed58)でCognitoとSlackの連携環境を作ってみました。  
Cognito設定が割と面倒で忘れそうなので、設定をスクリプトでできるようにしておきます。

[PythonでAmazon Cognitoのユーザープールを作成してみる - Qiita](https://qiita.com/kai_kou/items/f1623ac1f3d7aaab23fe) で紹介されているスクリプトを元にして、今回必要な設定を追加していきました。


# 環境構築

環境構築の方法は変わっていませんので、[参考記事](https://qiita.com/kai_kou/items/f1623ac1f3d7aaab23fe#%E7%92%B0%E5%A2%83%E6%A7%8B%E7%AF%89) の通りでお願いします。


# 実装

ソース全体は以下の通りです。

多少長くなったので、使ってないコメント文は削除しました。

|名称 | 設定値|
|---|---|
|POOL_NAME| ユーザプール名称|
|DOMAIN_NAME| Cognito* に設定するドメイン|
|CLIENT_ID|SlackのClientID|
|CLIENT_SECRET|SlackのClient Secret|
|AUTHORIZATION_ENDPOINT|OIDC ProviderのAuthorization Endpoint|
|TOKEN_ENDPOINT|OIDC ProviderのToken Endpoint|
|USERINFO_ENDPOINT|OIDC ProviderのUserinfo Endpoint|

は実際の物に合わせてください

``POOL_NAMEに当てはまるUSER_POOLが存在する場合は削除するようになっているので、既にある場合は注意してください``

```main.py
import boto3

POOL_NAME = 'test-pool'
PROVIDER_NAME = 'Slack'
DOMAIN_NAME = 'test'
DUMMY_URL = 'https://oidc.slack.dummy.com'
CALL_BACK_URL = 'http://localhost:3000'
SIGN_OUT_URL = 'http://localhost:3000'
CLIENT_ID = '<SlackのCLIENT ID>'
CLIENT_SECRET = '<SlackのCLIENT SECRET>'
OIDC_ISSUER = DUMMY_URL
AUTHORIZATION_ENDPOINT = 'https://XXXX.ngrok.io/authorization'# Authorizatin endpointのURL
TOKEN_ENDPOINT = 'https://XXXX.ngrok.io/token' #Token endpointのURL
USERINFO_ENDPOINT = 'https://XXXX.ngrok.io/userinfo' # Userinfo endpointのURL
JWKS_URI = DUMMY_URL

def find_userpool_by_pool_name(client, pool_name):
  """
  ユーザプール名に一致するプールのIDリストを返す。複数一致の可能性あり
  """
  return [pool['Id'] for pool in 
          client.list_user_pools(MaxResults=10)['UserPools'] 
          if pool_name == pool['Name']]

def main():
  client = boto3.client('cognito-idp')

  pool_name = POOL_NAME

  # 前回作ったUserDomainとUserPoolを削除
  for pool_id in find_userpool_by_pool_name(client, pool_name):
    if len(client.describe_user_pool_domain(Domain=DOMAIN_NAME)['DomainDescription']) > 0:
      client.delete_user_pool_domain(Domain=DOMAIN_NAME, UserPoolId=pool_id)
    client.delete_user_pool(UserPoolId=pool_id)

  # ユーザープールの作成
  user_pool = _create_user_pool(client, pool_name)
  user_pool_id = user_pool['UserPool']['Id']
  print('create user pool ', user_pool_id)

  # ドメインの作成
  user_pool_domain = _create_domain(client, DOMAIN_NAME, user_pool_id)
  print('create user pool domain')

  # OIDC Providerを作成
  identity_provider = _create_identity_provider(client, user_pool_id)
  print('create identity provider')
  # アプリクライアントの作成
  client_app = _create_user_pool_app_client(client, user_pool)
  print('create client app')

  client_id = client_app['UserPoolClient']['ClientId']

  # cognitoの認証ページURL
  u = "https://{}.auth.{}.amazoncognito.com/login?response_type=code&client_id={}&redirect_uri={}".format(
    DOMAIN_NAME,
    session.region_name,
    client_id,
    CALL_BACK_URL
  )
  print('cognito auth url: ', u)

def _create_domain(client, domain, user_pool_id):
  return client.create_user_pool_domain(
    Domain=domain,
    UserPoolId=user_pool_id,
  )

def _create_identity_provider(client, user_pool_id):
  return client.create_identity_provider(
      UserPoolId=user_pool_id,
      ProviderName=PROVIDER_NAME,
      ProviderType='OIDC',
      ProviderDetails={
          "client_id": CLIENT_ID,
          "client_secret": CLIENT_SECRET,
          "authorize_scopes": "openid",
          "oidc_issuer": OIDC_ISSUER,
          "attributes_request_method": "GET",
          "authorize_url": AUTHORIZATION_ENDPOINT,
          "attributes_url_add_attributes": "false",
          "token_url": TOKEN_ENDPOINT,
          "attributes_url": USERINFO_ENDPOINT,
          "jwks_uri": OIDC_ISSUER,
      },
      AttributeMapping={
        "email": "email",
      },
      IdpIdentifiers=[
          # 'identify'
      ]
  )

def _create_user_pool(client, pool_name):
  response = client.create_user_pool(
    PoolName=pool_name,
    Policies={
      'PasswordPolicy': {
        'MinimumLength': 8,
        'RequireUppercase': True,
        'RequireLowercase': True,
        'RequireNumbers': True,
        'RequireSymbols': True
      }
    },
    MfaConfiguration='OFF',
    AdminCreateUserConfig={
      'AllowAdminCreateUserOnly': False,
      'UnusedAccountValidityDays': 7,
    },
    Schema=[
      {
        'Name': 'email',
        'AttributeDataType': 'String',
        'DeveloperOnlyAttribute': False,
        'Mutable': True,
        'Required': True,
      },
    ],
  )
  return response


def _create_user_pool_app_client(client, user_pool):
  user_pool_id = user_pool['UserPool']['Id']
  user_pool_name = user_pool['UserPool']['Name']
  response = client.create_user_pool_client(
    UserPoolId=user_pool_id,
    ClientName=f'{user_pool_name}-client',
    RefreshTokenValidity=30,
    SupportedIdentityProviders=[
      PROVIDER_NAME,
    ],
    CallbackURLs=[
      CALL_BACK_URL,
    ],
    LogoutURLs=[
      SIGN_OUT_URL,
    ],
    AllowedOAuthFlows=[
      # 'code'|'implicit'|'client_credentials',
      'code',
    ],
    AllowedOAuthScopes=[
      'openid',
    ],
    AllowedOAuthFlowsUserPoolClient=True,
  )
  return response


def _set_user_pool_mfa_config(client, user_pool):
  user_pool_id = user_pool['UserPool']['Id']
  response = client.set_user_pool_mfa_config(
    UserPoolId=user_pool_id,
    SoftwareTokenMfaConfiguration={
      'Enabled': True # True|False
    },
    MfaConfiguration='ON' # 'OFF'|'ON'|'OPTIONAL'
  )
  return response

if __name__ == '__main__':
  main()

```

# 実行

これも元記事同様で以下の方法でいけます。

```console

$ python main.py

create user pool  ap-northeast-1_ZNVHkRgf6
create user pool domain
create identity provider
create client app
cognito auth url:  https://XXXXX.auth.ap-northeast-1.amazoncognito.com/login?response_type=code&client_id=XXXXXXXXXXXX&redirect_uri=http://localhost:3000

```

ツラツラと標準出力に結果が流れます。
最終行の`cognito auth url`のURLを開くとCognitoのログインページになっているので、これを開くとCognitoのログインページが開きます。

![](https://camo.qiitausercontent.com/3c9a17cbbfee8ff9bcd7536cf4c55ffb1a296943/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3333303531342f63373637336330642d386161622d366537642d323934372d3864613237346665333138652e706e67)


あとは設定値が間違っていなければSlackの認可に飛んでCognitoのUserPoolへ登録されます。

# 処理概要

抜粋して簡単に解説します

やっている処理自体は [CognitoIdentityProvider — Boto 3 Docs 1.9.156 documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cognito-idp.html) のサンプルコードを見れば大体わかったので、軽くハマったところだけ

## 処理の流れ

ざっくり以下の流れになってます

1. 前回作成したUser Poolを削除(試行錯誤していたため)
1. User Pool作成
1. ドメインの作成
1. OIDC Providerの作成
1. クライアントアプリの作成

順番で気をつけるのは以下２つぐらいですかね

1. User PoolのIDが以降の処理で必要なため最初に実行
1. クライアントアプリの作成の際にOIDC Providerを指定するので最後じゃないとエラーが出ます。

```python
def main():
  client = boto3.client('cognito-idp')

  pool_name = POOL_NAME

  # 前回作ったUserDomainとUserPoolを削除
  for pool_id in find_userpool_by_pool_name(client, pool_name):
    if len(client.describe_user_pool_domain(Domain=DOMAIN_NAME)['DomainDescription']) > 0:
      client.delete_user_pool_domain(Domain=DOMAIN_NAME, UserPoolId=pool_id)
    client.delete_user_pool(UserPoolId=pool_id)

  # ユーザープールの作成
  user_pool = _create_user_pool(client, pool_name)
  user_pool_id = user_pool['UserPool']['Id']
  print('create user pool ', user_pool_id)

  # ドメインの作成
  user_pool_domain = _create_domain(client, DOMAIN_NAME, user_pool_id)
  print('create user pool domain')

  # OIDC Providerを作成
  identity_provider = _create_identity_provider(client, user_pool_id)
  print('create identity provider')
  # アプリクライアントの作成
  client_app = _create_user_pool_app_client(client, user_pool)
  print('create client app')

```

## ややこしかったところ

ユーザ属性の指定がちょっとわからなかったです

<img width="1372" alt="スクリーンショット 2019-05-28 19.19.37.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330514/715114ff-7e8c-cc89-010f-521aa96bc45d.png">

どうやったら画像のように`email`を選択した状態になるのかわからず少しハマりましたが、以下のように`Schema`属性でした

```python
def _create_user_pool(client, pool_name):
  response = client.create_user_pool(
    PoolName=pool_name,
    Schema=[
      {
        'Name': 'email',
        'AttributeDataType': 'String',
        'DeveloperOnlyAttribute': False,
        'Mutable': True,
        'Required': True,
      },
    ],
  )
  return response
```

# ちなみに

一番盛大にハマったのはOIDC Providerの設定です。

最終的に以下のようなコードになったのですが

```python
def _create_identity_provider(client, user_pool_id):
  return client.create_identity_provider(
      UserPoolId=user_pool_id,
      ProviderName=PROVIDER_NAME,
      ProviderType='OIDC',
      ProviderDetails={
          "client_id": CLIENT_ID,
          "client_secret": CLIENT_SECRET,
          "authorize_scopes": "openid",
          "oidc_issuer": OIDC_ISSUER,
          "attributes_request_method": "GET",
          "authorize_url": AUTHORIZATION_ENDPOINT,
          "attributes_url_add_attributes": "false",
          "token_url": TOKEN_ENDPOINT,
          "attributes_url": USERINFO_ENDPOINT,
          "jwks_uri": OIDC_ISSUER,
      },
      AttributeMapping={
        "email": "email",
      },
      IdpIdentifiers=[
          # 'identify'
      ]
  )
```


Boto3ドキュメントの[CognitoIdentityProvider — Boto 3 Docs 1.9.156 documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cognito-idp.html#CognitoIdentityProvider.Client.create_user_pool) には以下のような記載になっています。

```python
response = client.create_identity_provider(
    UserPoolId='string',
    ProviderName='string',
    ProviderType='SAML'|'Facebook'|'Google'|'LoginWithAmazon'|'OIDC',
    ProviderDetails={
        'string': 'string'
    },
    AttributeMapping={
        'string': 'string'
    },
    IdpIdentifiers=[
        'string',
    ]
)
```

えっ？`ProviderDetails`は何をキーにすれば良いの？

って感じで調べたんですが、なかなかドキュメントが発見できず。。。

結局手動で操作した時の通信内容をブラウザのデバッガーで確認するという曲芸技に走りました。これで無事動きました。


<img width="1467" alt="firefoxdebug.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330514/229b394c-6ed2-4265-9b32-6426502a9252.png">


そしてその後ドキュメントの記載を見つけるという。。。

[ユーザープールへの OIDC ID プロバイダーの追加 - Amazon Cognito](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-pools-oidc-idp.html)

このページ内で折りたたまれている`OIDC IdP を追加するには (AWS CLI)`を開けると書いてありました。

開くまではページ内検索をしても引っかからない、Googleの検索結果にも表示されない

これは無理ゲーだった

# 参考

[CognitoIdentityProvider — Boto 3 Docs 1.9.156 documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cognito-idp.html)
[PythonでAmazon Cognitoのユーザープールを作成してみる - Qiita](https://qiita.com/kai_kou/items/f1623ac1f3d7aaab23fe)
[ユーザープールへの OIDC ID プロバイダーの追加 - Amazon Cognito](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-pools-oidc-idp.html)