fix(introspection): compute token expiry from refreshed, not created#663
Merged
H2CK merged 1 commit intoJun 20, 2026
Merged
Conversation
The introspection endpoint judged access-token expiry as `created + expire_time`. The refresh_token grant (OIDCApiController::getToken) mints a fresh access token on the existing row and advances `refreshed`, but leaves `created` at the original issuance time. As a result, once the *original* token's lifetime elapsed, every subsequently refreshed token was reported `active: false` by introspection even though it was still valid — so resource servers that validate via introspection (rather than the userinfo/JWT path) saw token renewal silently fail and rejected otherwise-good bearer tokens. Switch the expiry basis to `refreshed + expire_time`, matching UserInfoController, TokenValidationRequestListener and OIDCApiController, and include `token_refreshed` in the expired-token log context. Tests: existing valid-token cases now set `refreshed` (== `created`, as a freshly issued token does), and a new regression test asserts a token whose `created` is past its lifetime but `refreshed` is recent is reported active with a refreshed-based `exp`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
H2CK
approved these changes
Jun 20, 2026
Owner
|
Changes are ok for me. I will directly merge. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The token introspection endpoint (
IntrospectionController) computesaccess-token expiry as
created + expire_time:However, the
refresh_tokengrant inOIDCApiController::getToken()mints afresh access token on the existing
oc_oidc_access_tokensrow and advancesrefreshed, but leavescreatedat the original issuance time. (createdisset once in
LoginRedirectorControllerand never updated thereafter.)As a consequence, once the original token's lifetime has elapsed, every
subsequently refreshed access token is reported
active: falsebyintrospection — even though it was just issued and is perfectly valid.
This breaks token renewal for OAuth resource servers that validate bearer
tokens via introspection (RFC 7662) rather than the userinfo or JWT path:
after the first token lifetime, the access token can no longer be refreshed
into a usable state, and the client must re-authenticate from scratch.
Every other expiry check in the app already uses
refreshed:UserInfoController::getUserInfo()—getRefreshed() + expire_timeListener/TokenValidationRequestListener—getRefreshed() + expire_timeOIDCApiController::getToken()(authorization_code) —getRefreshed() + expire_timeIntrospection was the lone outlier using
created.Fix
Compute introspection expiry from
refreshed + expire_time, matching the restof the app. At initial issuance
created == refreshed, so freshly issued(never-refreshed) tokens are unaffected; only refreshed tokens are corrected.
The expired-token log context now also includes
token_refreshed.Tests
refreshed(==created, as afreshly issued token does).
testRefreshedTokenIsActiveAfterOriginalLifetimeasserts a token whose
createdis past its lifetime but whoserefreshedis recent is reported
active: truewith a refreshed-basedexp.Downstream context
Surfaced via the nextcloud-mcp-server OAuth integration: an MCP client (Mistral)
connected fine initially, but after the access-token lifetime expired,
automated refresh succeeded yet the freshly refreshed token was rejected
(
introspection active=false → 401), leaving the connector silently unusable.This PR was generated with the help of AI, and reviewed by a Human
🤖 Generated with Claude Code