Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do not decode payload when b64 header is false #631

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions lib/jwt/claims.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# frozen_string_literal: true

require_relative 'claims/audience'
require_relative 'claims/crit'
require_relative 'claims/decode_verifier'
require_relative 'claims/expiration'
require_relative 'claims/issued_at'
require_relative 'claims/issuer'
Expand All @@ -9,9 +11,8 @@
require_relative 'claims/numeric'
require_relative 'claims/required'
require_relative 'claims/subject'
require_relative 'claims/decode_verifier'
require_relative 'claims/verifier'
require_relative 'claims/verification_methods'
require_relative 'claims/verifier'

module JWT
# JWT Claim verifications
Expand All @@ -27,7 +28,6 @@ module JWT
# sub
# required
# numeric
#
module Claims
# Represents a claim verification error
Error = Struct.new(:message, keyword_init: true)
Expand Down
35 changes: 35 additions & 0 deletions lib/jwt/claims/crit.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

module JWT
module Claims
# Responsible of validation the crit header
class Crit
# Initializes a new Crit instance.
#
# @param expected_crits [String] the expected crit header values for the JWT token.
def initialize(expected_crits:)
@expected_crits = Array(expected_crits)
end

# Verifies the critical claim ('crit') in the JWT token header.
#
# @param context [Object] the context containing the JWT payload and header.
# @param _args [Hash] additional arguments (not used).
# @raise [JWT::InvalidCritError] if the crit claim is invalid.
# @return [nil]
def verify!(context:, **_args)
raise(JWT::InvalidCritError, 'Crit header missing') unless context.header['crit']
raise(JWT::InvalidCritError, 'Crit header should be an array') unless context.header['crit'].is_a?(Array)

missing = (expected_crits - context.header['crit'])
raise(JWT::InvalidCritError, "Crit header missing expected values: #{missing.join(', ')}") if missing.any?

nil
end

private

attr_reader :expected_crits
end
end
end
2 changes: 1 addition & 1 deletion lib/jwt/claims/verifier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ module Verifier
jti: ->(options) { Claims::JwtId.new(validator: options[:jti]) },
aud: ->(options) { Claims::Audience.new(expected_audience: options[:aud]) },
sub: ->(options) { Claims::Subject.new(expected_subject: options[:sub]) },

crit: ->(options) { Claims::Crit.new(expected_crits: options[:crit]) },
required: ->(options) { Claims::Required.new(required_claims: options[:required]) },
numeric: ->(*) { Claims::Numeric.new }
}.freeze
Expand Down
30 changes: 27 additions & 3 deletions lib/jwt/encoded_token.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class EncodedToken
# @param jwt [String] the encoded JWT token.
# @raise [ArgumentError] if the provided JWT is not a String.
def initialize(jwt)
raise ArgumentError 'Provided JWT must be a String' unless jwt.is_a?(String)
raise ArgumentError, 'Provided JWT must be a String' unless jwt.is_a?(String)

@jwt = jwt
@encoded_header, @encoded_payload, @encoded_signature = jwt.split('.')
Expand Down Expand Up @@ -57,7 +57,7 @@ def header
#
# @return [Hash] the payload.
def payload
@payload ||= encoded_payload == '' ? raise(JWT::DecodeError, 'Encoded payload is empty') : parse_and_decode(encoded_payload)
@payload ||= decode_payload
end

# Sets or returns the encoded payload of the JWT token.
Expand Down Expand Up @@ -85,6 +85,7 @@ def verify_signature!(algorithm:, key: nil, key_finder: nil)
raise ArgumentError, 'Provide either key or key_finder, not both or neither' if key.nil? == key_finder.nil?

key ||= key_finder.call(self)

return if valid_signature?(algorithm: algorithm, key: key)

raise JWT::VerificationError, 'Signature verification failed'
Expand All @@ -107,8 +108,31 @@ def valid_signature?(algorithm:, key:)

private

def decode_payload
raise JWT::DecodeError, 'Encoded payload is empty' if encoded_payload == ''

if unecoded_payload?
verify_claims!(crit: ['b64'])
return parse_unencoded(encoded_payload)
end

parse_and_decode(encoded_payload)
end

def unecoded_payload?
header['b64'] == false
end

def parse_and_decode(segment)
JWT::JSON.parse(::JWT::Base64.url_decode(segment))
parse(::JWT::Base64.url_decode(segment))
end

def parse_unencoded(segment)
parse(segment)
end

def parse(segment)
JWT::JSON.parse(segment)
rescue ::JSON::ParserError
raise JWT::DecodeError, 'Invalid segment encoding'
end
Expand Down
3 changes: 3 additions & 0 deletions lib/jwt/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ class InvalidAudError < DecodeError; end
# The InvalidSubError class is raised when the JWT subject (sub) claim is invalid.
class InvalidSubError < DecodeError; end

