Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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 [#722](https://github.com/jwt/ruby-jwt/pull/722) ([@anakinj](https://github.com/anakinj))
Comment thread
anakinj marked this conversation as resolved.
Outdated
- 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

Comment thread
anakinj marked this conversation as resolved.
@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/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
Comment thread
anakinj marked this conversation as resolved.

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
2 changes: 1 addition & 1 deletion spec/integration/readme_examples_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Comment thread
anakinj marked this conversation as resolved.
end

it 'works as expected' do
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