Skip to content

Commit

Permalink
Encapsulate token in a object
Browse files Browse the repository at this point in the history
  • Loading branch information
anakinj committed Sep 29, 2024
1 parent a40f8d7 commit fe7a3a3
Show file tree
Hide file tree
Showing 14 changed files with 83 additions and 73 deletions.
1 change: 1 addition & 0 deletions lib/jwt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
require 'jwt/error'
require 'jwt/jwk'
require 'jwt/claims'
require 'jwt/token'

# JSON Web Token implementation
#
Expand Down
6 changes: 2 additions & 4 deletions lib/jwt/claims.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@

module JWT
module Claims
VerificationContext = Struct.new(:payload, keyword_init: true)

VERIFIERS = {
verify_expiration: ->(options) { Claims::Expiration.new(leeway: options[:exp_leeway] || options[:leeway]) },
verify_not_before: ->(options) { Claims::NotBefore.new(leeway: options[:nbf_leeway] || options[:leeway]) },
Expand All @@ -26,11 +24,11 @@ module Claims
}.freeze

class << self
def verify!(payload, options)
def verify!(token, options)
VERIFIERS.each do |key, verifier_builder|
next unless options[key]

verifier_builder&.call(options)&.verify!(context: VerificationContext.new(payload: payload))
verifier_builder&.call(options)&.verify!(context: token)
end
end
end
Expand Down
68 changes: 13 additions & 55 deletions lib/jwt/decode.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,41 +10,35 @@ class Decode
def initialize(jwt, key, verify, options, &keyfinder)
raise JWT::DecodeError, 'Nil JSON web token' unless jwt

@jwt = jwt
@token = Token.new(jwt)
@key = key
@options = options
@segments = jwt.split('.')
@verify = verify
@signature = ''
@keyfinder = keyfinder
end

def decode_segments
validate_segment_count!
if @verify
decode_signature
verify_algo
set_key
verify_signature
verify_claims
Claims.verify!(token, @options)
end
raise JWT::DecodeError, 'Not enough or too many segments' unless header && payload

[payload, header]
[token.payload, token.header]
end

private

def verify_signature
return unless @key || @verify
attr_reader :token

def verify_signature
return if none_algorithm?

raise JWT::DecodeError, 'No verification key available' unless @key

return if Array(@key).any? { |key| verify_signature_for?(key) }

raise JWT::VerificationError, 'Signature verification failed'
token.verify!(algorithms: allowed_and_valid_algorithms, verification_keys: @key)
end

def verify_algo
Expand All @@ -55,16 +49,10 @@ def verify_algo

def set_key
@key = find_key(&@keyfinder) if @keyfinder
@key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks], allow_nil_kid: @options[:allow_nil_kid]).key_for(header['kid']) if @options[:jwks]
@key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks], allow_nil_kid: @options[:allow_nil_kid]).key_for(token.header['kid']) if @options[:jwks]
return unless (x5c_options = @options[:x5c])

@key = X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(header['x5c'])
end

def verify_signature_for?(key)
allowed_and_valid_algorithms.any? do |alg|
alg.verify(data: signing_input, signature: @signature, verification_key: key)
end
@key = X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(token.header['x5c'])
end

def allowed_and_valid_algorithms
Expand Down Expand Up @@ -103,57 +91,27 @@ def sort_by_alg_header(algs)
end

