Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

**Features:**

- Revamp error hierarchy: introduce `JWT::Error`, `JWT::TokenError`, `JWT::MalformedTokenError`, `JWT::SignatureError`, and `JWT::ClaimValidationError` grouping classes. `JWT::DecodeError` is now a deprecated alias for `JWT::Error` [#722](https://github.com/jwt/ruby-jwt/pull/722) ([@anakinj](https://github.com/anakinj))
- Add `enforce_hmac_key_length` configuration option [#716](https://github.com/jwt/ruby-jwt/pull/716) - ([@304](https://github.com/304))
- Your contribution here

Expand Down
2 changes: 1 addition & 1 deletion lib/jwt/claims.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class << self
# @param payload [Hash] the JWT payload.
# @param options [Array] the options for verifying the claims.
# @return [void]
# @raise [JWT::DecodeError] if any claim is invalid.
# @raise [JWT::ClaimValidationError] if any claim is invalid.
def verify_payload!(payload, *options)
Verifier.verify!(VerificationContext.new(payload: payload), *options)
end
Expand Down
2 changes: 1 addition & 1 deletion lib/jwt/claims/verifier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def errors(context, *options)
errors = []
iterate_verifiers(*options) do |verifier, verifier_options|
verify_one!(context, verifier, verifier_options)
rescue ::JWT::DecodeError => e
rescue ::JWT::ClaimValidationError => e
errors << Error.new(message: e.message)
end
errors
Expand Down
12 changes: 6 additions & 6 deletions lib/jwt/decode.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ class Decode
# @param verify [Boolean] whether to verify the token's signature.
# @param options [Hash] additional options for decoding and verification.
# @param keyfinder [Proc] an optional key finder block to dynamically find the key for verification.
# @raise [JWT::DecodeError] if decoding or verification fails.
# @raise [JWT::Error] if decoding or verification fails.
def initialize(jwt, key, verify, options, &keyfinder)
raise JWT::DecodeError, 'Nil JSON web token' unless jwt
raise JWT::MalformedTokenError, 'Nil JSON web token' unless jwt

@token = EncodedToken.new(jwt)
@key = key
Expand Down Expand Up @@ -51,14 +51,14 @@ def decode_segments
def verify_signature
return if none_algorithm?

raise JWT::DecodeError, 'No verification key available' unless @key
raise JWT::SignatureError, 'No verification key available' unless @key

token.verify_signature!(algorithm: allowed_and_valid_algorithms, key: @key)
end

def verify_algo
raise JWT::IncorrectAlgorithm, 'An algorithm must be specified' if allowed_algorithms.empty?
raise JWT::DecodeError, 'Token header not a JSON object' unless valid_token_header?
raise JWT::MalformedTokenError, 'Token header not a JSON object' unless valid_token_header?
raise JWT::IncorrectAlgorithm, 'Token is missing alg header' unless alg_in_header
raise JWT::IncorrectAlgorithm, 'Expected a different algorithm' if allowed_and_valid_algorithms.empty?
end
Expand Down Expand Up @@ -100,7 +100,7 @@ def find_key(&keyfinder)
# key can be of type [string, nil, OpenSSL::PKey, Array]
return key if key && !Array(key).empty?

raise JWT::DecodeError, 'No verification key available'
raise JWT::SignatureError, 'No verification key available'
end

def validate_segment_count!
Expand All @@ -109,7 +109,7 @@ def validate_segment_count!
return if !@verify && segment_count == 2 # If no verifying required, the signature is not needed
return if segment_count == 2 && none_algorithm?

raise JWT::DecodeError, 'Not enough or too many segments'
raise JWT::MalformedTokenError, 'Not enough or too many segments'
end

def none_algorithm?
Expand Down
14 changes: 7 additions & 7 deletions lib/jwt/encoded_token.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ def header
# Returns the payload of the JWT token. Access requires the signature and claims to have been verified.
#
# @return [Hash] the payload.
# @raise [JWT::DecodeError] if the signature has not been verified.
# @raise [JWT::TokenError] if the signature has not been verified.
def payload
raise JWT::DecodeError, 'Verify the token signature before accessing the payload' unless @signature_verified
raise JWT::DecodeError, 'Verify the token claims before accessing the payload' unless @claims_verified
raise JWT::TokenError, 'Verify the token signature before accessing the payload' unless @signature_verified
raise JWT::TokenError, 'Verify the token claims before accessing the payload' unless @claims_verified

decoded_payload
end
Expand Down Expand Up @@ -98,7 +98,7 @@ def signing_input
# @param signature [Hash] the parameters for signature verification (see {#verify_signature!}).
# @param claims [Array<Symbol>, Hash] the claims to verify (see {#verify_claims!}).
# @return [nil]
# @raise [JWT::DecodeError] if the signature or claim verification fails.
# @raise [JWT::Error] if the signature or claim verification fails.
def verify!(signature:, claims: nil)
verify_signature!(**signature)
claims.is_a?(Array) ? verify_claims!(*claims) : verify_claims!(claims)
Expand Down Expand Up @@ -152,7 +152,7 @@ def valid_signature?(algorithm: nil, key: nil, key_finder: nil)

# Verifies the claims of the token.
# @param options [Array<Symbol>, Hash] the claims to verify. By default, it checks the 'exp' claim.
# @raise [JWT::DecodeError] if the claims are invalid.
# @raise [JWT::ClaimValidationError] if the claims are invalid.
def verify_claims!(*options)
Claims::Verifier.verify!(ClaimsContext.new(self), *claims_options(options)).tap do
@claims_verified = true
Expand Down Expand Up @@ -187,7 +187,7 @@ def claims_options(options)
end

def decode_payload
raise JWT::DecodeError, 'Encoded payload is empty' if encoded_payload == ''
raise JWT::MalformedTokenError, 'Encoded payload is empty' if encoded_payload == ''

if unencoded_payload?
verify_claims!(crit: ['b64'])
Expand All @@ -212,7 +212,7 @@ def parse_unencoded(segment)
def parse(segment)
JWT::JSON.parse(segment)
rescue ::JSON::ParserError
raise JWT::DecodeError, 'Invalid segment encoding'
raise JWT::MalformedTokenError, 'Invalid segment encoding'
end

def decoded_payload
Expand Down
63 changes: 39 additions & 24 deletions lib/jwt/error.rb
Original file line number Diff line number Diff line change
@@ -1,54 +1,69 @@
# frozen_string_literal: true

module JWT
# The base error class for all JWT errors.
class Error < StandardError; end

# The EncodeError class is raised when there is an error encoding a JWT.
class EncodeError < StandardError; end
class EncodeError < Error; end

# The DecodeError class is raised when there is an error decoding a JWT.
class DecodeError < StandardError; end
# The TokenError class is the base class for all errors related to token processing.
class TokenError < Error; end

# The VerificationError class is raised when there is an error verifying a JWT.
class VerificationError < DecodeError; end
# The MalformedTokenError class is raised when the token is structurally invalid.
class MalformedTokenError < TokenError; end

# The ExpiredSignature class is raised when the JWT signature has expired.
class ExpiredSignature < DecodeError; end
# The Base64DecodeError class is raised when there is an error decoding a Base64-encoded string.
class Base64DecodeError < MalformedTokenError; end

# The IncorrectAlgorithm class is raised when the JWT algorithm is incorrect.
class IncorrectAlgorithm < DecodeError; end
# The SignatureError class is the base class for signature and algorithm related errors.
class SignatureError < TokenError; end

# The ImmatureSignature class is raised when the JWT signature is immature.
class ImmatureSignature < DecodeError; end
# The VerificationError class is raised when there is an error verifying a JWT signature.
class VerificationError < SignatureError; end

# The InvalidIssuerError class is raised when the JWT issuer is invalid.
class InvalidIssuerError < DecodeError; end
# The IncorrectAlgorithm class is raised when the JWT algorithm is incorrect.
class IncorrectAlgorithm < SignatureError; end

# The UnsupportedEcdsaCurve class is raised when the ECDSA curve is unsupported.
class UnsupportedEcdsaCurve < IncorrectAlgorithm; end

# The ClaimValidationError class is the base class for all claim validation errors.
class ClaimValidationError < TokenError; end

# The ExpiredSignature class is raised when the JWT token has expired.
class ExpiredSignature < ClaimValidationError; end

# The ImmatureSignature class is raised when the JWT token is not yet valid (nbf).
class ImmatureSignature < ClaimValidationError; end

# The InvalidIssuerError class is raised when the JWT issuer is invalid.
class InvalidIssuerError < ClaimValidationError; end

# The InvalidIatError class is raised when the JWT issued at (iat) claim is invalid.
class InvalidIatError < DecodeError; end
class InvalidIatError < ClaimValidationError; end

# The InvalidAudError class is raised when the JWT audience (aud) claim is invalid.
class InvalidAudError < DecodeError; end
class InvalidAudError < ClaimValidationError; end

# The InvalidSubError class is raised when the JWT subject (sub) claim is invalid.
class InvalidSubError < DecodeError; end
class InvalidSubError < ClaimValidationError; end

# The InvalidCritError class is raised when the JWT crit header is invalid.
class InvalidCritError < DecodeError; end
class InvalidCritError < ClaimValidationError; end

# The InvalidJtiError class is raised when the JWT ID (jti) claim is invalid.
class InvalidJtiError < DecodeError; end
class InvalidJtiError < ClaimValidationError; end

# The InvalidPayload class is raised when the JWT payload is invalid.
class InvalidPayload < DecodeError; end
class InvalidPayload < ClaimValidationError; end

# The MissingRequiredClaim class is raised when a required claim is missing from the JWT.
class MissingRequiredClaim < DecodeError; end

# The Base64DecodeError class is raised when there is an error decoding a Base64-encoded string.
class Base64DecodeError < DecodeError; end
class MissingRequiredClaim < ClaimValidationError; end

# The JWKError class is raised when there is an error with the JSON Web Key (JWK).
class JWKError < DecodeError; end
class JWKError < Error; end

# @deprecated Use {JWT::Error}, {JWT::TokenError}, or a more specific error class instead.
DecodeError = Error
end
2 changes: 1 addition & 1 deletion lib/jwt/jwa.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def resolve_and_sort(algorithms:, preferred_algorithm:)
# @api private
def create_signer(algorithm:, key:)
if key.is_a?(JWK::KeyBase)
validate_jwk_algorithms!(key, algorithm, DecodeError)
validate_jwk_algorithms!(key, algorithm, EncodeError)

return key
end
Expand Down
2 changes: 1 addition & 1 deletion lib/jwt/jwa/signing_algorithm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def verify(*)
end

def raise_verify_error!(message)
raise(DecodeError.new(message).tap { |e| e.set_backtrace(caller(1)) })
raise(VerificationError.new(message).tap { |e| e.set_backtrace(caller(1)) })
end

def raise_sign_error!(message)
Expand Down
8 changes: 4 additions & 4 deletions lib/jwt/jwk/key_finder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ def initialize(options)
# Returns the verification key for the given kid
# @param [String] kid the key id
def key_for(kid, key_field = :kid)
raise ::JWT::DecodeError, "Invalid type for #{key_field} header parameter" unless kid.nil? || kid.is_a?(String)
raise ::JWT::MalformedTokenError, "Invalid type for #{key_field} header parameter" unless kid.nil? || kid.is_a?(String)

jwk = resolve_key(kid, key_field)

raise ::JWT::DecodeError, 'No keys found in jwks' unless @jwks.any?
raise ::JWT::DecodeError, "Could not find public key for kid #{kid}" unless jwk
raise ::JWT::SignatureError, 'No keys found in jwks' unless @jwks.any?
raise ::JWT::SignatureError, "Could not find public key for kid #{kid}" unless jwk

jwk.verify_key
end
Expand All @@ -47,7 +47,7 @@ def call(token)
return key_for(field_value, key_field) if field_value
end

raise ::JWT::DecodeError, 'No key id (kid) or x5t found from token headers' unless @allow_nil_kid
raise ::JWT::SignatureError, 'No key id (kid) or x5t found from token headers' unless @allow_nil_kid

kid = token.header['kid']
key_for(kid)
Expand Down
2 changes: 1 addition & 1 deletion lib/jwt/token.rb
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def sign!(key:, algorithm:)

# Verifies the claims of the token.
# @param options [Array<Symbol>, Hash] the claims to verify.
# @raise [JWT::DecodeError] if the claims are invalid.
# @raise [JWT::ClaimValidationError] if the claims are invalid.
def verify_claims!(*options)
Claims::Verifier.verify!(self, *options)
end
Expand Down
10 changes: 5 additions & 5 deletions spec/integration/readme_examples_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -316,8 +316,8 @@
JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader })
rescue JWT::JWKError
# Handle problems with the provided JWKs
rescue JWT::DecodeError
# Handle other decode related issues e.g. no kid in header, no matching public key found etc.
rescue JWT::TokenError
# Handle other token related issues e.g. no kid in header, no matching public key found etc.
end

