Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

**Features:**

- Allow a leeway to be given for the `iat` claim verification [#747](https://github.com/jwt/ruby-jwt/pull/747) - ([@denis1011101](https://github.com/denis1011101))
- Your contribution here

**Fixes and enhancements:**
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,12 @@ rescue JWT::InvalidIatError
end
```

The comparison is exact by default. When the clock of the issuer can drift ahead of the clock of the verifier, the tolerated drift can be given explicitly:

```ruby
decoded_token = JWT.decode(token, hmac_secret, true, { verify_iat: { leeway: 30 }, algorithm: 'HS256' })
```

### Subject Claim

From [Oauth JSON Web Token 4.1.2. "sub" (Subject) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.2):
Expand Down
2 changes: 1 addition & 1 deletion lib/jwt/claims/decode_verifier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module DecodeVerifier
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]) },
verify_iss: ->(options) { options[:iss] && Claims::Issuer.new(issuers: options[:iss]) },
verify_iat: ->(*) { Claims::IssuedAt.new },
verify_iat: ->(options) { Claims::IssuedAt.new(leeway: options[:verify_iat].is_a?(Hash) ? options[:verify_iat][:leeway] : nil) },
verify_jti: ->(options) { Claims::JwtId.new(validator: options[:verify_jti]) },
verify_aud: ->(options) { options[:aud] && Claims::Audience.new(expected_audience: options[:aud]) },
verify_sub: ->(options) { options[:sub] && Claims::Subject.new(expected_subject: options[:sub]) },
Expand Down
13 changes: 12 additions & 1 deletion lib/jwt/claims/issued_at.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ module JWT
module Claims
# The IssuedAt class is responsible for validating the issued at claim ('iat') in a JWT token.
class IssuedAt
# Initializes a new IssuedAt instance.
#
# @param leeway [Integer] the drift (in seconds) to allow between the clock of the issuer and the clock of the verifier. Default: 0.
def initialize(leeway: 0)
@leeway = leeway || 0
end

# Verifies the issued at claim ('iat') in the JWT token.
#
# @param context [Object] the context containing the JWT payload.
Expand All @@ -15,8 +22,12 @@ def verify!(context:, **_args)
return unless context.payload.key?('iat')

iat = context.payload['iat']
raise(JWT::InvalidIatError, 'Invalid iat') if !iat.is_a?(::Numeric) || iat.to_f > Time.now.to_f
raise(JWT::InvalidIatError, 'Invalid iat') if !iat.is_a?(::Numeric) || iat.to_f > (Time.now.to_f + leeway)
end

private

attr_reader :leeway
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 @@ -8,7 +8,7 @@ module Verifier
exp: ->(options) { Claims::Expiration.new(leeway: options.dig(:exp, :leeway)) },
nbf: ->(options) { Claims::NotBefore.new(leeway: options.dig(:nbf, :leeway)) },
iss: ->(options) { Claims::Issuer.new(issuers: options[:iss]) },
iat: ->(*) { Claims::IssuedAt.new },
iat: ->(options) { Claims::IssuedAt.new(leeway: options.dig(:iat, :leeway)) },
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]) },
Expand Down
2 changes: 1 addition & 1 deletion lib/jwt/configuration/decode_configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class DecodeConfiguration
# @!attribute [rw] verify_iss
# @return [Boolean] whether to verify the issuer claim.
# @!attribute [rw] verify_iat
# @return [Boolean] whether to verify the issued at claim.
# @return [Boolean, Hash] whether to verify the issued at claim. A hash can be given to configure the claim, currently only `leeway` is supported.
# @!attribute [rw] verify_jti
# @return [Boolean] whether to verify the JWT ID claim.
# @!attribute [rw] verify_aud
Expand Down
40 changes: 39 additions & 1 deletion spec/jwt/claims/issued_at_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
RSpec.describe JWT::Claims::IssuedAt do
let(:payload) { { 'iat' => Time.now.to_f } }

subject(:verify!) { described_class.new.verify!(context: SpecSupport::Token.new(payload: payload)) }
let(:options) { {} }

subject(:verify!) { described_class.new(**options).verify!(context: SpecSupport::Token.new(payload: payload)) }

context 'when iat is now' do
it 'passes validation' do
Expand All @@ -18,6 +20,42 @@
verify!
end
end
context 'when the issuer clock is ahead of the verifier clock' do
let(:now) { Time.at(1_609_459_200.5) }
let(:payload) { { 'iat' => 1_609_459_201 } }

before { allow(Time).to receive(:now) { now } }

it 'fails validation' do
expect { verify! }.to raise_error(JWT::InvalidIatError)
end

context 'when a leeway covering the drift is given' do
let(:options) { { leeway: 1 } }

it 'passes validation' do
verify!
end
end

context 'when a leeway smaller than the drift is given' do
let(:payload) { { 'iat' => 1_609_459_260 } }
let(:options) { { leeway: 1 } }

it 'fails validation' do
expect { verify! }.to raise_error(JWT::InvalidIatError)
end
end
end

context 'when iat is positive infinity' do
let(:payload) { { 'iat' => Float::INFINITY } }

it 'fails validation' do
expect { verify! }.to raise_error(JWT::InvalidIatError)
end
end

context 'when iat is not a number' do
let(:payload) { { 'iat' => 'not_a_number' } }

Expand Down
9 changes: 9 additions & 0 deletions spec/jwt/claims_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@
end
end

context 'iat claim' do
let(:payload) { { 'iat' => Time.now.to_i + 10, 'pay' => 'load' } }

it 'verifies the iat' do
expect { described_class.verify_payload!(payload, iat: {}) }.to raise_error(JWT::InvalidIatError, 'Invalid iat')
described_class.verify_payload!(payload, iat: { leeway: 1000 })
end
end

context 'exp claim' do
let(:payload) { { 'exp' => Time.now.to_i - 10, 'pay' => 'load' } }

Expand Down
10 changes: 9 additions & 1 deletion spec/jwt/jwt_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -645,11 +645,19 @@
end
end

context 'when iat is 1 second before Time.now' do
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
end
end

Expand Down