Skip to content

馃悰 Guard expires_latency when a token has no known expiry - #765

Merged
pboling merged 1 commit into
ruby-oauth:mainfrom
iamibi:fix/expires-latency-no-expiry
Sep 29, 2026
Merged

pboling merged 1 commit into
ruby-oauth:mainfrom
iamibi:fix/expires-latency-no-expiry

Conversation

@iamibi

@iamibi iamibi commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

Fixes #764

AccessToken#initialize subtracted expires_latency from expires_at even when no expiry was known. A token whose response had neither expires_in nor expires_at, or had expires_in: 0, therefore raised NoMethodError (undefined method '-' for nil). For example, client.get_token(params, expires_latency: 30) failed against any provider that issues non-expiring tokens and so omits expires_in, which RFC 6749 搂5.1 allows.

Change

  • lib/oauth2/access_token.rb: @expires_at -= @expires_latency if @expires_at && @expires_latency, the form suggested in the #394 review. Such a token doesn't expire and keeps expires_latency. Tokens with a known expiry behave as before.
  • CHANGELOG.md: an entry under [Unreleased] / Fixed.

Tests

New examples in the expires_latency block of spec/oauth2/access_token_spec.rb:

  • when neither expires_in nor expires_at is provided: does not raise, does not expire, retains expires_latency;
  • when expires_in is zero: does not raise, does not expire;
  • when the token endpoint response omits expires_in: builds a non-expiring token via Client#get_token.

Against the unpatched code, all 6 new examples fail with the NoMethodError. With the fix, the full bundle exec kettle-test suite passes: 596 examples, 0 failures, 100% line coverage (558/558). The existing examples are unchanged.

Checklist

  • Specs added; they fail without the fix
  • CHANGELOG.md entry under [Unreleased]
  • DCO Signed-off-by and a signed commit
  • bundle exec rubocop-gradual: no new offenses
  • bin/rake reek: no new warnings
  • No RBS changes needed (no signature changes)

AccessToken#initialize subtracted expires_latency from expires_at
whenever expires_latency was set, without checking that expires_at was
set. So a token without expires_in/expires_at (RFC 6749 section 5.1
makes expires_in RECOMMENDED, not REQUIRED), or with expires_in: 0,
raised NoMethodError (undefined method '-' for nil). This broke
client.get_token(params, expires_latency: N) against providers that
issue non-expiring tokens and omit expires_in.

Only apply the latency when an expiry is known.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: iamibi <8592115+iamibi@users.noreply.github.com>
@iamibi

iamibi commented Sep 29, 2026

Copy link
Copy Markdown
Contributor Author

The red checks aren't from this change. They fail the same way on main at eec6bb2:

  • Style and Deps Unlocked: the unlocked lint stack now pulls in Lint/LtsRuby/UnavailableMethod, which isn't in .rubocop_gradual.lock (Style run, Deps Unlocked run)
  • Deps Locked: bundler-audit flags json 2.21.1 (CVE-2026-71847) in Gemfile.lock (run)
  • Windows: the appraisal bundle command fails before RSpec starts (run)
  • Heads and TruffleRuby 22.3/23.0: allowed failures, same as on main

Two fail only because this PR comes from a fork: the QLTY upload in Test Coverage (no OIDC token for fork PRs) and Auto Assign (read-only token). In that coverage job the suite itself passed, with 596 examples, 0 failures and 100% line coverage (job).

Happy to send separate PRs for any of these if that helps.

@pboling

pboling commented Sep 29, 2026

Copy link
Copy Markdown
Member

I am working on fixes for all the failures in main already. Thanks for fixing this issue!

I will be releasing a new patch version of oauth2 soon, so this should get into that release.

@pboling
pboling merged commit 9e2c294 into ruby-oauth:main Sep 29, 2026
29 of 40 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AccessToken raises NoMethodError when expires_latency is set and the token has no expiry

2 participants