From daaf09ae000ba2d406750764f93784ce472cb9e2 Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Mon, 27 Dec 2021 14:40:33 +0200 Subject: [PATCH 01/33] Base from previous branch --- lib/jwt.rb | 5 +++++ lib/jwt/decode.rb | 17 +++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/jwt.rb b/lib/jwt.rb index 834b5a87..e51b3fca 100644 --- a/lib/jwt.rb +++ b/lib/jwt.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'base64' +require 'jwt/extension' require 'jwt/json' require 'jwt/decode' require 'jwt/default_options' @@ -15,6 +16,10 @@ module JWT include JWT::DefaultOptions + def self.included(cls) + cls.include(JWT::Extension) + end + module_function def encode(payload, key, algorithm = 'HS256', header_fields = {}) diff --git a/lib/jwt/decode.rb b/lib/jwt/decode.rb index 5a288bfc..7a2fcb46 100644 --- a/lib/jwt/decode.rb +++ b/lib/jwt/decode.rb @@ -123,19 +123,28 @@ def algorithm end def header - @header ||= parse_and_decode @segments[0] + @header ||= decode_and_parse_header(@segments[0]) end def payload - @payload ||= parse_and_decode @segments[1] + @payload ||= decode_and_parse_payload(@segments[1]) end def signing_input @segments.first(2).join('.') end - def parse_and_decode(segment) - JWT::JSON.parse(Base64.urlsafe_decode64(segment)) + def decode_and_parse_header(raw_header) + json_parse(raw_header) + end + + def decode_and_parse_payload(raw_payload) + raw_payload = @options[:decode_payload_proc].call(header, raw_payload, @signature) if @options[:decode_payload_proc] + json_parse(raw_payload) + end + + def json_parse(decoded_segment) + JWT::JSON.parse(Base64.urlsafe_decode64(decoded_segment)) rescue ::JSON::ParserError, ArgumentError raise JWT::DecodeError, 'Invalid segment encoding' end From 3ae159af5f1fda1e31d1955646592e8d6e7a1be7 Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Mon, 27 Dec 2021 14:40:39 +0200 Subject: [PATCH 02/33] Base from previous branch --- lib/jwt/extension.rb | 13 +++++++++++ lib/jwt/extension/decode.rb | 29 +++++++++++++++++++++++++ lib/jwt/extension/encode.rb | 26 ++++++++++++++++++++++ spec/extension_spec.rb | 43 +++++++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+) create mode 100644 lib/jwt/extension.rb create mode 100644 lib/jwt/extension/decode.rb create mode 100644 lib/jwt/extension/encode.rb create mode 100644 spec/extension_spec.rb diff --git a/lib/jwt/extension.rb b/lib/jwt/extension.rb new file mode 100644 index 00000000..d47f807b --- /dev/null +++ b/lib/jwt/extension.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require_relative 'extension/decode' +require_relative 'extension/encode' + +module JWT + module Extension + def self.included(cls) + cls.extend(JWT::Extension::Decode) + cls.extend(JWT::Extension::Encode) + end + end +end diff --git a/lib/jwt/extension/decode.rb b/lib/jwt/extension/decode.rb new file mode 100644 index 00000000..cb243d2a --- /dev/null +++ b/lib/jwt/extension/decode.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module JWT + module Extension + module Decode + def decode_payload(&block) + @decode_payload = block if block_given? + @decode_payload + end + + def decode(payload, options = {}) + segments = ::JWT::Decode.new(payload, + options.delete(:key), + true, + create_decode_options(options)).decode_segments + { + header: segments.last, + payload: segments.first + } + end + + private + + def create_decode_options(given_options) + ::JWT::DefaultOptions::DEFAULT_OPTIONS.merge(decode_payload_proc: self.decode_payload).merge(given_options) + end + end + end +end diff --git a/lib/jwt/extension/encode.rb b/lib/jwt/extension/encode.rb new file mode 100644 index 00000000..518a5e0e --- /dev/null +++ b/lib/jwt/extension/encode.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module JWT + module Extension + module Encode + def algorithm(value = nil) + @algorithm = value unless value.nil? + @algorithm + end + + def signing_key(value = nil) + @signing_key = value unless value.nil? + @signing_key + end + + def encode(payload, options = {}) + ::JWT::Encode.new( + payload: payload, + key: self.signing_key, + algorithm: self.algorithm, + headers: options[:headers] + ).segments + end + end + end +end diff --git a/spec/extension_spec.rb b/spec/extension_spec.rb new file mode 100644 index 00000000..f6ae712d --- /dev/null +++ b/spec/extension_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'securerandom' + +RSpec.describe JWT::Extension do + subject(:extension) do + Class.new do + include JWT + end + end + + let(:secret) { SecureRandom.hex } + let(:payload) { { 'pay' => 'load'} } + let(:encoded_payload) { ::JWT.encode(payload, secret, 'HS256') } + + describe '.encode' do + it { is_expected.to respond_to(:encode) } + end + + describe '.decode' do + it { is_expected.to respond_to(:decode) } + + context 'when nothing special is defined' do + it 'verifies a token and returns the data' do + expect(extension.decode(encoded_payload, key: secret)).to eq(header: { 'alg' => 'HS256' }, payload: payload) + end + end + + context 'when a decode_payload block manipulates the payload' do + before do + extension.decode_payload do |_header, raw_payload, _signature| + payload_content = JWT::JSON.parse(Base64.urlsafe_decode64(raw_payload)) + payload_content['pay'].reverse! + Base64.urlsafe_encode64(JWT::JSON.generate(payload_content)) + end + end + + it 'lets decode_payload process the raw payload before verifying' do + expect(extension.decode(encoded_payload, key: secret)).to eq(header: { 'alg' => 'HS256' }, payload: {'pay' => 'daol'}) + end + end + end +end From 9f1879290e3881946a360ab9c080160d1ca59c46 Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Mon, 27 Dec 2021 14:59:32 +0200 Subject: [PATCH 03/33] Basic algorithm and signing key handling --- .rubocop.yml | 2 +- lib/jwt/error.rb | 1 + lib/jwt/extension/encode.rb | 13 +++++- .../decode_spec.rb} | 4 -- spec/extension/encode_spec.rb | 45 +++++++++++++++++++ 5 files changed, 58 insertions(+), 7 deletions(-) rename spec/{extension_spec.rb => extension/decode_spec.rb} (94%) create mode 100644 spec/extension/encode_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index 6c4769fa..436b680e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -37,7 +37,7 @@ Metrics/AbcSize: Max: 25 Metrics/ClassLength: - Max: 103 + Max: 140 Metrics/ModuleLength: Max: 100 diff --git a/lib/jwt/error.rb b/lib/jwt/error.rb index ce3f3a9f..c716f122 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 SigningKeyMissing < EncodeError; end class JWKError < DecodeError; end end diff --git a/lib/jwt/extension/encode.rb b/lib/jwt/extension/encode.rb index 518a5e0e..52987fcb 100644 --- a/lib/jwt/extension/encode.rb +++ b/lib/jwt/extension/encode.rb @@ -16,11 +16,20 @@ def signing_key(value = nil) def encode(payload, options = {}) ::JWT::Encode.new( payload: payload, - key: self.signing_key, + key: signing_key_from_options(options), algorithm: self.algorithm, - headers: options[:headers] + headers: Array(options[:headers]) ).segments end + + private + + def signing_key_from_options(options) + key = options[:signing_key] || self.signing_key + raise ::JWT::SigningKeyMissing, 'No key given for signing' if key.nil? + + key + end end end end diff --git a/spec/extension_spec.rb b/spec/extension/decode_spec.rb similarity index 94% rename from spec/extension_spec.rb rename to spec/extension/decode_spec.rb index f6ae712d..e1b78a55 100644 --- a/spec/extension_spec.rb +++ b/spec/extension/decode_spec.rb @@ -13,10 +13,6 @@ let(:payload) { { 'pay' => 'load'} } let(:encoded_payload) { ::JWT.encode(payload, secret, 'HS256') } - describe '.encode' do - it { is_expected.to respond_to(:encode) } - end - describe '.decode' do it { is_expected.to respond_to(:decode) } diff --git a/spec/extension/encode_spec.rb b/spec/extension/encode_spec.rb new file mode 100644 index 00000000..4cbe0afc --- /dev/null +++ b/spec/extension/encode_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'securerandom' + +RSpec.describe JWT::Extension do + subject(:extension) do + Class.new do + include JWT + end + end + + let(:secret) { SecureRandom.hex } + let(:payload) { { 'pay' => 'load'} } + + describe '.encode' do + it { is_expected.to respond_to(:encode) } + + context 'when algorithm is configured and no signing key is given or configured' do + before do + extension.algorithm('HS256') + end + + it 'raises an error about missing signing key' do + expect { extension.encode(payload) }.to raise_error(::JWT::SigningKeyMissing, 'No key given for signing') + end + end + + context 'when no algorithm is configured and key is given as a option' do + it 'raises an error about unsupported algoritm implementation' do + expect { extension.encode(payload, signing_key: secret) }.to raise_error(NotImplementedError, 'Unsupported signing method') + end + end + + context 'when algorithm and signing is configured' do + before do + extension.algorithm('HS256') + extension.signing_key(secret) + end + + it 'yields the same result as the raw encode' do + expect(extension.encode(payload)).to eq(::JWT.encode(payload, secret, 'HS256')) + end + end + end +end From 46b8b477b8574d3288962aaae4ac397919ead6a5 Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Mon, 27 Dec 2021 22:32:59 +0200 Subject: [PATCH 04/33] Example of Smart healthcard encoder --- lib/jwt/decode.rb | 25 +++++-- lib/jwt/encode.rb | 3 + lib/jwt/extension.rb | 2 + lib/jwt/extension/decode.rb | 37 +++++++--- lib/jwt/extension/encode.rb | 9 +-- lib/jwt/extension/keys.rb | 12 ++++ spec/extension/decode_spec.rb | 17 ++--- spec/extension/encode_spec.rb | 8 +-- spec/extension/example_deflating_spec.rb | 49 +++++++++++++ .../example_smart_health_card_spec.rb | 68 +++++++++++++++++++ 10 files changed, 199 insertions(+), 31 deletions(-) create mode 100644 lib/jwt/extension/keys.rb create mode 100644 spec/extension/example_deflating_spec.rb create mode 100644 spec/extension/example_smart_health_card_spec.rb diff --git a/lib/jwt/decode.rb b/lib/jwt/decode.rb index 7a2fcb46..63861623 100644 --- a/lib/jwt/decode.rb +++ b/lib/jwt/decode.rb @@ -135,17 +135,32 @@ def signing_input end def decode_and_parse_header(raw_header) - json_parse(raw_header) + json_parse(decode_header(raw_header)) end def decode_and_parse_payload(raw_payload) - raw_payload = @options[:decode_payload_proc].call(header, raw_payload, @signature) if @options[:decode_payload_proc] - json_parse(raw_payload) + decode_payload(raw_payload) + end + + def decode_payload(raw_segment) + if @options[:decode_payload_proc] + @options[:decode_payload_proc].call(raw_segment, header, @signature) + else + json_parse(Base64.urlsafe_decode64(raw_segment)) + end + rescue ArgumentError + raise JWT::DecodeError, 'Invalid segment encoding' + end + + def decode_header(raw_segment) + Base64.urlsafe_decode64(raw_segment) + rescue ArgumentError + raise JWT::DecodeError, 'Invalid segment encoding' end def json_parse(decoded_segment) - JWT::JSON.parse(Base64.urlsafe_decode64(decoded_segment)) - rescue ::JSON::ParserError, ArgumentError + JWT::JSON.parse(decoded_segment) + rescue ::JSON::ParserError raise JWT::DecodeError, 'Invalid segment encoding' end end diff --git a/lib/jwt/encode.rb b/lib/jwt/encode.rb index f5bca389..b337bebd 100644 --- a/lib/jwt/encode.rb +++ b/lib/jwt/encode.rb @@ -11,6 +11,7 @@ class Encode ALG_KEY = 'alg'.freeze def initialize(options) + @options = options @payload = options[:payload] @key = options[:key] _, @algorithm = Algos.find(options[:algorithm]) @@ -49,6 +50,8 @@ def encode_payload ClaimsValidator.new(@payload).validate! end + return @options[:encode_payload_proc].call(@payload) unless @options[:encode_payload_proc].nil? + encode(@payload) end diff --git a/lib/jwt/extension.rb b/lib/jwt/extension.rb index d47f807b..e9f1e69a 100644 --- a/lib/jwt/extension.rb +++ b/lib/jwt/extension.rb @@ -1,11 +1,13 @@ # frozen_string_literal: true +require_relative 'extension/keys' require_relative 'extension/decode' require_relative 'extension/encode' module JWT module Extension def self.included(cls) + cls.extend(JWT::Extension::Keys) cls.extend(JWT::Extension::Decode) cls.extend(JWT::Extension::Encode) end diff --git a/lib/jwt/extension/decode.rb b/lib/jwt/extension/decode.rb index cb243d2a..2696e134 100644 --- a/lib/jwt/extension/decode.rb +++ b/lib/jwt/extension/decode.rb @@ -8,21 +8,38 @@ def decode_payload(&block) @decode_payload end - def decode(payload, options = {}) - segments = ::JWT::Decode.new(payload, - options.delete(:key), - true, - create_decode_options(options)).decode_segments - { - header: segments.last, - payload: segments.first - } + def algorithms(value = nil) + @algorithms = value unless value.nil? + @algorithms + end + + def jwk_resolver(&block) + @jwk_resolver = block if block_given? + @jwk_resolver + end + + def decode!(payload, options = {}) + ::JWT::Decode.new(payload, + decode_signing_key_from_options(options), + true, + create_decode_options(options)).decode_segments end private + def decode_signing_key_from_options(options) + options[:signing_key] || self.signing_key + end + def create_decode_options(given_options) - ::JWT::DefaultOptions::DEFAULT_OPTIONS.merge(decode_payload_proc: self.decode_payload).merge(given_options) + ::JWT::DefaultOptions::DEFAULT_OPTIONS.merge(decode_payload_proc: self.decode_payload, + algorithms: self.decoding_algorithms, + jwks: self.jwk_resolver) + .merge(given_options) + end + + def decoding_algorithms + (Array(self.algorithm) + Array(self.algorithms)).uniq end end end diff --git a/lib/jwt/extension/encode.rb b/lib/jwt/extension/encode.rb index 52987fcb..1cb1e728 100644 --- a/lib/jwt/extension/encode.rb +++ b/lib/jwt/extension/encode.rb @@ -8,16 +8,17 @@ def algorithm(value = nil) @algorithm end - def signing_key(value = nil) - @signing_key = value unless value.nil? - @signing_key + def encode_payload(&block) + @encode_payload = block if block_given? + @encode_payload end - def encode(payload, options = {}) + def encode!(payload, options = {}) ::JWT::Encode.new( payload: payload, key: signing_key_from_options(options), algorithm: self.algorithm, + encode_payload_proc: self.encode_payload, headers: Array(options[:headers]) ).segments end diff --git a/lib/jwt/extension/keys.rb b/lib/jwt/extension/keys.rb new file mode 100644 index 00000000..467c7bb5 --- /dev/null +++ b/lib/jwt/extension/keys.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module JWT + module Extension + module Keys + def signing_key(value = nil) + @signing_key = value unless value.nil? + @signing_key + end + end + end +end diff --git a/spec/extension/decode_spec.rb b/spec/extension/decode_spec.rb index e1b78a55..be1ec215 100644 --- a/spec/extension/decode_spec.rb +++ b/spec/extension/decode_spec.rb @@ -6,6 +6,7 @@ subject(:extension) do Class.new do include JWT + algorithm 'HS256' end end @@ -13,26 +14,26 @@ let(:payload) { { 'pay' => 'load'} } let(:encoded_payload) { ::JWT.encode(payload, secret, 'HS256') } - describe '.decode' do - it { is_expected.to respond_to(:decode) } + describe '.decode!' do + it { is_expected.to respond_to(:decode!) } - context 'when nothing special is defined' do + context 'when nothing but algorithm is defined' do it 'verifies a token and returns the data' do - expect(extension.decode(encoded_payload, key: secret)).to eq(header: { 'alg' => 'HS256' }, payload: payload) + expect(extension.decode!(encoded_payload, signing_key: secret)).to eq([payload, { 'alg' => 'HS256' }]) end end context 'when a decode_payload block manipulates the payload' do before do - extension.decode_payload do |_header, raw_payload, _signature| + extension.decode_payload do |raw_payload, _header, _signature| payload_content = JWT::JSON.parse(Base64.urlsafe_decode64(raw_payload)) payload_content['pay'].reverse! - Base64.urlsafe_encode64(JWT::JSON.generate(payload_content)) + payload_content end end - it 'lets decode_payload process the raw payload before verifying' do - expect(extension.decode(encoded_payload, key: secret)).to eq(header: { 'alg' => 'HS256' }, payload: {'pay' => 'daol'}) + it 'uses the defined decode_payload to process the raw payload' do + expect(extension.decode!(encoded_payload, signing_key: secret)).to eq([{'pay' => 'daol'}, { 'alg' => 'HS256' }]) end end end diff --git a/spec/extension/encode_spec.rb b/spec/extension/encode_spec.rb index 4cbe0afc..2f50a4b1 100644 --- a/spec/extension/encode_spec.rb +++ b/spec/extension/encode_spec.rb @@ -13,7 +13,7 @@ let(:payload) { { 'pay' => 'load'} } describe '.encode' do - it { is_expected.to respond_to(:encode) } + it { is_expected.to respond_to(:encode!) } context 'when algorithm is configured and no signing key is given or configured' do before do @@ -21,13 +21,13 @@ end it 'raises an error about missing signing key' do - expect { extension.encode(payload) }.to raise_error(::JWT::SigningKeyMissing, 'No key given for signing') + expect { extension.encode!(payload) }.to raise_error(::JWT::SigningKeyMissing, 'No key given for signing') end end context 'when no algorithm is configured and key is given as a option' do it 'raises an error about unsupported algoritm implementation' do - expect { extension.encode(payload, signing_key: secret) }.to raise_error(NotImplementedError, 'Unsupported signing method') + expect { extension.encode!(payload, signing_key: secret) }.to raise_error(NotImplementedError, 'Unsupported signing method') end end @@ -38,7 +38,7 @@ end it 'yields the same result as the raw encode' do - expect(extension.encode(payload)).to eq(::JWT.encode(payload, secret, 'HS256')) + expect(extension.encode!(payload)).to eq(::JWT.encode(payload, secret, 'HS256')) end end end diff --git a/spec/extension/example_deflating_spec.rb b/spec/extension/example_deflating_spec.rb new file mode 100644 index 00000000..febfc477 --- /dev/null +++ b/spec/extension/example_deflating_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'securerandom' +require 'zlib' + +RSpec.describe 'Deflating payload processor' do + let(:secret) { SecureRandom.hex } + let(:payload) { { 'pay' => 'load'} } + + subject(:extension) do + the_secret = secret + Class.new do + include JWT + + algorithm 'HS512' + algorithms 'HS5256' + signing_key the_secret + + encode_payload do |payload| + io = StringIO.new + Zlib::GzipWriter.new(io).tap do |gz| + gz.write(::JWT::JSON.generate(payload)) + gz.close + end + ::Base64.urlsafe_encode64(io.string, padding: true) + end + + decode_payload do |raw_payload| + raw_json = Zlib::GzipReader.new(StringIO.new(::Base64.urlsafe_decode64(raw_payload))).read + ::JWT::JSON.parse(raw_json) + end + end + end + + context 'when encoding' do + it 'the encoded payload looks like its zipped' do + expect(subject.encode!(payload).split('.')[1]).to match(/H4.*==/) + end + end + + context 'when decoding presigned and zipped token' do + let(:secret) { 's3cr3t' } + + let(:presigned_token) { 'eyJhbGciOiJIUzUxMiJ9.H4sIAKTUyWEAA6tWKkisVLJSyslPTFGqBQAsM7zZDgAAAA==.GK1DXdMN7i6OA_1_xUYU3lThZwY94MgUYRivRIaLTIP-yrmZfxLrbpe3Llkrr1HIrDQhjPPwskiR5oob14hv9A' } + it 'verifies and decodes the payload' do + expect(subject.decode!(presigned_token)).to eq([{'pay' => 'load'}, {'alg' => 'HS512'}]) + end + end +end diff --git a/spec/extension/example_smart_health_card_spec.rb b/spec/extension/example_smart_health_card_spec.rb new file mode 100644 index 00000000..5f56f9e0 --- /dev/null +++ b/spec/extension/example_smart_health_card_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'securerandom' +require 'zlib' + +# Inspired by +# https://github.com/jwt/ruby-jwt/issues/428 + +RSpec.describe 'SMART Health Cards decoder and verifier' do + let(:jwk_keys) do + JSON.parse('{ "keys": [{ + "kty": "EC", + "kid": "3Kfdg-XwP-7gXyywtUfUADwBumDOPKMQx-iELL11W9s", + "use": "sig", + "alg": "ES256", + "crv": "P-256", + "x": "11XvRWy1I2S0EyJlyf_bWfw_TQ5CJJNLw78bHXNxcgw", + "y": "eZXwxvO1hvCY0KucrPfKo7yAyMT6Ajc3N7OkAB6VYy8", + "d": "FvOOk6hMixJ2o9zt4PCfan_UW7i4aOEnzj76ZaCI9Og" }]}') + end + + let(:smart_health_card_token) do + 'eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.3ZJJb9swEIX_SjC9ytoSR5VutQt0Q4sWTXMpfKCpscWCi8BFsBvov3dIO2haJDn1VN1GM_PxvUfegXAOOhi8H11XFG5EnjvFrB-QST_knNneFXhgapToCpoOaCEDvd1BV11ftldtednU-XL5MoOJQ3cH_jgidN9_M__GvTgVi1gQ6uk5oVTQ4ifzwuhnB7mZRF-1sMmAW-xRe8Hk17D9gdxHSbtB2Fu0LnI6uMrLvCJe_LsKupcYZyw6EyzHmyQfzo3sbAe4kZJoJyV0gD2SRyIHKb9ZSQP3-11JA_fFI-DPZIf2Y4ZM4QnClJDEg1eaZqxLZ-zFhDrm-N4MsV7lsJnJ4FaQ-dfMR1bVLqtFWS3qEuY5e1RN9byad39G7DzzwSW78cI9xguaGOdC49r0icBNL_Q-CXdH51Gd3w_dzCCb3Nh9EZMtnOgLPh0IwNMm1GUD82bOYDxHkOTs0KKO2h4mSEOG82BTK5q9EeqEqJPhMtqiqHbGKnqPUQvj3tiI7IUbJUtxrtYXb1CjZfLirXGj8ExSUBSiNP5TUNu4CmX6qicTrP_LBOv2XyfYxAaFCFb09PPjh-P6MDTjdfhCjV8.F248favB7uvtKSo9GbwIC-QtmpWeAsB-AtiFq2iACiDZQE0s38603dJp50vc1HEvZAB80RXecKQ1LYdkZbq8Rw' + end + + subject(:smart_health_card_decoded) do + test_class = self + + Class.new do + include JWT + + algorithm 'ES256' + + jwk_resolver do |_options| + test_class.jwk_keys + end + + decode_payload do |raw_payload, headers| + decoded_payload = ::Base64.urlsafe_decode64(raw_payload) + + raw_json = if headers['zip'] == 'DEF' + begin + Zlib::Inflate.inflate(decoded_payload) + rescue Zlib::DataError + zinflate = Zlib::Inflate.new(-::Zlib::MAX_WBITS) + zinflate.inflate(decoded_payload) + end + else + decoded_payload + end + + ::JWT::JSON.parse(raw_json) + end + end + end + + context 'when valid token is given' do + it 'extracts the payload' do + payload, header = smart_health_card_decoded.decode!(smart_health_card_token) + expect(payload).to include('iss' => 'https://spec.smarthealth.cards/examples/issuer') + expect(header).to eq( + {'alg' => 'ES256', + 'kid' => '3Kfdg-XwP-7gXyywtUfUADwBumDOPKMQx-iELL11W9s', + 'zip' => 'DEF'} + ) + end + end +end From 5dc71b23c7b5e1b690cfd128147ed19f94896f1f Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Mon, 27 Dec 2021 22:55:35 +0200 Subject: [PATCH 05/33] Lets not have private methods --- lib/jwt/extension/decode.rb | 2 -- lib/jwt/extension/encode.rb | 2 -- 2 files changed, 4 deletions(-) diff --git a/lib/jwt/extension/decode.rb b/lib/jwt/extension/decode.rb index 2696e134..c8f42e05 100644 --- a/lib/jwt/extension/decode.rb +++ b/lib/jwt/extension/decode.rb @@ -25,8 +25,6 @@ def decode!(payload, options = {}) create_decode_options(options)).decode_segments end - private - def decode_signing_key_from_options(options) options[:signing_key] || self.signing_key end diff --git a/lib/jwt/extension/encode.rb b/lib/jwt/extension/encode.rb index 1cb1e728..c6d801cc 100644 --- a/lib/jwt/extension/encode.rb +++ b/lib/jwt/extension/encode.rb @@ -23,8 +23,6 @@ def encode!(payload, options = {}) ).segments end - private - def signing_key_from_options(options) key = options[:signing_key] || self.signing_key raise ::JWT::SigningKeyMissing, 'No key given for signing' if key.nil? From fb255aa7a08a2389028a9a35e8f3531277dc0f2c Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Mon, 27 Dec 2021 22:59:32 +0200 Subject: [PATCH 06/33] Fix typo --- spec/extension/example_smart_health_card_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/extension/example_smart_health_card_spec.rb b/spec/extension/example_smart_health_card_spec.rb index 5f56f9e0..afe39054 100644 --- a/spec/extension/example_smart_health_card_spec.rb +++ b/spec/extension/example_smart_health_card_spec.rb @@ -23,7 +23,7 @@ 'eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.3ZJJb9swEIX_SjC9ytoSR5VutQt0Q4sWTXMpfKCpscWCi8BFsBvov3dIO2haJDn1VN1GM_PxvUfegXAOOhi8H11XFG5EnjvFrB-QST_knNneFXhgapToCpoOaCEDvd1BV11ftldtednU-XL5MoOJQ3cH_jgidN9_M__GvTgVi1gQ6uk5oVTQ4ifzwuhnB7mZRF-1sMmAW-xRe8Hk17D9gdxHSbtB2Fu0LnI6uMrLvCJe_LsKupcYZyw6EyzHmyQfzo3sbAe4kZJoJyV0gD2SRyIHKb9ZSQP3-11JA_fFI-DPZIf2Y4ZM4QnClJDEg1eaZqxLZ-zFhDrm-N4MsV7lsJnJ4FaQ-dfMR1bVLqtFWS3qEuY5e1RN9byad39G7DzzwSW78cI9xguaGOdC49r0icBNL_Q-CXdH51Gd3w_dzCCb3Nh9EZMtnOgLPh0IwNMm1GUD82bOYDxHkOTs0KKO2h4mSEOG82BTK5q9EeqEqJPhMtqiqHbGKnqPUQvj3tiI7IUbJUtxrtYXb1CjZfLirXGj8ExSUBSiNP5TUNu4CmX6qicTrP_LBOv2XyfYxAaFCFb09PPjh-P6MDTjdfhCjV8.F248favB7uvtKSo9GbwIC-QtmpWeAsB-AtiFq2iACiDZQE0s38603dJp50vc1HEvZAB80RXecKQ1LYdkZbq8Rw' end - subject(:smart_health_card_decoded) do + subject(:smart_health_card_decoder) do test_class = self Class.new do @@ -56,7 +56,7 @@ context 'when valid token is given' do it 'extracts the payload' do - payload, header = smart_health_card_decoded.decode!(smart_health_card_token) + payload, header = smart_health_card_decoder.decode!(smart_health_card_token) expect(payload).to include('iss' => 'https://spec.smarthealth.cards/examples/issuer') expect(header).to eq( {'alg' => 'ES256', From 71beeed03ae0ed69f3c04114616cf1ea149a0606 Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Fri, 7 Jan 2022 08:27:33 +0200 Subject: [PATCH 07/33] Decode class rewritten to increase understanding and to soon support custom algorithms --- lib/jwt.rb | 1 + lib/jwt/decode.rb | 34 +---- lib/jwt/decode_token.rb | 141 ++++++++++++++++++ lib/jwt/encode.rb | 16 +- lib/jwt/extension/decode.rb | 32 ++-- lib/jwt/extension/encode.rb | 35 +++-- lib/jwt/extension/keys.rb | 5 + .../example_custom_algorithm_spec.rb | 49 ++++++ 8 files changed, 254 insertions(+), 59 deletions(-) create mode 100644 lib/jwt/decode_token.rb create mode 100644 spec/extension/example_custom_algorithm_spec.rb diff --git a/lib/jwt.rb b/lib/jwt.rb index e51b3fca..38d1ab4d 100644 --- a/lib/jwt.rb +++ b/lib/jwt.rb @@ -2,6 +2,7 @@ require 'base64' require 'jwt/extension' +require 'jwt/decode_token' require 'jwt/json' require 'jwt/decode' require 'jwt/default_options' diff --git a/lib/jwt/decode.rb b/lib/jwt/decode.rb index 63861623..5a288bfc 100644 --- a/lib/jwt/decode.rb +++ b/lib/jwt/decode.rb @@ -123,44 +123,20 @@ def algorithm end def header - @header ||= decode_and_parse_header(@segments[0]) + @header ||= parse_and_decode @segments[0] end def payload - @payload ||= decode_and_parse_payload(@segments[1]) + @payload ||= parse_and_decode @segments[1] end def signing_input @segments.first(2).join('.') end - def decode_and_parse_header(raw_header) - json_parse(decode_header(raw_header)) - end - - def decode_and_parse_payload(raw_payload) - decode_payload(raw_payload) - end - - def decode_payload(raw_segment) - if @options[:decode_payload_proc] - @options[:decode_payload_proc].call(raw_segment, header, @signature) - else - json_parse(Base64.urlsafe_decode64(raw_segment)) - end - rescue ArgumentError - raise JWT::DecodeError, 'Invalid segment encoding' - end - - def decode_header(raw_segment) - Base64.urlsafe_decode64(raw_segment) - rescue ArgumentError - raise JWT::DecodeError, 'Invalid segment encoding' - end - - def json_parse(decoded_segment) - JWT::JSON.parse(decoded_segment) - rescue ::JSON::ParserError + def parse_and_decode(segment) + JWT::JSON.parse(Base64.urlsafe_decode64(segment)) + rescue ::JSON::ParserError, ArgumentError raise JWT::DecodeError, 'Invalid segment encoding' end end diff --git a/lib/jwt/decode_token.rb b/lib/jwt/decode_token.rb new file mode 100644 index 00000000..82837536 --- /dev/null +++ b/lib/jwt/decode_token.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true + +require 'jwt/signature' +require 'jwt/verify' +require 'jwt/x5c_key_finder' + +module JWT + class DecodeToken + def initialize(token, options = {}) + raise ArgumentError, 'Provided token is not a String object' unless token.is_a?(String) + + @token = token + @options = options + end + + def decoded_segments + validate_segment_count! + + if verify? + verify_alg_header! + verify_signature! + verify_claims! + end + + [payload, header] + end + + private + + attr_reader :token, :options + + def algorithms + @algorithms ||= Array(options[:algorithms]) + end + + def segments + @segments ||= token.split('.') + end + + def signature + @signature ||= Base64.urlsafe_decode64(segments[2] || '') + end + + def header + @header ||= decode_header(segments[0]) + end + + def payload + @payload ||= decode_payload(segments[1]) + end + + def signing_input + segments.first(2).join('.') + end + + def verify? + options[:verify] != false + end + + def key + @key ||= + if options[:jwks] + ::JWT::JWK::KeyFinder.new(jwks: options[:jwks]).key_for(header['kid']) + elsif (x5c_options = options[:x5c]) + ::JWT::X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(header['x5c']) + else + options[:key] + end + end + + def verify_alg_header! + return unless valid_algorithms.empty? + + raise JWT::IncorrectAlgorithm, 'Expected a different algorithm' + end + + def valid_algorithms + @valid_algorithms ||= algorithms.select do |algorithm| + if algorithm.is_a?(String) + header['alg'] == algorithm + else + algorithm.valid_alg?(header['alg']) + end + end + end + + def verify_signature! + return if valid_algorithms.any? { |algorithm| verify_signature_for?(algorithm, key) } + + raise JWT::VerificationError, 'Signature verification failed' + end + + def verify_signature_for?(algorithm, key) + if algorithm.is_a?(String) + raise JWT::DecodeError, 'No verification key available' unless key + + Array(key).any? { |k| Signature.verify(algorithm, k, signing_input, signature) } + else + algorithm.verify(signing_input, signature, key: key, header: header, payload: payload) + end + end + + def verify_claims! + Verify.verify_claims(payload, options) + Verify.verify_required_claims(payload, options) + end + + def validate_segment_count! + segment_count = segments.size + + return if segment_count == 3 + return if segment_count == 2 && (!verify? || header['alg'] == 'none') + + raise JWT::DecodeError, 'Not enough or too many segments' + end + + def decode_header(raw_header) + decode_segment_default(raw_header) + end + + def decode_payload(raw_segment) + if @options[:decode_payload_proc] + @options[:decode_payload_proc].call(raw_segment, header, signature) + else + decode_segment_default(raw_segment) + end + end + + def decode_segment_default(raw_segment) + json_parse(Base64.urlsafe_decode64(raw_segment)) + rescue ArgumentError + raise JWT::DecodeError, 'Invalid segment encoding' + end + + def json_parse(decoded_segment) + JWT::JSON.parse(decoded_segment) + rescue ::JSON::ParserError + raise JWT::DecodeError, 'Invalid segment encoding' + end + end +end diff --git a/lib/jwt/encode.rb b/lib/jwt/encode.rb index b337bebd..00c8d280 100644 --- a/lib/jwt/encode.rb +++ b/lib/jwt/encode.rb @@ -14,7 +14,13 @@ def initialize(options) @options = options @payload = options[:payload] @key = options[:key] - _, @algorithm = Algos.find(options[:algorithm]) + + if (@algorithm_implementation = options[:algorithm_implementation]).nil? + _, @algorithm = Algos.find(options[:algorithm]) + else + @algorithm = @algorithm_implementation.alg + end + @headers = options[:headers].each_with_object({}) { |(key, value), headers| headers[key.to_s] = value } end @@ -58,7 +64,13 @@ def encode_payload def encode_signature return '' if @algorithm == ALG_NONE - Base64.urlsafe_encode64(JWT::Signature.sign(@algorithm, encoded_header_and_payload, @key), padding: false) + Base64.urlsafe_encode64(signature, padding: false) + end + + def signature + return @algorithm_implementation.sign(encoded_header_and_payload, key: @key) if @algorithm_implementation + + JWT::Signature.sign(@algorithm, encoded_header_and_payload, @key) end def encode(data) diff --git a/lib/jwt/extension/decode.rb b/lib/jwt/extension/decode.rb index c8f42e05..c4957e06 100644 --- a/lib/jwt/extension/decode.rb +++ b/lib/jwt/extension/decode.rb @@ -18,26 +18,24 @@ def jwk_resolver(&block) @jwk_resolver end - def decode!(payload, options = {}) - ::JWT::Decode.new(payload, - decode_signing_key_from_options(options), - true, - create_decode_options(options)).decode_segments + def decode!(token, options = {}) + Internals.decode!(token, options, self) end - def decode_signing_key_from_options(options) - options[:signing_key] || self.signing_key - end - - def create_decode_options(given_options) - ::JWT::DefaultOptions::DEFAULT_OPTIONS.merge(decode_payload_proc: self.decode_payload, - algorithms: self.decoding_algorithms, - jwks: self.jwk_resolver) - .merge(given_options) - end + module Internals + class << self + def decode!(token, options, context) + ::JWT::DecodeToken.new(token, build_decode_options(options, context)).decoded_segments + end - def decoding_algorithms - (Array(self.algorithm) + Array(self.algorithms)).uniq + def build_decode_options(options, context) + ::JWT::DefaultOptions::DEFAULT_OPTIONS.merge(key: options[:signing_key] || context.verification_key || context.signing_key, + decode_payload_proc: context.decode_payload, + algorithms: (Array(context.algorithm) + Array(context.algorithms)).uniq, + jwks: context.jwk_resolver) + .merge(options) + end + end end end end diff --git a/lib/jwt/extension/encode.rb b/lib/jwt/extension/encode.rb index c6d801cc..abbb643f 100644 --- a/lib/jwt/extension/encode.rb +++ b/lib/jwt/extension/encode.rb @@ -14,20 +14,33 @@ def encode_payload(&block) end def encode!(payload, options = {}) - ::JWT::Encode.new( - payload: payload, - key: signing_key_from_options(options), - algorithm: self.algorithm, - encode_payload_proc: self.encode_payload, - headers: Array(options[:headers]) - ).segments + Internals.encode!(payload, options, self) end - def signing_key_from_options(options) - key = options[:signing_key] || self.signing_key - raise ::JWT::SigningKeyMissing, 'No key given for signing' if key.nil? + module Internals + class << self + def encode!(payload, options, context) + ::JWT::Encode.new(build_options(payload, options, context)).segments + end - key + def build_options(payload, options, context) + opts = { + payload: payload, + key: options[:key] || context.signing_key, + encode_payload_proc: context.encode_payload, + headers: Array(options[:headers]) + } + + if (algo = context.algorithm).is_a?(String) + opts[:algorithm] = algo + raise ::JWT::SigningKeyMissing, 'No key given for signing' if opts[:key].nil? + else + opts[:algorithm_implementation] = algo + end + + opts + end + end end end end diff --git a/lib/jwt/extension/keys.rb b/lib/jwt/extension/keys.rb index 467c7bb5..fa541db5 100644 --- a/lib/jwt/extension/keys.rb +++ b/lib/jwt/extension/keys.rb @@ -7,6 +7,11 @@ def signing_key(value = nil) @signing_key = value unless value.nil? @signing_key end + + def verification_key(value = nil) + @verification_key = value unless value.nil? + @verification_key + end end end end diff --git a/spec/extension/example_custom_algorithm_spec.rb b/spec/extension/example_custom_algorithm_spec.rb new file mode 100644 index 00000000..8faacd65 --- /dev/null +++ b/spec/extension/example_custom_algorithm_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +RSpec.describe 'Custom Signing algorithm' do + let(:payload) { { 'pay' => 'load'} } + + let(:signing_algo) do + Class.new do + class << self + def alg + 'CustomStatic' + end + + def valid_alg?(_alg) + true + end + + def sign(_to_sign, _options) + 'static' + end + + def verify(_to_verify, signature, _options) + signature == 'static' + end + end + end + end + + subject(:extension) do + algo = signing_algo + + Class.new do + include JWT + algorithm algo + end + end + + context 'when encoding' do + it 'adds the custom signature to the end' do + expect(::Base64.decode64(subject.encode!(payload).split('.')[2])).to eq('static') + end + end + + context 'when decoding signed token' do + let(:presigned_token) { subject.encode!(payload) } + it 'verifies and decodes the payload' do + expect(subject.decode!(presigned_token)).to eq([{'pay' => 'load'}, {'alg' => 'CustomStatic'}]) + end + end +end From ff613e4a25181a96dbf9a39f96ba50304a02ed8d Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Fri, 7 Jan 2022 21:44:10 +0200 Subject: [PATCH 08/33] Extracted verify_claims --- lib/jwt/decode.rb | 16 +++++----------- lib/jwt/decode_behaviour.rb | 14 ++++++++++++++ lib/jwt/decode_token.rb | 13 ++++--------- lib/jwt/verify.rb | 2 +- 4 files changed, 24 insertions(+), 21 deletions(-) create mode 100644 lib/jwt/decode_behaviour.rb diff --git a/lib/jwt/decode.rb b/lib/jwt/decode.rb index 5a288bfc..cda56b3e 100644 --- a/lib/jwt/decode.rb +++ b/lib/jwt/decode.rb @@ -1,14 +1,11 @@ # frozen_string_literal: true -require 'json' +require_relative 'decode_behaviour' -require 'jwt/signature' -require 'jwt/verify' -require 'jwt/x5c_key_finder' -# JWT::Decode module module JWT # Decoding logic for JWT class Decode + include DecodeBehaviour def initialize(jwt, key, verify, options, &keyfinder) raise(JWT::DecodeError, 'Nil JSON web token') unless jwt @jwt = jwt @@ -27,7 +24,7 @@ def decode_segments verify_algo set_key verify_signature - verify_claims + verify_claims!(options) end raise(JWT::DecodeError, 'Not enough or too many segments') unless header && payload [payload, header] @@ -35,6 +32,8 @@ def decode_segments private + attr_reader :options + def verify_signature return unless @key || @verify @@ -93,11 +92,6 @@ def find_key(&keyfinder) raise JWT::DecodeError, 'No verification key available' end - def verify_claims - Verify.verify_claims(payload, @options) - Verify.verify_required_claims(payload, @options) - end - def validate_segment_count! return if segment_length == 3 return if !@verify && segment_length == 2 # If no verifying required, the signature is not needed diff --git a/lib/jwt/decode_behaviour.rb b/lib/jwt/decode_behaviour.rb new file mode 100644 index 00000000..e09e0d27 --- /dev/null +++ b/lib/jwt/decode_behaviour.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'jwt/signature' +require 'jwt/verify' +require 'jwt/x5c_key_finder' + +module JWT + module DecodeBehaviour + def verify_claims!(claim_options) + Verify.verify_claims(payload, claim_options) + Verify.verify_required_claims(payload, claim_options) + end + end +end diff --git a/lib/jwt/decode_token.rb b/lib/jwt/decode_token.rb index 82837536..135ba30a 100644 --- a/lib/jwt/decode_token.rb +++ b/lib/jwt/decode_token.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -require 'jwt/signature' -require 'jwt/verify' -require 'jwt/x5c_key_finder' +require_relative 'decode_behaviour' module JWT class DecodeToken + include DecodeBehaviour + def initialize(token, options = {}) raise ArgumentError, 'Provided token is not a String object' unless token.is_a?(String) @@ -19,7 +19,7 @@ def decoded_segments if verify? verify_alg_header! verify_signature! - verify_claims! + verify_claims!(options) end [payload, header] @@ -100,11 +100,6 @@ def verify_signature_for?(algorithm, key) end end - def verify_claims! - Verify.verify_claims(payload, options) - Verify.verify_required_claims(payload, options) - end - def validate_segment_count! segment_count = segments.size diff --git a/lib/jwt/verify.rb b/lib/jwt/verify.rb index c5ba0a10..58292a25 100644 --- a/lib/jwt/verify.rb +++ b/lib/jwt/verify.rb @@ -18,7 +18,7 @@ class << self def verify_claims(payload, options) options.each do |key, val| - next unless key.to_s =~ /verify/ + next unless key.to_s =~ /verify./ Verify.send(key, payload, options) if val end end From f35ba8280e5f59c85743b90265377fc8685ea6b2 Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Fri, 7 Jan 2022 22:03:51 +0200 Subject: [PATCH 09/33] Extracted even more methods and tests are passing --- lib/jwt/decode.rb | 71 ++++++++++--------------------------- lib/jwt/decode_behaviour.rb | 53 +++++++++++++++++++++++++++ lib/jwt/decode_token.rb | 53 --------------------------- 3 files changed, 72 insertions(+), 105 deletions(-) diff --git a/lib/jwt/decode.rb b/lib/jwt/decode.rb index cda56b3e..76ae1c8d 100644 --- a/lib/jwt/decode.rb +++ b/lib/jwt/decode.rb @@ -6,21 +6,18 @@ module JWT # Decoding logic for JWT class Decode include DecodeBehaviour - def initialize(jwt, key, verify, options, &keyfinder) - raise(JWT::DecodeError, 'Nil JSON web token') unless jwt - @jwt = jwt + def initialize(token, key, verify, options, &keyfinder) + raise(JWT::DecodeError, 'Nil JSON web token') unless token + @token = token @key = key @options = options - @segments = jwt.split('.') @verify = verify - @signature = '' @keyfinder = keyfinder end def decode_segments validate_segment_count! if @verify - decode_crypto verify_algo set_key verify_signature @@ -32,7 +29,11 @@ def decode_segments private - attr_reader :options + attr_reader :options, :token + + def verify? + @verify != false + end def verify_signature return unless @key || @verify @@ -54,14 +55,14 @@ def verify_algo def set_key @key = find_key(&@keyfinder) if @keyfinder - @key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks]).key_for(header['kid']) if @options[:jwks] - if (x5c_options = @options[:x5c]) + @key = ::JWT::JWK::KeyFinder.new(jwks: options[:jwks]).key_for(header['kid']) if options[:jwks] + if (x5c_options = options[:x5c]) @key = X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(header['x5c']) end end def verify_signature_for?(key) - Signature.verify(algorithm, key, signing_input, @signature) + Signature.verify(algorithm, key, signing_input, signature) end def options_includes_algo_in_header? @@ -70,14 +71,14 @@ def options_includes_algo_in_header? def allowed_algorithms # Order is very important - first check for string keys, next for symbols - algos = if @options.key?('algorithm') - @options['algorithm'] - elsif @options.key?(:algorithm) - @options[:algorithm] - elsif @options.key?('algorithms') - @options['algorithms'] - elsif @options.key?(:algorithms) - @options[:algorithms] + algos = if options.key?('algorithm') + options['algorithm'] + elsif options.key?(:algorithm) + options[:algorithm] + elsif options.key?('algorithms') + options['algorithms'] + elsif options.key?(:algorithms) + options[:algorithms] else [] end @@ -92,46 +93,12 @@ def find_key(&keyfinder) raise JWT::DecodeError, 'No verification key available' end - def validate_segment_count! - return if segment_length == 3 - return if !@verify && segment_length == 2 # If no verifying required, the signature is not needed - return if segment_length == 2 && none_algorithm? - - raise(JWT::DecodeError, 'Not enough or too many segments') - end - - def segment_length - @segments.count - end - def none_algorithm? algorithm.casecmp('none').zero? end - def decode_crypto - @signature = Base64.urlsafe_decode64(@segments[2] || '') - end - def algorithm header['alg'] end - - def header - @header ||= parse_and_decode @segments[0] - end - - def payload - @payload ||= parse_and_decode @segments[1] - end - - def signing_input - @segments.first(2).join('.') - end - - def parse_and_decode(segment) - JWT::JSON.parse(Base64.urlsafe_decode64(segment)) - rescue ::JSON::ParserError, ArgumentError - raise JWT::DecodeError, 'Invalid segment encoding' - end end end diff --git a/lib/jwt/decode_behaviour.rb b/lib/jwt/decode_behaviour.rb index e09e0d27..2f2c7602 100644 --- a/lib/jwt/decode_behaviour.rb +++ b/lib/jwt/decode_behaviour.rb @@ -6,9 +6,62 @@ module JWT module DecodeBehaviour + def segments + @segments ||= token.split('.') + end + + def signature + @signature ||= Base64.urlsafe_decode64(segments[2] || '') + end + + def header + @header ||= decode_header(segments[0]) + end + + def payload + @payload ||= decode_payload(segments[1]) + end + + def signing_input + segments.first(2).join('.') + end + + def validate_segment_count! + segment_count = segments.size + + return if segment_count == 3 + return if segment_count == 2 && (!verify? || header['alg'] == 'none') + + raise JWT::DecodeError, 'Not enough or too many segments' + end + def verify_claims!(claim_options) Verify.verify_claims(payload, claim_options) Verify.verify_required_claims(payload, claim_options) end + + def decode_header(raw_header) + decode_segment_default(raw_header) + end + + def decode_payload(raw_segment) + if options[:decode_payload_proc] + options[:decode_payload_proc].call(raw_segment, header, signature) + else + decode_segment_default(raw_segment) + end + end + + def decode_segment_default(raw_segment) + json_parse(Base64.urlsafe_decode64(raw_segment)) + rescue ArgumentError + raise JWT::DecodeError, 'Invalid segment encoding' + end + + def json_parse(decoded_segment) + JWT::JSON.parse(decoded_segment) + rescue ::JSON::ParserError + raise JWT::DecodeError, 'Invalid segment encoding' + end end end diff --git a/lib/jwt/decode_token.rb b/lib/jwt/decode_token.rb index 135ba30a..5bb0881a 100644 --- a/lib/jwt/decode_token.rb +++ b/lib/jwt/decode_token.rb @@ -33,26 +33,6 @@ def algorithms @algorithms ||= Array(options[:algorithms]) end - def segments - @segments ||= token.split('.') - end - - def signature - @signature ||= Base64.urlsafe_decode64(segments[2] || '') - end - - def header - @header ||= decode_header(segments[0]) - end - - def payload - @payload ||= decode_payload(segments[1]) - end - - def signing_input - segments.first(2).join('.') - end - def verify? options[:verify] != false end @@ -99,38 +79,5 @@ def verify_signature_for?(algorithm, key) algorithm.verify(signing_input, signature, key: key, header: header, payload: payload) end end - - def validate_segment_count! - segment_count = segments.size - - return if segment_count == 3 - return if segment_count == 2 && (!verify? || header['alg'] == 'none') - - raise JWT::DecodeError, 'Not enough or too many segments' - end - - def decode_header(raw_header) - decode_segment_default(raw_header) - end - - def decode_payload(raw_segment) - if @options[:decode_payload_proc] - @options[:decode_payload_proc].call(raw_segment, header, signature) - else - decode_segment_default(raw_segment) - end - end - - def decode_segment_default(raw_segment) - json_parse(Base64.urlsafe_decode64(raw_segment)) - rescue ArgumentError - raise JWT::DecodeError, 'Invalid segment encoding' - end - - def json_parse(decoded_segment) - JWT::JSON.parse(decoded_segment) - rescue ::JSON::ParserError - raise JWT::DecodeError, 'Invalid segment encoding' - end end end From 08b2d50b0157a3e65454cfc76fe8aa6eeebc1794 Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Fri, 7 Jan 2022 22:13:10 +0200 Subject: [PATCH 10/33] Avoid instance variables --- lib/jwt/decode.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/jwt/decode.rb b/lib/jwt/decode.rb index 76ae1c8d..525a697f 100644 --- a/lib/jwt/decode.rb +++ b/lib/jwt/decode.rb @@ -17,7 +17,7 @@ def initialize(token, key, verify, options, &keyfinder) def decode_segments validate_segment_count! - if @verify + if verify? verify_algo set_key verify_signature @@ -36,7 +36,7 @@ def verify? end def verify_signature - return unless @key || @verify + return unless @key || verify? return if none_algorithm? From 4181ecb421f1c727f2ff2ba88800ef650aa4c49e Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Fri, 7 Jan 2022 22:28:08 +0200 Subject: [PATCH 11/33] Renamed shared methods module --- lib/jwt/decode.rb | 24 ++++++------------- ...{decode_behaviour.rb => decode_methods.rb} | 22 ++++++++++++++++- lib/jwt/decode_token.rb | 23 +++--------------- 3 files changed, 31 insertions(+), 38 deletions(-) rename lib/jwt/{decode_behaviour.rb => decode_methods.rb} (68%) diff --git a/lib/jwt/decode.rb b/lib/jwt/decode.rb index 525a697f..36cea594 100644 --- a/lib/jwt/decode.rb +++ b/lib/jwt/decode.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true -require_relative 'decode_behaviour' +require_relative 'decode_methods' module JWT - # Decoding logic for JWT class Decode - include DecodeBehaviour + include DecodeMethods def initialize(token, key, verify, options, &keyfinder) raise(JWT::DecodeError, 'Nil JSON web token') unless token @token = token @@ -19,7 +18,6 @@ def decode_segments validate_segment_count! if verify? verify_algo - set_key verify_signature verify_claims!(options) end @@ -36,13 +34,13 @@ def verify? end def verify_signature - return unless @key || verify? + return unless key || verify? return if none_algorithm? - raise JWT::DecodeError, 'No verification key available' unless @key + raise JWT::DecodeError, 'No verification key available' unless key - return if Array(@key).any? { |key| verify_signature_for?(key) } + return if Array(key).any? { |k| verify_signature_for?(algorithm, k) } raise(JWT::VerificationError, 'Signature verification failed') end @@ -53,16 +51,8 @@ def verify_algo raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') unless options_includes_algo_in_header? end - def set_key - @key = find_key(&@keyfinder) if @keyfinder - @key = ::JWT::JWK::KeyFinder.new(jwks: options[:jwks]).key_for(header['kid']) if options[:jwks] - if (x5c_options = options[:x5c]) - @key = X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(header['x5c']) - end - end - - def verify_signature_for?(key) - Signature.verify(algorithm, key, signing_input, signature) + def key + @key ||= (@keyfinder && find_key(&@keyfinder)) || resolve_key end def options_includes_algo_in_header? diff --git a/lib/jwt/decode_behaviour.rb b/lib/jwt/decode_methods.rb similarity index 68% rename from lib/jwt/decode_behaviour.rb rename to lib/jwt/decode_methods.rb index 2f2c7602..0c3c4ab1 100644 --- a/lib/jwt/decode_behaviour.rb +++ b/lib/jwt/decode_methods.rb @@ -5,7 +5,7 @@ require 'jwt/x5c_key_finder' module JWT - module DecodeBehaviour + module DecodeMethods def segments @segments ||= token.split('.') end @@ -35,6 +35,26 @@ def validate_segment_count! raise JWT::DecodeError, 'Not enough or too many segments' end + def verify_signature_for?(algorithm, key) + if algorithm.is_a?(String) + raise JWT::DecodeError, 'No verification key available' unless key + + Array(key).any? { |k| Signature.verify(algorithm, k, signing_input, signature) } + else + algorithm.verify(signing_input, signature, key: key, header: header, payload: payload) + end + end + + def resolve_key + if options[:jwks] + ::JWT::JWK::KeyFinder.new(jwks: options[:jwks]).key_for(header['kid']) + elsif (x5c_options = options[:x5c]) + ::JWT::X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(header['x5c']) + else + options[:key] + end + end + def verify_claims!(claim_options) Verify.verify_claims(payload, claim_options) Verify.verify_required_claims(payload, claim_options) diff --git a/lib/jwt/decode_token.rb b/lib/jwt/decode_token.rb index 5bb0881a..3efc115a 100644 --- a/lib/jwt/decode_token.rb +++ b/lib/jwt/decode_token.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require_relative 'decode_behaviour' +require_relative 'decode_methods' module JWT class DecodeToken - include DecodeBehaviour + include DecodeMethods def initialize(token, options = {}) raise ArgumentError, 'Provided token is not a String object' unless token.is_a?(String) @@ -38,14 +38,7 @@ def verify? end def key - @key ||= - if options[:jwks] - ::JWT::JWK::KeyFinder.new(jwks: options[:jwks]).key_for(header['kid']) - elsif (x5c_options = options[:x5c]) - ::JWT::X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(header['x5c']) - else - options[:key] - end + @key ||= resolve_key end def verify_alg_header! @@ -69,15 +62,5 @@ def verify_signature! raise JWT::VerificationError, 'Signature verification failed' end - - def verify_signature_for?(algorithm, key) - if algorithm.is_a?(String) - raise JWT::DecodeError, 'No verification key available' unless key - - Array(key).any? { |k| Signature.verify(algorithm, k, signing_input, signature) } - else - algorithm.verify(signing_input, signature, key: key, header: header, payload: payload) - end - end end end From 4b78ebe8021d498a0d42f2f1841abf69d9a8729d Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Fri, 7 Jan 2022 22:34:34 +0200 Subject: [PATCH 12/33] Decrease differences on old behaviour --- lib/jwt.rb | 2 +- lib/jwt/decode.rb | 13 +++---------- lib/jwt/decode_methods.rb | 4 ++++ lib/jwt/decode_token.rb | 4 ---- 4 files changed, 8 insertions(+), 15 deletions(-) diff --git a/lib/jwt.rb b/lib/jwt.rb index 38d1ab4d..24115508 100644 --- a/lib/jwt.rb +++ b/lib/jwt.rb @@ -31,6 +31,6 @@ def encode(payload, key, algorithm = 'HS256', header_fields = {}) end def decode(jwt, key = nil, verify = true, options = {}, &keyfinder) - Decode.new(jwt, key, verify, DEFAULT_OPTIONS.merge(options), &keyfinder).decode_segments + Decode.new(jwt, DEFAULT_OPTIONS.merge(key: key, verify: verify).merge(options), &keyfinder).decode_segments end end diff --git a/lib/jwt/decode.rb b/lib/jwt/decode.rb index 36cea594..93c525d5 100644 --- a/lib/jwt/decode.rb +++ b/lib/jwt/decode.rb @@ -5,12 +5,11 @@ module JWT class Decode include DecodeMethods - def initialize(token, key, verify, options, &keyfinder) + + def initialize(token, options, &keyfinder) raise(JWT::DecodeError, 'Nil JSON web token') unless token @token = token - @key = key @options = options - @verify = verify @keyfinder = keyfinder end @@ -29,20 +28,14 @@ def decode_segments attr_reader :options, :token - def verify? - @verify != false - end - def verify_signature - return unless key || verify? - return if none_algorithm? raise JWT::DecodeError, 'No verification key available' unless key return if Array(key).any? { |k| verify_signature_for?(algorithm, k) } - raise(JWT::VerificationError, 'Signature verification failed') + raise JWT::VerificationError, 'Signature verification failed' end def verify_algo diff --git a/lib/jwt/decode_methods.rb b/lib/jwt/decode_methods.rb index 0c3c4ab1..c13e5cfd 100644 --- a/lib/jwt/decode_methods.rb +++ b/lib/jwt/decode_methods.rb @@ -6,6 +6,10 @@ module JWT module DecodeMethods + def verify? + options[:verify] != false + end + def segments @segments ||= token.split('.') end diff --git a/lib/jwt/decode_token.rb b/lib/jwt/decode_token.rb index 3efc115a..82c2f97e 100644 --- a/lib/jwt/decode_token.rb +++ b/lib/jwt/decode_token.rb @@ -33,10 +33,6 @@ def algorithms @algorithms ||= Array(options[:algorithms]) end - def verify? - options[:verify] != false - end - def key @key ||= resolve_key end From 4f965d1625cfc73352c6414c3eed724791feecbf Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Fri, 7 Jan 2022 22:35:26 +0200 Subject: [PATCH 13/33] Methods with bangs --- lib/jwt/decode.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/jwt/decode.rb b/lib/jwt/decode.rb index 93c525d5..1150fa1e 100644 --- a/lib/jwt/decode.rb +++ b/lib/jwt/decode.rb @@ -16,8 +16,8 @@ def initialize(token, options, &keyfinder) def decode_segments validate_segment_count! if verify? - verify_algo - verify_signature + verify_algo! + verify_signature! verify_claims!(options) end raise(JWT::DecodeError, 'Not enough or too many segments') unless header && payload @@ -28,7 +28,7 @@ def decode_segments attr_reader :options, :token - def verify_signature + def verify_signature! return if none_algorithm? raise JWT::DecodeError, 'No verification key available' unless key @@ -38,10 +38,10 @@ def verify_signature raise JWT::VerificationError, 'Signature verification failed' end - def verify_algo - raise(JWT::IncorrectAlgorithm, 'An algorithm must be specified') if allowed_algorithms.empty? - raise(JWT::IncorrectAlgorithm, 'Token is missing alg header') unless algorithm - raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') unless options_includes_algo_in_header? + def verify_algo! + raise JWT::IncorrectAlgorithm, 'An algorithm must be specified' if allowed_algorithms.empty? + raise JWT::IncorrectAlgorithm, 'Token is missing alg header' unless algorithm + raise JWT::IncorrectAlgorithm, 'Expected a different algorithm' unless options_includes_algo_in_header? end def key From 589542d72ef58ed32e0ba08234db5a483ebdd33c Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Fri, 7 Jan 2022 22:37:03 +0200 Subject: [PATCH 14/33] Unify raising --- lib/jwt/decode.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/jwt/decode.rb b/lib/jwt/decode.rb index 1150fa1e..b866033f 100644 --- a/lib/jwt/decode.rb +++ b/lib/jwt/decode.rb @@ -7,7 +7,7 @@ class Decode include DecodeMethods def initialize(token, options, &keyfinder) - raise(JWT::DecodeError, 'Nil JSON web token') unless token + raise JWT::DecodeError, 'Nil JSON web token' unless token @token = token @options = options @keyfinder = keyfinder @@ -20,13 +20,13 @@ def decode_segments verify_signature! verify_claims!(options) end - raise(JWT::DecodeError, 'Not enough or too many segments') unless header && payload + raise JWT::DecodeError, 'Not enough or too many segments' unless header && payload [payload, header] end private - attr_reader :options, :token + attr_reader :options, :token, :keyfinder def verify_signature! return if none_algorithm? @@ -45,7 +45,7 @@ def verify_algo! end def key - @key ||= (@keyfinder && find_key(&@keyfinder)) || resolve_key + @key ||= (keyfinder && find_key(@keyfinder)) || resolve_key end def options_includes_algo_in_header? From 938872374c18913673b8562bac3e93e9359e50d9 Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Fri, 7 Jan 2022 22:45:33 +0200 Subject: [PATCH 15/33] Raise the no verification key error from one place only --- lib/jwt/decode.rb | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/jwt/decode.rb b/lib/jwt/decode.rb index b866033f..b948dae5 100644 --- a/lib/jwt/decode.rb +++ b/lib/jwt/decode.rb @@ -31,7 +31,7 @@ def decode_segments def verify_signature! return if none_algorithm? - raise JWT::DecodeError, 'No verification key available' unless key + raise JWT::DecodeError, 'No verification key available' if Array(key).empty? return if Array(key).any? { |k| verify_signature_for?(algorithm, k) } @@ -45,7 +45,7 @@ def verify_algo! end def key - @key ||= (keyfinder && find_key(@keyfinder)) || resolve_key + @key ||= use_keyfinder || resolve_key end def options_includes_algo_in_header? @@ -68,12 +68,10 @@ def allowed_algorithms Array(algos) end - def find_key(&keyfinder) - key = (keyfinder.arity == 2 ? yield(header, payload) : yield(header)) + def use_keyfinder + return nil unless keyfinder + (keyfinder.arity == 2 ? keyfinder.call(header, payload) : keyfinder.call(header)) # key can be of type [string, nil, OpenSSL::PKey, Array] - return key if key && !Array(key).empty? - - raise JWT::DecodeError, 'No verification key available' end def none_algorithm? From d7dad0f628c992aa19ffc4830df7fbdd64d8de9d Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Sat, 8 Jan 2022 14:29:17 +0200 Subject: [PATCH 16/33] block support for the .decode! method --- lib/jwt/extension/decode.rb | 8 +++- lib/jwt/extension/keys.rb | 5 +++ spec/extension/decode_spec.rb | 37 ++++++++++++++++++- .../example_custom_algorithm_spec.rb | 4 +- spec/extension/example_deflating_spec.rb | 2 +- 5 files changed, 49 insertions(+), 7 deletions(-) diff --git a/lib/jwt/extension/decode.rb b/lib/jwt/extension/decode.rb index c4957e06..e112b593 100644 --- a/lib/jwt/extension/decode.rb +++ b/lib/jwt/extension/decode.rb @@ -19,7 +19,11 @@ def jwk_resolver(&block) end def decode!(token, options = {}) - Internals.decode!(token, options, self) + payload, header = Internals.decode!(token, options, self) + + return yield(payload, header) if block_given? + + [payload, header] end module Internals @@ -29,7 +33,7 @@ def decode!(token, options, context) end def build_decode_options(options, context) - ::JWT::DefaultOptions::DEFAULT_OPTIONS.merge(key: options[:signing_key] || context.verification_key || context.signing_key, + ::JWT::DefaultOptions::DEFAULT_OPTIONS.merge(key: options[:key] || context.verification_key || context.signing_key, decode_payload_proc: context.decode_payload, algorithms: (Array(context.algorithm) + Array(context.algorithms)).uniq, jwks: context.jwk_resolver) diff --git a/lib/jwt/extension/keys.rb b/lib/jwt/extension/keys.rb index fa541db5..69f9443c 100644 --- a/lib/jwt/extension/keys.rb +++ b/lib/jwt/extension/keys.rb @@ -12,6 +12,11 @@ def verification_key(value = nil) @verification_key = value unless value.nil? @verification_key end + + def key(value = nil) + verification_key(value) + signing_key(value) + end end end end diff --git a/spec/extension/decode_spec.rb b/spec/extension/decode_spec.rb index be1ec215..02235685 100644 --- a/spec/extension/decode_spec.rb +++ b/spec/extension/decode_spec.rb @@ -4,9 +4,12 @@ RSpec.describe JWT::Extension do subject(:extension) do + secret_key = secret + Class.new do include JWT algorithm 'HS256' + key secret_key end end @@ -19,7 +22,7 @@ context 'when nothing but algorithm is defined' do it 'verifies a token and returns the data' do - expect(extension.decode!(encoded_payload, signing_key: secret)).to eq([payload, { 'alg' => 'HS256' }]) + expect(extension.decode!(encoded_payload, key: secret)).to eq([payload, { 'alg' => 'HS256' }]) end end @@ -33,7 +36,37 @@ end it 'uses the defined decode_payload to process the raw payload' do - expect(extension.decode!(encoded_payload, signing_key: secret)).to eq([{'pay' => 'daol'}, { 'alg' => 'HS256' }]) + expect(extension.decode!(encoded_payload)).to eq([{'pay' => 'daol'}, { 'alg' => 'HS256' }]) + end + end + + context 'when block given' do + it 'calls it with payload and header' do + expect { |b| extension.decode!(encoded_payload, &b) }.to yield_with_args(payload, { 'alg' => 'HS256' }) + end + end + + context 'when given block returns something' do + it 'returns what the block returned' do + expect(extension.decode!(encoded_payload) { '123' }).to eq('123') + end + end + + context 'when signing key is invalid' do + it 'raises JWT::VerificationError' do + expect { extension.decode!(encoded_payload, key: 'invalid') }.to raise_error(JWT::VerificationError, 'Signature verification failed') + end + end + + context 'when algorithm is not matching the one in the token' do + it 'raises JWT::VerificationError' do + expect { extension.decode!(encoded_payload, algorithms: ['HS512']) }.to raise_error(JWT::IncorrectAlgorithm, 'Expected a different algorithm') + end + end + + context 'when one of the given algorithms match' do + it 'raises JWT::VerificationError' do + expect(extension.decode!(encoded_payload, algorithms: ['HS512', 'HS256'])).to eq([payload, { 'alg' => 'HS256' }]) end end end diff --git a/spec/extension/example_custom_algorithm_spec.rb b/spec/extension/example_custom_algorithm_spec.rb index 8faacd65..e885d8ba 100644 --- a/spec/extension/example_custom_algorithm_spec.rb +++ b/spec/extension/example_custom_algorithm_spec.rb @@ -10,8 +10,8 @@ def alg 'CustomStatic' end - def valid_alg?(_alg) - true + def valid_alg?(algorithm_from_header) + algorithm_from_header == self.alg end def sign(_to_sign, _options) diff --git a/spec/extension/example_deflating_spec.rb b/spec/extension/example_deflating_spec.rb index febfc477..8cd8f9af 100644 --- a/spec/extension/example_deflating_spec.rb +++ b/spec/extension/example_deflating_spec.rb @@ -40,8 +40,8 @@ context 'when decoding presigned and zipped token' do let(:secret) { 's3cr3t' } - let(:presigned_token) { 'eyJhbGciOiJIUzUxMiJ9.H4sIAKTUyWEAA6tWKkisVLJSyslPTFGqBQAsM7zZDgAAAA==.GK1DXdMN7i6OA_1_xUYU3lThZwY94MgUYRivRIaLTIP-yrmZfxLrbpe3Llkrr1HIrDQhjPPwskiR5oob14hv9A' } + it 'verifies and decodes the payload' do expect(subject.decode!(presigned_token)).to eq([{'pay' => 'load'}, {'alg' => 'HS512'}]) end From 61fa3d5cbce5ed24f5d5d828bc0db46117f6137e Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Sun, 9 Jan 2022 00:12:53 +0200 Subject: [PATCH 17/33] Refactor ::JWT::Decode class to support custom algorithms --- .rubocop_todo.yml | 7 ------ lib/jwt/algos/none.rb | 4 +++- lib/jwt/claims_validator.rb | 2 +- lib/jwt/decode.rb | 9 +++---- lib/jwt/encode.rb | 44 +++++++++++++++-------------------- lib/jwt/extension/encode.rb | 10 ++++---- lib/jwt/jwk/ec.rb | 4 ---- spec/extension/decode_spec.rb | 14 +++++++++++ 8 files changed, 46 insertions(+), 48 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index c1442e11..9d92df65 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -83,13 +83,6 @@ Style/FrozenStringLiteralComment: - 'lib/jwt/security_utils.rb' - 'ruby-jwt.gemspec' -# Offense count: 2 -# Cop supports --auto-correct. -Style/HashTransformKeys: - Exclude: - - 'lib/jwt/claims_validator.rb' - - 'lib/jwt/encode.rb' - # Offense count: 1 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, Autocorrect. diff --git a/lib/jwt/algos/none.rb b/lib/jwt/algos/none.rb index 17d15f14..a1d13703 100644 --- a/lib/jwt/algos/none.rb +++ b/lib/jwt/algos/none.rb @@ -5,7 +5,9 @@ module None SUPPORTED = %w[none].freeze - def sign(*); end + def sign(*) + '' + end def verify(*) true diff --git a/lib/jwt/claims_validator.rb b/lib/jwt/claims_validator.rb index 27030c99..42c075b0 100644 --- a/lib/jwt/claims_validator.rb +++ b/lib/jwt/claims_validator.rb @@ -9,7 +9,7 @@ class ClaimsValidator ].freeze def initialize(payload) - @payload = payload.each_with_object({}) { |(k, v), h| h[k.to_sym] = v } + @payload = payload.transform_keys(&:to_sym) end def validate! diff --git a/lib/jwt/decode.rb b/lib/jwt/decode.rb index b948dae5..6f86d876 100644 --- a/lib/jwt/decode.rb +++ b/lib/jwt/decode.rb @@ -53,8 +53,12 @@ def options_includes_algo_in_header? end def allowed_algorithms + Array(algorithm_from_options) + end + + def algorithm_from_options # Order is very important - first check for string keys, next for symbols - algos = if options.key?('algorithm') + if options.key?('algorithm') options['algorithm'] elsif options.key?(:algorithm) options[:algorithm] @@ -62,10 +66,7 @@ def allowed_algorithms options['algorithms'] elsif options.key?(:algorithms) options[:algorithms] - else - [] end - Array(algos) end def use_keyfinder diff --git a/lib/jwt/encode.rb b/lib/jwt/encode.rb index 00c8d280..f0de1bc7 100644 --- a/lib/jwt/encode.rb +++ b/lib/jwt/encode.rb @@ -1,35 +1,36 @@ # frozen_string_literal: true -require_relative './algos' -require_relative './claims_validator' +require_relative 'algos' +require_relative 'claims_validator' -# JWT::Encode module module JWT - # Encoding logic for JWT class Encode - ALG_NONE = 'none'.freeze - ALG_KEY = 'alg'.freeze - def initialize(options) @options = options @payload = options[:payload] - @key = options[:key] + @key = options[:key] - if (@algorithm_implementation = options[:algorithm_implementation]).nil? - _, @algorithm = Algos.find(options[:algorithm]) + if (algo = options[:algorithm]).is_a?(String) || algo.nil? + _, @alg = Algos.find(algo) else - @algorithm = @algorithm_implementation.alg + @algorithm = algo end - @headers = options[:headers].each_with_object({}) { |(key, value), headers| headers[key.to_s] = value } + @headers = (options[:headers] || {}).transform_keys(&:to_s) + + headers['alg'] = algorithm ? algorithm.alg : alg end def segments - @segments ||= combine(encoded_header_and_payload, encoded_signature) + ClaimsValidator.new(payload).validate! if payload.is_a?(Hash) + + combine(encoded_header_and_payload, encoded_signature) end private + attr_reader :payload, :headers, :options, :algorithm, :key, :alg + def encoded_header @encoded_header ||= encode_header end @@ -47,30 +48,23 @@ def encoded_header_and_payload end def encode_header - @headers[ALG_KEY] = @algorithm - encode(@headers) + encode(headers) end def encode_payload - if @payload && @payload.is_a?(Hash) - ClaimsValidator.new(@payload).validate! - end - - return @options[:encode_payload_proc].call(@payload) unless @options[:encode_payload_proc].nil? + return options[:encode_payload_proc].call(payload) if options[:encode_payload_proc] - encode(@payload) + encode(payload) end def encode_signature - return '' if @algorithm == ALG_NONE - Base64.urlsafe_encode64(signature, padding: false) end def signature - return @algorithm_implementation.sign(encoded_header_and_payload, key: @key) if @algorithm_implementation + return algorithm.sign(encoded_header_and_payload, key: key) if algorithm - JWT::Signature.sign(@algorithm, encoded_header_and_payload, @key) + JWT::Signature.sign(alg, encoded_header_and_payload, key) end def encode(data) diff --git a/lib/jwt/extension/encode.rb b/lib/jwt/extension/encode.rb index abbb643f..c57415ba 100644 --- a/lib/jwt/extension/encode.rb +++ b/lib/jwt/extension/encode.rb @@ -28,14 +28,12 @@ def build_options(payload, options, context) payload: payload, key: options[:key] || context.signing_key, encode_payload_proc: context.encode_payload, - headers: Array(options[:headers]) + headers: options[:headers], + algorithm: context.algorithm } - if (algo = context.algorithm).is_a?(String) - opts[:algorithm] = algo - raise ::JWT::SigningKeyMissing, 'No key given for signing' if opts[:key].nil? - else - opts[:algorithm_implementation] = algo + if opts[:algorithm].is_a?(String) && opts[:key].nil? + raise ::JWT::SigningKeyMissing, 'No key given for signing' end opts diff --git a/lib/jwt/jwk/ec.rb b/lib/jwt/jwk/ec.rb index e3634810..d5ef3924 100644 --- a/lib/jwt/jwk/ec.rb +++ b/lib/jwt/jwk/ec.rb @@ -75,10 +75,6 @@ def encode_octets(octets) Base64.urlsafe_encode64(octets, padding: false) end - def encode_open_ssl_bn(key_part) - Base64.urlsafe_encode64(key_part.to_s(BINARY), padding: false) - end - class << self def import(jwk_data) # See https://tools.ietf.org/html/rfc7518#section-6.2.1 for an diff --git a/spec/extension/decode_spec.rb b/spec/extension/decode_spec.rb index 02235685..cde522b5 100644 --- a/spec/extension/decode_spec.rb +++ b/spec/extension/decode_spec.rb @@ -69,5 +69,19 @@ expect(extension.decode!(encoded_payload, algorithms: ['HS512', 'HS256'])).to eq([payload, { 'alg' => 'HS256' }]) end end + + context 'when payload is invalid JSON' do + before do + extension.encode_payload do |payload| + Base64.urlsafe_encode64(payload.inspect, padding: false) + end + end + + let(:encoded_payload) { extension.encode!(payload) } + + it 'raises JWT::DecodeError' do + expect { extension.decode!(encoded_payload) }.to raise_error(JWT::DecodeError, 'Invalid segment encoding') + end + end end end From f0b7d96ee5bb69966fdbdc3087058d3b7b0a7ac3 Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Sun, 9 Jan 2022 11:44:55 +0200 Subject: [PATCH 18/33] Some refactorings --- lib/jwt/encode.rb | 19 +++++++++++++------ lib/jwt/verify.rb | 2 +- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/lib/jwt/encode.rb b/lib/jwt/encode.rb index f0de1bc7..dd4af4bc 100644 --- a/lib/jwt/encode.rb +++ b/lib/jwt/encode.rb @@ -10,10 +10,10 @@ def initialize(options) @payload = options[:payload] @key = options[:key] - if (algo = options[:algorithm]).is_a?(String) || algo.nil? - _, @alg = Algos.find(algo) - else + if (algo = options[:algorithm]).respond_to?(:sign) @algorithm = algo + else + _, @alg = Algos.find(algo) end @headers = (options[:headers] || {}).transform_keys(&:to_s) @@ -22,8 +22,7 @@ def initialize(options) end def segments - ClaimsValidator.new(payload).validate! if payload.is_a?(Hash) - + validate_claims! combine(encoded_header_and_payload, encoded_signature) end @@ -52,7 +51,9 @@ def encode_header end def encode_payload - return options[:encode_payload_proc].call(payload) if options[:encode_payload_proc] + if (encode_proc = options[:encode_payload_proc]) + return encode_proc.call(payload) + end encode(payload) end @@ -67,6 +68,12 @@ def signature JWT::Signature.sign(alg, encoded_header_and_payload, key) end + def validate_claims! + return unless payload.is_a?(Hash) + + ClaimsValidator.new(payload).validate! + end + def encode(data) Base64.urlsafe_encode64(JWT::JSON.generate(data), padding: false) end diff --git a/lib/jwt/verify.rb b/lib/jwt/verify.rb index 58292a25..2ed625c3 100644 --- a/lib/jwt/verify.rb +++ b/lib/jwt/verify.rb @@ -18,7 +18,7 @@ class << self def verify_claims(payload, options) options.each do |key, val| - next unless key.to_s =~ /verify./ + next unless /verify./.match?(key.to_s) Verify.send(key, payload, options) if val end end From 604a23e932f50ef57c8f7e8f09434fe61f6ebe5c Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Sun, 9 Jan 2022 11:53:37 +0200 Subject: [PATCH 19/33] Refactor decoding a litte more --- lib/jwt/decode.rb | 12 ++++-------- lib/jwt/decode_methods.rb | 18 +++++++++++------- lib/jwt/decode_token.rb | 4 ++-- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/jwt/decode.rb b/lib/jwt/decode.rb index 6f86d876..a886ac3f 100644 --- a/lib/jwt/decode.rb +++ b/lib/jwt/decode.rb @@ -33,14 +33,14 @@ def verify_signature! raise JWT::DecodeError, 'No verification key available' if Array(key).empty? - return if Array(key).any? { |k| verify_signature_for?(algorithm, k) } + return if Array(key).any? { |single_key| verify_signature_for?(algorithm_in_header, single_key) } raise JWT::VerificationError, 'Signature verification failed' end def verify_algo! raise JWT::IncorrectAlgorithm, 'An algorithm must be specified' if allowed_algorithms.empty? - raise JWT::IncorrectAlgorithm, 'Token is missing alg header' unless algorithm + raise JWT::IncorrectAlgorithm, 'Token is missing alg header' unless algorithm_in_header raise JWT::IncorrectAlgorithm, 'Expected a different algorithm' unless options_includes_algo_in_header? end @@ -49,7 +49,7 @@ def key end def options_includes_algo_in_header? - allowed_algorithms.any? { |alg| alg.casecmp(algorithm).zero? } + allowed_algorithms.any? { |alg| alg.casecmp(algorithm_in_header).zero? } end def allowed_algorithms @@ -76,11 +76,7 @@ def use_keyfinder end def none_algorithm? - algorithm.casecmp('none').zero? - end - - def algorithm - header['alg'] + algorithm_in_header.casecmp('none').zero? end end end diff --git a/lib/jwt/decode_methods.rb b/lib/jwt/decode_methods.rb index c13e5cfd..9e93b01e 100644 --- a/lib/jwt/decode_methods.rb +++ b/lib/jwt/decode_methods.rb @@ -26,6 +26,10 @@ def payload @payload ||= decode_payload(segments[1]) end + def algorithm_in_header + header['alg'] + end + def signing_input segments.first(2).join('.') end @@ -43,15 +47,15 @@ def verify_signature_for?(algorithm, key) if algorithm.is_a?(String) raise JWT::DecodeError, 'No verification key available' unless key - Array(key).any? { |k| Signature.verify(algorithm, k, signing_input, signature) } + Array(key).any? { |single_key| Signature.verify(algorithm, single_key, signing_input, signature) } else algorithm.verify(signing_input, signature, key: key, header: header, payload: payload) end end def resolve_key - if options[:jwks] - ::JWT::JWK::KeyFinder.new(jwks: options[:jwks]).key_for(header['kid']) + if (jwks = options[:jwks]) + ::JWT::JWK::KeyFinder.new(jwks: jwks).key_for(header['kid']) elsif (x5c_options = options[:x5c]) ::JWT::X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(header['x5c']) else @@ -69,11 +73,11 @@ def decode_header(raw_header) end def decode_payload(raw_segment) - if options[:decode_payload_proc] - options[:decode_payload_proc].call(raw_segment, header, signature) - else - decode_segment_default(raw_segment) + if (decode_proc = options[:decode_payload_proc]) + return decode_proc.call(raw_segment, header, signature) end + + decode_segment_default(raw_segment) end def decode_segment_default(raw_segment) diff --git a/lib/jwt/decode_token.rb b/lib/jwt/decode_token.rb index 82c2f97e..0229475d 100644 --- a/lib/jwt/decode_token.rb +++ b/lib/jwt/decode_token.rb @@ -46,9 +46,9 @@ def verify_alg_header! def valid_algorithms @valid_algorithms ||= algorithms.select do |algorithm| if algorithm.is_a?(String) - header['alg'] == algorithm + algorithm == algorithm_in_header else - algorithm.valid_alg?(header['alg']) + algorithm.valid_alg?(algorithm_in_header) end end end From e13bdbd32cc32981dbe61565423fa7959ece1be8 Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Sun, 9 Jan 2022 15:16:23 +0200 Subject: [PATCH 20/33] Renamed ::JWT::Extension to ::JWT:DSL --- .reek.yml | 3 +++ lib/jwt.rb | 4 ++-- lib/jwt/decode.rb | 1 + lib/jwt/decode_methods.rb | 1 + lib/jwt/decode_token.rb | 1 + lib/jwt/dsl.rb | 16 ++++++++++++++++ lib/jwt/{extension => dsl}/decode.rb | 3 ++- lib/jwt/{extension => dsl}/encode.rb | 3 ++- lib/jwt/{extension => dsl}/keys.rb | 3 ++- lib/jwt/extension.rb | 15 --------------- spec/{extension => dsl}/decode_spec.rb | 2 +- spec/{extension => dsl}/encode_spec.rb | 2 +- .../example_custom_algorithm_spec.rb | 0 .../{extension => dsl}/example_deflating_spec.rb | 0 .../example_smart_health_card_spec.rb | 0 15 files changed, 32 insertions(+), 22 deletions(-) create mode 100644 .reek.yml create mode 100644 lib/jwt/dsl.rb rename lib/jwt/{extension => dsl}/decode.rb (95%) rename lib/jwt/{extension => dsl}/encode.rb (94%) rename lib/jwt/{extension => dsl}/keys.rb (89%) delete mode 100644 lib/jwt/extension.rb rename spec/{extension => dsl}/decode_spec.rb (98%) rename spec/{extension => dsl}/encode_spec.rb (97%) rename spec/{extension => dsl}/example_custom_algorithm_spec.rb (100%) rename spec/{extension => dsl}/example_deflating_spec.rb (100%) rename spec/{extension => dsl}/example_smart_health_card_spec.rb (100%) diff --git a/.reek.yml b/.reek.yml new file mode 100644 index 00000000..1de8bb22 --- /dev/null +++ b/.reek.yml @@ -0,0 +1,3 @@ +detectors: + IrresponsibleModule: + enabled: false diff --git a/lib/jwt.rb b/lib/jwt.rb index 24115508..e7fbb36f 100644 --- a/lib/jwt.rb +++ b/lib/jwt.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'base64' -require 'jwt/extension' +require 'jwt/dsl' require 'jwt/decode_token' require 'jwt/json' require 'jwt/decode' @@ -18,7 +18,7 @@ module JWT include JWT::DefaultOptions def self.included(cls) - cls.include(JWT::Extension) + cls.include(::JWT::DSL) end module_function diff --git a/lib/jwt/decode.rb b/lib/jwt/decode.rb index a886ac3f..8cc43454 100644 --- a/lib/jwt/decode.rb +++ b/lib/jwt/decode.rb @@ -3,6 +3,7 @@ require_relative 'decode_methods' module JWT + # Backwards compatible Decoding logic for the JWT Gem. Used by the ::JWT.decode method class Decode include DecodeMethods diff --git a/lib/jwt/decode_methods.rb b/lib/jwt/decode_methods.rb index 9e93b01e..6abc9855 100644 --- a/lib/jwt/decode_methods.rb +++ b/lib/jwt/decode_methods.rb @@ -5,6 +5,7 @@ require 'jwt/x5c_key_finder' module JWT + # Shared methods and behaviours used by ::JWT::DecodeToken and ::JWT::Decode module DecodeMethods def verify? options[:verify] != false diff --git a/lib/jwt/decode_token.rb b/lib/jwt/decode_token.rb index 0229475d..d4a7844e 100644 --- a/lib/jwt/decode_token.rb +++ b/lib/jwt/decode_token.rb @@ -3,6 +3,7 @@ require_relative 'decode_methods' module JWT + # Decode logic to support the ::JWT::Extensions::Decode functionality class DecodeToken include DecodeMethods diff --git a/lib/jwt/dsl.rb b/lib/jwt/dsl.rb new file mode 100644 index 00000000..af4c5fd3 --- /dev/null +++ b/lib/jwt/dsl.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require_relative 'dsl/keys' +require_relative 'dsl/decode' +require_relative 'dsl/encode' + +module JWT + # Module to gather the different parts of the DSL + module DSL + def self.included(cls) + cls.extend(JWT::DSL::Keys) + cls.extend(JWT::DSL::Decode) + cls.extend(JWT::DSL::Encode) + end + end +end diff --git a/lib/jwt/extension/decode.rb b/lib/jwt/dsl/decode.rb similarity index 95% rename from lib/jwt/extension/decode.rb rename to lib/jwt/dsl/decode.rb index e112b593..884e4972 100644 --- a/lib/jwt/extension/decode.rb +++ b/lib/jwt/dsl/decode.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true module JWT - module Extension + module DSL + # DSL methods for decoding related functionality module Decode def decode_payload(&block) @decode_payload = block if block_given? diff --git a/lib/jwt/extension/encode.rb b/lib/jwt/dsl/encode.rb similarity index 94% rename from lib/jwt/extension/encode.rb rename to lib/jwt/dsl/encode.rb index c57415ba..97ddeabd 100644 --- a/lib/jwt/extension/encode.rb +++ b/lib/jwt/dsl/encode.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true module JWT - module Extension + module DSL + # DSL methods for encoding related functionality module Encode def algorithm(value = nil) @algorithm = value unless value.nil? diff --git a/lib/jwt/extension/keys.rb b/lib/jwt/dsl/keys.rb similarity index 89% rename from lib/jwt/extension/keys.rb rename to lib/jwt/dsl/keys.rb index 69f9443c..9f02db0b 100644 --- a/lib/jwt/extension/keys.rb +++ b/lib/jwt/dsl/keys.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true module JWT - module Extension + module DSL + # DSL methods for setting keys module Keys def signing_key(value = nil) @signing_key = value unless value.nil? diff --git a/lib/jwt/extension.rb b/lib/jwt/extension.rb deleted file mode 100644 index e9f1e69a..00000000 --- a/lib/jwt/extension.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -require_relative 'extension/keys' -require_relative 'extension/decode' -require_relative 'extension/encode' - -module JWT - module Extension - def self.included(cls) - cls.extend(JWT::Extension::Keys) - cls.extend(JWT::Extension::Decode) - cls.extend(JWT::Extension::Encode) - end - end -end diff --git a/spec/extension/decode_spec.rb b/spec/dsl/decode_spec.rb similarity index 98% rename from spec/extension/decode_spec.rb rename to spec/dsl/decode_spec.rb index cde522b5..e1a4b259 100644 --- a/spec/extension/decode_spec.rb +++ b/spec/dsl/decode_spec.rb @@ -2,7 +2,7 @@ require 'securerandom' -RSpec.describe JWT::Extension do +RSpec.describe JWT::DSL do subject(:extension) do secret_key = secret diff --git a/spec/extension/encode_spec.rb b/spec/dsl/encode_spec.rb similarity index 97% rename from spec/extension/encode_spec.rb rename to spec/dsl/encode_spec.rb index 2f50a4b1..afb8712e 100644 --- a/spec/extension/encode_spec.rb +++ b/spec/dsl/encode_spec.rb @@ -2,7 +2,7 @@ require 'securerandom' -RSpec.describe JWT::Extension do +RSpec.describe JWT::DSL do subject(:extension) do Class.new do include JWT diff --git a/spec/extension/example_custom_algorithm_spec.rb b/spec/dsl/example_custom_algorithm_spec.rb similarity index 100% rename from spec/extension/example_custom_algorithm_spec.rb rename to spec/dsl/example_custom_algorithm_spec.rb diff --git a/spec/extension/example_deflating_spec.rb b/spec/dsl/example_deflating_spec.rb similarity index 100% rename from spec/extension/example_deflating_spec.rb rename to spec/dsl/example_deflating_spec.rb diff --git a/spec/extension/example_smart_health_card_spec.rb b/spec/dsl/example_smart_health_card_spec.rb similarity index 100% rename from spec/extension/example_smart_health_card_spec.rb rename to spec/dsl/example_smart_health_card_spec.rb From 6b3611db7d1f79c7bc6999a65520c3bd37d0999c Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Sun, 9 Jan 2022 15:27:48 +0200 Subject: [PATCH 21/33] Disabled MissingSafeMethod --- .reek.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.reek.yml b/.reek.yml index 1de8bb22..15a0ddc1 100644 --- a/.reek.yml +++ b/.reek.yml @@ -1,3 +1,5 @@ detectors: IrresponsibleModule: enabled: false + MissingSafeMethod: + enabled: false \ No newline at end of file From 581d4b75f8f73fc245c8ada0b8c57902e5a2432d Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Sun, 9 Jan 2022 21:02:01 +0200 Subject: [PATCH 22/33] No need to check the keyfinder arity before calling --- lib/jwt/decode.rb | 3 +-- spec/jwt_spec.rb | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/jwt/decode.rb b/lib/jwt/decode.rb index 8cc43454..144cb1c0 100644 --- a/lib/jwt/decode.rb +++ b/lib/jwt/decode.rb @@ -71,8 +71,7 @@ def algorithm_from_options end def use_keyfinder - return nil unless keyfinder - (keyfinder.arity == 2 ? keyfinder.call(header, payload) : keyfinder.call(header)) + keyfinder&.call(header, payload) # key can be of type [string, nil, OpenSSL::PKey, Array] end diff --git a/spec/jwt_spec.rb b/spec/jwt_spec.rb index 239b1a4b..fab21818 100644 --- a/spec/jwt_spec.rb +++ b/spec/jwt_spec.rb @@ -712,9 +712,8 @@ describe 'when keyfinder given with 3 arguments' do let(:token) { JWT.encode(payload, 'HS256', 'HS256') } it 'decodes the token but does not pass the payload' do - expect(JWT.decode(token, nil, true, algorithm: 'HS256') do |header, token_payload, nothing| - expect(token_payload).to eq(nil) # This behaviour is not correct, the payload should be available in the keyfinder - expect(nothing).to eq(nil) + expect(JWT.decode(token, nil, true, algorithm: 'HS256') do |header, token_payload, _nothing| + expect(token_payload).to eq(payload) header['alg'] end).to include(payload) end From d46755152beb4832572e8e8af0f79c79f9d9fefb Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Sun, 9 Jan 2022 21:19:28 +0200 Subject: [PATCH 23/33] Leeway configuration for DSL --- .reek.yml | 2 ++ lib/jwt/decode.rb | 2 +- lib/jwt/dsl/decode.rb | 8 ++++++++ spec/dsl/decode_spec.rb | 25 +++++++++++++++++++++++-- 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/.reek.yml b/.reek.yml index 15a0ddc1..23309df5 100644 --- a/.reek.yml +++ b/.reek.yml @@ -2,4 +2,6 @@ detectors: IrresponsibleModule: enabled: false MissingSafeMethod: + enabled: false + NilCheck: enabled: false \ No newline at end of file diff --git a/lib/jwt/decode.rb b/lib/jwt/decode.rb index 144cb1c0..0bcc30c0 100644 --- a/lib/jwt/decode.rb +++ b/lib/jwt/decode.rb @@ -70,9 +70,9 @@ def algorithm_from_options end end + # key can be of type [string, nil, OpenSSL::PKey, Array] def use_keyfinder keyfinder&.call(header, payload) - # key can be of type [string, nil, OpenSSL::PKey, Array] end def none_algorithm? diff --git a/lib/jwt/dsl/decode.rb b/lib/jwt/dsl/decode.rb index 884e4972..1488ef5f 100644 --- a/lib/jwt/dsl/decode.rb +++ b/lib/jwt/dsl/decode.rb @@ -4,6 +4,8 @@ module JWT module DSL # DSL methods for decoding related functionality module Decode + DEFAULT_EXPIRATION_LEEWAY = 0 + def decode_payload(&block) @decode_payload = block if block_given? @decode_payload @@ -19,6 +21,11 @@ def jwk_resolver(&block) @jwk_resolver end + def expiration_leeway(value = nil) + @expiration_leeway = value unless value.nil? + @expiration_leeway || DEFAULT_EXPIRATION_LEEWAY + end + def decode!(token, options = {}) payload, header = Internals.decode!(token, options, self) @@ -36,6 +43,7 @@ def decode!(token, options, context) def build_decode_options(options, context) ::JWT::DefaultOptions::DEFAULT_OPTIONS.merge(key: options[:key] || context.verification_key || context.signing_key, decode_payload_proc: context.decode_payload, + leeway: context.expiration_leeway, algorithms: (Array(context.algorithm) + Array(context.algorithms)).uniq, jwks: context.jwk_resolver) .merge(options) diff --git a/spec/dsl/decode_spec.rb b/spec/dsl/decode_spec.rb index e1a4b259..e1d792a6 100644 --- a/spec/dsl/decode_spec.rb +++ b/spec/dsl/decode_spec.rb @@ -14,7 +14,8 @@ end let(:secret) { SecureRandom.hex } - let(:payload) { { 'pay' => 'load'} } + let(:exp) { Time.now.to_i + 60 } + let(:payload) { { 'pay' => 'load', 'exp' => exp } } let(:encoded_payload) { ::JWT.encode(payload, secret, 'HS256') } describe '.decode!' do @@ -36,7 +37,7 @@ end it 'uses the defined decode_payload to process the raw payload' do - expect(extension.decode!(encoded_payload)).to eq([{'pay' => 'daol'}, { 'alg' => 'HS256' }]) + expect(extension.decode!(encoded_payload).first['pay']).to eq('daol') end end @@ -83,5 +84,25 @@ expect { extension.decode!(encoded_payload) }.to raise_error(JWT::DecodeError, 'Invalid segment encoding') end end + + context 'when token is expired' do + let(:exp) { Time.now.to_i - 20 } + + it 'allows token to be 30 seconds overdue' do + expect { extension.decode!(encoded_payload) }.to raise_error(JWT::ExpiredSignature, 'Signature has expired') + end + end + + context 'when expiration_leeway is set to 30 seconds' do + before do + extension.expiration_leeway 30 + end + + let(:exp) { Time.now.to_i - 20 } + + it 'allows token to be 30 seconds overdue' do + expect(extension.decode!(encoded_payload)).to eq([payload, { 'alg' => 'HS256' }]) + end + end end end From 9a3bb7e0f3253ae343863e147c27e8432239b234 Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Sun, 9 Jan 2022 21:27:24 +0200 Subject: [PATCH 24/33] Nil token handling --- lib/jwt/decode_token.rb | 2 +- spec/dsl/decode_spec.rb | 42 ++++++++++++++++++++++++++--------------- spec/dsl/encode_spec.rb | 14 +++++++------- 3 files changed, 35 insertions(+), 23 deletions(-) diff --git a/lib/jwt/decode_token.rb b/lib/jwt/decode_token.rb index d4a7844e..83ec64b8 100644 --- a/lib/jwt/decode_token.rb +++ b/lib/jwt/decode_token.rb @@ -8,7 +8,7 @@ class DecodeToken include DecodeMethods def initialize(token, options = {}) - raise ArgumentError, 'Provided token is not a String object' unless token.is_a?(String) + raise JWT::DecodeError, 'Provided token is not a String object' unless token.is_a?(String) @token = token @options = options diff --git a/spec/dsl/decode_spec.rb b/spec/dsl/decode_spec.rb index e1d792a6..1aa87c3a 100644 --- a/spec/dsl/decode_spec.rb +++ b/spec/dsl/decode_spec.rb @@ -3,7 +3,7 @@ require 'securerandom' RSpec.describe JWT::DSL do - subject(:extension) do + subject(:jwt_class) do secret_key = secret Class.new do @@ -23,13 +23,25 @@ context 'when nothing but algorithm is defined' do it 'verifies a token and returns the data' do - expect(extension.decode!(encoded_payload, key: secret)).to eq([payload, { 'alg' => 'HS256' }]) + expect(jwt_class.decode!(encoded_payload, key: secret)).to eq([payload, { 'alg' => 'HS256' }]) + end + end + + context 'when token is nil' do + it 'raises JWT::DecodeError' do + expect { jwt_class.decode!(nil) }.to raise_error(JWT::DecodeError, 'Provided token is not a String object') + end + end + + context 'when token is a 1' do + it 'raises JWT::DecodeError' do + expect { jwt_class.decode!(1) }.to raise_error(JWT::DecodeError, 'Provided token is not a String object') end end context 'when a decode_payload block manipulates the payload' do before do - extension.decode_payload do |raw_payload, _header, _signature| + jwt_class.decode_payload do |raw_payload, _header, _signature| payload_content = JWT::JSON.parse(Base64.urlsafe_decode64(raw_payload)) payload_content['pay'].reverse! payload_content @@ -37,51 +49,51 @@ end it 'uses the defined decode_payload to process the raw payload' do - expect(extension.decode!(encoded_payload).first['pay']).to eq('daol') + expect(jwt_class.decode!(encoded_payload).first['pay']).to eq('daol') end end context 'when block given' do it 'calls it with payload and header' do - expect { |b| extension.decode!(encoded_payload, &b) }.to yield_with_args(payload, { 'alg' => 'HS256' }) + expect { |b| jwt_class.decode!(encoded_payload, &b) }.to yield_with_args(payload, { 'alg' => 'HS256' }) end end context 'when given block returns something' do it 'returns what the block returned' do - expect(extension.decode!(encoded_payload) { '123' }).to eq('123') + expect(jwt_class.decode!(encoded_payload) { '123' }).to eq('123') end end context 'when signing key is invalid' do it 'raises JWT::VerificationError' do - expect { extension.decode!(encoded_payload, key: 'invalid') }.to raise_error(JWT::VerificationError, 'Signature verification failed') + expect { jwt_class.decode!(encoded_payload, key: 'invalid') }.to raise_error(JWT::VerificationError, 'Signature verification failed') end end context 'when algorithm is not matching the one in the token' do it 'raises JWT::VerificationError' do - expect { extension.decode!(encoded_payload, algorithms: ['HS512']) }.to raise_error(JWT::IncorrectAlgorithm, 'Expected a different algorithm') + expect { jwt_class.decode!(encoded_payload, algorithms: ['HS512']) }.to raise_error(JWT::IncorrectAlgorithm, 'Expected a different algorithm') end end context 'when one of the given algorithms match' do it 'raises JWT::VerificationError' do - expect(extension.decode!(encoded_payload, algorithms: ['HS512', 'HS256'])).to eq([payload, { 'alg' => 'HS256' }]) + expect(jwt_class.decode!(encoded_payload, algorithms: ['HS512', 'HS256'])).to eq([payload, { 'alg' => 'HS256' }]) end end context 'when payload is invalid JSON' do before do - extension.encode_payload do |payload| + jwt_class.encode_payload do |payload| Base64.urlsafe_encode64(payload.inspect, padding: false) end end - let(:encoded_payload) { extension.encode!(payload) } + let(:encoded_payload) { jwt_class.encode!(payload) } it 'raises JWT::DecodeError' do - expect { extension.decode!(encoded_payload) }.to raise_error(JWT::DecodeError, 'Invalid segment encoding') + expect { jwt_class.decode!(encoded_payload) }.to raise_error(JWT::DecodeError, 'Invalid segment encoding') end end @@ -89,19 +101,19 @@ let(:exp) { Time.now.to_i - 20 } it 'allows token to be 30 seconds overdue' do - expect { extension.decode!(encoded_payload) }.to raise_error(JWT::ExpiredSignature, 'Signature has expired') + expect { jwt_class.decode!(encoded_payload) }.to raise_error(JWT::ExpiredSignature, 'Signature has expired') end end context 'when expiration_leeway is set to 30 seconds' do before do - extension.expiration_leeway 30 + jwt_class.expiration_leeway 30 end let(:exp) { Time.now.to_i - 20 } it 'allows token to be 30 seconds overdue' do - expect(extension.decode!(encoded_payload)).to eq([payload, { 'alg' => 'HS256' }]) + expect(jwt_class.decode!(encoded_payload)).to eq([payload, { 'alg' => 'HS256' }]) end end end diff --git a/spec/dsl/encode_spec.rb b/spec/dsl/encode_spec.rb index afb8712e..8c2eb5e2 100644 --- a/spec/dsl/encode_spec.rb +++ b/spec/dsl/encode_spec.rb @@ -3,7 +3,7 @@ require 'securerandom' RSpec.describe JWT::DSL do - subject(:extension) do + subject(:jwt_class) do Class.new do include JWT end @@ -17,28 +17,28 @@ context 'when algorithm is configured and no signing key is given or configured' do before do - extension.algorithm('HS256') + jwt_class.algorithm('HS256') end it 'raises an error about missing signing key' do - expect { extension.encode!(payload) }.to raise_error(::JWT::SigningKeyMissing, 'No key given for signing') + expect { jwt_class.encode!(payload) }.to raise_error(::JWT::SigningKeyMissing, 'No key given for signing') end end context 'when no algorithm is configured and key is given as a option' do it 'raises an error about unsupported algoritm implementation' do - expect { extension.encode!(payload, signing_key: secret) }.to raise_error(NotImplementedError, 'Unsupported signing method') + expect { jwt_class.encode!(payload, signing_key: secret) }.to raise_error(NotImplementedError, 'Unsupported signing method') end end context 'when algorithm and signing is configured' do before do - extension.algorithm('HS256') - extension.signing_key(secret) + jwt_class.algorithm('HS256') + jwt_class.signing_key(secret) end it 'yields the same result as the raw encode' do - expect(extension.encode!(payload)).to eq(::JWT.encode(payload, secret, 'HS256')) + expect(jwt_class.encode!(payload)).to eq(::JWT.encode(payload, secret, 'HS256')) end end end From 215305784466be8ecb9dab004054da8ac4b7f10f Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Sun, 9 Jan 2022 21:32:44 +0200 Subject: [PATCH 25/33] Fix minor reek issue --- lib/jwt/decode.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/jwt/decode.rb b/lib/jwt/decode.rb index 0bcc30c0..d33e8ea4 100644 --- a/lib/jwt/decode.rb +++ b/lib/jwt/decode.rb @@ -32,9 +32,11 @@ def decode_segments def verify_signature! return if none_algorithm? - raise JWT::DecodeError, 'No verification key available' if Array(key).empty? + keys = Array(key) - return if Array(key).any? { |single_key| verify_signature_for?(algorithm_in_header, single_key) } + raise JWT::DecodeError, 'No verification key available' if keys.empty? + + return if keys.any? { |single_key| verify_signature_for?(algorithm_in_header, single_key) } raise JWT::VerificationError, 'Signature verification failed' end From 73855bf2d22aa5a5b05bf9af12b8bed7b782929f Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Sun, 9 Jan 2022 21:39:33 +0200 Subject: [PATCH 26/33] Reduce amount of instance variables in ::JWT:Encode --- lib/jwt/encode.rb | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/lib/jwt/encode.rb b/lib/jwt/encode.rb index dd4af4bc..b13f0619 100644 --- a/lib/jwt/encode.rb +++ b/lib/jwt/encode.rb @@ -7,8 +7,6 @@ module JWT class Encode def initialize(options) @options = options - @payload = options[:payload] - @key = options[:key] if (algo = options[:algorithm]).respond_to?(:sign) @algorithm = algo @@ -23,12 +21,20 @@ def initialize(options) def segments validate_claims! - combine(encoded_header_and_payload, encoded_signature) + self.class.combine(encoded_header_and_payload, encoded_signature) end private - attr_reader :payload, :headers, :options, :algorithm, :key, :alg + attr_reader :headers, :options, :algorithm, :alg + + def payload + options[:payload] + end + + def key + options[:key] + end def encoded_header @encoded_header ||= encode_header @@ -43,11 +49,11 @@ def encoded_signature end def encoded_header_and_payload - @encoded_header_and_payload ||= combine(encoded_header, encoded_payload) + @encoded_header_and_payload ||= self.class.combine(encoded_header, encoded_payload) end def encode_header - encode(headers) + self.class.encode(headers) end def encode_payload @@ -55,7 +61,7 @@ def encode_payload return encode_proc.call(payload) end - encode(payload) + self.class.encode(payload) end def encode_signature @@ -74,12 +80,14 @@ def validate_claims! ClaimsValidator.new(payload).validate! end - def encode(data) - Base64.urlsafe_encode64(JWT::JSON.generate(data), padding: false) - end + class << self + def encode(data) + Base64.urlsafe_encode64(JWT::JSON.generate(data), padding: false) + end - def combine(*parts) - parts.join('.') + def combine(*parts) + parts.join('.') + end end end end From c8921db16384ea0f274ade61015cab390f869524 Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Sun, 9 Jan 2022 21:41:18 +0200 Subject: [PATCH 27/33] Added newline in .reek.yml --- .reek.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.reek.yml b/.reek.yml index 23309df5..680696fc 100644 --- a/.reek.yml +++ b/.reek.yml @@ -4,4 +4,4 @@ detectors: MissingSafeMethod: enabled: false NilCheck: - enabled: false \ No newline at end of file + enabled: false From 1edad1b48e6c239c02b48dd20412fcd7645a8a78 Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Sun, 9 Jan 2022 21:54:49 +0200 Subject: [PATCH 28/33] Do not include default options --- lib/jwt.rb | 4 +--- lib/jwt/default_options.rb | 16 ++++++++++++---- lib/jwt/dsl/decode.rb | 14 ++++++-------- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/lib/jwt.rb b/lib/jwt.rb index e7fbb36f..aaa089a4 100644 --- a/lib/jwt.rb +++ b/lib/jwt.rb @@ -15,8 +15,6 @@ # Should be up to date with the latest spec: # https://tools.ietf.org/html/rfc7519 module JWT - include JWT::DefaultOptions - def self.included(cls) cls.include(::JWT::DSL) end @@ -31,6 +29,6 @@ def encode(payload, key, algorithm = 'HS256', header_fields = {}) end def decode(jwt, key = nil, verify = true, options = {}, &keyfinder) - Decode.new(jwt, DEFAULT_OPTIONS.merge(key: key, verify: verify).merge(options), &keyfinder).decode_segments + Decode.new(jwt, DefaultOptions::DECODE_DEFAULT_OPTIONS.merge(key: key, verify: verify).merge(options), &keyfinder).decode_segments end end diff --git a/lib/jwt/default_options.rb b/lib/jwt/default_options.rb index fc02c70f..800ab8bb 100644 --- a/lib/jwt/default_options.rb +++ b/lib/jwt/default_options.rb @@ -1,16 +1,24 @@ +# frozen_string_literal: true + module JWT module DefaultOptions - DEFAULT_OPTIONS = { + LEEWAY_DEFAULT = 0 + + VERIFY_CLAIMS_DEFAULTS = { + leeway: LEEWAY_DEFAULT, verify_expiration: true, verify_not_before: true, verify_iss: false, verify_iat: false, verify_jti: false, verify_aud: false, - verify_sub: false, - leeway: 0, + verify_sub: false + }.freeze + + DECODE_DEFAULT_OPTIONS = { + verify: true, algorithms: ['HS256'], required_claims: [] - }.freeze + }.merge(VERIFY_CLAIMS_DEFAULTS).freeze end end diff --git a/lib/jwt/dsl/decode.rb b/lib/jwt/dsl/decode.rb index 1488ef5f..39a28bf3 100644 --- a/lib/jwt/dsl/decode.rb +++ b/lib/jwt/dsl/decode.rb @@ -4,8 +4,6 @@ module JWT module DSL # DSL methods for decoding related functionality module Decode - DEFAULT_EXPIRATION_LEEWAY = 0 - def decode_payload(&block) @decode_payload = block if block_given? @decode_payload @@ -23,7 +21,7 @@ def jwk_resolver(&block) def expiration_leeway(value = nil) @expiration_leeway = value unless value.nil? - @expiration_leeway || DEFAULT_EXPIRATION_LEEWAY + @expiration_leeway || ::JWT::DefaultOptions::LEEWAY_DEFAULT end def decode!(token, options = {}) @@ -41,11 +39,11 @@ def decode!(token, options, context) end def build_decode_options(options, context) - ::JWT::DefaultOptions::DEFAULT_OPTIONS.merge(key: options[:key] || context.verification_key || context.signing_key, - decode_payload_proc: context.decode_payload, - leeway: context.expiration_leeway, - algorithms: (Array(context.algorithm) + Array(context.algorithms)).uniq, - jwks: context.jwk_resolver) + ::JWT::DefaultOptions::DECODE_DEFAULT_OPTIONS.merge(key: options[:key] || context.verification_key || context.signing_key, + decode_payload_proc: context.decode_payload, + leeway: context.expiration_leeway, + algorithms: (Array(context.algorithm) + Array(context.algorithms)).uniq, + jwks: context.jwk_resolver) .merge(options) end end From 1e48820342ec8c756892cd7b020f6f98f01eb90f Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Sun, 9 Jan 2022 22:01:46 +0200 Subject: [PATCH 29/33] Split defaults even more --- lib/jwt/default_options.rb | 7 ++++--- lib/jwt/dsl/decode.rb | 15 ++++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/jwt/default_options.rb b/lib/jwt/default_options.rb index 800ab8bb..d33c00d9 100644 --- a/lib/jwt/default_options.rb +++ b/lib/jwt/default_options.rb @@ -3,6 +3,7 @@ module JWT module DefaultOptions LEEWAY_DEFAULT = 0 + ALGORITHMS_DEFAULT = ['HS256'].freeze VERIFY_CLAIMS_DEFAULTS = { leeway: LEEWAY_DEFAULT, @@ -12,13 +13,13 @@ module DefaultOptions verify_iat: false, verify_jti: false, verify_aud: false, - verify_sub: false + verify_sub: false, + required_claims: [] }.freeze DECODE_DEFAULT_OPTIONS = { verify: true, - algorithms: ['HS256'], - required_claims: [] + algorithms: ALGORITHMS_DEFAULT }.merge(VERIFY_CLAIMS_DEFAULTS).freeze end end diff --git a/lib/jwt/dsl/decode.rb b/lib/jwt/dsl/decode.rb index 39a28bf3..111198c0 100644 --- a/lib/jwt/dsl/decode.rb +++ b/lib/jwt/dsl/decode.rb @@ -11,7 +11,7 @@ def decode_payload(&block) def algorithms(value = nil) @algorithms = value unless value.nil? - @algorithms + Array(@algorithms || ::JWT::DefaultOptions::ALGORITHMS_DEFAULT) end def jwk_resolver(&block) @@ -39,12 +39,13 @@ def decode!(token, options, context) end def build_decode_options(options, context) - ::JWT::DefaultOptions::DECODE_DEFAULT_OPTIONS.merge(key: options[:key] || context.verification_key || context.signing_key, - decode_payload_proc: context.decode_payload, - leeway: context.expiration_leeway, - algorithms: (Array(context.algorithm) + Array(context.algorithms)).uniq, - jwks: context.jwk_resolver) - .merge(options) + JWT::DefaultOptions::VERIFY_CLAIMS_DEFAULTS.merge( + key: options[:key] || context.verification_key || context.signing_key, + decode_payload_proc: context.decode_payload, + leeway: context.expiration_leeway, + algorithms: (Array(context.algorithm) + Array(context.algorithms)).uniq, + jwks: context.jwk_resolver + ).merge(options) end end end From 11bb8b3a8de6310021417732e986c589d356dc4f Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Sun, 9 Jan 2022 22:18:23 +0200 Subject: [PATCH 30/33] Added a few lines in the README --- README.md | 20 +++++++++++++++++++ .../example_custom_algorithm_spec.rb | 0 .../{ => examples}/example_deflating_spec.rb | 0 .../example_smart_health_card_spec.rb | 0 4 files changed, 20 insertions(+) rename spec/dsl/{ => examples}/example_custom_algorithm_spec.rb (100%) rename spec/dsl/{ => examples}/example_deflating_spec.rb (100%) rename spec/dsl/{ => examples}/example_smart_health_card_spec.rb (100%) diff --git a/README.md b/README.md index 625cfcec..228230d4 100644 --- a/README.md +++ b/README.md @@ -584,6 +584,26 @@ jwk_hash = jwk.export jwk_hash_with_private_key = jwk.export(include_private: true) ``` +## DSL for creating JWT encoders and decoders + +As an alternative to `::JWT.decode` and `::JWT.encode` there is a possibility to use a DSL to define the behaviour for the token handling. This approach allows you to define custom algorithms, encoders and logic for you JWT tokens. + +A few examples use-cases be found from the [specs](spec/dsl/examples) + +```ruby + require 'jwt' + + module AppToken + include ::JWT + algorithm 'HS256' + key 'async_secret' + end + + encoded_token = AppToken.encode!(data: 'data', exp: Time.now.to_i+3600) + + payload, headers = AppToken.decode!(encoded_token) +``` + # Development and Tests We depend on [Bundler](http://rubygems.org/gems/bundler) for defining gemspec and performing releases to rubygems.org, which can be done with diff --git a/spec/dsl/example_custom_algorithm_spec.rb b/spec/dsl/examples/example_custom_algorithm_spec.rb similarity index 100% rename from spec/dsl/example_custom_algorithm_spec.rb rename to spec/dsl/examples/example_custom_algorithm_spec.rb diff --git a/spec/dsl/example_deflating_spec.rb b/spec/dsl/examples/example_deflating_spec.rb similarity index 100% rename from spec/dsl/example_deflating_spec.rb rename to spec/dsl/examples/example_deflating_spec.rb diff --git a/spec/dsl/example_smart_health_card_spec.rb b/spec/dsl/examples/example_smart_health_card_spec.rb similarity index 100% rename from spec/dsl/example_smart_health_card_spec.rb rename to spec/dsl/examples/example_smart_health_card_spec.rb From c37595dd8b9d74a5bdad7f5d36280d016cfa5431 Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Sun, 9 Jan 2022 22:41:01 +0200 Subject: [PATCH 31/33] Allow keys to be procs --- README.md | 2 +- lib/jwt/decode_methods.rb | 4 +++- lib/jwt/dsl.rb | 1 - lib/jwt/dsl/decode.rb | 1 - lib/jwt/dsl/encode.rb | 1 - lib/jwt/dsl/keys.rb | 13 +++++++------ lib/jwt/encode.rb | 6 +++++- spec/dsl/decode_spec.rb | 12 ++++++++++++ spec/dsl/encode_spec.rb | 22 ++++++++++++++++++++++ 9 files changed, 50 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 228230d4..0906d0e7 100644 --- a/README.md +++ b/README.md @@ -596,7 +596,7 @@ A few examples use-cases be found from the [specs](spec/dsl/examples) module AppToken include ::JWT algorithm 'HS256' - key 'async_secret' + key { 'secret' } end encoded_token = AppToken.encode!(data: 'data', exp: Time.now.to_i+3600) diff --git a/lib/jwt/decode_methods.rb b/lib/jwt/decode_methods.rb index 6abc9855..0a0b95fd 100644 --- a/lib/jwt/decode_methods.rb +++ b/lib/jwt/decode_methods.rb @@ -59,8 +59,10 @@ def resolve_key ::JWT::JWK::KeyFinder.new(jwks: jwks).key_for(header['kid']) elsif (x5c_options = options[:x5c]) ::JWT::X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(header['x5c']) + elsif (key = options[:key]).respond_to?(:call) + key.call(header) else - options[:key] + key end end diff --git a/lib/jwt/dsl.rb b/lib/jwt/dsl.rb index af4c5fd3..c594983c 100644 --- a/lib/jwt/dsl.rb +++ b/lib/jwt/dsl.rb @@ -5,7 +5,6 @@ require_relative 'dsl/encode' module JWT - # Module to gather the different parts of the DSL module DSL def self.included(cls) cls.extend(JWT::DSL::Keys) diff --git a/lib/jwt/dsl/decode.rb b/lib/jwt/dsl/decode.rb index 111198c0..a3a0edaf 100644 --- a/lib/jwt/dsl/decode.rb +++ b/lib/jwt/dsl/decode.rb @@ -2,7 +2,6 @@ module JWT module DSL - # DSL methods for decoding related functionality module Decode def decode_payload(&block) @decode_payload = block if block_given? diff --git a/lib/jwt/dsl/encode.rb b/lib/jwt/dsl/encode.rb index 97ddeabd..511d0cc8 100644 --- a/lib/jwt/dsl/encode.rb +++ b/lib/jwt/dsl/encode.rb @@ -2,7 +2,6 @@ module JWT module DSL - # DSL methods for encoding related functionality module Encode def algorithm(value = nil) @algorithm = value unless value.nil? diff --git a/lib/jwt/dsl/keys.rb b/lib/jwt/dsl/keys.rb index 9f02db0b..ca2932e1 100644 --- a/lib/jwt/dsl/keys.rb +++ b/lib/jwt/dsl/keys.rb @@ -2,21 +2,22 @@ module JWT module DSL - # DSL methods for setting keys module Keys - def signing_key(value = nil) + def signing_key(value = nil, &block) @signing_key = value unless value.nil? + @signing_key = block if block_given? @signing_key end - def verification_key(value = nil) + def verification_key(value = nil, &block) @verification_key = value unless value.nil? + @verification_key = block if block_given? @verification_key end - def key(value = nil) - verification_key(value) - signing_key(value) + def key(value = nil, &block) + verification_key(value, &block) + signing_key(value, &block) end end end diff --git a/lib/jwt/encode.rb b/lib/jwt/encode.rb index b13f0619..789b110e 100644 --- a/lib/jwt/encode.rb +++ b/lib/jwt/encode.rb @@ -33,7 +33,11 @@ def payload end def key - options[:key] + if (key = options[:key]).respond_to?(:call) + key.call + else + key + end end def encoded_header diff --git a/spec/dsl/decode_spec.rb b/spec/dsl/decode_spec.rb index 1aa87c3a..8006a09e 100644 --- a/spec/dsl/decode_spec.rb +++ b/spec/dsl/decode_spec.rb @@ -116,5 +116,17 @@ expect(jwt_class.decode!(encoded_payload)).to eq([payload, { 'alg' => 'HS256' }]) end end + + context 'when key is given as block' do + let(:secret) { 'HS256' } + + before do + jwt_class.key { |header| header['alg'] } + end + + it 'uses the block to resolve the key' do + expect(jwt_class.decode!(encoded_payload)).to eq([payload, { 'alg' => 'HS256' }]) + end + end end end diff --git a/spec/dsl/encode_spec.rb b/spec/dsl/encode_spec.rb index 8c2eb5e2..d3c0a725 100644 --- a/spec/dsl/encode_spec.rb +++ b/spec/dsl/encode_spec.rb @@ -41,5 +41,27 @@ expect(jwt_class.encode!(payload)).to eq(::JWT.encode(payload, secret, 'HS256')) end end + + context 'when key is given as block' do + before do + jwt_class.algorithm('HS256') + jwt_class.key { secret } + end + + it 'uses the secret from the block' do + expect(jwt_class.encode!(payload)).to eq(::JWT.encode(payload, secret, 'HS256')) + end + end + + context 'when signing_key is given as block' do + before do + jwt_class.algorithm('HS256') + jwt_class.signing_key { secret } + end + + it 'uses the secret from the block' do + expect(jwt_class.encode!(payload)).to eq(::JWT.encode(payload, secret, 'HS256')) + end + end end end From 1ef54ff392e0add25b64b398b3cec4cfbb856037 Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Sun, 9 Jan 2022 22:54:33 +0200 Subject: [PATCH 32/33] Possibility to give the expiration time on the JWT class --- lib/jwt/dsl/encode.rb | 8 +++++++- lib/jwt/encode.rb | 11 ++++++++++- spec/dsl/encode_spec.rb | 13 +++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/lib/jwt/dsl/encode.rb b/lib/jwt/dsl/encode.rb index 511d0cc8..e53957a7 100644 --- a/lib/jwt/dsl/encode.rb +++ b/lib/jwt/dsl/encode.rb @@ -13,6 +13,11 @@ def encode_payload(&block) @encode_payload end + def expiration(value = nil) + @expiration = value unless value.nil? + @expiration + end + def encode!(payload, options = {}) Internals.encode!(payload, options, self) end @@ -29,7 +34,8 @@ def build_options(payload, options, context) key: options[:key] || context.signing_key, encode_payload_proc: context.encode_payload, headers: options[:headers], - algorithm: context.algorithm + algorithm: context.algorithm, + expiration: context.expiration } if opts[:algorithm].is_a?(String) && opts[:key].nil? diff --git a/lib/jwt/encode.rb b/lib/jwt/encode.rb index 789b110e..35ad3520 100644 --- a/lib/jwt/encode.rb +++ b/lib/jwt/encode.rb @@ -29,7 +29,7 @@ def segments attr_reader :headers, :options, :algorithm, :alg def payload - options[:payload] + @payload ||= append_exp(options[:payload]) end def key @@ -84,6 +84,15 @@ def validate_claims! ClaimsValidator.new(payload).validate! end + def append_exp(payload) + return payload unless (expiration = options[:expiration]) + return payload if payload.key?('exp') || payload.key?(:exp) + + payload['exp'] = Time.now.to_i + expiration + + payload + end + class << self def encode(data) Base64.urlsafe_encode64(JWT::JSON.generate(data), padding: false) diff --git a/spec/dsl/encode_spec.rb b/spec/dsl/encode_spec.rb index d3c0a725..dacc0f27 100644 --- a/spec/dsl/encode_spec.rb +++ b/spec/dsl/encode_spec.rb @@ -63,5 +63,18 @@ expect(jwt_class.encode!(payload)).to eq(::JWT.encode(payload, secret, 'HS256')) end end + + context 'when expiration is set on the class and is negative' do + before do + jwt_class.algorithm('HS256') + jwt_class.expiration(-10) + jwt_class.signing_key(secret) + end + + it 'will only generate expired tokens' do + token = jwt_class.encode!(payload) + expect { jwt_class.decode!(token) }.to raise_error(JWT::ExpiredSignature, 'Signature has expired') + end + end end end From 03b1adb8b6fc7610a8f66f984240746cf3c7daf3 Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Thu, 13 Jan 2022 22:57:13 +0200 Subject: [PATCH 33/33] Allow parameters to be specified --- lib/jwt/dsl/decode.rb | 2 +- lib/jwt/dsl/encode.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/jwt/dsl/decode.rb b/lib/jwt/dsl/decode.rb index a3a0edaf..4daba00e 100644 --- a/lib/jwt/dsl/decode.rb +++ b/lib/jwt/dsl/decode.rb @@ -42,7 +42,7 @@ def build_decode_options(options, context) key: options[:key] || context.verification_key || context.signing_key, decode_payload_proc: context.decode_payload, leeway: context.expiration_leeway, - algorithms: (Array(context.algorithm) + Array(context.algorithms)).uniq, + algorithms: (Array(options[:algorithms]) + Array(context.algorithm) + Array(context.algorithms)).uniq, jwks: context.jwk_resolver ).merge(options) end diff --git a/lib/jwt/dsl/encode.rb b/lib/jwt/dsl/encode.rb index e53957a7..c75e0858 100644 --- a/lib/jwt/dsl/encode.rb +++ b/lib/jwt/dsl/encode.rb @@ -34,8 +34,8 @@ def build_options(payload, options, context) key: options[:key] || context.signing_key, encode_payload_proc: context.encode_payload, headers: options[:headers], - algorithm: context.algorithm, - expiration: context.expiration + algorithm: options[:algorithm] || context.algorithm, + expiration: options[:expiration] || context.expiration } if opts[:algorithm].is_a?(String) && opts[:key].nil?