Signed-off-by: SAY-5 <say.apm35@gmail.com> Signed-off-by: Sai Asish Y <say.apm35@gmail.com> Co-authored-by: Konstantin Konstantinov <KKonstantinov@users.noreply.github.com> Co-authored-by: Konstantin Konstantinov <konstantin@mach5technology.com>
2.3 KiB
| @modelcontextprotocol/client |
|---|
| patch |
Let saveTokens failures surface after a successful token refresh. In auth(), one try
wrapped both refreshAuthorization() and the provider.saveTokens() that persists its
result, and the catch deliberately swallows anything that is not an OAuthError — plus
ServerError — so that a failed refresh falls through to a fresh authorization request.
A persistence error thrown by the provider landed in that same branch: it was discarded
with no log and no rethrow, and auth() continued to startAuthorization() and returned
'REDIRECT'.
Against an authorization server that rotates refresh tokens (the OAuth 2.1 default, and
Keycloak's) this loses credentials rather than merely hiding an error. The exchange has
already succeeded server-side, so the old refresh token is invalidated at the moment the
new one is issued; dropping the new token set leaves nothing usable on either side. On a
headless or CLI client, where redirectToAuthorization is typically a no-op, the fallthrough
is silent and the client is left with stale tokens and no indication of why.
The try/catch now covers only refreshAuthorization(). Persisting the result happens
after it, on an unguarded path, so a provider's I/O error propagates to the caller.
Refresh-request failures keep their existing control flow exactly: a ServerError or an
unknown error still falls through to a new authorization flow, a non-ServerError
OAuthError is still rethrown, and InsecureTokenEndpointError is still surfaced. The
SEP-2352 issuer stamp written with the refreshed tokens is unchanged.
Those fallbacks no longer happen in silence, though. Both routes to an unexplained
re-authorization now emit a console.warn naming the cause: the in-place fallthrough in
the refresh block, and auth()'s outer recovery for invalid_grant, invalid_client,
and unauthorized_client, which discards stored credentials and retries. The second one
matters most in practice — an expired, revoked, or rotation-reuse-detected refresh token
is reported as invalid_grant, which is precisely the state a dropped token set leaves
behind for the next call.
Consumers whose OAuthClientProvider.saveTokens can reject should note that auth() may
now reject where it previously returned 'REDIRECT' — that rejection is the failure that
was being discarded.