PythonでAWS Cognito認証

急に自分内Djangoブームが到来したので、Djangoに乗り換えはじめました。で、先日Node.jsで作ったAWSのCognito認証をPythonで作り直したので晒してみます。

事前準備
===
PythonでAWSにアクセスするにはIAMユーザーが必要となります。ロールにAmazonCognitoPowerUserを設定しておいてください。認証だけならロールをAmazonCognitoReadOnlyとかに落としても動くと思います。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/140718/f46ab763-2642-3b79-1605-9d3c559d8949.png)

アクセスキーも必要となりますので、認証情報タブを押してアクセスキーを作っておいてください。

つづいてAWSにPythonでアクセスするためのライブラリであるboto3を入れます

```bash
pip install boto3
```

AWS Cognitoで認証する
===
.awsに設定をしていなくても、boto3を使う時にregion等の設定を行うことができます。アプリの認証用ユーザーは固定すると思いますので、.awsを使うよりboto3を使う時に設定するほうが現実的だろうと思います。

AWS_ACCESS_KEY_IDやAWS_SECRET_ACCESS_KEY等は直接プログラムに埋め込むのは危険です(特にVCSを使ってる場合は注意)。外部ファイルに記述して読み込んで使ってください。

```python
import boto3

def cognito_auth(user, passwd):
	# 認証開始
	try:
		aws_client = boto3.client('cognito-idp',
			region_name = "ap-northeast-1",
			aws_access_key_id = "ここにAWS_ACCESS_KEY_IDを設定します",
			aws_secret_access_key = "ここにAWS_SECRET_ACCESS_KEYを設定します",
		)

		aws_result = aws_client.admin_initiate_auth(
			UserPoolId = "ここにユーザープールIDを設定します",
			ClientId = "ここにアプリクライアントIDを設定します",
			AuthFlow = "ADMIN_NO_SRP_AUTH",
			AuthParameters = {
				"USERNAME": user,
				"PASSWORD": passwd,
			}
		)

		# 認証完了
		return aws_result

	except :
		# 認証失敗
		return None
```
認証が成功すれば

```python
aws_result["AuthenticationResult"]["AccessToken"]
aws_result["AuthenticationResult"]["RefreshToken"]
aws_result["AuthenticationResult"]["IdToken"]
```
のようにしてトークンを拾えます。

トークンが有効かチェック
===
トークンはjwt形式なのでjwtライブラリで分解して……ってやろうとしたら、VC Build Tool入れて、pycrypto入れて、pyjwt入れて、winrandomの参照先変えて、decode命令呼んだら、RS256デコードしようとしてエラー吐いて死んだ／(^o^)＼

どうやってもWindowsだと暗号解除部分でエラー出すので、自力で確認するの諦めて、AWSにおまかせする方向にしました。

というわけで、保有しているトークンを使ってAWSからユーザー情報を取得します。トークンが無効ならAWSがエラーを出してくれるはずです。期限切れの確認はやってないので、もしかしたら間違ってるかもしれません。

認証成功時に

```python
# 認証成功
req.session["aws_cognito"] = {
	"user": "認証したユーザーの名前",
	"access_token": aws_result["AuthenticationResult"]["AccessToken"],
	"refresh_token": aws_result["AuthenticationResult"]["RefreshToken"]
}
```
のようにして、req.sessionにトークンを保存した前提でのプログラムとなります。

```python
import boto3

def cognito_auth_check(req):
	# 認証開始
	# *********************************************************************
	try:
		# AWSクライアント認証
		aws_client = boto3.client('cognito-idp',
			region_name = "ap-northeast-1",
			aws_access_key_id = "ここにAWS_ACCESS_KEY_IDを設定します",
			aws_secret_access_key = "ここにAWS_SECRET_ACCESS_KEYを設定します",
		)

		try:
			# 認証済みトークンの有効性確認
			return aws_client.get_user(AccessToken=req.session["aws_cognito"]["access_token"])

		except:
			# 失敗。期限切れかもしれないのでREFRESH_TOKENで再認証
			try:
				aws_result = aws_client.admin_initiate_auth(
					UserPoolId = "ここにユーザープールIDを設定します",
					ClientId = "ここにアプリクライアントIDを設定します",
					AuthFlow = "REFRESH_TOKEN_AUTH",
					AuthParameters = {
						"USERNAME": req.session["aws_cognito"]["user"],
						"REFRESH_TOKEN": req.session["aws_cognito"]["refresh_token"]
					}
				)
				# アクセストークン更新
				req.session["aws_cognito"]["access_token"] = aws_result["AuthenticationResult"]["AccessToken"]

				# 再度認証確認してみる
				return aws_client.get_user(AccessToken=req.session["aws_cognito"]["access_token"])

			except:
				# REFRESH_TOKENすら期限切れや、接続障害によって失敗する可能性
				return None

	except:
		# AWSへの接続失敗。IAM変えた？　AWS接続障害の可能性も
		return None
```
成功すればユーザー情報が取得できます。

Djangoでデコレーターとして使う
===
デコレーターを作っておくとDjangoのlogin_requiredデコレーターのように

```python
@login_required
def api(req):
	return HttpResponse('認証済みページの表示')
```
と、関数の前にポンッとデコレーターを置いたらページを認証が必要な状態にできるのが便利そうです。なのでデコレーターも作ってみました。

Djangoでのプログラムです。

```python
from django.http import HttpResponse

# AWS Cognitoで認証済みであることを要求するデコレーター
def cognito_required(func):
	def wrap(req, *args, **kwargs):
		# 認証確認
		req.user = cognito_auth_check(req)
		if req.user != None:
			# デコレータ下の関数を実行
			return func(req, *args, **kwargs)

		# 失敗終了
		return HttpResponse('認証が必要です', status=401)

	wrap.__doc__=func.__doc__
	wrap.__name__=func.__name__
	return wrap
```
これを、account/aws_auth.pyとかで作っておけば、

```python
from account.aws_auth import cognito_required

@cognito_required
def api(req):
	return HttpResponse('認証済みページの表示')
```

と、簡単に認証が必要なapiにすることができてステキ♡