目的
Amazon Cognitoのユーザープールを触っていたときにSecretHash値のところでハマったのでメモとして書いています。なお、Amazon Cognitoのユーザープール全体の話は別の記事として書きます。
どこでハマったの?
Amazon Cognitoのサービスを使ったことがなかったので使ってみようと思いまして、まずはユーザープールを新規作成しました。その後、アプリクライアントを追加して、適当に新規ユーザーを作成しました。
ただ、ユーザーの作成で作成したパスワードは仮のパスワードなので、ログイン後にパスワードを再設定する必要があります。ユーザーテーブルを見てみると、アカウントのステータスがFORCE_CHANGE_PASSWORDとなっていますね。現実のアプリでもパスワード再設定が要求されたりしますが、裏ではこのようなことになっていたんですねー。
疑似的にパスワード再設定を行うためにCLIを利用して以下の2つのコマンドを実行します。
$ aws cognito-idp admin-initiate-auth \
--user-pool-id <プール ID> \
--client-id <アプリクライアントID> \
--auth-flow ADMIN_NO_SRP_AUTH \
--auth-parameters \
USERNAME=<ユーザー名>,PASSWORD=<仮パスワード>
$ aws cognito-idp admin-respond-to-auth-challenge \
--user-pool-id <プール ID> \
--client-id <アプリクライアントID> \
--session <上のコマンドのリターンに含まれているSession情報> \
--challenge-name NEW_PASSWORD_REQUIRED \
--challenge-responses USERNAME=<ユーザー名>,NEW_PASSWORD=<新しいパスワード>
1つ目のコマンドでセッション情報を入手し、2つ目のコマンドで初期パスワードの変更を行っています。これでアカウントのステータスがCONFIRMEDになり、以降は認証を行えばトークンが発行されるようになります。
とまあ普通はすんなり進むんですが、私はここでハマりました。。。
1つ目のコマンドを叩いたときに以下のエラーが出たんです。
An error occurred (NotAuthorizedException) when calling the AdminInitiateAuth operation: Unable to verify secret hash for client <アプリクライアントID>
はじめはなぜここでsecret hashがでてくるのか意味すらわからず、スペルミスや渡すデータがおかしいのかなと思ったんですが、どうやら調べてみるとアプリクライアントを作成するときにやってしまっていたようです。。。
上図は、アプリクライアントを追加するときの画面ですが、デフォルト設定のままだと「クライアントシークレットを生成」にチェックが入っていて、コマンドにsecret hashが必須になるようです。secret hashがないのでUnable to verify secret hashとなってしまっていたということですね。
逆に言えば、こちらのチェックを外すことでこの問題は回避することができます。ただ、せっかく(?)ハマったのでチェックをつけた場合の対処方法についてどうすればいいか調べて実際にやってみることにしました。
ここからが本題です。
SecretHash値とは
「クライアントシークレットを生成」にチェックが入っている場合、エラー文にもある通りSecretHash値が必要になります。具体的には、先ほどのコマンドのauth-parameters, challenge-responses部分にSECRET_HASHというパラメータ項目を追加する必要があります。
$ aws cognito-idp admin-initiate-auth \
--user-pool-id <プール ID> \
--client-id <アプリクライアントID> \
--auth-flow ADMIN_NO_SRP_AUTH \
--auth-parameters \
USERNAME=<ユーザー名>,PASSWORD=<仮パスワード>,SECRET_HASH=<SecretHash値>
$ aws cognito-idp admin-respond-to-auth-challenge \
--user-pool-id <プール ID> \
--client-id <アプリクライアントID> \
--session <上のコマンドのリターンに含まれているSession情報> \
--challenge-name NEW_PASSWORD_REQUIRED \
--challenge-responses USERNAME=<ユーザー名>,NEW_PASSWORD=<新しいパスワード>,SECRET_HASH=<SecretHash値>
ではSecretHash値とはなんでしょうか。AWSの公式リファレンスによると以下のように定義されています。
SecretHash 値は、Base64でエンコードされたキーつきハッシュメッセージ認証コード(HMAC)であり、ユーザープールクライアントおよびユーザー名、さらにメッセージ内のクライアント ID を使用して計算されたものです。次の擬似コードは、この値の計算方法を示します。この擬似コードで + は連結を表し、HMAC_SHA256 は HmacSHA256 を使用して HMAC 値を生成する関数を、Base64 は Base-64 でエンコードされ たバージョンのハッシュ出力を生成する関数を示します。
# 擬似コード
Base64 ( HMAC_SHA256 ( "Client Secret Key", "Username" + "Client Id" ) )
引用元 : Amazon Cognito デベロッパーガイド -SecretHash 値の計算-
HMACとSHA-256
引用文の中にでてきたHmacSHA256ですがこれはHMAC(Hash-based Message Authentication Code)と呼ばれるメッセージ認証符号(MAC; Message Authentication Code)のことで、ハッシュ関数にSHA-256を使用します。
SHA-256とは、National Security Agency(NSA)によって設計されました。Secure Hash Algorithm シリーズのハッシュ関数 SHA-2のバリエーションの中の1つであり、Secure Hash Algorithm のハッシュ値が256bitである。ほかにもSHA-512がある。
このサイトでSHA-256を試してみることができます。例えば以下の文章をハッシュしてみると、
数学において、一変数の非負値関数の積分は、最も単純な場合には、その関数のグラフと x 軸の間の面積と見なすことができる。ルベーグ積分(ルベーグせきぶん、英: Lebesgue integral)とは、より多くの関数を積分できるように拡張したものである。ルベーグ積分においては、被積分関数は連続である必要はなく、至るところ不連続でもよいし、関数値として無限大をとることがあってもよい。さらに、関数の定義域も拡張され、測度空間と呼ばれる空間で定義された関数を被積分関数とすることもできる。
引用: ルベーグ積分 フリー百科事典『ウィキペディア(Wikipedia)』
A29ED395198106FA229C2259FCF024321729AFD59D01D3D0F2BF70BF79F5C74E
と64桁の文字列が得られます。次にHMACですが、以下のようなアルゴリズムで定義されるようです。
$$ HMAC_K (m) = h((K\oplus opad) || h((K \oplus ipad) || m)) $$
$h, K, m$はそれぞれハッシュ関数、秘密鍵、認証対象メッセージで、$opad, ipad$は定数パディング, $\oplus$は排他的論理和, "$||$"はビット列の連結です。なので今回の場合は、$h$がSHA-256, $K$がクライアントシークレット、$m$が"Username" + "Client Id"です。頑張れば定義に従って計算できそうな気もしますが、おとなしくPythonのライブラリを使います。どうやら標準ライブラリのhmc, hshlibを使えば簡単に実装できそうです。
import hmac, hashlib
# 入力データの定義
username = "<ユーザー名>"
app_client_id = "<アプリクライアントID>"
key = "<クライアントシークレット>"
# バイト型に変換
message = bytes(sys.argv[1]+sys.argv[2],'utf-8')
key = bytes(sys.argv[3],'utf-8')
# HmacSHA256の実行
secret_hash = hmac.new(key, message, digestmod=hashlib.sha256).digest()
print(secret_hash)
途中でバイト型に変換しているのはstr型で渡すと
TypeError: key: expected bytes or bytearray, but got 'str'
となるためです。hashlibにも以下のような注釈があります。
注釈 文字列オブジェクトを update() に渡すのはサポートされていません。ハッシュはバイトには機能しますが、文字には機能しないからです。
最後に得られたハッシュ値をBase64でエンコードすればSecret Hash値になります。無事にアカウントのステータスをCONFIRMEDにすることができました。