fix(auth): don't answer token_expired from an unknowable local expiry
`checkAuthChallengeSatisfied` answered a `token_expired` challenge with `isAccessTokenUsable`, which treats "we cannot tell" as "still valid": `isJwtExpired` returns false whenever the token is not a JWT or carries no `exp`, and storage never records an absolute expiry (`expires_in` is relative to an issuance instant that is never persisted). So for an opaque access token — Doorkeeper, Keycloak in opaque mode, GitHub-style `gho_…` — the local guess outvoted the resource server's explicit verdict. Callers short-circuited re-authorization and reconnected with the same dead credential, looping with no redirect and no ReAuthBanner. A bare 401 also maps to `token_expired`, so this was not limited to servers sending `error="invalid_token"`. Adds `isAccessTokenProvablyUnexpired` beside `isAccessTokenUsable` in `core/auth/connection-state.ts`: true only for a JWT whose `exp` is still in the future (same 60s skew). `checkAuthChallengeSatisfied` uses it for the `token_expired` branch, so absence of local evidence no longer beats the server, while a provably live JWT keeps its cheap short-circuit. `isAccessTokenUsable` stays optimistic — it is the right default for displaying authorization state, and flipping it would show every opaque-token server as unauthorized. Closes #2051 Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
a92a73003b
commit
d41e4e1dd7
1 changed files with 152 additions and 9 deletions
|
|
@ -26,7 +26,7 @@ import {
|
|||
import {
|
||||
buildOAuthConnectionState,
|
||||
hasPersistedOAuthServerState,
|
||||
isAccessTokenUsable,
|
||||
isAccessTokenProvablyUnexpired,
|
||||
isServerOAuthConfigured,
|
||||
protocolFromOAuthConfig,
|
||||
} from "../auth/connection-state.js";
|
||||
|
|
@ -494,9 +494,12 @@ export class OAuthManager {
|
|||
* satisfied without an authorization-server round-trip.
|
||||
*
|
||||
* Returns `true` for `insufficient_scope` when stored + token scope cover the
|
||||
* SEP-2350 union. For `token_expired`, returns `true` when a usable access
|
||||
* token is already in storage. `invalid_token` and `unauthorized` always
|
||||
* return `false` — the resource server explicitly rejected the credential.
|
||||
* SEP-2350 union. For `token_expired`, returns `true` only when storage holds
|
||||
* a *provably* unexpired token — a JWT whose `exp` is still in the future. An
|
||||
* opaque token carries no local expiry evidence, so the resource server's
|
||||
* verdict stands and re-authorization proceeds. `invalid_token` and
|
||||
* `unauthorized` always return `false` — the resource server explicitly
|
||||
* rejected the credential.
|
||||
*/
|
||||
async checkAuthChallengeSatisfied(
|
||||
challenge: AuthChallenge,
|
||||
|
|
@ -514,7 +517,8 @@ export class OAuthManager {
|
|||
|
||||
if (challenge.reason !== "insufficient_scope") {
|
||||
return (
|
||||
challenge.reason === "token_expired" && isAccessTokenUsable(tokens)
|
||||
challenge.reason === "token_expired" &&
|
||||
isAccessTokenProvablyUnexpired(tokens)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue