はじめに
OpenID Connect Providerが発行するID TokenをRubyのJWTライブラリを利用して検証してみました。よくあるサンプルコードは自前のRSA鍵を利用して検証していたりするので、実際にdexidp/dexが発行したID TokenのJWTを検証するコードをRuby+JWTライブラリで実行してみました。
テストはDexを利用していますが(/.well-known/openid-configurationで情報を提供する)一般的なOpenID Connect Providerであれば同様に動くはずです。
環境
- Ubuntu 24.04 LTS 64bit版
- ruby (3.3.5)
- ruby-bundler (2.5.16)
- dexidp/dep (server, https://example.com/dex) + example-app (client)
JWTはDex付属のexample-appを利用して入手しています。Ubuntuパッケージのruby-jwtはバージョンが古く、サンプルがそのままでは実行できませんでしたので、bundlerを利用しています。
References
- https://github.com/jwt/ruby-jwt/tree/v2.2.1#json-web-key-jwk
- https://qiita.com/zakuroishikuro/items/5b08d65d4fef79982f19
- https://stackoverflow.com/questions/38998702/verify-a-signature-in-jwt-io
- https://stackoverflow.com/questions/46121275/ruby-rsa-from-exponent-and-modulus-strings
ライブラリの準備
作業用のディレクトリにGemfileを作成して、最新のJWTライブラリを利用しています。
source 'https://rubygems.org'
gem 'json'
gem 'jwt'
gem 'httpclient'
gem 'openssl'
gem 'mutex_m'
Gemfileのある作業用ディレクトリで、次のコマンドを実行し、./lib/ruby以下にライブラリファイルをダウンロードします。
$ bundle config set path lib
$ bundle install
Fetching gem metadata from https://rubygems.org/...
Resolving dependencies...
Fetching httpclient 2.8.3
Fetching base64 0.2.0
Installing base64 0.2.0
Fetching json 2.9.1
Installing httpclient 2.8.3
Installing json 2.9.1 with native extensions
Fetching mutex_m 0.3.0
Installing mutex_m 0.3.0
Fetching openssl 3.3.0
Installing openssl 3.3.0 with native extensions
Fetching jwt 2.10.1
Installing jwt 2.10.1
Bundle complete! 5 Gemfile dependencies, 7 gems now installed.
Bundled gems are installed into `./lib`
Dexが発行したID Tokenの準備
"idtoken.txt"ファイルにID Tokenの内容をコピーしておきます。ピリオドで3つのBase64エンコードされた情報が連結されていますが、Webブラウザに出力されたID Token(JWT)をそのままコピーしています。
eyJhbGc ... wOTMifQ.eyJpc3MiO ... EFiZSJ9.r_vHqAr3 ... hjvzhzf9w
ruby-jwtのサンプルコードを試す
次のようなファイルを準備し、bundle execコマンドで実行します。JWKS_URI変数とJWT_TOKEN_FILE変数の内容は適切なURL,ファイル名に変更してください。
#!/usr/bin/ruby
require 'bundler/setup'
Bundler.require
## prepare JWT string
JWT_TOKEN_FILE = "./idtoken.txt"
jwt_text = open(JWT_TOKEN_FILE).read().strip() ## strip() is essential to avoid future verification error.
puts "--- unverified output ---"
claim,algo = JWT.decode(jwt_text, nil, false)
puts claim,algo ## the algo object will be used later.
## find jwks_uri from the unverified claim
oidc_config = JSON.parse(HTTPClient.new.get(claim["iss"] + "/.well-known/openid-configuration").body, symbolize_names: true)
JWKS_URI = oidc_config[:jwks_uri]
puts "--- verified output ---"
## from: https://github.com/jwt/ruby-jwt/tree/v2.2.1#json-web-key-jwk
jwk_loader = ->(options) do
## from: https://qiita.com/zakuroishikuro/items/5b08d65d4fef79982f19
pub_keys = JSON.parse(HTTPClient.new.get(JWKS_URI).body, symbolize_names: true)
@cached_keys = nil if options[:invalidate] # need to reload the keys
@cached_keys ||= pub_keys
end
claim,algo = JWT.decode(jwt_text, nil, true, { algorithms: [algo["alg"]], jwks: jwk_loader }) ## algo["alg"] == "RS256"
puts claim,algo
実際には最初のclaim["iss"]が信頼できるサイトか確認する必要があります。
サンプルコードの実行
作成したコードを実行してみます。
$ cat > idtoken.txt
eyJhbGci....
$ bundle exec ruby ./verify-idtoken.rb
正常に処理されれば次のように検証された内容が返されます。
--- unverified output ---
{"iss"=>"https://example.com/dex", "sub"=>"Cj....cA", "aud"=>"example-app", "exp"=>1576040265, ..., "iat"=>1575953865, "name"=>"Yasuhiro Abe"}
{"alg"=>"RS256", "kid"=>"be215b1....460975ac"}
--- verified output ---
{"iss"=>"https://example.com/dex", "sub"=>"Cj....cA", "aud"=>"example-app", "exp"=>1576040265, ..., "iat"=>1575953865, "name"=>"Yasuhiro Abe"}
{"alg"=>"RS256", "kid"=>"be215b1....460975ac"}
idtoken.txt
の内容が古いものだと次のようにkid
に対応する公開鍵がないとエラーになります。
.../lib/ruby/3.3.0/gems/jwt-2.10.1/lib/jwt/jwk/key_finder.rb:25:in `key_for': Could not find public key for kid 69ba8d782bacd99772120aa88153269876ca40f2 (JWT::DecodeError)
from .../lib/ruby/3.3.0/gems/jwt-2.10.1/lib/jwt/decode.rb:63:in `set_key'
from .../lib/ruby/3.3.0/gems/jwt-2.10.1/lib/jwt/decode.rb:34:in `decode_segments'
from .../lib/ruby/3.3.0/gems/jwt-2.10.1/lib/jwt.rb:51:in `block in decode'
from .../lib/ruby/3.3.0/gems/jwt-2.10.1/lib/jwt/deprecations.rb:9:in `context'
from .../lib/ruby/3.3.0/gems/jwt-2.10.1/lib/jwt.rb:50:in `decode'
from ./verify-idtoken.rb:27:in `<main>'