Summary
On /settings/external-auth, a provider row can simultaneously render:
- an
Authenticated button (disabled, i.e. treated as signed in), and
Error: token failed to validate next to the provider name,
while a "Test Validate…" run pops the Application link is valid. toast. These three signals contradict each other.
Reproduced after re-authenticating a GitHub external auth link. A hard page reload clears the error.
Root cause
The badge and the error text are sourced from two different react-query entries that are never resynced.
site/src/pages/UserSettingsPage/ExternalAuthPage/ExternalAuthPageView.tsx:
const authenticated = externalAuth
? externalAuth.authenticated // ["external-auth", providerId] -> GET /external-auth/{id}
: (link?.authenticated ?? false); // ["external-auth"] -> GET /external-auth
...
{link?.validate_error && (
<span>
<span className="pl-[1em] text-content-destructive">Error: </span>
{link?.validate_error}
</span>
)}
- The button reads the per-provider query, backed by
externalAuthByID (coderd/externalauth.go), which calls config.ValidateToken() live.
- The error text reads the list query, backed by
listUserExternalAuths (coderd/externalauth.go), which calls cfg.RefreshToken() and records meta.ValidateError = err.Error().
validateExternalAuth in site/src/api/queries/externalAuth.ts only writes the child cache key and never invalidates the list:
export const validateExternalAuth = (queryClient: QueryClient) => ({
mutationFn: API.getExternalAuthProvider,
onSuccess: (data, providerId) => {
queryClient.setQueryData(["external-auth", providerId], data);
},
});
The re-auth popup path has the same gap: startPollingExternalAuth only polls externalAuthProvider(providerID). Only unlinkExternalAuths invalidates ["external-auth"]. So once the list query has captured a validate_error at page load, nothing short of a reload clears it — even though the per-provider query has since flipped to authenticated: true.
Secondary contributor
The two endpoints don't execute the same logic, so they can legitimately disagree even within a single page load:
- List path:
RefreshToken → validateWithRetry → returns InvalidTokenError("token failed to validate") after a 1s bounded retry loop (coderd/externalauth/externalauth.go).
- By-ID path:
ValidateToken directly — no refresh, no retry.
A transient 401 from the provider immediately after a refresh (the case the retry loop in validateWithRetry was added for) will surface as an error on the list while the by-ID call succeeds.
Steps to reproduce
- Have a GitHub external auth link that is in a failed-validation state.
- Load
/settings/external-auth — the row shows Error: token failed to validate.
- Re-authenticate via the login popup, or use the row menu → "Test Validate…".
- The button flips to
Authenticated and a Application link is valid. toast appears, but the error text remains.
Expected
The error should not render alongside an Authenticated state.
Suggested fix
- Have
validateExternalAuth.onSuccess (and the polling success path) also invalidateQueries({ queryKey: ["external-auth"], exact: true }) so the list refetches.
- Additionally / alternatively, gate the error render on
!authenticated so a stale validate_error can never appear next to an Authenticated badge.
Option 2 alone fixes the visual contradiction; option 1 is needed for the displayed error to actually reflect current state.
Created on behalf of @jakehwll
Summary
On
/settings/external-auth, a provider row can simultaneously render:Authenticatedbutton (disabled, i.e. treated as signed in), andError: token failed to validatenext to the provider name,while a "Test Validate…" run pops the
Application link is valid.toast. These three signals contradict each other.Reproduced after re-authenticating a GitHub external auth link. A hard page reload clears the error.
Root cause
The badge and the error text are sourced from two different react-query entries that are never resynced.
site/src/pages/UserSettingsPage/ExternalAuthPage/ExternalAuthPageView.tsx:externalAuthByID(coderd/externalauth.go), which callsconfig.ValidateToken()live.listUserExternalAuths(coderd/externalauth.go), which callscfg.RefreshToken()and recordsmeta.ValidateError = err.Error().validateExternalAuthinsite/src/api/queries/externalAuth.tsonly writes the child cache key and never invalidates the list:The re-auth popup path has the same gap:
startPollingExternalAuthonly pollsexternalAuthProvider(providerID). OnlyunlinkExternalAuthsinvalidates["external-auth"]. So once the list query has captured avalidate_errorat page load, nothing short of a reload clears it — even though the per-provider query has since flipped toauthenticated: true.Secondary contributor
The two endpoints don't execute the same logic, so they can legitimately disagree even within a single page load:
RefreshToken→validateWithRetry→ returnsInvalidTokenError("token failed to validate")after a 1s bounded retry loop (coderd/externalauth/externalauth.go).ValidateTokendirectly — no refresh, no retry.A transient 401 from the provider immediately after a refresh (the case the retry loop in
validateWithRetrywas added for) will surface as an error on the list while the by-ID call succeeds.Steps to reproduce
/settings/external-auth— the row showsError: token failed to validate.Authenticatedand aApplication link is valid.toast appears, but the error text remains.Expected
The error should not render alongside an
Authenticatedstate.Suggested fix
validateExternalAuth.onSuccess(and the polling success path) alsoinvalidateQueries({ queryKey: ["external-auth"], exact: true })so the list refetches.!authenticatedso a stalevalidate_errorcan never appear next to anAuthenticatedbadge.Option 2 alone fixes the visual contradiction; option 1 is needed for the displayed error to actually reflect current state.
Created on behalf of @jakehwll