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..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 @@ -494,35 +467,34 @@ 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) - 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 @@ -616,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 @@ -632,39 +604,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 @@ -675,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 @@ -762,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 @@ -780,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 @@ -791,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 @@ -823,129 +782,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