クライアントシークレットなしのCognitoユーザプールをDjangoで使う方法

# はじめに

[Django](https://www.djangoproject.com/)の認証に[Cognito](https://aws.amazon.com/jp/cognito/)を使う場合、いくつかライブラリが提供されているが、Django3.x非対応だったり、DRFベースだったりしてフロントエンドで使うためのものとしては選択肢が少ない。

[django-cognito-redux](https://github.com/patriotresearch/django-cognito-redux)は上記案件に対応している貴重なライブラリである。
ただし、ユーザープールのアプリクライアントでシークレットキーを設定することが前提条件となっているため、他のウェブアプリで共有したい場合ネックとなる。

今回、関数のオーバライドによりクライアントキーなしでも利用できるようにしてみた。

# 対象読者

* 認証基盤にAWS Cognitoを使っている方
* DjangoおよびVue.jsなどJavascript系のウェブフレームワークのフロントエンド開発者


# 環境

* Python-3.9.6
* Django-3.2.5
* django-cognito-redux-1.4.5

# セットアップ

[Githubのレポジトリ](https://github.com/higebobo/django-cognito-sample)にソースがあるのでREADME.mdに従って設定する。

# 動作確認

Djangoのアプリケーションを起動しブラウザで`http://localhost:8000/`にアクセスする。

![FireShot Capture 051 - The install worked successfully! Congratulations! - localhost.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691494/df5d77d8-90a0-cc83-6297-a5325aed5cb2.png)

特に問題がなければ管理画面`http://localhost:8000/admin/`にアクセスする。

![FireShot Capture 052 - ログイン - Django サイト管理 - localhost.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691494/e42e7918-4586-21a4-92b0-a913ebdec08f.png)

Cognitoユーザプールの既存アカウントでログインしてみる。

![FireShot Capture 053 - サイト管理 - Django サイト管理 - localhost.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691494/2fac86f1-3ebb-3b7e-d4d6-ea2042525388.png)

ログインに成功すると管理画面のダッシュボードが表示される。
このサンプルではCognitoユーザにパーミッションを与えていないので特に何も表示されないがログインは成功した。

この後、ログアウトして管理者(`create superuser`で作成したユーザ)で再度ログインする。

![FireShot Capture 054 - 変更する ユーザー を選択 - Django サイト管理 - localhost.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691494/95ae343f-5006-db4e-b428-2172ec07324f.png)

すると管理者以外にCognitoのユーザが追加されている。
`email`、`firstname`および`lastname`はCognitoのユーザ情報から取得して登録している。

# 解説

今回のサンプルは認証のみ行っている。
スクラッチから作る場合はまず手動でプロジェクト配下に`account`ディレクトリを以下の構成で作る。

```
account/
├── __init__.py
├── backends.py
└── helpers.py
```

[backends.py](https://github.com/higebobo/django-cognito-sample/blob/main/account/backends.py)に認証バックエンドを定義する。

```python
class AwsCognitoAuthentication:

    def authenticate(self, request, username=None, password=None):
        if username is None:
            user, _, _ = m_helpers.process_request(request)
            return user
        else:
            try:
                result = initiate_auth(
                    {
                        'username': username,
                        'password': password,
                        'auth_flow': 'USER_PASSWORD_AUTH'
                    }
                )
        # ...
```

`initiate_auth`関数はクライアントシークレットの有無で使い分けたいので、冒頭で振り分けている。

```python
if getattr(settings, 'APP_SECRET_KEY'):
    # クライアントシークレットがあるとき
    from django_cognito.authentication.cognito.helpers import initiate_auth
else:
    # ないとき
    from .helpers import initiate_auth
```

[helpers.py](https://github.com/higebobo/django-cognito-sample/blob/main/account/helpers.py)はオリジナルを改変して以下のようにする。

```python
def initiate_auth(data, param_mapping=None):
    if ('username' in data and 'password' in data) \
            or ('username' in param_mapping and 'password' in param_mapping):
        auth_flow = constants.USER_PASSWORD_FLOW
        username = parse_parameter(data, param_mapping, 'username')
        password = parse_parameter(data, param_mapping, 'password')

        return initiate_auth_without_secret(username, auth_flow, password)

    else:
        raise ValueError('Unsupported auth flow')


def initiate_auth_without_secret(username, auth_flow, password=None,
                                 refresh_token=None, srp_a=None):
    auth_parameters = {}
    # ...
```

上記ができたら[settings.py](https://github.com/higebobo/django-cognito-sample/blob/main/mysite/settings.py)に設定を加える

```python
# ...
INSTALLED_APPS = [
    'django.contrib.admin',
    # ...
    'account',
]

# ...

# backend
AUTHENTICATION_BACKENDS = [
  'django.contrib.auth.backends.ModelBackend',
  'account.backends.AwsCognitoAuthentication',
]
# ...
```

# まとめ

今回のキモとなっているのはオリジナルの[actions.py](https://github.com/patriotresearch/django-cognito-redux/blob/master/django_cognito/authentication/cognito/actions.py)の以下の行

```python
def initiate_auth(username, auth_flow, password=None, refresh_token=None, srp_a=None):
    auth_parameters = {'SECRET_HASH': utils.get_cognito_secret_hash(username)}
```

ここでCognitoに渡すパラメータとして`SECRET_HASH`を必須としている。なので

```python
def initiate_auth(username, auth_flow, password=None, refresh_token=None, srp_a=None):
    auth_parameters = {}
```

とした。もちろんこのままではクライアントシークレットがある場合は認証できないので呼び出す関数を使い分けている。