cognitoのJWT(JSON Web Token)の署名を確認する on Rails


はじめに

ネットにcognitoのtoken利用に関してまとまった情報がないので備忘的にまとめてみます

cognitoにおけるtokenの扱い

cognitoのtokenはJWTのフォーマットに則っており、token利用時は署名確認が義務付けられています。
※JWTのフォーマットの説明はネットを探せば出てくるので割愛します。

そこで、token使用時の署名確認手順をamazonページでも確認してみます。
ユーザープールのトークンの使用
。。。成る程、意味が判りません(JWT前提なので当然っちゃ当然ですがJWTの仕様とが判らないと理解不能です。

以下抜粋・・・

ユーザープールのトークンの使用
1. ユーザープールの JSON Web トークン (JWT) セットをダウンロードして保存します。
  それらをhttps://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.jsonで検索できます。

2. JWT 形式からトークン文字列をデコードします。

3. iss クレームを確認します。
  これは、ユーザープールと一致する必要があります。
  たとえば、us-east-1 リージョンで作成されたユーザープールの iss 値が https://cognito-idp.us-east-4.amazonaws.com/{userPoolId} であるとします。

4. token_use クレームを確認します。
  ウェブ API でアクセストークンのみを受け入れている場合、その値は access にする必要があります。
  ID トークンのみを使用している場合、その値は id にする必要があります。
  両方のトークンを使用している場合、その値は id または access のどちらかです。

5. JWTトークンヘッダーから kid を取得すると、ステップ 1 で保存された対応する JSON Web キーが取得されます。

6. デコードされた JWT トークンの署名を確認します。

7. exp クレームを確認し、トークンが期限切れでないことを確認します。
  トークン内でクレームを信頼し、要件に満たすものとしてそれを使用できます。

いまいち理解できなかったので、実際にrubyで署名チェックしてみました

Gemfile
gem 'jwt'
sample.rb
require 'jwt'

# AWS SDK設定
client = Aws::CognitoIdentityProvider::Client.new(
  region: '{region}',
  access_key_id: '{access_key_id}',
  secret_access_key: '{secret_access_key}'
)

# user pools認証
# id/pass認証
# とりあえず認証APIを実行してみる
resp = client.admin_initiate_auth(
  auth_flow: 'ADMIN_NO_SRP_AUTH',
  user_pool_id: '{user_pool_id}',
  client_id: '{client_id}',
  # ログインしたいユーザの情報
  auth_parameters: {
    USERNAME: '{USERNAME}',
    PASSWORD: '{PASSWORD}'
  }
)

# 各種TOKENを引っこ抜く
access_token = resp.authentication_result.access_token
refresh_token = resp.authentication_result.refresh_token
id_token = resp.authentication_result.id_token

# 1. ユーザープールの JSON Web トークン (JWT) セットをダウンロードして保存します
jwks = JSON.parse(open('https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json').read)

# 2. JWT 形式からトークン文字列をデコードします。  
decode = JWT.decode id_token, nil, false

# ここからペイロード部分の確認
# 3. iss クレームを確認します。
return false if decode[0]['iss'] != 'https://cognito-idp.us-east-4.amazonaws.com/{userPoolId}'

# 4. token_use クレームを確認します。
return false if decode[0]['token_use'] != 'id'

# 5. JWT トークンヘッダーから kid を取得
kid = decode[1]['kid']
# kidが取れたので公開鍵を生成
# openssl_bnの実装は後述の引用先「RubyでGoogleのJWKにあるmodulusとexponentからOpenSSLを使って公開鍵を生成する」を参照
# jwks['keys']は jwks['keys'][{配列番号}]['kid']でkidが一致するものを使う
modulus = openssl_bn(jwks['keys'][0]['n'])
exponent = openssl_bn(jwks['keys'][0]['e'])
sequence = OpenSSL::ASN1::Sequence.new(
  [OpenSSL::ASN1::Integer.new(modulus),
   OpenSSL::ASN1::Integer.new(exponent)]
)
public_key = OpenSSL::PKey::RSA.new(sequence.to_der)

# 6. デコードされた JWT トークンの署名を確認
# 7. exp クレームを確認し、トークンが期限切れでないことを確認
# ↓で期限切れもチェックしてくれます。(実は1-7の順番じゃないほうが効率的ですが折角なので順番どおりにチェックしました。
JWT.decode id_token, public_key, true, algorithm: 'RS256'

public_key生成はコチラの記事を引用しています。

「RubyでGoogleのJWKにあるmodulusとexponentからOpenSSLを使って公開鍵を生成する」

最後に

ここまでやってやっとtoken利用の資格を得ることが出来ます。
逆にちゃんと署名チェックしないと改竄検知出来ないので、絶対にやりたいですね。

ということで、次はここまでを踏まえてSSOの実装を考えようとおもいます。