JWT Decoder for Rails APIs
Paste the token your Rails API was handed, read the header and payload inside it, and take away the ruby-jwt call that would verify that same token. The decoding happens in your browser.
The token
This page decodes the token and stops there. Only your Rails app, holding the signing key, can say whether it is genuine.
Header
{
"alg": "HS256",
"typ": "JWT"
}Payload
{
"iss": "https://auth.example.com",
"sub": "user_84213",
"aud": "rails-api",
"jti": "9c1f4a7e-2b80-4d33-9e51-6a0d7f2c18b4",
"iat": 1756000000,
"nbf": 1756000000,
"exp": 1756003600,
"role": "editor"
}Verify this token with ruby-jwt
token = request.authorization.to_s.delete_prefix("Bearer ")
secret = Rails.application.credentials.jwt_hmac_secret
begin
payload, _header = JWT.decode(token, secret, true, {
algorithm: "HS256",
iss: "https://auth.example.com",
verify_iss: true,
aud: "rails-api",
verify_aud: true
})
rescue JWT::ExpiredSignature
return render json: { error: "token_expired" }, status: :unauthorized
rescue JWT::DecodeError
return render json: { error: "token_rejected" }, status: :unauthorized
endPin the algorithm. Left to itself, JWT.decode honours whatever the token's own header asks for, which is how a token signed with none gets accepted.
Registered claims
- Algorithm
- Type
- Issuer
- Subject
- Audience
- Issued at
- Not before
- Expires
- JWT ID
- Signature
Everything runs in your browser. Nothing is uploaded.
What arrives in the Authorization header
A Rails API is handed the token as one line: Authorization: Bearer, then three base64url parts joined with dots. The first part is the header and names the signing algorithm in alg. The second is the payload, a JSON object whose registered claims are the ones every library agrees on - iss for whoever issued it, sub for whoever it is about, aud for whoever is meant to accept it, jti for the token's own identifier, and iat, nbf and exp written as plain seconds since 1 January 1970 rather than as dates. The third part is the signature, raw bytes rather than text, which is why it does not decode into anything readable. Only the registered claims get a row of their own in the table above. A role, a tenant id, anything else your identity provider writes into the token, appears in the payload block and nowhere else, because nothing outside your own app knows what those mean.
Decoding is not verification
Everything above came out of base64, not out of a cipher, so anybody holding the token can read it and this page is doing nothing privileged. What none of it establishes is that the token is genuine. That is JWT.decode(token, key, true, algorithm: "HS256") - the third argument switches verification on, and the fourth pins the algorithm you are willing to accept. Leave either one out and ruby-jwt takes the algorithm from the token's own header, which is the caller's to write. A token whose header says none carries no signature at all and sails through; a token signed with the HMAC of your published RSA public key verifies against a call that will do either. Both are old, both still reach code review, and both are closed by the same two arguments.
Where the key lives in a Rails app
The signing key belongs in Rails.application.credentials, encrypted in the repository and opened by a master key the repository never holds. Reading it from ENV on the server works until somebody prints the environment into a log or a crash report, and a committed initializer or a checked-in .env has no version at which it is safe. Which key you need depends on who signs. If your own app issues the tokens it is usually HS256 with one shared secret, and that one string both signs and verifies, so everything that mints a token and everything that accepts one must hold it. If an identity provider signs them it is usually RS256, you hold only the public half, and that half is not a secret at all - fetch it from the provider's JWKS endpoint and cache it. The difference shows up on rotation: changing an HS256 secret invalidates every token already in circulation the moment the new value is live, so verify against both values for at least the lifetime of one token before dropping the old one.