diff --git a/CHANGELOG.md b/CHANGELOG.md index 84239f87b..c24e2ca0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/jwt/claims.rb b/lib/jwt/claims.rb index 45ed547b1..eb2ce14fb 100644 --- a/lib/jwt/claims.rb +++ b/lib/jwt/claims.rb @@ -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 diff --git a/lib/jwt/claims/verifier.rb b/lib/jwt/claims/verifier.rb index 81ce8a23d..2785cbb75 100644 --- a/lib/jwt/claims/verifier.rb +++ b/lib/jwt/claims/verifier.rb @@ -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 diff --git a/lib/jwt/decode.rb b/lib/jwt/decode.rb index e6b8e74dd..50a34fce4 100644 --- a/lib/jwt/decode.rb +++ b/lib/jwt/decode.rb @@ -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 @@ -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 @@ -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! @@ -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? diff --git a/lib/jwt/encoded_token.rb b/lib/jwt/encoded_token.rb index 214981df6..7c6bd9cd4 100644 --- a/lib/jwt/encoded_token.rb +++ b/lib/jwt/encoded_token.rb @@ -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 @@ -98,7 +98,7 @@ def signing_input # @param signature [Hash] the parameters for signature verification (see {#verify_signature!}). # @param claims [Array, 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) @@ -152,7 +152,7 @@ def valid_signature?(algorithm: nil, key: nil, key_finder: nil) # Verifies the claims of the token. # @param options [Array, 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 @@ -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']) @@ -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 diff --git a/lib/jwt/error.rb b/lib/jwt/error.rb index 2a0f8a2ce..9f056897a 100644 --- a/lib/jwt/error.rb +++ b/lib/jwt/error.rb @@ -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 diff --git a/lib/jwt/jwa.rb b/lib/jwt/jwa.rb index e5c051988..78cac30df 100644 --- a/lib/jwt/jwa.rb +++ b/lib/jwt/jwa.rb @@ -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 diff --git a/lib/jwt/jwa/signing_algorithm.rb b/lib/jwt/jwa/signing_algorithm.rb index b4590a8b0..b9fac2053 100644 --- a/lib/jwt/jwa/signing_algorithm.rb +++ b/lib/jwt/jwa/signing_algorithm.rb @@ -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) diff --git a/lib/jwt/jwk/key_finder.rb b/lib/jwt/jwk/key_finder.rb index c7387841e..6fd18bb8e 100644 --- a/lib/jwt/jwk/key_finder.rb +++ b/lib/jwt/jwk/key_finder.rb @@ -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 @@ -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) diff --git a/lib/jwt/token.rb b/lib/jwt/token.rb index 0c643886f..f6cd7a4df 100644 --- a/lib/jwt/token.rb +++ b/lib/jwt/token.rb @@ -104,7 +104,7 @@ def sign!(key:, algorithm:) # Verifies the claims of the token. # @param options [Array, 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 diff --git a/spec/integration/readme_examples_spec.rb b/spec/integration/readme_examples_spec.rb index ea9f779c5..8405db4ab 100644 --- a/spec/integration/readme_examples_spec.rb +++ b/spec/integration/readme_examples_spec.rb @@ -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 @@ -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 @@ -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 diff --git a/spec/jwt/encoded_token_spec.rb b/spec/jwt/encoded_token_spec.rb index ce38a231c..ec11a6ee5 100644 --- a/spec/jwt/encoded_token_spec.rb +++ b/spec/jwt/encoded_token_spec.rb @@ -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 @@ -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 @@ -81,7 +81,7 @@ 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 @@ -89,13 +89,13 @@ 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 @@ -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 @@ -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 @@ -451,16 +451,16 @@ 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' }) @@ -468,7 +468,7 @@ 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' }) diff --git a/spec/jwt/error_spec.rb b/spec/jwt/error_spec.rb new file mode 100644 index 000000000..414ac67e4 --- /dev/null +++ b/spec/jwt/error_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +RSpec.describe 'JWT error hierarchy' do + context 'base classes' do + it 'JWT::Error inherits from StandardError' do + expect(JWT::Error).to be < StandardError + end + + it 'JWT::EncodeError inherits from JWT::Error' do + expect(JWT::EncodeError).to be < JWT::Error + end + + it 'JWT::TokenError inherits from JWT::Error' do + expect(JWT::TokenError).to be < JWT::Error + end + end + + context 'backwards compatibility' do + it 'JWT::DecodeError is an alias for JWT::Error' do + expect(JWT::DecodeError).to eq(JWT::Error) + end + end + + context 'malformed token errors' do + it 'JWT::MalformedTokenError inherits from JWT::TokenError' do + expect(JWT::MalformedTokenError).to be < JWT::TokenError + end + + it 'JWT::Base64DecodeError inherits from JWT::MalformedTokenError' do + expect(JWT::Base64DecodeError).to be < JWT::MalformedTokenError + end + end + + context 'signature errors' do + it 'JWT::SignatureError inherits from JWT::TokenError' do + expect(JWT::SignatureError).to be < JWT::TokenError + end + + it 'JWT::VerificationError inherits from JWT::SignatureError' do + expect(JWT::VerificationError).to be < JWT::SignatureError + end + + it 'JWT::IncorrectAlgorithm inherits from JWT::SignatureError' do + expect(JWT::IncorrectAlgorithm).to be < JWT::SignatureError + end + + it 'JWT::UnsupportedEcdsaCurve inherits from JWT::IncorrectAlgorithm' do + expect(JWT::UnsupportedEcdsaCurve).to be < JWT::IncorrectAlgorithm + end + end + + context 'claim validation errors' do + it 'JWT::ClaimValidationError inherits from JWT::TokenError' do + expect(JWT::ClaimValidationError).to be < JWT::TokenError + end + + %i[ + ExpiredSignature + ImmatureSignature + InvalidIssuerError + InvalidIatError + InvalidAudError + InvalidSubError + InvalidCritError + InvalidJtiError + InvalidPayload + MissingRequiredClaim + ].each do |error_class| + it "JWT::#{error_class} inherits from JWT::ClaimValidationError" do + expect(JWT.const_get(error_class)).to be < JWT::ClaimValidationError + end + end + end + + context 'JWK errors' do + it 'JWT::JWKError inherits from JWT::Error' do + expect(JWT::JWKError).to be < JWT::Error + end + end + + context 'error groups do not overlap' do + it 'claim validation errors are not signature errors' do + expect(JWT::ClaimValidationError).not_to be <= JWT::SignatureError + expect(JWT::SignatureError).not_to be <= JWT::ClaimValidationError + end + + it 'claim validation errors are not malformed token errors' do + expect(JWT::ClaimValidationError).not_to be <= JWT::MalformedTokenError + expect(JWT::MalformedTokenError).not_to be <= JWT::ClaimValidationError + end + + it 'signature errors are not malformed token errors' do + expect(JWT::SignatureError).not_to be <= JWT::MalformedTokenError + expect(JWT::MalformedTokenError).not_to be <= JWT::SignatureError + end + end +end diff --git a/spec/jwt/jwa/ecdsa_spec.rb b/spec/jwt/jwa/ecdsa_spec.rb index 4a14d6de7..2d56d858a 100644 --- a/spec/jwt/jwa/ecdsa_spec.rb +++ b/spec/jwt/jwa/ecdsa_spec.rb @@ -58,10 +58,10 @@ end context 'when the verification key is not an OpenSSL::PKey::EC instance' do - it 'raises a JWT::DecodeError' do + it 'raises a JWT::VerificationError' do expect do instance.verify(data: data, signature: '', verification_key: 'not_a_key') - end.to raise_error(JWT::DecodeError, 'The given key is a String. It has to be an OpenSSL::PKey::EC instance') + end.to raise_error(JWT::VerificationError, 'The given key is a String. It has to be an OpenSSL::PKey::EC instance') end end @@ -82,7 +82,7 @@ end context 'when the signing key is a public key' do - it 'raises a JWT::DecodeError' do + it 'raises a JWT::VerificationError' do public_key = test_pkey('ec256-public.pem') expect do instance.sign(data: data, signing_key: public_key) @@ -91,7 +91,7 @@ end context 'when the signing key is not an OpenSSL::PKey::EC instance' do - it 'raises a JWT::DecodeError' do + it 'raises a JWT::VerificationError' do expect do instance.sign(data: data, signing_key: 'not_a_key') end.to raise_error(JWT::EncodeError, 'The given key is a String. It has to be an OpenSSL::PKey::EC instance') @@ -99,11 +99,11 @@ end context 'when the signing key is invalid' do - it 'raises a JWT::DecodeError' do + it 'raises a JWT::UnsupportedEcdsaCurve' do invalid_key = OpenSSL::PKey::EC.generate('sect571r1') expect do instance.sign(data: data, signing_key: invalid_key) - end.to raise_error(JWT::DecodeError, "The ECDSA curve 'sect571r1' is not supported") + end.to raise_error(JWT::UnsupportedEcdsaCurve, "The ECDSA curve 'sect571r1' is not supported") end end end diff --git a/spec/jwt/jwa/hmac_spec.rb b/spec/jwt/jwa/hmac_spec.rb index b499db165..88da2f0e2 100644 --- a/spec/jwt/jwa/hmac_spec.rb +++ b/spec/jwt/jwa/hmac_spec.rb @@ -20,8 +20,8 @@ allow(OpenSSL::HMAC).to receive(:digest).and_raise(OpenSSL::HMACError.new('EVP_PKEY_new_mac_key: malloc failure')) end - it 'raises JWT::DecodeError' do - expect { subject }.to raise_error(JWT::DecodeError, 'OpenSSL 3.0 does not support nil or empty hmac_secret') + it 'raises JWT::VerificationError' do + expect { subject }.to raise_error(JWT::VerificationError, 'OpenSSL 3.0 does not support nil or empty hmac_secret') end end @@ -52,8 +52,8 @@ allow(OpenSSL::HMAC).to receive(:digest).and_raise(OpenSSL::HMACError.new('EVP_PKEY_new_mac_key: malloc failure')) end - it 'raises JWT::DecodeError' do - expect { subject }.to raise_error(JWT::DecodeError, 'OpenSSL 3.0 does not support nil or empty hmac_secret') + it 'raises JWT::VerificationError' do + expect { subject }.to raise_error(JWT::VerificationError, 'OpenSSL 3.0 does not support nil or empty hmac_secret') end end @@ -122,7 +122,7 @@ let(:hmac_secret) { 'short' } it 'raises error' do - expect { subject }.to raise_error(JWT::DecodeError, 'HMAC key must be at least 32 bytes for HS256 algorithm') + expect { subject }.to raise_error(JWT::VerificationError, 'HMAC key must be at least 32 bytes for HS256 algorithm') end end @@ -156,7 +156,7 @@ let(:hmac_secret) { 123 } it 'raises error' do - expect { subject }.to raise_error(JWT::DecodeError, 'HMAC key expected to be a String') + expect { subject }.to raise_error(JWT::VerificationError, 'HMAC key expected to be a String') end end @@ -175,7 +175,7 @@ let(:hmac_secret) { 'short' } it 'raises error' do - expect { subject }.to raise_error(JWT::DecodeError, 'HMAC key must be at least 32 bytes for HS256 algorithm') + expect { subject }.to raise_error(JWT::VerificationError, 'HMAC key must be at least 32 bytes for HS256 algorithm') end end diff --git a/spec/jwt/jwk/decode_with_jwk_spec.rb b/spec/jwt/jwk/decode_with_jwk_spec.rb index 314f47bca..129e05ce4 100644 --- a/spec/jwt/jwk/decode_with_jwk_spec.rb +++ b/spec/jwt/jwk/decode_with_jwk_spec.rb @@ -34,7 +34,7 @@ end it 'raises an exception' do expect { described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: public_jwks }) }.to raise_error( - JWT::DecodeError, /Could not find public key for kid .*/ + JWT::SignatureError, /Could not find public key for kid .*/ ) end end @@ -63,7 +63,7 @@ let(:public_jwks) { { keys: [] } } it 'raises an exception' do expect { described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: public_jwks }) }.to raise_error( - JWT::DecodeError, /No keys found in jwks/ + JWT::SignatureError, /No keys found in jwks/ ) end end @@ -72,7 +72,7 @@ let(:token_headers) { {} } it 'raises an exception' do expect { described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: public_jwks }) }.to raise_error( - JWT::DecodeError, 'No key id (kid) or x5t found from token headers' + JWT::SignatureError, 'No key id (kid) or x5t found from token headers' ) end end @@ -116,7 +116,7 @@ let(:token_headers) { { kid: 5 } } it 'raises an exception' do expect { described_class.decode(signed_token, nil, true, { algorithms: ['RS512'], jwks: public_jwks }) }.to raise_error( - JWT::DecodeError, 'Invalid type for kid header parameter' + JWT::MalformedTokenError, 'Invalid type for kid header parameter' ) end end @@ -131,16 +131,16 @@ context 'when RSA key is pointed to as HMAC secret' do let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, 'is not really relevant in the scenario', 'HS256', { kid: rsa_jwk.kid }) } - it 'raises JWT::DecodeError' do - expect { described_class.decode(signed_token, nil, true, algorithms: ['HS256'], jwks: jwks) }.to raise_error(JWT::DecodeError, 'HMAC key expected to be a String') + it 'raises JWT::VerificationError' do + expect { described_class.decode(signed_token, nil, true, algorithms: ['HS256'], jwks: jwks) }.to raise_error(JWT::VerificationError, 'HMAC key expected to be a String') end end context 'when EC key is pointed to as HMAC secret' do let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, 'is not really relevant in the scenario', 'HS256', { kid: ec_jwk_secp384r1.kid }) } - it 'raises JWT::DecodeError' do - expect { described_class.decode(signed_token, nil, true, algorithms: ['HS256'], jwks: jwks) }.to raise_error(JWT::DecodeError, 'HMAC key expected to be a String') + it 'raises JWT::VerificationError' do + expect { described_class.decode(signed_token, nil, true, algorithms: ['HS256'], jwks: jwks) }.to raise_error(JWT::VerificationError, 'HMAC key expected to be a String') end end @@ -169,7 +169,7 @@ it 'fails in some way' do expect { described_class.decode(signed_token, nil, true, algorithms: ['ES384'], jwks: jwks) }.to( - raise_error(JWT::DecodeError, 'The given key is a String. It has to be an OpenSSL::PKey::EC instance') + raise_error(JWT::VerificationError, 'The given key is a String. It has to be an OpenSSL::PKey::EC instance') ) end end diff --git a/spec/jwt/jwk/ec_spec.rb b/spec/jwt/jwk/ec_spec.rb index 86349ef87..0f893ce2c 100644 --- a/spec/jwt/jwk/ec_spec.rb +++ b/spec/jwt/jwk/ec_spec.rb @@ -156,8 +156,8 @@ context 'when the jwk has HS256 as the alg parameter' do let(:rsa) { described_class.new(ec_key, alg: 'HS256') } - it 'raises JWT::DecodeError' do - expect { rsa.verify(data: data, signature: 'signature') }.to raise_error(JWT::DecodeError, 'HMAC key expected to be a String') + it 'raises JWT::VerificationError' do + expect { rsa.verify(data: data, signature: 'signature') }.to raise_error(JWT::VerificationError, 'HMAC key expected to be a String') end end end diff --git a/spec/jwt/jwk/rsa_spec.rb b/spec/jwt/jwk/rsa_spec.rb index 321dafee1..7980edb0f 100644 --- a/spec/jwt/jwk/rsa_spec.rb +++ b/spec/jwt/jwk/rsa_spec.rb @@ -133,8 +133,8 @@ context 'when the jwk has HS256 as the alg parameter' do let(:rsa) { described_class.new(rsa_key, alg: 'HS256') } - it 'raises JWT::DecodeError' do - expect { rsa.verify(data: data, signature: 'signature') }.to raise_error(JWT::DecodeError, 'HMAC key expected to be a String') + it 'raises JWT::VerificationError' do + expect { rsa.verify(data: data, signature: 'signature') }.to raise_error(JWT::VerificationError, 'HMAC key expected to be a String') end end end diff --git a/spec/jwt/jwt_spec.rb b/spec/jwt/jwt_spec.rb index f2b69beea..1cf684eb6 100644 --- a/spec/jwt/jwt_spec.rb +++ b/spec/jwt/jwt_spec.rb @@ -83,7 +83,7 @@ it 'should fail to decode the token' do expect do JWT.decode encoded_token, nil, true - end.to raise_error JWT::DecodeError + end.to raise_error JWT::IncorrectAlgorithm end end end @@ -105,13 +105,13 @@ expect(jwt_payload).to eq payload end - it 'wrong secret should raise JWT::DecodeError' do + it 'wrong secret should raise JWT::VerificationError' do expect do JWT.decode data[alg], 'wrong_secret', true, algorithm: alg end.to raise_error JWT::VerificationError end - it 'wrong secret and verify = false should not raise JWT::DecodeError' do + it 'wrong secret and verify = false should not raise an error' do expect do JWT.decode data[alg], 'wrong_secret', false end.not_to raise_error @@ -141,15 +141,15 @@ expect(jwt_payload).to eq payload end - it 'wrong key should raise JWT::DecodeError' do + it 'wrong key should raise JWT::VerificationError' do key = test_pkey('rsa-2048-wrong-public.pem') expect do JWT.decode data[alg], key, true, algorithm: alg - end.to raise_error JWT::DecodeError + end.to raise_error JWT::VerificationError end - it 'wrong key and verify = false should not raise JWT::DecodeError' do + it 'wrong key and verify = false should not raise an error' do key = test_pkey('rsa-2048-wrong-public.pem') expect do @@ -169,7 +169,7 @@ data[alg] = JWT.encode(payload, data["#{alg}_private"], alg) end - let(:wrong_key) { test_pkey('ec256-wrong-public.pem') } + let(:wrong_key) { OpenSSL::PKey::EC.generate(data["#{alg}_private"].group.curve_name) } it 'should generate a valid token' do jwt_payload, header = JWT.decode data[alg], data["#{alg}_public"], true, algorithm: alg @@ -185,13 +185,13 @@ expect(jwt_payload).to eq payload end - it 'wrong key should raise JWT::DecodeError' do + it 'wrong key should raise JWT::VerificationError' do expect do - JWT.decode data[alg], wrong_key - end.to raise_error JWT::DecodeError + JWT.decode data[alg], wrong_key, true, algorithm: alg + end.to raise_error JWT::VerificationError end - it 'wrong key and verify = false should not raise JWT::DecodeError' do + it 'wrong key and verify = false should not raise an error' do expect do JWT.decode data[alg], wrong_key, false end.not_to raise_error @@ -234,13 +234,13 @@ expect(jwt_payload).to eq payload end - it 'wrong key should raise JWT::DecodeError' do + it 'wrong key should raise JWT::VerificationError' do expect do - JWT.decode data[alg], wrong_key - end.to raise_error JWT::DecodeError + JWT.decode data[alg], wrong_key, true, algorithm: alg + end.to raise_error JWT::VerificationError end - it 'wrong key and verify = false should not raise JWT::DecodeError' do + it 'wrong key and verify = false should not raise an error' do expect do JWT.decode data[alg], wrong_key, false end.not_to raise_error @@ -249,7 +249,7 @@ end context 'Invalid' do - it 'algorithm should raise DecodeError' do + it 'invalid algorithm should raise EncodeError' do expect do JWT.encode payload, 'secret', 'HS255' end.to raise_error JWT::EncodeError @@ -257,7 +257,7 @@ it 'raises "No verification key available" error' do token = JWT.encode({}, 'foo') - expect { JWT.decode(token, nil, true) }.to raise_error(JWT::DecodeError, 'No verification key available') + expect { JWT.decode(token, nil, true) }.to raise_error(JWT::SignatureError, 'No verification key available') end it 'ECDSA curve_name should raise JWT::IncorrectAlgorithm' do @@ -424,7 +424,7 @@ JWT.decode(token, nil, true, algorithm: 'HS256') do nil end - end.to raise_error JWT::DecodeError, 'No verification key available' + end.to raise_error JWT::SignatureError, 'No verification key available' end it 'should raise JWT::IncorrectAlgorithm when algorithms array does not contain algorithm' do @@ -466,18 +466,18 @@ end context 'invalid header format' do - it 'should raise JWT::DecodeError' do + it 'should raise JWT::MalformedTokenError' do expect do JWT.decode data[:invalid_header_token] - end.to raise_error JWT::DecodeError + end.to raise_error JWT::MalformedTokenError end end context 'invalid 2-segment header format' do - it 'should raise JWT::DecodeError' do + it 'should raise JWT::MalformedTokenError' do expect do JWT.decode data[:invalid_2_segment_header_token] - end.to raise_error JWT::DecodeError, 'Not enough or too many segments' + end.to raise_error JWT::MalformedTokenError, 'Not enough or too many segments' end end @@ -485,7 +485,7 @@ it 'should raise JWT::IncorrectAlgorithm' do expect do JWT.decode data[:empty_token_2_segment] - end.to raise_error JWT::DecodeError + end.to raise_error JWT::IncorrectAlgorithm end end end @@ -538,22 +538,28 @@ end end + context 'when nil is passed as the token' do + it 'raises JWT::MalformedTokenError' do + expect { JWT.decode(nil, nil, true) }.to raise_error(JWT::MalformedTokenError, 'Nil JSON web token') + end + end + context 'a token with no segments' do - it 'raises JWT::DecodeError' do - expect { JWT.decode('ThisIsNotAValidJWTToken', nil, true) }.to raise_error(JWT::DecodeError, 'Not enough or too many segments') + it 'raises JWT::MalformedTokenError' do + expect { JWT.decode('ThisIsNotAValidJWTToken', nil, true) }.to raise_error(JWT::MalformedTokenError, 'Not enough or too many segments') end end context 'a token with not enough segments' do - it 'raises JWT::DecodeError' do + it 'raises JWT::MalformedTokenError' do token = JWT.encode('ThisIsNotAValidJWTToken', 'secret').split('.').slice(1, 2).join - expect { JWT.decode(token, nil, true) }.to raise_error(JWT::DecodeError, 'Not enough or too many segments') + expect { JWT.decode(token, nil, true) }.to raise_error(JWT::MalformedTokenError, 'Not enough or too many segments') end end context 'a token with not too many segments' do - it 'raises JWT::DecodeError' do - expect { JWT.decode('ThisIsNotAValidJWTToken.second.third.signature', nil, true) }.to raise_error(JWT::DecodeError, 'Not enough or too many segments') + it 'raises JWT::MalformedTokenError' do + expect { JWT.decode('ThisIsNotAValidJWTToken.second.third.signature', nil, true) }.to raise_error(JWT::MalformedTokenError, 'Not enough or too many segments') end end @@ -734,9 +740,9 @@ describe 'when token signed with nil and decoded with nil' do let(:no_key_token) { JWT.encode(payload, nil, 'HS512') } - it 'raises JWT::DecodeError' do + it 'raises JWT::SignatureError' do pending 'Different behaviour on OpenSSL 3.0 (https://github.com/openssl/openssl/issues/13089)' if JWT.openssl_3_hmac_empty_key_regression? - expect { JWT.decode(no_key_token, nil, true, algorithms: 'HS512') }.to raise_error(JWT::DecodeError, 'No verification key available') + expect { JWT.decode(no_key_token, nil, true, algorithms: 'HS512') }.to raise_error(JWT::SignatureError, 'No verification key available') end end @@ -753,8 +759,8 @@ JWT.configuration.strict_base64_decoding = true end - it 'raises JWT::DecodeError' do - expect { JWT.decode(token, 'secret', true, algorithm: 'HS256') }.to raise_error(JWT::DecodeError, 'Invalid base64 encoding') + it 'raises JWT::Base64DecodeError' do + expect { JWT.decode(token, 'secret', true, algorithm: 'HS256') }.to raise_error(JWT::Base64DecodeError, 'Invalid base64 encoding') end end @@ -951,7 +957,7 @@ def verify(*) end it 'raises error on decoding' do - expect { JWT.decode(expected_token, 'secret', true, algorithm: custom_algorithm.new) }.to raise_error(JWT::DecodeError, /missing the verify method/) + expect { JWT.decode(expected_token, 'secret', true, algorithm: custom_algorithm.new) }.to raise_error(JWT::VerificationError, /missing the verify method/) end end end diff --git a/spec/jwt/token_spec.rb b/spec/jwt/token_spec.rb index 3f4438419..93dec3a74 100644 --- a/spec/jwt/token_spec.rb +++ b/spec/jwt/token_spec.rb @@ -42,7 +42,7 @@ context 'with mismatching algorithm provided in sign call' do it 'signs the token' do - expect { token.sign!(algorithm: %w[RS384 RS512], key: jwk) }.to raise_error(JWT::DecodeError, 'Provided JWKs do not support one of the specified algorithms: RS384, RS512') + expect { token.sign!(algorithm: %w[RS384 RS512], key: jwk) }.to raise_error(JWT::EncodeError, 'Provided JWKs do not support one of the specified algorithms: RS384, RS512') end end end