Skip to content
Merged
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
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,9 @@ end

From [Oauth JSON Web Token 4.1.6. "iat" (Issued At) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.6):

> The `iat` (issued at) claim identifies the time at which the JWT was issued. This claim can be used to determine the age of the JWT. The `leeway` option is not taken into account when verifying this claim. The `iat_leeway` option was removed in version 2.2.0. Its value MUST be a number containing a **_NumericDate_** value. Use of this claim is OPTIONAL.
> The `iat` (issued at) claim identifies the time at which the JWT was issued. This claim can be used to determine the age of the JWT. Its value MUST be a number containing a **_NumericDate_** value. Use of this claim is OPTIONAL.

The global `leeway` option does not apply to `iat`. To allow for clock drift, pass `leeway` under `verify_iat`, as shown below. The `iat_leeway` option was removed in version 2.2.0.

```ruby
iat = Time.now.to_i
Expand All @@ -548,6 +550,12 @@ rescue JWT::InvalidIatError
end
```

By default, `iat` verification allows no clock drift. To allow drift between the issuer and verifier clocks, pass a leeway value 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
85 changes: 84 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,87 @@
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 at the boundary of the allowed drift' do
let(:now) { Time.at(1_609_459_200) }

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

context 'when no leeway is given' do
context 'when iat is exactly now' do
let(:payload) { { 'iat' => 1_609_459_200 } }

it 'passes validation' do
verify!
end
end

context 'when iat is one second after now' do
let(:payload) { { 'iat' => 1_609_459_201 } }

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

context 'when a leeway is given' do
let(:options) { { leeway: 30 } }

context 'when iat is exactly at the end of the leeway window' do
let(:payload) { { 'iat' => 1_609_459_230 } }

it 'passes validation' do
verify!
end
end

context 'when iat is one second past the leeway window' do
let(:payload) { { 'iat' => 1_609_459_231 } }

it 'fails validation' do
expect { verify! }.to raise_error(JWT::InvalidIatError)
end
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
Loading