Ruby JWT gem如何从远程OpenID Connect授权服务器自动获取公钥以验证JWT签名
Great question! I’ve worked through this exact scenario before—replicating that .NET-style automatic public key fetching from an OIDC identity server in Ruby/Rails using the JWT gem. Here are two solid approaches to achieve this:
Option 1: Use the jwks-rsa Gem (Recommended)
The jwks-rsa gem is purpose-built for this use case: it handles fetching public keys from an OIDC server's JWKS endpoint, caching them, and automatically handling key rotation. It integrates seamlessly with the Ruby JWT gem.
Step 1: Add the Gem
Add this to your Gemfile and run bundle install:
gem 'jwks-rsa' gem 'jwt'
Step 2: Implement Token Verification
Here’s how to set it up (in a Rails initializer like config/initializers/jwt.rb, or directly in your authentication logic):
require 'jwt' require 'jwks-rsa' # Initialize the JWKS client with your identity server's details jwks_client = Jwks::RSA.new( jwks_uri: 'https://uri.to.my.identity.server/.well-known/jwks.json', cache: true, cache_ttl: 3600 # Cache keys for 1 hour (adjust as needed) ) # Configure JWT verification options jwt_verification_options = { algorithm: 'RS256', verify_iss: true, iss: 'https://uri.to.my.identity.server', # Match your identity server's issuer verify_iat: true, verify_expiration: true, # Tell JWT gem to use the JWKS client to fetch the appropriate key keyfinder: ->(header) { jwks_client.get_key(header) } } # Example verification method def verify_access_token(token) begin JWT.decode(token, nil, true, jwt_verification_options) rescue JWT::DecodeError => e Rails.logger.error "Token verification failed: #{e.message}" nil end end
This setup automatically:
- Fetches the JWKS from your identity server’s endpoint
- Caches the public keys to avoid repeated network calls
- Looks up the correct public key using the
kidvalue in the JWT header - Handles key rotation (if your identity server updates its keys)
Option 2: Manual Implementation (For Full Control)
If you prefer not to add another gem, you can implement the logic yourself by leveraging the OIDC discovery endpoint and JWKS parsing.
Step 1: Fetch OIDC Discovery Document & JWKS
require 'jwt' require 'net/http' require 'json' # Cache storage for public keys and last fetch time $jwt_public_keys = {} $jwks_last_fetched = nil JWKS_CACHE_TTL = 3600 # 1 hour cache def fetch_jwks_uri(authority) # Get the OIDC discovery document discovery_uri = URI("#{authority}/.well-known/openid-configuration") response = Net::HTTP.get(discovery_uri) JSON.parse(response)['jwks_uri'] end def refresh_jwks_cache(jwks_uri) # Skip refresh if cache is still valid return if $jwks_last_fetched && (Time.now - $jwks_last_fetched) < JWKS_CACHE_TTL # Fetch and parse JWKS response = Net::HTTP.get(URI(jwks_uri)) jwks = JSON.parse(response) # Convert JWKS keys to RSA public keys and cache $jwt_public_keys = jwks['keys'].each_with_object({}) do |key, hash| rsa_key = OpenSSL::PKey::RSA.new(JWT::JWK.new(key).to_pem) hash[key['kid']] = rsa_key end $jwks_last_fetched = Time.now end def verify_jwt(token, authority) begin # Get JWKS URI from discovery endpoint jwks_uri = fetch_jwks_uri(authority) # Refresh cache if needed refresh_jwks_cache(jwks_uri) # Extract kid from JWT header to find matching public key header = JWT.decode(token, nil, false)[1] public_key = $jwt_public_keys[header['kid']] raise "No public key found for kid: #{header['kid']}" unless public_key # Verify the token JWT.decode(token, public_key, true, { algorithm: 'RS256', iss: authority }) rescue => e puts "Token verification error: #{e.message}" nil end end
Key Notes for Manual Implementation:
- Caching is critical: Avoid fetching JWKS on every token verification to prevent performance hits and rate-limiting issues.
- Handle errors: Add error handling for network failures, invalid discovery documents, or missing keys.
- Key rotation: The cache TTL ensures you’ll pick up new keys when your identity server rotates them.
Final Recommendations
Stick with the jwks-rsa gem for most cases—it’s maintained, handles edge cases you might miss (like key rotation and cache invalidation), and keeps your code clean. Only opt for the manual approach if you need full control over every step of the process.
内容的提问来源于stack exchange,提问作者user888734