## This is not in the example but verifies that the cache is invalidated after 5 minutes
Expand All @@ -334,7 +334,7 @@
jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), 'yet-another-new-kid')
headers = { kid: jwk.kid }
token = JWT.encode(payload, jwk.signing_key, 'RS512', headers)
expect { JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader }) }.to raise_error(JWT::DecodeError, 'Could not find public key for kid yet-another-new-kid')
expect { JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader }) }.to raise_error(JWT::SignatureError, 'Could not find public key for kid yet-another-new-kid')
end

it 'works as expected' do
Expand Down Expand Up @@ -367,8 +367,8 @@
JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwks_loader })
rescue JWT::JWKError
# Handle problems with the provided JWKs
rescue JWT::DecodeError
# Handle other decode related issues e.g. no kid in header, no matching public key found etc.
rescue JWT::TokenError
# Handle other token related issues e.g. no kid in header, no matching public key found etc.
end
end
end
Expand Down
24 changes: 12 additions & 12 deletions spec/jwt/encoded_token_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

context 'when payload is not provided' do
it 'raises decode error' do
expect { token.unverified_payload }.to raise_error(JWT::DecodeError, 'Encoded payload is empty')
expect { token.unverified_payload }.to raise_error(JWT::MalformedTokenError, 'Encoded payload is empty')
end
end
end
Expand All @@ -45,7 +45,7 @@
let(:encoded_token) { '' }

