diff --git a/Gemfile.lock b/Gemfile.lock index 699338d..09b98e2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,133 +2,150 @@ PATH remote: . specs: traitify (2.1.0) - activesupport (>= 5.1, < 8.x) faraday (~> 2.5) faraday-net_http (~> 3.0) faraday-retry (~> 2.2) + jwt (~> 2.0) GEM remote: https://rubygems.org/ specs: - activesupport (7.1.4) + activesupport (7.1.5.2) base64 + benchmark (>= 0.3) bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) + logger (>= 1.4.2) minitest (>= 5.1) mutex_m + securerandom (>= 0.3) tzinfo (~> 2.0) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) - ast (2.4.2) - base64 (0.2.0) - bigdecimal (3.1.8) + ast (2.4.3) + base64 (0.3.0) + benchmark (0.4.1) + bigdecimal (3.3.0) binding_of_caller (1.0.1) debug_inspector (>= 1.2.0) coderay (1.1.3) - concurrent-ruby (1.3.4) - connection_pool (2.4.1) + concurrent-ruby (1.3.5) + connection_pool (2.5.4) crack (1.0.0) bigdecimal rexml debug_inspector (1.2.0) - diff-lcs (1.5.1) + diff-lcs (1.6.2) docile (1.4.1) - drb (2.2.1) + drb (2.2.3) faraday (2.8.1) base64 faraday-net_http (>= 2.0, < 3.1) ruby2_keywords (>= 0.0.4) faraday-net_http (3.0.2) - faraday-retry (2.2.1) + faraday-retry (2.3.2) faraday (~> 2.0) - hashdiff (1.1.1) - i18n (1.14.6) + hashdiff (1.2.1) + i18n (1.14.7) concurrent-ruby (~> 1.0) - json (2.7.2) - language_server-protocol (3.17.0.3) + json (2.15.1) + jwt (2.10.2) + base64 + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + logger (1.7.0) method_source (1.1.0) - minitest (5.25.1) - mutex_m (0.2.0) - parallel (1.26.3) - parser (3.3.5.0) + minitest (5.25.5) + mutex_m (0.3.0) + parallel (1.27.0) + parser (3.3.9.0) ast (~> 2.4.1) racc - pry (0.14.2) + prism (1.5.1) + pry (0.15.2) coderay (~> 1.1) method_source (~> 1.0) public_suffix (5.1.1) racc (1.8.1) - rack (3.1.16) + rack (3.2.2) rainbow (3.1.1) - rake (13.2.1) - regexp_parser (2.9.2) - rexml (3.3.9) - rspec (3.13.0) + rake (13.3.0) + regexp_parser (2.11.3) + rexml (3.4.4) + rspec (3.13.1) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) - rspec-core (3.13.1) + rspec-core (3.13.5) rspec-support (~> 3.13.0) - rspec-expectations (3.13.3) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-mocks (3.13.2) + rspec-mocks (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-support (3.13.1) - rubocop (1.66.1) + rspec-support (3.13.6) + rubocop (1.81.1) json (~> 2.3) - language_server-protocol (>= 3.17.0) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 2.4, < 3.0) - rubocop-ast (>= 1.32.2, < 2.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.47.1, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 3.0) - rubocop-airbnb (7.0.0) - rubocop (~> 1.61) - rubocop-performance (~> 1.20) - rubocop-rails (~> 2.24) - rubocop-rspec (~> 2.26) - rubocop-ast (1.32.3) - parser (>= 3.3.1.0) - rubocop-capybara (2.21.0) - rubocop (~> 1.41) - rubocop-factory_bot (2.26.1) - rubocop (~> 1.61) - rubocop-performance (1.22.1) - rubocop (>= 1.48.1, < 2.0) - rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rails (2.26.2) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-airbnb (8.0.0) + lint_roller (~> 1.1) + rubocop (~> 1.72) + rubocop-capybara (~> 2.22) + rubocop-factory_bot (~> 2.27) + rubocop-performance (~> 1.24) + rubocop-rails (~> 2.30) + rubocop-rspec (~> 3.5) + rubocop-ast (1.47.1) + parser (>= 3.3.7.2) + prism (~> 1.4) + rubocop-capybara (2.22.1) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + rubocop-factory_bot (2.27.1) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + rubocop-performance (1.26.0) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) + rubocop-rails (2.33.4) activesupport (>= 4.2.0) + lint_roller (~> 1.1) rack (>= 1.1) - rubocop (>= 1.52.0, < 2.0) - rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rspec (2.31.0) - rubocop (~> 1.40) - rubocop-capybara (~> 2.17) - rubocop-factory_bot (~> 2.22) - rubocop-rspec_rails (~> 2.28) - rubocop-rspec_rails (2.29.1) - rubocop (~> 1.61) - rubocop-traitify (1.3.0.pre.alpha.0) - rubocop-airbnb (~> 7.0.0) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) + rubocop-rspec (3.7.0) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + rubocop-traitify (1.3.0) + rubocop-airbnb (~> 8.0.0) ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) + securerandom (0.3.2) simplecov (0.21.2) docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) - simplecov-html (0.13.1) + simplecov-html (0.13.2) simplecov_json_formatter (0.1.4) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (2.6.0) - webmock (3.24.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.1.0) + webmock (3.25.1) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) @@ -150,4 +167,4 @@ DEPENDENCIES webmock (~> 3.18) BUNDLED WITH - 2.4.22 + 2.3.16 diff --git a/lib/.DS_Store b/lib/.DS_Store new file mode 100644 index 0000000..596a1e2 Binary files /dev/null and b/lib/.DS_Store differ diff --git a/lib/traitify.rb b/lib/traitify.rb index ce3ec06..42c3862 100644 --- a/lib/traitify.rb +++ b/lib/traitify.rb @@ -1,5 +1,8 @@ require "active_support" require "active_support/core_ext/object/deep_dup" +require "ostruct" +require "jwt" +require "openssl" require "traitify/configuration" require "traitify/client" require "traitify/data" @@ -37,5 +40,56 @@ def log(level, message) logger.info message end end + + def valid_jwt_token?(token) + algorithm = "RS256" + return false unless jwt_public_keys && jwt_public_keys.any? + + public_keys = jwt_public_keys.map { |key| OpenSSL::PKey::RSA.new(key) } + + public_keys.each do |public_key| + decoded_token = JWT.decode(token, public_key, true, { + algorithm: algorithm, + iss: "Traitify by Paradox", + verify_iss: true, + verify_iat: true, + verify_nbf: true, + verify_jti: true + }) + + payload = decoded_token[0] + validate_claims(payload) + return true + rescue JWT::ExpiredSignature, JWT::DecodeError, JWT::VerificationError => e + Traitify.logger.warn("[JWT] #{e.class.name}: #{e.message}") + next + rescue => e + Traitify.logger.error("[JWT] Unexpected error: #{e.class} - #{e.message}") + next + end + + false + end + + private + + def validate_claims(payload) + current_time = Time.now.to_i + + iat_value = payload["iat"] || payload[:iat] + if iat_value && iat_value > current_time + raise JWT::InvalidIatError.new("Token issued in the future") + end + + nbf_value = payload["nbf"] || payload[:nbf] + if nbf_value && nbf_value > current_time + raise JWT::DecodeError.new("Token not yet valid") + end + + jti_value = payload["jti"] || payload[:jti] + if jti_value.nil? || jti_value.empty? + raise JWT::DecodeError.new("Missing JWT ID (jti)") + end + end end end diff --git a/lib/traitify/.DS_Store b/lib/traitify/.DS_Store new file mode 100644 index 0000000..4b96c0e Binary files /dev/null and b/lib/traitify/.DS_Store differ diff --git a/lib/traitify/configuration.rb b/lib/traitify/configuration.rb index b3ea70c..529a3a8 100644 --- a/lib/traitify/configuration.rb +++ b/lib/traitify/configuration.rb @@ -1,15 +1,16 @@ module Traitify module Configuration VALID_OPTIONS_KEYS = [ - :host, - :public_key, - :secret_key, - :version, :auto_retry, :deck_id, + :host, :image_pack, + :jwt_public_keys, :locale_key, - :retry_options + :public_key, + :retry_options, + :secret_key, + :version ].freeze attr_accessor(*VALID_OPTIONS_KEYS) diff --git a/spec/.DS_Store b/spec/.DS_Store new file mode 100644 index 0000000..723f2f4 Binary files /dev/null and b/spec/.DS_Store differ diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 2df20ce..358334f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,12 +1,21 @@ require "active_support" require "active_support/core_ext/object/conversions" require "active_support/core_ext/object/json" +require "active_support/core_ext/numeric/time" require "webmock/rspec" require "pry" require "simplecov" SimpleCov.start :test_frameworks +module Rails + class << self + def logger + @logger ||= Logger.new($stdout) + end + end +end + require "Traitify" Dir[File.expand_path("spec/support/**/*.rb", __FILE__)].each{ |f| require f } diff --git a/spec/traitify/concerns/token_authenticatable_spec.rb b/spec/traitify/concerns/token_authenticatable_spec.rb new file mode 100644 index 0000000..dcd243b --- /dev/null +++ b/spec/traitify/concerns/token_authenticatable_spec.rb @@ -0,0 +1,402 @@ +require "spec_helper" + +describe Traitify::Concerns::TokenAuthenticatable do + # Create a test controller class that includes the concernmodule + let(:test_controller_class) do + Class.new do + def self.before_action(method_name) + # Do nothing in test environment + end + + include Traitify::Concerns::TokenAuthenticatable + + attr_accessor :request, :response + + def initialize + @request = Object.new + @response = Object.new + end + + attr_reader :request + end + end + + let(:controller){ test_controller_class.new } + + before do + ENV.delete("JWT_PUBLIC_KEY") + ENV.delete("JWT_PUBLIC_KEY_LEGACY") + end + + describe "#authenticate_with_token" do + context "when no authorization header is present" do + before do + allow(controller.request).to receive(:headers).and_return({}) + end + + it "renders unauthorized error" do + expect(controller).to receive(:render).with( + json: {error: "Missing token"}, + status: :unauthorized + ) + + controller.send(:authenticate_with_token) + end + end + + context "when authorization header is present but token is blank" do + before do + allow(controller.request).to receive(:headers).and_return({"Authorization" => ""}) + end + + it "renders unauthorized error" do + expect(controller).to receive(:render).with( + json: {error: "Missing token"}, + status: :unauthorized + ) + + controller.send(:authenticate_with_token) + end + end + + context "when valid token is present" do + let(:valid_token){ "valid.jwt.token" } + + before do + allow(controller.request).to receive(:headers) + .and_return({"Authorization" => "Bearer #{valid_token}"}) + allow(controller).to receive(:valid_token?).with(valid_token).and_return(true) + end + + it "does not render any error" do + expect(controller).not_to receive(:render) + + controller.send(:authenticate_with_token) + end + end + + context "when invalid token is present" do + let(:invalid_token){ "invalid.jwt.token" } + + before do + allow(controller.request).to receive(:headers) + .and_return({"Authorization" => "Bearer #{invalid_token}"}) + allow(controller).to receive(:valid_token?).with(invalid_token).and_return(false) + end + + it "renders unauthorized error" do + expect(controller).to receive(:render).with( + json: {error: "Invalid token"}, + status: :unauthorized + ) + + controller.send(:authenticate_with_token) + end + end + end + + describe "#valid_token?" do + let(:private_key){ OpenSSL::PKey::RSA.new(2048) } + let(:public_key){ private_key.public_key } + let(:valid_payload) do + { + iss: "Traitify by Paradox", + iat: Time.now.to_i, + nbf: Time.now.to_i, + jti: "unique-token-id" + } + end + + before do + ENV["JWT_PUBLIC_KEY"] = public_key.to_pem + end + + context "with valid token" do + let(:valid_token) do + JWT.encode(valid_payload, private_key, "RS256") + end + + it "returns true" do + expect(controller.send(:valid_token?, valid_token)).to be true + end + end + + context "with invalid signature" do + let(:invalid_token) do + other_private_key = OpenSSL::PKey::RSA.new(2048) + JWT.encode(valid_payload, other_private_key, "RS256") + end + + it "returns false" do + expect(controller.send(:valid_token?, invalid_token)).to be false + end + end + + context "with malformed token" do + let(:malformed_token){ "not.a.valid.jwt" } + + it "returns false" do + expect(controller.send(:valid_token?, malformed_token)).to be false + end + end + + context "with expired token" do + let(:expired_payload) do + valid_payload.merge(iat: 1.hour.ago.to_i, exp: 1.hour.ago.to_i) + end + let(:expired_token) do + JWT.encode(expired_payload, private_key, "RS256") + end + + it "returns false" do + expect(controller.send(:valid_token?, expired_token)).to be false + end + end + + context "with wrong issuer" do + let(:wrong_issuer_payload) do + valid_payload.merge(iss: "Wrong Issuer") + end + let(:wrong_issuer_token) do + JWT.encode(wrong_issuer_payload, private_key, "RS256") + end + + it "returns false" do + expect(controller.send(:valid_token?, wrong_issuer_token)).to be false + end + end + + context "with multiple public keys" do + let(:legacy_private_key){ OpenSSL::PKey::RSA.new(2048) } + let(:legacy_public_key){ legacy_private_key.public_key } + + before do + ENV["JWT_PUBLIC_KEY_LEGACY"] = legacy_public_key.to_pem + end + + context "when token is signed with current key" do + let(:current_token) do + JWT.encode(valid_payload, private_key, "RS256") + end + + it "returns true" do + expect(controller.send(:valid_token?, current_token)).to be true + end + end + + context "when token is signed with legacy key" do + let(:legacy_token) do + JWT.encode(valid_payload, legacy_private_key, "RS256") + end + + it "returns true" do + expect(controller.send(:valid_token?, legacy_token)).to be true + end + end + end + + context "when no public keys are configured" do + before do + ENV.delete("JWT_PUBLIC_KEY") + ENV.delete("JWT_PUBLIC_KEY_LEGACY") + end + + it "raises an error" do + expect{ + controller.send(:valid_token?, "any.token") + }.to raise_error(/No JWT public keys configured/) + end + end + end + + describe "#validate_claims" do + let(:current_time){ Time.now.to_i } + + context "with valid claims" do + let(:valid_payload) do + { + iat: current_time - 100, + nbf: current_time - 50, + jti: "unique-token-id" + } + end + + it "does not raise an error" do + expect{ controller.send(:validate_claims, valid_payload) }.not_to raise_error + end + end + + context "with future iat" do + let(:future_iat_payload) do + { + iat: current_time + 100, + nbf: current_time - 50, + jti: "unique-token-id" + } + end + + it "raises InvalidIatError" do + expect{ + controller.send(:validate_claims, + future_iat_payload) + }.to raise_error(JWT::InvalidIatError, "Token issued in the future") + end + end + + context "with future nbf" do + let(:future_nbf_payload) do + { + iat: current_time - 100, + nbf: current_time + 50, + jti: "unique-token-id" + } + end + + it "raises DecodeError" do + expect{ + controller.send(:validate_claims, + future_nbf_payload) + }.to raise_error(JWT::DecodeError, "Token not yet valid") + end + end + + context "with missing jti" do + let(:missing_jti_payload) do + { + iat: current_time - 100, + nbf: current_time - 50 + } + end + + it "raises DecodeError" do + expect{ + controller.send(:validate_claims, + missing_jti_payload) + }.to raise_error(JWT::DecodeError, "Missing JWT ID (jti)") + end + end + + context "with blank jti" do + let(:blank_jti_payload) do + { + iat: current_time - 100, + nbf: current_time - 50, + jti: "" + } + end + + it "raises DecodeError" do + expect{ + controller.send(:validate_claims, + blank_jti_payload) + }.to raise_error(JWT::DecodeError, "Missing JWT ID (jti)") + end + end + + context "with nil jti" do + let(:nil_jti_payload) do + { + iat: current_time - 100, + nbf: current_time - 50, + jti: nil + } + end + + it "raises DecodeError" do + expect{ + controller.send(:validate_claims, + nil_jti_payload) + }.to raise_error(JWT::DecodeError, "Missing JWT ID (jti)") + end + end + + context "with missing iat" do + let(:missing_iat_payload) do + { + nbf: current_time - 50, + jti: "unique-token-id" + } + end + + it "does not raise an error" do + expect{ controller.send(:validate_claims, missing_iat_payload) }.not_to raise_error + end + end + + context "with missing nbf" do + let(:missing_nbf_payload) do + { + iat: current_time - 100, + jti: "unique-token-id" + } + end + + it "does not raise an error" do + expect{ controller.send(:validate_claims, missing_nbf_payload) }.not_to raise_error + end + end + end + + describe "#load_public_keys" do + let(:private_key){ OpenSSL::PKey::RSA.new(2048) } + let(:public_key){ private_key.public_key } + + context "with JWT_PUBLIC_KEY set" do + before do + ENV["JWT_PUBLIC_KEY"] = public_key.to_pem + end + + it "loads the public key" do + keys = controller.send(:load_public_keys) + expect(keys.length).to eq(1) + expect(keys.first).to be_a(OpenSSL::PKey::RSA) + end + end + + context "with JWT_PUBLIC_KEY_LEGACY set" do + let(:legacy_private_key){ OpenSSL::PKey::RSA.new(2048) } + let(:legacy_public_key){ legacy_private_key.public_key } + + before do + ENV["JWT_PUBLIC_KEY_LEGACY"] = legacy_public_key.to_pem + end + + it "loads the legacy public key" do + keys = controller.send(:load_public_keys) + expect(keys.length).to eq(1) + expect(keys.first).to be_a(OpenSSL::PKey::RSA) + end + end + + context "with multiple key sources" do + let(:key1_private){ OpenSSL::PKey::RSA.new(2048) } + let(:key1_public){ key1_private.public_key } + let(:key2_private){ OpenSSL::PKey::RSA.new(2048) } + let(:key2_public){ key2_private.public_key } + + before do + ENV["JWT_PUBLIC_KEY"] = key1_public.to_pem + ENV["JWT_PUBLIC_KEY_LEGACY"] = key2_public.to_pem + end + + it "loads all public keys from all sources" do + keys = controller.send(:load_public_keys) + expect(keys.length).to eq(2) + expect(keys).to all(be_a(OpenSSL::PKey::RSA)) + end + end + + context "with no keys configured" do + before do + ENV.delete("JWT_PUBLIC_KEY") + ENV.delete("JWT_PUBLIC_KEY_LEGACY") + end + + it "raises an error" do + expect{ + controller.send(:load_public_keys) + }.to raise_error(/No JWT public keys configured/) + end + end + end +end diff --git a/spec/traitify_spec.rb b/spec/traitify_spec.rb new file mode 100644 index 0000000..8c90acc --- /dev/null +++ b/spec/traitify_spec.rb @@ -0,0 +1,242 @@ +require "spec_helper" + +describe Traitify do + describe ".valid_jwt_token?" do + let(:private_key){ OpenSSL::PKey::RSA.new(2048) } + let(:public_key){ private_key.public_key } + let(:valid_payload) do + { + iss: "Traitify by Paradox", + iat: Time.now.to_i, + nbf: Time.now.to_i, + jti: "unique-token-id" + } + end + + before do + Traitify.jwt_public_keys = [public_key.to_pem] + end + + after do + Traitify.jwt_public_keys = nil + end + + context "with valid token" do + let(:valid_token) do + JWT.encode(valid_payload, private_key, "RS256") + end + + it "returns true" do + expect(Traitify.valid_jwt_token?(valid_token)).to be true + end + end + + context "with invalid signature" do + let(:invalid_token) do + other_private_key = OpenSSL::PKey::RSA.new(2048) + JWT.encode(valid_payload, other_private_key, "RS256") + end + + it "returns false" do + expect(Traitify.valid_jwt_token?(invalid_token)).to be false + end + end + + context "with malformed token" do + let(:malformed_token){ "not.a.valid.jwt" } + + it "returns false" do + expect(Traitify.valid_jwt_token?(malformed_token)).to be false + end + end + + context "with expired token" do + let(:expired_payload) do + valid_payload.merge(iat: 1.hour.ago.to_i, exp: 1.hour.ago.to_i) + end + let(:expired_token) do + JWT.encode(expired_payload, private_key, "RS256") + end + + it "returns false" do + expect(Traitify.valid_jwt_token?(expired_token)).to be false + end + end + + context "with wrong issuer" do + let(:wrong_issuer_payload) do + valid_payload.merge(iss: "Wrong Issuer") + end + let(:wrong_issuer_token) do + JWT.encode(wrong_issuer_payload, private_key, "RS256") + end + + it "returns false" do + expect(Traitify.valid_jwt_token?(wrong_issuer_token)).to be false + end + end + + context "with multiple public keys" do + let(:legacy_private_key){ OpenSSL::PKey::RSA.new(2048) } + let(:legacy_public_key){ legacy_private_key.public_key } + + before do + Traitify.jwt_public_keys = [public_key.to_pem, legacy_public_key.to_pem] + end + + context "when token is signed with current key" do + let(:current_token) do + JWT.encode(valid_payload, private_key, "RS256") + end + + it "returns true" do + expect(Traitify.valid_jwt_token?(current_token)).to be true + end + end + + context "when token is signed with legacy key" do + let(:legacy_token) do + JWT.encode(valid_payload, legacy_private_key, "RS256") + end + + it "returns true" do + expect(Traitify.valid_jwt_token?(legacy_token)).to be true + end + end + end + + context "with future iat" do + let(:future_iat_payload) do + { + iss: "Traitify by Paradox", + iat: Time.now.to_i + 100, + nbf: Time.now.to_i - 50, + jti: "unique-token-id" + } + end + let(:future_iat_token) do + JWT.encode(future_iat_payload, private_key, "RS256") + end + + it "returns false" do + expect(Traitify.valid_jwt_token?(future_iat_token)).to be false + end + end + + context "with future nbf" do + let(:future_nbf_payload) do + { + iss: "Traitify by Paradox", + iat: Time.now.to_i - 100, + nbf: Time.now.to_i + 50, + jti: "unique-token-id" + } + end + let(:future_nbf_token) do + JWT.encode(future_nbf_payload, private_key, "RS256") + end + + it "returns false" do + expect(Traitify.valid_jwt_token?(future_nbf_token)).to be false + end + end + + context "with missing jti" do + let(:missing_jti_payload) do + { + iss: "Traitify by Paradox", + iat: Time.now.to_i - 100, + nbf: Time.now.to_i - 50 + } + end + let(:missing_jti_token) do + JWT.encode(missing_jti_payload, private_key, "RS256") + end + + it "returns false" do + expect(Traitify.valid_jwt_token?(missing_jti_token)).to be false + end + end + + context "with blank jti" do + let(:blank_jti_payload) do + { + iss: "Traitify by Paradox", + iat: Time.now.to_i - 100, + nbf: Time.now.to_i - 50, + jti: "" + } + end + let(:blank_jti_token) do + JWT.encode(blank_jti_payload, private_key, "RS256") + end + + it "returns false" do + expect(Traitify.valid_jwt_token?(blank_jti_token)).to be false + end + end + + context "with nil jti" do + let(:nil_jti_payload) do + { + iss: "Traitify by Paradox", + iat: Time.now.to_i - 100, + nbf: Time.now.to_i - 50, + jti: nil + } + end + let(:nil_jti_token) do + JWT.encode(nil_jti_payload, private_key, "RS256") + end + + it "returns false" do + expect(Traitify.valid_jwt_token?(nil_jti_token)).to be false + end + end + + context "with missing iat" do + let(:missing_iat_payload) do + { + iss: "Traitify by Paradox", + nbf: Time.now.to_i - 50, + jti: "unique-token-id" + } + end + let(:missing_iat_token) do + JWT.encode(missing_iat_payload, private_key, "RS256") + end + + it "returns true" do + expect(Traitify.valid_jwt_token?(missing_iat_token)).to be true + end + end + + context "with missing nbf" do + let(:missing_nbf_payload) do + { + iss: "Traitify by Paradox", + iat: Time.now.to_i - 100, + jti: "unique-token-id" + } + end + let(:missing_nbf_token) do + JWT.encode(missing_nbf_payload, private_key, "RS256") + end + + it "returns true" do + expect(Traitify.valid_jwt_token?(missing_nbf_token)).to be true + end + end + + context "when no public keys are configured" do + before do + Traitify.jwt_public_keys = nil + end + + it "returns false" do + expect(Traitify.valid_jwt_token?("any.token")).to be false + end + end + end +end + diff --git a/traitify.gemspec b/traitify.gemspec index b96af29..69f9aa0 100644 --- a/traitify.gemspec +++ b/traitify.gemspec @@ -18,10 +18,10 @@ Gem::Specification.new do |spec| spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ["lib"] - spec.add_runtime_dependency "activesupport", ">= 5.1", "< 8.x" spec.add_runtime_dependency "faraday", "~> 2.5" spec.add_runtime_dependency "faraday-net_http", "~> 3.0" spec.add_runtime_dependency "faraday-retry", "~> 2.2" + spec.add_runtime_dependency "jwt", "~> 2.0" spec.add_development_dependency "binding_of_caller", "~> 1.0" spec.add_development_dependency "bundler", "~> 2.2"