# The InvalidCritError class is raised when the JWT crit header is invalid.
class InvalidCritError < DecodeError; end

# The InvalidJtiError class is raised when the JWT ID (jti) claim is invalid.
class InvalidJtiError < DecodeError; end

Expand Down
45 changes: 45 additions & 0 deletions spec/jwt/claims/crit_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

RSpec.describe JWT::Claims::Crit do
subject(:verify!) { described_class.new(expected_crits: expected_crits).verify!(context: SpecSupport::Token.new(header: header)) }
let(:expected_crits) { [] }
let(:header) { {} }

context 'when header is missing' do
it 'raises JWT::InvalidCritError' do
expect { verify! }.to raise_error(JWT::InvalidCritError, 'Crit header missing')
end
end

context 'when header is not an array' do
let(:header) { { 'crit' => 'not_an_array' } }

it 'raises JWT::InvalidCritError' do
expect { verify! }.to raise_error(JWT::InvalidCritError, 'Crit header should be an array')
end
end

context 'when header is an array and not containing the expected value' do
let(:header) { { 'crit' => %w[crit1] } }
let(:expected_crits) { %w[crit2] }
it 'raises an InvalidCritError' do
expect { verify! }.to raise_error(JWT::InvalidCritError, 'Crit header missing expected values: crit2')
end
end

context 'when header is an array containing exactly the expected values' do
let(:header) { { 'crit' => %w[crit1 crit2] } }
let(:expected_crits) { %w[crit1 crit2] }
it 'does not raise an error' do
expect(verify!).to eq(nil)
end
end

context 'when header is an array containing at least the expected values' do
let(:header) { { 'crit' => %w[crit1 crit2 crit3] } }
let(:expected_crits) { %w[crit1 crit2] }
it 'does not raise an error' do
expect(verify!).to eq(nil)
end
end
end
51 changes: 50 additions & 1 deletion spec/jwt/encoded_token_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

RSpec.describe JWT::EncodedToken do
let(:payload) { { 'pay' => 'load' } }
let(:encoded_token) { JWT.encode(payload, 'secret', 'HS256') }
let(:header) { {} }
let(:encoded_token) { JWT::Token.new(payload: payload, header: header).tap { |t| t.sign!(algorithm: 'HS256', key: 'secret') }.jwt }
let(:detached_payload_token) do
JWT::Token.new(payload: payload).tap do |t|
t.detach_payload!
t.sign!(algorithm: 'HS256', key: 'secret')
end
end

subject(:token) { described_class.new(encoded_token) }

describe '#payload' do
Expand All @@ -28,6 +30,26 @@
end
end
end

context 'when payload is not encoded and the b64 crit is enabled' do
subject(:token) { described_class.new(encoded_token) }
let(:encoded_token) { 'eyJhbGciOiJIUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..signature' }
before { token.encoded_payload = '{"foo": "bar"}' }

it 'does not raise' do
expect(token.payload).to eq({ 'foo' => 'bar' })
end
end

context 'when payload is not encoded and the b64 crit is NOT enabled' do
let(:encoded_token) { 'eyJhbGciOiJIUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..signature' }
before { token.encoded_payload = '{"foo": "bar"}' }

it 'raises an error' do
pending 'Need to decide how to handle this case'
expect { token.payload }.to raise_error
end
end
end

describe '#header' do
Expand Down Expand Up @@ -99,6 +121,17 @@
expect { token.verify_signature!(algorithm: 'HS256', key: 'key', key_finder: 'finder') }.to raise_error(ArgumentError, 'Provide either key or key_finder, not both or neither')
end
end

context 'when payload is not encoded' do
let(:encoded_token) { 'eyJhbGciOiJIUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..A5dxf2s96_n5FLueVuW1Z_vh161FwXZC4YLPff6dmDY' }
before { token.encoded_payload = '$.02' }

let(:key) { Base64.urlsafe_decode64('AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow') }

it 'does not raise' do
expect(token.verify_signature!(algorithm: 'HS256', key: key)).to eq(nil)
end
end
end

describe '#verify_claims!' do
Expand Down Expand Up @@ -150,6 +183,22 @@
end
end
end

context 'when header contains crits header' do
let(:header) { { crit: ['b64'] } }

context 'when expected crits are missing' do
it 'raises an error' do
expect { token.verify_claims!(crit: ['other']) }.to raise_error(JWT::InvalidCritError, 'Crit header missing expected values: other')
end
end

context 'when expected crits are present' do
it 'passes verification' do
expect { token.verify_claims!(crit: ['b64']) }.not_to raise_error
end
end
end
end

describe '#valid_claims?' do
Expand Down
2 changes: 1 addition & 1 deletion spec/spec_support/token.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module SpecSupport
Token = Struct.new(:payload, keyword_init: true)
Token = Struct.new(:payload, :header, keyword_init: true)
end
Loading