it 'raises decode error' do
expect { token.unverified_payload }.to raise_error(JWT::DecodeError, 'Invalid segment encoding')
expect { token.unverified_payload }.to raise_error(JWT::MalformedTokenError, 'Invalid segment encoding')
end
end
end
Expand Down Expand Up @@ -81,21 +81,21 @@
before { token.verify_signature!(algorithm: 'HS256', key: 'secret') }

it 'raises an error' do
expect { token.payload }.to raise_error(JWT::DecodeError, 'Verify the token claims before accessing the payload')
expect { token.payload }.to raise_error(JWT::TokenError, 'Verify the token claims before accessing the payload')
end
end

context 'when token is verified using #valid_signature? but is not valid' do
before { token.valid_signature?(algorithm: 'HS256', key: 'wrong') }

it 'raises an error' do
expect { token.payload }.to raise_error(JWT::DecodeError, 'Verify the token signature before accessing the payload')
expect { token.payload }.to raise_error(JWT::TokenError, 'Verify the token signature before accessing the payload')
end
end

context 'when token is not verified' do
it 'raises an error' do
expect { token.payload }.to raise_error(JWT::DecodeError, 'Verify the token signature before accessing the payload')
expect { token.payload }.to raise_error(JWT::TokenError, 'Verify the token signature before accessing the payload')
end
end
end
Expand All @@ -107,7 +107,7 @@
let(:encoded_token) { '' }

