diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ee2623..b2cef3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,13 @@ ### Fixed +- Auth tokens now backdate the JWT `iat` claim by `Client::AUTH_IAT_LEEWAY_SECONDS` + (5s). `iat` is a whole-second value (RFC 7519 NumericDate) and the server applies + minimal forward leeway, so stamping `iat = Time.now.to_i` caused a small fraction of + requests to be rejected with `token used before issue at (iat)` (HTTP 401) whenever the + caller's clock was even marginally ahead of the server and the second-truncation landed + on a boundary. Backdating keeps the token safely behind the server clock. The legacy + `stream-chat-ruby` client never sent `iat`, so upgrades from it newly exposed this. - `event_class_for_type` now references `GetStream::Generated::Models::*Event` (was `StreamChat::*Event`, which raised `NameError` at runtime). `parse_event` resolves known event types correctly. diff --git a/lib/getstream_ruby/client.rb b/lib/getstream_ruby/client.rb index 4fc38c3..26dd349 100644 --- a/lib/getstream_ruby/client.rb +++ b/lib/getstream_ruby/client.rb @@ -25,6 +25,16 @@ module GetStreamRuby class Client + # Backdate the JWT `iat` claim by this many seconds. + # + # JWT timestamps are whole-second (RFC 7519 NumericDate), so `Time.now.to_i` + # truncates to the second. The server applies minimal forward leeway on + # `iat`, so stamping `iat = now` lets a small fraction of requests be + # rejected ("token used before issue at (iat)") whenever the caller's clock + # is even marginally ahead of the server and the truncation lands on a + # second boundary. Backdating absorbs that sub-second skew. + AUTH_IAT_LEEWAY_SECONDS = 5 + attr_reader :configuration def initialize(config = nil, api_key: nil, api_secret: nil, **options) @@ -274,7 +284,7 @@ def configure_adapter(connection) def generate_auth_header JWT.encode( { - iat: Time.now.to_i, + iat: Time.now.to_i - AUTH_IAT_LEEWAY_SECONDS, server: true, }, @configuration.api_secret, diff --git a/spec/auth_token_spec.rb b/spec/auth_token_spec.rb new file mode 100644 index 0000000..e502325 --- /dev/null +++ b/spec/auth_token_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'jwt' + +# Server auth token (JWT) generation. Guards against the clock-skew 401 +# regression: `iat` is backdated so a caller whose clock is marginally ahead of +# the server does not get intermittently rejected with +# "token used before issue at (iat)". +RSpec.describe 'server auth token' do + + let(:secret) { 's' } + let(:client) { GetStreamRuby.manual(api_key: 'k', api_secret: secret) } + + def decode_payload + header = client.send(:generate_auth_header) + JWT.decode(header, secret, true, algorithm: 'HS256').first + end + + it 'signs a server token with the server claim' do + + expect(decode_payload['server']).to be(true) + + end + + it 'backdates iat by AUTH_IAT_LEEWAY_SECONDS to absorb client/server clock skew' do + + before = Time.now.to_i + iat = decode_payload['iat'] + after = Time.now.to_i + + # iat must sit at least the leeway behind "now" at signing time, and never + # ahead of it, so the server never sees a future-dated token. + expect(iat).to be <= (before - GetStreamRuby::Client::AUTH_IAT_LEEWAY_SECONDS) + expect(iat).to be >= (after - GetStreamRuby::Client::AUTH_IAT_LEEWAY_SECONDS - 1) + + end + + it 'keeps the leeway positive so the backdate is actually applied' do + + expect(GetStreamRuby::Client::AUTH_IAT_LEEWAY_SECONDS).to be > 0 + + end + +end