From 76481b8a40b3893907a49bf8dd9d8fa01173f723 Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Wed, 28 Feb 2024 14:29:42 +0200 Subject: [PATCH] Configurabe base64 behaviour and log deprecations once by default --- CHANGELOG.md | 2 ++ README.md | 17 +++++++++++++++++ lib/jwt.rb | 1 + lib/jwt/base64.rb | 6 ++++-- lib/jwt/configuration/container.rb | 17 ++++++++++++++--- lib/jwt/deprecations.rb | 29 +++++++++++++++++++++++++++++ lib/jwt/error.rb | 1 + lib/jwt/jwa/hmac_rbnacl.rb | 4 ++-- lib/jwt/jwa/hmac_rbnacl_fixed.rb | 4 ++-- spec/jwt/jwt_spec.rb | 11 +++++++++++ spec/spec_helper.rb | 7 ++++++- 11 files changed, 89 insertions(+), 10 deletions(-) create mode 100644 lib/jwt/deprecations.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index f83ddcb3..f7e66c77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,12 @@ **Features:** +- Configurable base64 decode behaviour [#589](https://github.com/jwt/ruby-jwt/pull/589) ([@anakinj](https://github.com/anakinj)) - Your contribution here **Fixes and enhancements:** +- Output deprecation warnings once [#589](https://github.com/jwt/ruby-jwt/pull/589) ([@anakinj](https://github.com/anakinj)) - Your contribution here ## [v2.8.0](https://github.com/jwt/ruby-jwt/tree/v2.8.0) (2024-02-17) diff --git a/README.md b/README.md index ca9cf98a..41beb8f1 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,23 @@ The JWT spec supports NONE, HMAC, RSASSA, ECDSA and RSASSA-PSS algorithms for cr See: [ JSON Web Algorithms (JWA) 3.1. "alg" (Algorithm) Header Parameter Values for JWS](https://tools.ietf.org/html/rfc7518#section-3.1) +### Deprecation warnings + +Deprecation warnings are logged once (`:once` option) by default to avoid spam in logs. Other options are `:silent` to completely silence warnings and `:warn` to log every time a deprecated path is executed. + +```ruby + JWT.configuration.deprecation_warnings = :warn # default is :once +``` + +### Base64 decoding + +In the past the gem has been supporting the Base64 decoding specified in [RFC2045](https://www.rfc-editor.org/rfc/rfc2045) allowing newlines and blanks in the base64 encoded payload. In future versions base64 decoding will be stricter and only comply to [RFC4648](https://www.rfc-editor.org/rfc/rfc4648). + +The stricter base64 decoding when processing tokens can be done via the `strict_base64_decoding` configuration accessor. +```ruby + JWT.configuration.strict_base64_decoding = true # default is false +``` + ### **NONE** * none - unsigned token diff --git a/lib/jwt.rb b/lib/jwt.rb index d42aaa66..19b987ce 100644 --- a/lib/jwt.rb +++ b/lib/jwt.rb @@ -5,6 +5,7 @@ require 'jwt/json' require 'jwt/decode' require 'jwt/configuration' +require 'jwt/deprecations' require 'jwt/encode' require 'jwt/error' require 'jwt/jwk' diff --git a/lib/jwt/base64.rb b/lib/jwt/base64.rb index f6c47684..e6ee0622 100644 --- a/lib/jwt/base64.rb +++ b/lib/jwt/base64.rb @@ -17,9 +17,11 @@ def url_decode(str) ::Base64.urlsafe_decode64(str) rescue ArgumentError => e raise unless e.message == 'invalid base64' + raise Base64DecodeError, 'Invalid base64 encoding' if JWT.configuration.strict_base64_decoding - warn('[DEPRECATION] Invalid base64 input detected, could be because of invalid padding, trailing whitespaces or newline chars. Graceful handling of invalid input will be dropped in the next major version of ruby-jwt') - loose_urlsafe_decode64(str) + loose_urlsafe_decode64(str).tap do + Deprecations.warning('Invalid base64 input detected, could be because of invalid padding, trailing whitespaces or newline chars. Graceful handling of invalid input will be dropped in the next major version of ruby-jwt') + end end def loose_urlsafe_decode64(str) diff --git a/lib/jwt/configuration/container.rb b/lib/jwt/configuration/container.rb index 19417bcf..dccaf237 100644 --- a/lib/jwt/configuration/container.rb +++ b/lib/jwt/configuration/container.rb @@ -6,15 +6,26 @@ module JWT module Configuration class Container - attr_accessor :decode, :jwk + attr_accessor :decode, :jwk, :strict_base64_decoding + attr_reader :deprecation_warnings def initialize reset! end def reset! - @decode = DecodeConfiguration.new - @jwk = JwkConfiguration.new + @decode = DecodeConfiguration.new + @jwk = JwkConfiguration.new + @strict_base64_decoding = false + + self.deprecation_warnings = :once + end + + DEPRECATION_WARNINGS_VALUES = %i[once warn silent].freeze + def deprecation_warnings=(value) + raise ArgumentError, "Invalid deprecation_warnings value #{value}. Supported values: #{DEPRECATION_WARNINGS_VALUES}" unless DEPRECATION_WARNINGS_VALUES.include?(value) + + @deprecation_warnings = value end end end diff --git a/lib/jwt/deprecations.rb b/lib/jwt/deprecations.rb new file mode 100644 index 00000000..92f0e6cf --- /dev/null +++ b/lib/jwt/deprecations.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module JWT + # Deprecations module to handle deprecation warnings in the gem + module Deprecations + class << self + def warning(message) + case JWT.configuration.deprecation_warnings + when :warn + warn("[DEPRECATION WARNING] #{message}") + when :once + return if record_warned(message) + + warn("[DEPRECATION WARNING] #{message}") + end + end + + private + + def record_warned(message) + @warned ||= [] + return true if @warned.include?(message) + + @warned << message + false + end + end + end +end diff --git a/lib/jwt/error.rb b/lib/jwt/error.rb index ce3f3a9f..a5d91651 100644 --- a/lib/jwt/error.rb +++ b/lib/jwt/error.rb @@ -17,6 +17,7 @@ class InvalidSubError < DecodeError; end class InvalidJtiError < DecodeError; end class InvalidPayload < DecodeError; end class MissingRequiredClaim < DecodeError; end + class Base64DecodeError < DecodeError; end class JWKError < DecodeError; end end diff --git a/lib/jwt/jwa/hmac_rbnacl.rb b/lib/jwt/jwa/hmac_rbnacl.rb index e67309c7..67c1ac41 100644 --- a/lib/jwt/jwa/hmac_rbnacl.rb +++ b/lib/jwt/jwa/hmac_rbnacl.rb @@ -7,7 +7,7 @@ module HmacRbNaCl SUPPORTED = MAPPING.keys class << self def sign(algorithm, msg, key) - warn("[DEPRECATION] The use of the algorithm #{algorithm} is deprecated and will be removed in the next major version of ruby-jwt") + Deprecations.warning("The use of the algorithm #{algorithm} is deprecated and will be removed in the next major version of ruby-jwt") if (hmac = resolve_algorithm(algorithm)) hmac.auth(key_for_rbnacl(hmac, key).encode('binary'), msg.encode('binary')) else @@ -16,7 +16,7 @@ def sign(algorithm, msg, key) end def verify(algorithm, key, signing_input, signature) - warn("[DEPRECATION] The use of the algorithm #{algorithm} is deprecated and will be removed in the next major version of ruby-jwt") + Deprecations.warning("The use of the algorithm #{algorithm} is deprecated and will be removed in the next major version of ruby-jwt") if (hmac = resolve_algorithm(algorithm)) hmac.verify(key_for_rbnacl(hmac, key).encode('binary'), signature.encode('binary'), signing_input.encode('binary')) else diff --git a/lib/jwt/jwa/hmac_rbnacl_fixed.rb b/lib/jwt/jwa/hmac_rbnacl_fixed.rb index 9712a3cc..cece2d52 100644 --- a/lib/jwt/jwa/hmac_rbnacl_fixed.rb +++ b/lib/jwt/jwa/hmac_rbnacl_fixed.rb @@ -9,7 +9,7 @@ module HmacRbNaClFixed class << self def sign(algorithm, msg, key) key ||= '' - warn("[DEPRECATION] The use of the algorithm #{algorithm} is deprecated and will be removed in the next major version of ruby-jwt") + Deprecations.warning("The use of the algorithm #{algorithm} is deprecated and will be removed in the next major version of ruby-jwt") raise JWT::DecodeError, 'HMAC key expected to be a String' unless key.is_a?(String) if (hmac = resolve_algorithm(algorithm)) && key.bytesize <= hmac.key_bytes @@ -21,7 +21,7 @@ def sign(algorithm, msg, key) def verify(algorithm, key, signing_input, signature) key ||= '' - warn("[DEPRECATION] The use of the algorithm #{algorithm} is deprecated and will be removed in the next major version of ruby-jwt") + Deprecations.warning("The use of the algorithm #{algorithm} is deprecated and will be removed in the next major version of ruby-jwt") raise JWT::DecodeError, 'HMAC key expected to be a String' unless key.is_a?(String) if (hmac = resolve_algorithm(algorithm)) && key.bytesize <= hmac.key_bytes diff --git a/spec/jwt/jwt_spec.rb b/spec/jwt/jwt_spec.rb index 13d0beec..796dabb8 100644 --- a/spec/jwt/jwt_spec.rb +++ b/spec/jwt/jwt_spec.rb @@ -774,6 +774,17 @@ end end + context 'when token ends with a newline char and strict_decoding enabled' do + let(:token) { "#{JWT.encode(payload, 'secret', 'HS256')}\n" } + before do + 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') + end + end + context 'when multiple algorithms given' do let(:token) { JWT.encode(payload, 'secret', 'HS256') } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 7dcfffba..e6333c19 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -15,7 +15,12 @@ c.syntax = :expect end config.include(SpecSupport::TestKeys) - config.before(:example) { JWT.configuration.reset! } + + config.before(:example) do + JWT.configuration.reset! + JWT.configuration.deprecation_warnings = :warn + end + config.run_all_when_everything_filtered = true config.filter_run :focus config.order = 'random'