it 'raises decode error' do
expect { token.header }.to raise_error(JWT::DecodeError, 'Invalid segment encoding')
expect { token.header }.to raise_error(JWT::MalformedTokenError, 'Invalid segment encoding')
end
end
end
Expand Down Expand Up @@ -308,7 +308,7 @@
end
context 'when payload is not provided' do
it 'raises decode error' do
expect { token.verify_claims!(:exp, :nbf) }.to raise_error(JWT::DecodeError, 'Encoded payload is empty')
expect { token.verify_claims!(:exp, :nbf) }.to raise_error(JWT::MalformedTokenError, 'Encoded payload is empty')
end
end
end
Expand Down Expand Up @@ -451,24 +451,24 @@
expect(token.unverified_payload).to eq({ 'pay' => 'load' })
expect(token.header).to eq({ 'alg' => 'HS256' })

expect { token.payload }.to raise_error(JWT::DecodeError, 'Verify the token signature before accessing the payload')
expect { token.payload }.to raise_error(JWT::TokenError, 'Verify the token signature before accessing the payload')

expect(token.valid_signature?(algorithm: 'HS256', key: 'invalid_signing_key')).to be(false)
expect { token.payload }.to raise_error(JWT::DecodeError, 'Verify the token signature before accessing the payload')
expect { token.payload }.to raise_error(JWT::TokenError, 'Verify the token signature before accessing the payload')

expect(token.valid_signature?(algorithm: 'HS256', key: 'secret_signing_key')).to be(true)
expect { token.payload }.to raise_error(JWT::DecodeError, 'Verify the token claims before accessing the payload')
expect { token.payload }.to raise_error(JWT::TokenError, 'Verify the token claims before accessing the payload')

expect(token.valid_claims?(iss: 'issuer')).to be(false)
expect { token.payload }.to raise_error(JWT::DecodeError, 'Verify the token claims before accessing the payload')
expect { token.payload }.to raise_error(JWT::TokenError, 'Verify the token claims before accessing the payload')

expect(token.valid_claims?).to be(true)
expect(token.payload).to eq({ 'pay' => 'load' })

token = described_class.new(encoded_token)

expect(token.valid?(signature: { algorithm: 'HS256', key: 'invalid_signing_key' })).to be(false)
expect { token.payload }.to raise_error(JWT::DecodeError, 'Verify the token signature before accessing the payload')
expect { token.payload }.to raise_error(JWT::TokenError, 'Verify the token signature before accessing the payload')

expect(token.valid?(signature: { algorithm: 'HS256', key: 'secret_signing_key' })).to be(true)
expect(token.payload).to eq({ 'pay' => 'load' })
Expand Down
Loading
Loading