Error message
Signature verification failed (JWT::VerificationError)
Also printed as
- JWT::VerificationError: Signature verification failed
Uncaught, Ruby prints the location followed by "Signature verification failed (JWT::VerificationError)"; the text is the same in ruby-jwt 2.10 and 3.3.
ruby-jwt (Ruby) — JWT::VerificationError from JWT.decode
What it means
ruby-jwt decoded the token and checked its signature with the key you passed, and the check failed. The token is well formed and the algorithm is allowed; the key does not belong to the signer, or the token was altered. `JWT::VerificationError` is a subclass of `JWT::DecodeError`, so `rescue JWT::DecodeError` catches it together with malformed-token errors.
Why it happens
The configured key is not the signer's
commonA public key from another environment, an outdated key after rotation, or a key pair regenerated on every boot in a development setup that leaked into production.
Verifying with the wrong key of a JWKS
occasionalTaking `keys.first` from an issuer's JWKS works until the issuer signs with another key. Let ruby-jwt select by `kid` with the `jwks:` option.
Different HMAC secrets
occasionalRails apps often derive the secret from `secret_key_base`; a service that uses another value — or the same value with a trailing newline — computes a different MAC.
How to fix it
- 1.
Select keys by kid from the issuer's JWKS
The `jwks:` option accepts a loader; ruby-jwt calls it again with `kid_not_found: true` when the token names an unknown key, so rotations are picked up.
ruby require "jwt" require "net/http" jwks_loader = ->(options) do @jwks = nil if options[:kid_not_found] && @jwks_loaded_at.to_i < Time.now.to_i - 300 # refetch on rotation @jwks ||= begin @jwks_loaded_at = Time.now.to_i JSON.parse(Net::HTTP.get(URI("https://issuer.example.com/.well-known/jwks.json"))) end end payload, header = JWT.decode(token, nil, true, algorithms: ["ES256"], jwks: jwks_loader, iss: "https://issuer.example.com/", verify_iss: true) - 2.
Prove the keys match
Identical hashes of the public halves rule out the key; then look for modifications of the token in transit.
bash openssl pkey -in issuer-private.pem -pubout | sha256sum openssl pkey -pubin -in configured-public.pem | sha256sum # must be identical