require "jwt" # gem install jwt -v 3.3.0require "net/http"require "json" class AccessTokenVerifier JWKS_URI = URI("https://issuer.example.com/.well-known/jwks.json") def verify(token) payload, _header = JWT.decode(token, nil, true, algorithms: ["ES256"], # never take the algorithm from the token jwks: method(:load_jwks), iss: "https://issuer.example.com", verify_iss: true, aud: "api://orders", verify_aud: true, required_claims: ["exp"]) payload end private # ruby-jwt calls this to look up keys by kid; when a kid is missing it calls it # again with kid_not_found: true. Refetch then, at most once every 5 minutes. def load_jwks(options) @jwks = nil if options[:kid_not_found] && @fetched_at.to_i < Time.now.to_i - 300 @jwks ||= begin @fetched_at = Time.now.to_i keys = JWT::JWK::Set.new(JSON.parse(Net::HTTP.get(JWKS_URI))) keys.reject! { |key| key[:use] == "enc" } # "use" is optional: drop only encryption keys keys end endendverifier = AccessTokenVerifier.newFile.foreach("tokens.txt") do |line| label, token = line.split begin claims = verifier.verify(token) puts "#{label}: accepted, sub=#{claims["sub"]}" rescue JWT::DecodeError => e # base class of every verification error puts "#{label}: rejected: #{e.class}: #{e.message}" endendHow it works
jwks:accepts aJWT::JWK::Setor anything callable; a callable lets the class cache the set and decide when to refetch.- On a missing
kid, the gem calls the loader again withkid_not_found: true; the loader refetches only if the cache is older than 5 minutes. algorithms: ["ES256"]pins the algorithm;iss:withverify_iss: true,aud:withverify_aud: trueandrequired_claims:define a valid token.- All verification errors inherit from
JWT::DecodeError, with specific classes such asJWT::InvalidAudErrorandJWT::ExpiredSignature.
Gotchas
- Audience and issuer are checked only when asked: without
aud:andverify_aud: true, the token forapi://billingis accepted. - The filter in many examples,
select! { |key| key[:use] == "sig" }, removes every key when the JWKS omitsuse(which is optional) — then every token fails. Drop only keys markedenc, as above. - Tokens without
kidare rejected whenjwks:is used, unlessallow_nil_kid: trueis passed. Net::HTTP.getwaits up to 60 seconds to connect and 60 seconds per read. UseNet::HTTP.startwith shortopen_timeoutandread_timeoutvalues, so a slow issuer cannot stall every request that needs a refetch.