From cb7e42526adb4f9e32e3adf547c5d9bdd6fd772a Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Sat, 5 Sep 2026 22:18:30 +0300 Subject: [PATCH 1/4] Move the custom algorithm contract specs out of jwt_spec These examples describe what a class must implement to be usable as an algorithm, which is the JWT::JWA::SigningAlgorithm contract, not the JWT.encode/JWT.decode surface that jwt_spec is supposed to cover. Pure move: the block depended only on the shared payload let, and the full suite still reports the same 1246 example descriptions. --- spec/jwt/jwa/signing_algorithm_spec.rb | 128 +++++++++++++++++++++++++ spec/jwt/jwt_spec.rb | 125 ------------------------ 2 files changed, 128 insertions(+), 125 deletions(-) create mode 100644 spec/jwt/jwa/signing_algorithm_spec.rb diff --git a/spec/jwt/jwa/signing_algorithm_spec.rb b/spec/jwt/jwa/signing_algorithm_spec.rb new file mode 100644 index 00000000..253f938a --- /dev/null +++ b/spec/jwt/jwa/signing_algorithm_spec.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +RSpec.describe JWT::JWA::SigningAlgorithm do + let(:payload) { { 'user_id' => 'some@user.tld' } } + + let(:custom_algorithm) do + Class.new do + include JWT::JWA::SigningAlgorithm + + def initialize(signature: 'custom_signature', alg: 'custom') + @signature = signature + @alg = alg + end + + def sign(*) + @signature + end + + def verify(data:, signature:, verification_key:) # rubocop:disable Lint/UnusedMethodArgument + signature == @signature + end + end + end + + let(:token) { JWT.encode(payload, 'secret', custom_algorithm.new) } + let(:expected_token) { 'eyJhbGciOiJjdXN0b20ifQ.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.Y3VzdG9tX3NpZ25hdHVyZQ' } + + it 'can be used for encoding' do + expect(token).to eq(expected_token) + end + + it 'can be used for decoding' do + expect(JWT.decode(token, 'secret', true, algorithm: custom_algorithm.new)).to eq([payload, { 'alg' => 'custom' }]) + end + + context 'when multiple custom algorithms are given for decoding' do + it 'tries until the first match' do + expect(JWT.decode(token, 'secret', true, algorithms: [custom_algorithm.new(signature: 'not_this'), custom_algorithm.new])).to eq([payload, { 'alg' => 'custom' }]) + end + end + + context 'when class has custom header method' do + before do + custom_algorithm.class_eval do + def header(*) + { 'alg' => alg, 'foo' => 'bar' } + end + end + end + + it 'uses the provided header' do + expect(JWT.decode(token, 'secret', true, algorithm: custom_algorithm.new)).to eq([payload, { 'alg' => 'custom', 'foo' => 'bar' }]) + end + end + + context 'when class is not utilizing the ::JWT::JWA::SigningAlgorithm module' do + let(:custom_algorithm) do + Class.new do + attr_reader :alg + + def initialize(signature: 'custom_signature', alg: 'custom') + @signature = signature + @alg = alg + end + + def header(*) + { 'alg' => @alg, 'foo' => 'bar' } + end + + def sign(*) + @signature + end + + def verify(*) + true + end + end + end + + it 'raises an error' do + expect { token }.to raise_error(ArgumentError, 'Custom algorithms are required to include JWT::JWA::SigningAlgorithm') + end + end + + context 'when alg is not matching' do + it 'fails the validation process' do + expect { JWT.decode(token, 'secret', true, algorithms: custom_algorithm.new(alg: 'not_a_match')) }.to raise_error(JWT::IncorrectAlgorithm, 'Expected a different algorithm') + end + end + + context 'when signature is not matching' do + it 'fails the validation process' do + expect { JWT.decode(token, 'secret', true, algorithms: custom_algorithm.new(signature: 'not_a_match')) }.to raise_error(JWT::VerificationError, 'Signature verification failed') + end + end + + context 'when #sign method is missing' do + before do + custom_algorithm.instance_eval do + remove_method :sign + end + end + + it 'raises an error on encoding' do + expect { token }.to raise_error(JWT::EncodeError, /missing the sign method/) + end + + it 'allows decoding' do + expect(JWT.decode(expected_token, 'secret', true, algorithm: custom_algorithm.new)).to eq([payload, { 'alg' => 'custom' }]) + end + end + + context 'when #verify method is missing' do + before do + custom_algorithm.instance_eval do + remove_method :verify + end + end + + it 'can be used for encoding' do + expect(token).to eq(expected_token) + end + + it 'raises error on decoding' do + expect { JWT.decode(expected_token, 'secret', true, algorithm: custom_algorithm.new) }.to raise_error(JWT::VerificationKeyError, /missing the verify method/) + end + end +end diff --git a/spec/jwt/jwt_spec.rb b/spec/jwt/jwt_spec.rb index 735b9bb6..33148b05 100644 --- a/spec/jwt/jwt_spec.rb +++ b/spec/jwt/jwt_spec.rb @@ -823,129 +823,4 @@ expect { JWT.decode(token, 'secret', true, algorithm: 'invalid-HS256') }.to raise_error(JWT::IncorrectAlgorithm, 'Expected a different algorithm') end end - - context 'when algorithm is a custom class' do - let(:custom_algorithm) do - Class.new do - include JWT::JWA::SigningAlgorithm - - def initialize(signature: 'custom_signature', alg: 'custom') - @signature = signature - @alg = alg - end - - def sign(*) - @signature - end - - def verify(data:, signature:, verification_key:) # rubocop:disable Lint/UnusedMethodArgument - signature == @signature - end - end - end - - let(:token) { JWT.encode(payload, 'secret', custom_algorithm.new) } - let(:expected_token) { 'eyJhbGciOiJjdXN0b20ifQ.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.Y3VzdG9tX3NpZ25hdHVyZQ' } - - it 'can be used for encoding' do - expect(token).to eq(expected_token) - end - - it 'can be used for decoding' do - expect(JWT.decode(token, 'secret', true, algorithm: custom_algorithm.new)).to eq([payload, { 'alg' => 'custom' }]) - end - - context 'when multiple custom algorithms are given for decoding' do - it 'tries until the first match' do - expect(JWT.decode(token, 'secret', true, algorithms: [custom_algorithm.new(signature: 'not_this'), custom_algorithm.new])).to eq([payload, { 'alg' => 'custom' }]) - end - end - - context 'when class has custom header method' do - before do - custom_algorithm.class_eval do - def header(*) - { 'alg' => alg, 'foo' => 'bar' } - end - end - end - - it 'uses the provided header' do - expect(JWT.decode(token, 'secret', true, algorithm: custom_algorithm.new)).to eq([payload, { 'alg' => 'custom', 'foo' => 'bar' }]) - end - end - - context 'when class is not utilizing the ::JWT::JWA::SigningAlgorithm module' do - let(:custom_algorithm) do - Class.new do - attr_reader :alg - - def initialize(signature: 'custom_signature', alg: 'custom') - @signature = signature - @alg = alg - end - - def header(*) - { 'alg' => @alg, 'foo' => 'bar' } - end - - def sign(*) - @signature - end - - def verify(*) - true - end - end - end - - it 'raises an error' do - expect { token }.to raise_error(ArgumentError, 'Custom algorithms are required to include JWT::JWA::SigningAlgorithm') - end - end - - context 'when alg is not matching' do - it 'fails the validation process' do - expect { JWT.decode(token, 'secret', true, algorithms: custom_algorithm.new(alg: 'not_a_match')) }.to raise_error(JWT::IncorrectAlgorithm, 'Expected a different algorithm') - end - end - - context 'when signature is not matching' do - it 'fails the validation process' do - expect { JWT.decode(token, 'secret', true, algorithms: custom_algorithm.new(signature: 'not_a_match')) }.to raise_error(JWT::VerificationError, 'Signature verification failed') - end - end - - context 'when #sign method is missing' do - before do - custom_algorithm.instance_eval do - remove_method :sign - end - end - - it 'raises an error on encoding' do - expect { token }.to raise_error(JWT::EncodeError, /missing the sign method/) - end - - it 'allows decoding' do - expect(JWT.decode(expected_token, 'secret', true, algorithm: custom_algorithm.new)).to eq([payload, { 'alg' => 'custom' }]) - end - end - - context 'when #verify method is missing' do - before do - custom_algorithm.instance_eval do - remove_method :verify - end - end - - it 'can be used for encoding' do - expect(token).to eq(expected_token) - end - - it 'raises error on decoding' do - expect { JWT.decode(expected_token, 'secret', true, algorithm: custom_algorithm.new) }.to raise_error(JWT::VerificationKeyError, /missing the verify method/) - end - end - end end From 0ca7b45849f636fc000d7c8bd9062034e967932f Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Sat, 5 Sep 2026 22:19:51 +0300 Subject: [PATCH 2/4] Trim duplicated iat semantics from jwt_spec The verify_iat block re-tested what claims/issued_at_spec already covers at the unit level: a float iat, an integer iat, one second of clock drift, and a leeway covering that drift. The only thing jwt_spec can say that the claim spec cannot is that the decode options reach the verifier, so it now asserts that and nothing else, including the default of leaving iat unverified, which nothing covered before. --- spec/jwt/jwt_spec.rb | 33 +++++++-------------------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/spec/jwt/jwt_spec.rb b/spec/jwt/jwt_spec.rb index 33148b05..a429759b 100644 --- a/spec/jwt/jwt_spec.rb +++ b/spec/jwt/jwt_spec.rb @@ -632,39 +632,20 @@ describe '::JWT.decode with verify_iat parameter' do let!(:time_now) { Time.now } - let(:token) { JWT.encode({ pay: 'load', iat: iat }, 'secret', 'HS256') } - - subject(:decoded_token) { JWT.decode(token, 'secret', true, verify_iat: true) } + let(:token) { JWT.encode({ pay: 'load', iat: time_now.to_i + 1 }, 'secret', 'HS256') } before { allow(Time).to receive(:now) { time_now } } - context 'when iat is exactly the same as Time.now and iat is given as a float' do - let(:iat) { time_now.to_f } - it 'considers iat valid' do - expect(decoded_token).to be_an(Array) - end + it 'leaves iat unverified by default' do + expect(JWT.decode(token, 'secret', true)).to be_an(Array) end - context 'when iat is exactly the same as Time.now and iat is given as floored integer' do - let(:iat) { time_now.to_f.floor } - it 'considers iat valid' do - expect(decoded_token).to be_an(Array) - end + it 'verifies iat when the option is given' do + expect { JWT.decode(token, 'secret', true, verify_iat: true) }.to raise_error(JWT::InvalidIatError, 'Invalid iat') end - context 'when iat is 1 second after Time.now' do - let(:iat) { time_now.to_i + 1 } - it 'raises an error' do - expect { decoded_token }.to raise_error(JWT::InvalidIatError, 'Invalid iat') - end - - context 'when a leeway covering the drift is given' do - subject(:decoded_token) { JWT.decode(token, 'secret', true, verify_iat: { leeway: 1 }) } - - it 'considers iat valid' do - expect(decoded_token).to be_an(Array) - end - end + it 'passes the leeway on to the iat verification' do + expect(JWT.decode(token, 'secret', true, verify_iat: { leeway: 1 })).to be_an(Array) end end From 6870cb2c0f9ef1d0b91568b42c46b691ffe1428d Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Sat, 5 Sep 2026 22:20:55 +0300 Subject: [PATCH 3/4] Drop an unused let from the issuer claim context --- spec/jwt/jwt_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/jwt/jwt_spec.rb b/spec/jwt/jwt_spec.rb index a429759b..e75dd9e1 100644 --- a/spec/jwt/jwt_spec.rb +++ b/spec/jwt/jwt_spec.rb @@ -494,7 +494,6 @@ context 'issuer claim' do let(:iss) { 'ruby-jwt-gem' } - let(:invalid_token) { JWT.encode payload, data[:secret] } let(:token) do iss_payload = payload.merge(iss: iss) From f5a14bb56eb20b5761b543bcef2baefb505ffb6f Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Sat, 5 Sep 2026 22:28:12 +0300 Subject: [PATCH 4/4] Replace the shared data hash in jwt_spec with per-context lets The fixture was a 29 entry hash mixing symbol and string keys, holding everything from an HMAC secret to hardcoded tokens to key fixtures, with six '' placeholders that existed only so before(:each) could overwrite them by mutating a memoized let. Each value now lives where it is used. The HMAC and RSA loops iterate a map of algorithm to expected token rather than indexing the fixture by the loop variable; the ECDSA and PSS loops build their token in a let; the four malformed token strings are inline at their single use each. One value was doing something other than it looked: a keyfinder example resolved its key by using the decoded payload as an index into the shared hash, and the multi key context passed the hardcoded HS256 token string as an HMAC secret. Both now say what they mean. Same 118 examples, and the whole suite reports identical example descriptions before and after. --- spec/jwt/jwt_spec.rb | 203 +++++++++++++++++++------------------------ 1 file changed, 91 insertions(+), 112 deletions(-) diff --git a/spec/jwt/jwt_spec.rb b/spec/jwt/jwt_spec.rb index e75dd9e1..69d4a02b 100644 --- a/spec/jwt/jwt_spec.rb +++ b/spec/jwt/jwt_spec.rb @@ -2,46 +2,13 @@ RSpec.describe JWT do let(:payload) { { 'user_id' => 'some@user.tld' } } - - let(:data) do - { - :empty_token => 'e30K.e30K.e30K', - :empty_token_2_segment => 'e30K.e30K.', - :invalid_header_token => 'W10.e30K.e30K', - :invalid_2_segment_header_token => 'WyJIUzI1NiJd.e30K', - :secret => 'My$ecretK3y', - :rsa_private => test_pkey('rsa-2048-private.pem'), - :rsa_public => test_pkey('rsa-2048-public.pem'), - :wrong_rsa_public => test_pkey('rsa-2048-wrong-public.pem'), - 'ES256_private' => test_pkey('ec256-private.pem'), - 'ES256_public' => test_pkey('ec256-public.pem'), - 'ES256_private_v2' => test_pkey('ec256-private-v2.pem'), - 'ES256_public_v2' => test_pkey('ec256-public-v2.pem'), - 'ES384_private' => test_pkey('ec384-private.pem'), - 'ES384_public' => test_pkey('ec384-public.pem'), - 'ES512_private' => test_pkey('ec512-private.pem'), - 'ES512_public' => test_pkey('ec512-public.pem'), - 'ES256K_private' => test_pkey('ec256k-private.pem'), - 'ES256K_public' => test_pkey('ec256k-public.pem'), - 'NONE' => 'eyJhbGciOiJub25lIn0.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.', - 'HS256' => 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.kWOVtIOpWcG7JnyJG0qOkTDbOy636XrrQhMm_8JrRQ8', - 'HS384' => 'eyJhbGciOiJIUzM4NCJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.VuV4j4A1HKhWxCNzEcwc9qVF3frrEu-BRLzvYPkbWO0LENRGy5dOiBQ34remM3XH', - 'HS512' => 'eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.8zNtCBTJIZTHpZ-BkhR-6sZY1K85Nm5YCKqV3AxRdsBJDt_RR-REH2db4T3Y0uQwNknhrCnZGvhNHrvhDwV1kA', - 'RS256' => 'eyJhbGciOiJSUzI1NiJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.eSXvWP4GViiwUALj_-qTxU68I1oM0XjgDsCZBBUri2Ghh9d75QkVDoZ_v872GaqunN5A5xcnBK0-cOq-CR6OwibgJWfOt69GNzw5RrOfQ2mz3QI3NYEq080nF69h8BeqkiaXhI24Q51joEgfa9aj5Y-oitLAmtDPYTm7vTcdGufd6AwD3_3jajKBwkh0LPSeMtbe_5EyS94nFoEF9OQuhJYjUmp7agsBVa8FFEjVw5jEgVqkvERSj5hSY4nEiCAomdVxIKBfykyi0d12cgjhI7mBFwWkPku8XIPGZ7N8vpiSLdM68BnUqIK5qR7NAhtvT7iyLFgOqhZNUQ6Ret5VpQ', - 'RS384' => 'eyJhbGciOiJSUzM4NCJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.Sfgk56moPghtsjaP4so6tOy3I553mgwX-5gByMC6dX8lpeWgsxSeAd_K8IyO7u4lwYOL0DSftnqO1HEOuN1AKyBbDvaTXz3u2xNA2x4NYLdW4AZA6ritbYcKLO5BHTXw5ueMbtA1jjGXP0zI_aK2iJTMBmB8SCF88RYBUH01Tyf4PlLj98pGL-v3prZd6kZkIeRJ3326h04hslcB5HQKmgeBk24QNLIoIC-CD329HPjJ7TtGx01lj-ehTBnwVbBGzYFAyoalV5KgvL_MDOfWPr1OYHnR5s_Fm6_3Vg4u6lBljvHOrmv4Nfx7d8HLgbo8CwH4qn1wm6VQCtuDd-uhRg', - 'RS512' => 'eyJhbGciOiJSUzUxMiJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.LIIAUEuCkGNdpYguOO5LoW4rZ7ED2POJrB0pmEAAchyTdIK4HKh1jcLxc6KyGwZv40njCgub3y72q6vcQTn7oD0zWFCVQRIDW1911Ii2hRNHuigiPUnrnZh1OQ6z65VZRU6GKs8omoBGU9vrClBU0ODqYE16KxYmE_0n4Xw2h3D_L1LF0IAOtDWKBRDa3QHwZRM9sHsHNsBuD5ye9KzDYN1YALXj64LBfA-DoCKfpVAm9NkRPOyzjR2X2C3TomOSJgqWIVHJucudKDDAZyEbO4RA5pI-UFYy1370p9bRajvtDyoBuLDCzoSkMyQ4L2DnLhx5CbWcnD7Cd3GUmnjjTA', - 'ES256' => '', - 'ES384' => '', - 'ES512' => '', - 'PS256' => '', - 'PS384' => '', - 'PS512' => '' - } - end + let(:secret) { 'My$ecretK3y' } + let(:rsa_private) { test_pkey('rsa-2048-private.pem') } + let(:rsa_public) { test_pkey('rsa-2048-public.pem') } context 'alg: NONE' do let(:alg) { 'none' } - let(:encoded_token) { data['NONE'] } + let(:encoded_token) { 'eyJhbGciOiJub25lIn0.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.' } it 'should generate a valid token' do token = JWT.encode payload, nil, alg @@ -89,16 +56,20 @@ end end - %w[HS256 HS384 HS512].each do |alg| + { + 'HS256' => 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.kWOVtIOpWcG7JnyJG0qOkTDbOy636XrrQhMm_8JrRQ8', + 'HS384' => 'eyJhbGciOiJIUzM4NCJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.VuV4j4A1HKhWxCNzEcwc9qVF3frrEu-BRLzvYPkbWO0LENRGy5dOiBQ34remM3XH', + 'HS512' => 'eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.8zNtCBTJIZTHpZ-BkhR-6sZY1K85Nm5YCKqV3AxRdsBJDt_RR-REH2db4T3Y0uQwNknhrCnZGvhNHrvhDwV1kA' + }.each do |alg, encoded_token| context "alg: #{alg}" do it 'should generate a valid token' do - token = JWT.encode payload, data[:secret], alg + token = JWT.encode payload, secret, alg - expect(token).to eq data[alg] + expect(token).to eq encoded_token end it 'should decode a valid token' do - jwt_payload, header = JWT.decode data[alg], data[:secret], true, algorithm: alg + jwt_payload, header = JWT.decode encoded_token, secret, true, algorithm: alg expect(header['alg']).to eq alg expect(jwt_payload).to eq payload @@ -106,35 +77,39 @@ it 'wrong secret should raise JWT::VerificationError' do expect do - JWT.decode data[alg], 'wrong_secret', true, algorithm: alg + JWT.decode encoded_token, 'wrong_secret', true, algorithm: alg end.to raise_error JWT::VerificationError end it 'wrong secret and verify = false should not raise an error' do expect do - JWT.decode data[alg], 'wrong_secret', false + JWT.decode encoded_token, 'wrong_secret', false end.not_to raise_error end end end - %w[RS256 RS384 RS512].each do |alg| + { + 'RS256' => 'eyJhbGciOiJSUzI1NiJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.eSXvWP4GViiwUALj_-qTxU68I1oM0XjgDsCZBBUri2Ghh9d75QkVDoZ_v872GaqunN5A5xcnBK0-cOq-CR6OwibgJWfOt69GNzw5RrOfQ2mz3QI3NYEq080nF69h8BeqkiaXhI24Q51joEgfa9aj5Y-oitLAmtDPYTm7vTcdGufd6AwD3_3jajKBwkh0LPSeMtbe_5EyS94nFoEF9OQuhJYjUmp7agsBVa8FFEjVw5jEgVqkvERSj5hSY4nEiCAomdVxIKBfykyi0d12cgjhI7mBFwWkPku8XIPGZ7N8vpiSLdM68BnUqIK5qR7NAhtvT7iyLFgOqhZNUQ6Ret5VpQ', + 'RS384' => 'eyJhbGciOiJSUzM4NCJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.Sfgk56moPghtsjaP4so6tOy3I553mgwX-5gByMC6dX8lpeWgsxSeAd_K8IyO7u4lwYOL0DSftnqO1HEOuN1AKyBbDvaTXz3u2xNA2x4NYLdW4AZA6ritbYcKLO5BHTXw5ueMbtA1jjGXP0zI_aK2iJTMBmB8SCF88RYBUH01Tyf4PlLj98pGL-v3prZd6kZkIeRJ3326h04hslcB5HQKmgeBk24QNLIoIC-CD329HPjJ7TtGx01lj-ehTBnwVbBGzYFAyoalV5KgvL_MDOfWPr1OYHnR5s_Fm6_3Vg4u6lBljvHOrmv4Nfx7d8HLgbo8CwH4qn1wm6VQCtuDd-uhRg', + 'RS512' => 'eyJhbGciOiJSUzUxMiJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.LIIAUEuCkGNdpYguOO5LoW4rZ7ED2POJrB0pmEAAchyTdIK4HKh1jcLxc6KyGwZv40njCgub3y72q6vcQTn7oD0zWFCVQRIDW1911Ii2hRNHuigiPUnrnZh1OQ6z65VZRU6GKs8omoBGU9vrClBU0ODqYE16KxYmE_0n4Xw2h3D_L1LF0IAOtDWKBRDa3QHwZRM9sHsHNsBuD5ye9KzDYN1YALXj64LBfA-DoCKfpVAm9NkRPOyzjR2X2C3TomOSJgqWIVHJucudKDDAZyEbO4RA5pI-UFYy1370p9bRajvtDyoBuLDCzoSkMyQ4L2DnLhx5CbWcnD7Cd3GUmnjjTA' + }.each do |alg, encoded_token| context "alg: #{alg}" do it 'should generate a valid token' do - token = JWT.encode payload, data[:rsa_private], alg + token = JWT.encode payload, rsa_private, alg - expect(token).to eq data[alg] + expect(token).to eq encoded_token end it 'should decode a valid token' do - jwt_payload, header = JWT.decode data[alg], data[:rsa_public], true, algorithm: alg + jwt_payload, header = JWT.decode encoded_token, rsa_public, true, algorithm: alg expect(header['alg']).to eq alg expect(jwt_payload).to eq payload end it 'should decode a valid token using algorithm hash string key' do - jwt_payload, header = JWT.decode data[alg], data[:rsa_public], true, 'algorithm' => alg + jwt_payload, header = JWT.decode encoded_token, rsa_public, true, 'algorithm' => alg expect(header['alg']).to eq alg expect(jwt_payload).to eq payload @@ -144,7 +119,7 @@ key = test_pkey('rsa-2048-wrong-public.pem') expect do - JWT.decode data[alg], key, true, algorithm: alg + JWT.decode encoded_token, key, true, algorithm: alg end.to raise_error JWT::VerificationError end @@ -152,7 +127,7 @@ key = test_pkey('rsa-2048-wrong-public.pem') expect do - JWT.decode data[alg], key, false + JWT.decode encoded_token, key, false end.not_to raise_error end end @@ -160,14 +135,13 @@ %w[ES256 ES384 ES512 ES256K].each do |alg| context "alg: #{alg}" do - before(:each) do - data[alg] = JWT.encode(payload, data["#{alg}_private"], alg) - end - - let(:wrong_key) { OpenSSL::PKey::EC.generate(data["#{alg}_private"].group.curve_name) } + let(:private_key) { test_pkey("ec#{alg[2..-1].downcase}-private.pem") } + let(:public_key) { test_pkey("ec#{alg[2..-1].downcase}-public.pem") } + let(:encoded_token) { JWT.encode(payload, private_key, alg) } + let(:wrong_key) { OpenSSL::PKey::EC.generate(private_key.group.curve_name) } it 'should generate a valid token' do - header, body, signature = data[alg].split('.') + header, body, signature = encoded_token.split('.') expect(header).to eql(Base64.urlsafe_encode64({ alg: alg }.to_json, padding: false)) expect(body).to eql(Base64.urlsafe_encode64(payload.to_json, padding: false)) @@ -175,7 +149,7 @@ end it 'should decode a valid token' do - jwt_payload, header = JWT.decode data[alg], data["#{alg}_public"], true, algorithm: alg + jwt_payload, header = JWT.decode encoded_token, public_key, true, algorithm: alg expect(header['alg']).to eq alg expect(jwt_payload).to eq payload @@ -183,13 +157,13 @@ it 'wrong key should raise JWT::VerificationError' do expect do - JWT.decode data[alg], wrong_key, true, algorithm: alg + JWT.decode encoded_token, wrong_key, true, algorithm: alg end.to raise_error JWT::VerificationError end it 'wrong key and verify = false should not raise an error' do expect do - JWT.decode data[alg], wrong_key, false + JWT.decode encoded_token, wrong_key, false end.not_to raise_error end end @@ -199,14 +173,13 @@ context "alg: #{alg}" do before(:each) do skip 'OpenSSL gem missing RSA-PSS support' unless OpenSSL::PKey::RSA.method_defined?(:sign_pss) - - data[alg] = JWT.encode payload, data[:rsa_private], alg end - let(:wrong_key) { data[:wrong_rsa_public] } + let(:encoded_token) { JWT.encode(payload, rsa_private, alg) } + let(:wrong_key) { test_pkey('rsa-2048-wrong-public.pem') } it 'should generate a valid token' do - token = data[alg] + token = encoded_token header, body, signature = token.split('.') @@ -215,7 +188,7 @@ # Validate signature is made of up header and body of JWT translated_alg = alg.gsub('PS', 'sha') - valid_signature = data[:rsa_public].verify_pss( + valid_signature = rsa_public.verify_pss( translated_alg, JWT::Base64.url_decode(signature), [header, body].join('.'), @@ -226,7 +199,7 @@ end it 'should decode a valid token' do - jwt_payload, header = JWT.decode data[alg], data[:rsa_public], true, algorithm: alg + jwt_payload, header = JWT.decode encoded_token, rsa_public, true, algorithm: alg expect(header['alg']).to eq alg expect(jwt_payload).to eq payload @@ -234,13 +207,13 @@ it 'wrong key should raise JWT::VerificationError' do expect do - JWT.decode data[alg], wrong_key, true, algorithm: alg + JWT.decode encoded_token, wrong_key, true, algorithm: alg end.to raise_error JWT::VerificationError end it 'wrong key and verify = false should not raise an error' do expect do - JWT.decode data[alg], wrong_key, false + JWT.decode encoded_token, wrong_key, false end.not_to raise_error end end @@ -268,7 +241,7 @@ it 'ECDSA curve_name mismatch should raise JWT::IncorrectAlgorithm when decoding' do key = OpenSSL::PKey::EC.generate('secp256k1') - token = JWT.encode payload, data['ES256_private'], 'ES256' + token = JWT.encode payload, test_pkey('ec256-private.pem'), 'ES256' expect do JWT.decode token, key, true, algorithm: 'ES256' @@ -303,12 +276,12 @@ context 'when encoded payload is used to extract key through find_key' do it 'should be able to find a key using the block passed to decode' do - payload_data = { key: 'secret' } - token = JWT.encode payload_data, data[:secret], 'HS256' + keys = { secret: secret } + token = JWT.encode({ key: 'secret' }, secret, 'HS256') expect do JWT.decode(token, nil, true, { algorithm: 'HS256' }) do |_headers, payload| - data[payload['key'].to_sym] + keys[payload['key'].to_sym] end end.not_to raise_error end @@ -317,9 +290,9 @@ iss = 'My_Awesome_Company' iss_payload = { data: 'data', iss: iss } - secrets = { iss => ['hmac_secret2', data[:secret]] } + secrets = { iss => ['hmac_secret2', secret] } - token = JWT.encode iss_payload, data[:secret], 'HS256' + token = JWT.encode iss_payload, secret, 'HS256' expect do JWT.decode(token, nil, true, { iss: iss, verify_iss: true, algorithm: 'HS256' }) do |_headers, payload| @@ -332,9 +305,9 @@ iss = 'My_Awesome_Company' iss_payload = { data: 'data', iss: iss } - secrets = { iss => data[:secret] } + secrets = { iss => secret } - token = JWT.encode iss_payload, data[:secret], 'HS256' + token = JWT.encode iss_payload, secret, 'HS256' expect do JWT.decode(token, nil, true, { iss: iss, verify_iss: true, algorithm: 'HS256' }) do |_headers, payload| @@ -347,9 +320,9 @@ iss = 'My_Awesome_Company' iss_payload = { data: 'data', iss: iss } - secrets = { iss => ['hmac_secret2', data[:secret]] } + secrets = { iss => ['hmac_secret2', secret] } - token = JWT.encode iss_payload, data[:secret], 'HS256' + token = JWT.encode iss_payload, secret, 'HS256' expect do JWT.decode(token, nil, true, { iss: iss, verify_iss: true, algorithm: 'HS256' }) do |_headers, payload| @@ -362,9 +335,9 @@ issuers = %w[My_Awesome_Company1 My_Awesome_Company2] iss_payload = { data: 'data', iss: issuers.first } - secrets = { issuers.first => data[:secret], issuers.last => 'hmac_secret2' } + secrets = { issuers.first => secret, issuers.last => 'hmac_secret2' } - token = JWT.encode iss_payload, data[:secret], 'HS256' + token = JWT.encode iss_payload, secret, 'HS256' expect do JWT.decode(token, nil, true, { iss: issuers, verify_iss: true, algorithm: 'HS256' }) do |_headers, payload| @@ -377,9 +350,9 @@ issuers = %w[My_Awesome_Company1 My_Awesome_Company2] iss_payload = { data: 'data', iss: issuers.first } - secrets = { issuers.first => [data[:secret], 'hmac_secret1'], issuers.last => 'hmac_secret2' } + secrets = { issuers.first => [secret, 'hmac_secret1'], issuers.last => 'hmac_secret2' } - token = JWT.encode iss_payload, data[:secret], 'HS256' + token = JWT.encode iss_payload, secret, 'HS256' expect do JWT.decode(token, nil, true, { iss: issuers, verify_iss: true, algorithm: 'HS256' }) do |_headers, payload| @@ -391,19 +364,19 @@ context 'algorithm' do it 'should raise JWT::IncorrectAlgorithm on mismatch' do - token = JWT.encode payload, data[:secret], 'HS256' + token = JWT.encode payload, secret, 'HS256' expect do - JWT.decode token, data[:secret], true, algorithm: 'HS384' + JWT.decode token, secret, true, algorithm: 'HS384' end.to raise_error JWT::IncorrectAlgorithm expect do - JWT.decode token, data[:secret], true, algorithm: 'HS256' + JWT.decode token, secret, true, algorithm: 'HS256' end.not_to raise_error end it 'should raise JWT::IncorrectAlgorithm on mismatch prior to kid public key network call' do - token = JWT.encode payload, data[:rsa_private], 'RS256' + token = JWT.encode payload, rsa_private, 'RS256' expect do JWT.decode(token, nil, true, { algorithms: ['RS384'] }) do |_, _| @@ -429,30 +402,30 @@ end it 'should raise JWT::IncorrectAlgorithm when algorithms array does not contain algorithm' do - token = JWT.encode payload, data[:secret], 'HS512' + token = JWT.encode payload, secret, 'HS512' expect do - JWT.decode token, data[:secret], true, algorithms: ['HS384'] + JWT.decode token, secret, true, algorithms: ['HS384'] end.to raise_error JWT::IncorrectAlgorithm expect do - JWT.decode token, data[:secret], true, 'algorithms' => ['HS384'] + JWT.decode token, secret, true, 'algorithms' => ['HS384'] end.to raise_error JWT::IncorrectAlgorithm expect do - JWT.decode token, data[:secret], true, algorithms: %w[HS512 HS384] + JWT.decode token, secret, true, algorithms: %w[HS512 HS384] end.not_to raise_error expect do - JWT.decode token, data[:secret], true, 'algorithms' => %w[HS512 HS384] + JWT.decode token, secret, true, 'algorithms' => %w[HS512 HS384] end.not_to raise_error end context 'no algorithm provided' do it 'should use the default decode algorithm' do - token = JWT.encode payload, data[:rsa_public].to_s + token = JWT.encode payload, rsa_public.to_s - jwt_payload, header = JWT.decode token, data[:rsa_public].to_s + jwt_payload, header = JWT.decode token, rsa_public.to_s expect(header['alg']).to eq 'HS256' expect(jwt_payload).to eq payload @@ -462,14 +435,14 @@ context 'token is missing algorithm' do it 'should raise JWT::IncorrectAlgorithm' do expect do - JWT.decode data[:empty_token] + JWT.decode 'e30K.e30K.e30K' end.to raise_error JWT::IncorrectAlgorithm end context 'invalid header format' do it 'should raise JWT::MalformedTokenError' do expect do - JWT.decode data[:invalid_header_token] + JWT.decode 'W10.e30K.e30K' end.to raise_error JWT::MalformedTokenError end end @@ -477,7 +450,7 @@ context 'invalid 2-segment header format' do it 'should raise JWT::MalformedTokenError' do expect do - JWT.decode data[:invalid_2_segment_header_token] + JWT.decode 'WyJIUzI1NiJd.e30K' end.to raise_error JWT::MalformedTokenError, 'Not enough or too many segments' end end @@ -485,7 +458,7 @@ context '2-segment token' do it 'should raise JWT::IncorrectAlgorithm' do expect do - JWT.decode data[:empty_token_2_segment] + JWT.decode 'e30K.e30K.' end.to raise_error JWT::IncorrectAlgorithm end end @@ -497,31 +470,31 @@ let(:token) do iss_payload = payload.merge(iss: iss) - JWT.encode iss_payload, data[:secret] + JWT.encode iss_payload, secret end it 'if verify_iss is set to false (default option) should not raise JWT::InvalidIssuerError' do expect do - JWT.decode token, data[:secret], true, iss: iss, algorithm: 'HS256' + JWT.decode token, secret, true, iss: iss, algorithm: 'HS256' end.not_to raise_error end context 'when verify_iss is set to true and no issues given' do it 'does not raise' do expect do - JWT.decode(token, data[:secret], true, verify_iss: true, algorithm: 'HS256') + JWT.decode(token, secret, true, verify_iss: true, algorithm: 'HS256') end.not_to raise_error end end end context 'audience claim' do - let(:token) { JWT.encode(payload, data[:secret]) } + let(:token) { JWT.encode(payload, secret) } context 'when verify_aud is set to true and no audience given' do it 'does not raise' do expect do - JWT.decode(token, data[:secret], true, verify_aud: true, algorithm: 'HS256') + JWT.decode(token, secret, true, verify_aud: true, algorithm: 'HS256') end.not_to raise_error end end @@ -615,11 +588,11 @@ enc = JWT.encode(payload, 'secret', 'hs256') expect(JWT.decode(enc, 'secret')).to eq([payload, { 'alg' => 'HS256' }]) - enc = JWT.encode(payload, data[:rsa_private], 'rs512') - expect(JWT.decode(enc, data[:rsa_public], true, algorithm: 'RS512')).to eq([payload, { 'alg' => 'RS512' }]) + enc = JWT.encode(payload, rsa_private, 'rs512') + expect(JWT.decode(enc, rsa_public, true, algorithm: 'RS512')).to eq([payload, { 'alg' => 'RS512' }]) - enc = JWT.encode(payload, data[:rsa_private], 'RS512') - expect(JWT.decode(enc, data[:rsa_public], true, algorithm: 'rs512')).to eq([payload, { 'alg' => 'RS512' }]) + enc = JWT.encode(payload, rsa_private, 'RS512') + expect(JWT.decode(enc, rsa_public, true, algorithm: 'rs512')).to eq([payload, { 'alg' => 'RS512' }]) end it 'raises error for invalid algorithm' do @@ -655,9 +628,11 @@ before do expect(JWT::X5cKeyFinder).to receive(:new).with(root_certificates, nil).and_return(key_finder) - expect(key_finder).to receive(:from).and_return(data[:rsa_public]) + expect(key_finder).to receive(:from).and_return(rsa_public) end - subject(:decoded_token) { JWT.decode(data[alg], nil, true, algorithm: alg, x5c: { root_certificates: root_certificates }) } + let(:encoded_token) { JWT.encode(payload, rsa_private, alg) } + + subject(:decoded_token) { JWT.decode(encoded_token, nil, true, algorithm: alg, x5c: { root_certificates: root_certificates }) } it 'calls X5cKeyFinder#from to verify the signature and return the payload' do jwt_payload, header = decoded_token @@ -742,16 +717,20 @@ end context 'when keyfinder resolves to multiple keys and multiple algorithms given' do + let(:ec_private) { test_pkey('ec256-private.pem') } + let(:ec_private_v2) { test_pkey('ec256-private-v2.pem') } + let(:hmac_key) { 'a-shared-hmac-key' } + let(:iss_key_mappings) do { - 'ES256' => [data['ES256_public_v2'], data['ES256_public']], - 'HS256' => data['HS256'] + 'ES256' => [test_pkey('ec256-public-v2.pem'), test_pkey('ec256-public.pem')], + 'HS256' => hmac_key } end context 'with issue with ES256 keys' do it 'tries until the first match' do - token = JWT.encode(payload, data['ES256_private'], 'ES256', 'iss' => 'ES256') + token = JWT.encode(payload, ec_private, 'ES256', 'iss' => 'ES256') result = JWT.decode(token, nil, true, algorithm: %w[ES256 HS256]) do |header, _| iss_key_mappings[header['iss']] end @@ -760,7 +739,7 @@ end it 'tries until the first match' do - token = JWT.encode(payload, data['ES256_private_v2'], 'ES256', 'iss' => 'ES256') + token = JWT.encode(payload, ec_private_v2, 'ES256', 'iss' => 'ES256') result = JWT.decode(token, nil, true, algorithm: %w[ES256 HS256]) do |header, _| iss_key_mappings[header['iss']] end @@ -771,7 +750,7 @@ context 'with issue with HS256 keys' do it 'tries until the first match' do - token = JWT.encode(payload, data['HS256'], 'HS256', 'iss' => 'HS256') + token = JWT.encode(payload, hmac_key, 'HS256', 'iss' => 'HS256') result = JWT.decode(token, nil, true, algorithm: %w[ES256 HS256]) do |header, _| iss_key_mappings[header['iss']] end