はじめに
RailsでCognitoのJWTトークンの署名を検証する方法について解説します。
Cognitoが提供するJWK(JSON Web Key)形式の公開鍵をRSA形式で読み込み、JWTの署名検証を行います。
今回はCognitoの設定やトークンの取得方法には触れておりません。
環境
本実装は、以下の環境を前提としています。
Ruby: 3.2.2
Ruby on Rails: 7.0.2
手順
- 環境変数の設定
- 公開鍵を取得
- JWK形式からRSA公開鍵形式に変更
- JWT署名検証
環境変数の設定
Cognitoユーザープールを設定し、以下の情報を環境変数に設定します。
リージョン
クライアントID
ユーザープールID
AWS_REGION=your-region
COGNITO_CLIENT_ID=your-client-id
COGNITO_USER_POOL_ID=your-user-pool-id
CognitoのJWKs(JSON Web Key Set)を取得
Cognitoが提供するJWK形式の公開鍵セットを取得します。
以下のメソッドでは、環境変数を用いてCognitoから公開鍵を取得し、JSON形式で返します。
# Cognitoの公開鍵をJWKS形式で提供するURL
JWK_URL = "https://cognito-idp.#{ENV['AWS_REGION']}.amazonaws.com/#{ENV['COGNITO_USER_POOL_ID']}/.well-known/jwks.json"
def self.get_cognito_public_keys
uri = URI(JWK_URL)
response = Net::HTTP.get(uri)
JSON.parse(response)['keys']
end
JWK形式からRSA公開鍵形式に変更
Cognitoから提供される公開鍵は、JWK (JSON Web Key) 形式であり、JSON形式で公開鍵情報(kty, kid, n, e などの属性)を持っています。
JWK形式はWeb上で公開鍵を配布するための一般的な形式ですが、直接的に暗号ライブラリ(この場合 OpenSSL)で署名の検証に使用できません。
そのため、公開鍵をJWK形式からRSA公開鍵形式に変換します。
# JWKSの取得とキャッシュ管理
def self.get_cognito_public_keys
# RailsキャッシュからJWKを取得し、あればそれを返す
cached_keys = Rails.cache.read('cognito_jwk_keys')
return cached_keys if cached_keys
# JWKを再取得し、キャッシュに保存
uri = URI(JWK_URL)
response = Net::HTTP.get(uri)
keys = JSON.parse(response)['keys']
# RailsキャッシュにJWKを保存し、有効期限を1時間後に設定
Rails.cache.write('cognito_jwk_keys', keys, expires_in: 1.hour)
keys
end
JWT署名の検証
次にJWTトークンをデコードし、署名を検証した上でトークンのペイロード部分だけを返す関数を作成します。
def self.verify_token(token)
# トークンヘッダから公開鍵の kid を取得
header = JWT.decode(token, nil, false).last
kid = header['kid']
# 公開鍵を取得し、トークンの署名検証
public_key = find_public_key(kid)
JWT.decode(token, public_key, true, {
iss: "https://cognito-idp.#{ENV['AWS_REGION']}.amazonaws.com/#{ENV['COGNITO_USER_POOL_ID']}",
verify_iss: true,
algorithms: ['RS256']
}).first
end
メソッドの説明
header = JWT.decode(token, nil, false).last
トークンをデコードしてヘッダー部分を取得します。この中からkidを使い、該当する公開鍵を探します。
public_key = find_public_key(kid)
kidに対応する公開鍵を取得し、RSA形式に変換して返します。
JWT.decode(token, public_key, true, {...})
公開鍵で署名の有効性を検証します。ここで検証されるのは、トークンの発行者(iss)がCognitoのユーザープールと一致し、アルゴリズムがRS256であることです。
ソースコード
JWK_URL = "https://cognito-idp.#{ENV['AWS_REGION']}.amazonaws.com/#{ENV['COGNITO_USER_POOL_ID']}/.well-known/jwks.json"
def self.verify_token(token)
begin
# トークンのヘッダーをデコードし、公開鍵のkid(キーID)を取得
header = JWT.decode(token, nil, false).last
kid = header['kid']
# JWKから公開鍵を取得
public_key = find_public_key(kid)
# トークンの検証
decoded_token = JWT.decode(token, public_key, true, {
iss: "https://cognito-idp.#{ENV['AWS_REGION']}.amazonaws.com/#{ENV['COGNITO_USER_POOL_ID']}",
verify_iss: true,
algorithms: ['RS256']
}).first
# デコードされたトークンを返す
decoded_token
rescue JWT::DecodeError => e
Rails.logger.error "トークンの検証に失敗しました: #{e.message}"
nil
end
end
# kid に対応する公開鍵を取得
def self.find_public_key(kid)
keys = get_cognito_public_keys
jwk = keys.find { |key| key['kid'] == kid }
raise "公開鍵が見つかりません (kid: #{kid})" unless jwk
n = OpenSSL::BN.new(decode_base64url(jwk['n']), 2)
e = OpenSSL::BN.new(decode_base64url(jwk['e']), 2)
# ASN.1 DER形式で公開鍵を構築
sequence = OpenSSL::ASN1::Sequence([
OpenSSL::ASN1::Integer.new(n),
OpenSSL::ASN1::Integer.new(e)
])
rsa_public_key = OpenSSL::PKey::RSA.new(sequence.to_der) # 公開鍵生成
end
# JWKSの取得とキャッシュ管理
def self.get_cognito_public_keys
# RailsキャッシュからJWKを取得し、あればそれを返す
cached_keys = Rails.cache.read('cognito_jwk_keys')
return cached_keys if cached_keys
# JWKを再取得し、キャッシュに保存
uri = URI(JWK_URL)
response = Net::HTTP.get(uri)
keys = JSON.parse(response)['keys']
# RailsキャッシュにJWKを保存し、有効期限を1時間後に設定
Rails.cache.write('cognito_jwk_keys', keys, expires_in: 1.hour)
keys
end
# Base64URL形式のデコード
def self.decode_base64url(str)
Base64.urlsafe_decode64(str + padding)
end
まとめ
この記事では、RailsでCognitoのJWTトークンの署名検証を行う方法について解説しました。この記事を読んでいただいた方の役に立ててば幸いです
参考
https://github.com/jwt/ruby-jwt
https://zenn.dev/ndjndj/articles/1a2c2a8f803dc7