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


はじめに

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

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

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

対象読者

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

環境

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

セットアップ

GithubのレポジトリにソースがあるのでREADME.mdに従って設定する。

動作確認

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

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

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

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

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

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

解説

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

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

backends.pyに認証バックエンドを定義する。

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関数はクライアントシークレットの有無で使い分けたいので、冒頭で振り分けている。

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

helpers.pyはオリジナルを改変して以下のようにする。

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に設定を加える

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

# ...

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

まとめ

今回のキモとなっているのはオリジナルのactions.pyの以下の行

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を必須としている。なので

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

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