def find_key(&keyfinder)
key = (keyfinder.arity == 2 ? yield(header, payload) : yield(header))
key = (keyfinder.arity == 2 ? yield(token.header, token.payload) : yield(token.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 verify_claims
Claims.verify!(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
return if segment_length == 2 && none_algorithm?
return if token.segment_count == 3
return if !@verify && token.segment_count == 2 # If no verifying required, the signature is not needed
return if token.segment_count == 2 && none_algorithm?

raise JWT::DecodeError, 'Not enough or too many segments'
end

def segment_length
@segments.count
end

def none_algorithm?
alg_in_header == 'none'
end

def decode_signature
@signature = ::JWT::Base64.url_decode(@segments[2] || '')
end

def alg_in_header
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(::JWT::Base64.url_decode(segment))
rescue ::JSON::ParserError
raise JWT::DecodeError, 'Invalid segment encoding'
token.header['alg']
end
end
end
8 changes: 4 additions & 4 deletions lib/jwt/encode.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ module JWT
# Encoding logic for JWT
class Encode
def initialize(options)
@payload = options[:payload]
@key = options[:key]
@algorithm = JWA.resolve(options[:algorithm])
@headers = options[:headers].transform_keys(&:to_s)
@payload = options[:payload]
@key = options[:key]
@algorithm = JWA.resolve(options[:algorithm])
@headers = options[:headers].transform_keys(&:to_s)
end

def segments
Expand Down
49 changes: 49 additions & 0 deletions lib/jwt/token.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

module JWT
class Token
attr_reader :segments, :jwt

def initialize(jwt)
raise ArgumentError 'jwt is nil' if jwt.nil?

@segments = jwt.split('.')
end

def segment_count
@segments.length
end

def signature
@signature ||= ::JWT::Base64.url_decode(segments[2] || '')
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 verify!(algorithms:, verification_keys:)
return if Array(algorithms).any? do |algorithm|
Array(verification_keys).any? do |verification_key|
algorithm.verify(data: signing_input, signature: signature, verification_key: verification_key)
end
end

raise JWT::VerificationError, 'Signature verification failed'
end

def parse_and_decode(segment)
JWT::JSON.parse(::JWT::Base64.url_decode(segment))
rescue ::JSON::ParserError
raise JWT::DecodeError, 'Invalid segment encoding'
end
end
end
2 changes: 1 addition & 1 deletion spec/jwt/claims/audience_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
let(:scalar_aud) { 'ruby-jwt-aud' }
let(:array_aud) { %w[ruby-jwt-aud test-aud ruby-ruby-ruby] }

subject(:verify!) { described_class.new(expected_audience: expected_audience).verify!(context: JWT::Claims::VerificationContext.new(payload: payload)) }
subject(:verify!) { described_class.new(expected_audience: expected_audience).verify!(context: SpecSupport::Token.new(payload: payload)) }

context 'when the singular audience does not match' do
let(:expected_audience) { 'no-match' }
Expand Down
2 changes: 1 addition & 1 deletion spec/jwt/claims/expiration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
let(:payload) { { 'exp' => (Time.now.to_i + 5) } }
let(:leeway) { 0 }

subject(:verify!) { described_class.new(leeway: leeway).verify!(context: JWT::Claims::VerificationContext.new(payload: payload)) }
subject(:verify!) { described_class.new(leeway: leeway).verify!(context: SpecSupport::Token.new(payload: payload)) }

context 'when token is expired' do
let(:payload) { { 'exp' => (Time.now.to_i - 5) } }
Expand Down
2 changes: 1 addition & 1 deletion spec/jwt/claims/issued_at_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
RSpec.describe JWT::Claims::IssuedAt do
let(:payload) { { 'iat' => Time.now.to_f } }

subject(:verify!) { described_class.new.verify!(context: JWT::Claims::VerificationContext.new(payload: payload)) }
subject(:verify!) { described_class.new.verify!(context: SpecSupport::Token.new(payload: payload)) }

context 'when iat is now' do
it 'passes validation' do
Expand Down
2 changes: 1 addition & 1 deletion spec/jwt/claims/issuer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
let(:payload) { { 'iss' => issuer } }
let(:expected_issuers) { 'ruby-jwt-gem' }

subject(:verify!) { described_class.new(issuers: expected_issuers).verify!(context: JWT::Claims::VerificationContext.new(payload: payload)) }
subject(:verify!) { described_class.new(issuers: expected_issuers).verify!(context: SpecSupport::Token.new(payload: payload)) }

context 'when expected issuer is a string that matches the payload' do
it 'passes validation' do
Expand Down
4 changes: 2 additions & 2 deletions spec/jwt/claims/jwt_id_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
let(:payload) { { 'jti' => jti } }
let(:validator) { nil }

subject(:verify!) { described_class.new(validator: validator).verify!(context: JWT::Claims::VerificationContext.new(payload: payload)) }
subject(:verify!) { described_class.new(validator: validator).verify!(context: SpecSupport::Token.new(payload: payload)) }
context 'when payload contains a jti' do
it 'passes validation' do
verify!
Expand Down Expand Up @@ -56,7 +56,7 @@

context 'when jti validator has 2 args' do
it 'the second arg is the payload' do
described_class.new(validator: ->(_jti, pl) { expect(pl).to eq(payload) }).verify!(context: JWT::Claims::VerificationContext.new(payload: payload))
described_class.new(validator: ->(_jti, pl) { expect(pl).to eq(payload) }).verify!(context: SpecSupport::Token.new(payload: payload))
end
end
end
6 changes: 3 additions & 3 deletions spec/jwt/claims/not_before_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@
describe '#verify!' do
context 'when nbf is in the future' do
it 'raises JWT::ImmatureSignature' do
expect { described_class.new(leeway: 0).verify!(context: JWT::Claims::VerificationContext.new(payload: payload)) }.to raise_error JWT::ImmatureSignature
expect { described_class.new(leeway: 0).verify!(context: SpecSupport::Token.new(payload: payload)) }.to raise_error JWT::ImmatureSignature
end
end

context 'when nbf is in the past' do
let(:payload) { { 'nbf' => (Time.now.to_i - 5) } }

it 'does not raise error' do
expect { described_class.new(leeway: 0).verify!(context: JWT::Claims::VerificationContext.new(payload: payload)) }.not_to raise_error
expect { described_class.new(leeway: 0).verify!(context: SpecSupport::Token.new(payload: payload)) }.not_to raise_error
end
end

context 'when leeway is given' do
it 'does not raise error' do
expect { described_class.new(leeway: 10).verify!(context: JWT::Claims::VerificationContext.new(payload: payload)) }.not_to raise_error
expect { described_class.new(leeway: 10).verify!(context: SpecSupport::Token.new(payload: payload)) }.not_to raise_error
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion spec/jwt/claims/required_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
RSpec.describe JWT::Claims::Required do
let(:payload) { { 'data' => 'value' } }

subject(:verify!) { described_class.new(required_claims: required_claims).verify!(context: JWT::Claims::VerificationContext.new(payload: payload)) }
subject(:verify!) { described_class.new(required_claims: required_claims).verify!(context: SpecSupport::Token.new(payload: payload)) }

context 'when payload is missing the required claim' do
let(:required_claims) { ['exp'] }
Expand Down
1 change: 1 addition & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
require 'jwt'

require_relative 'spec_support/test_keys'
require_relative 'spec_support/token'

puts "OpenSSL::VERSION: #{OpenSSL::VERSION}"
puts "OpenSSL::OPENSSL_VERSION: #{OpenSSL::OPENSSL_VERSION}"
Expand Down
3 changes: 3 additions & 0 deletions spec/spec_support/token.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module SpecSupport
Token = Struct.new(:payload, keyword_init: true)
end

0 comments on commit fe7a3a3

Please sign in to comment.