Description
When a template requires multiple coder_external_auth providers (e.g., GitLab and an OIDC provider), clicking the "Login with..." button for one provider causes all other auth buttons to show a loading spinner and become disabled. The user cannot interact with the second auth button until they refresh the page after completing the first authentication.
Environment
- Coder version: 2.29.6
- Browser: Chrome (likely affects all browsers)
Steps to Reproduce
- Configure two or more external auth providers in Coder (e.g., GitLab and an OIDC/PING provider)
- Create a template that requires both providers using
coder_external_auth data sources:
data "coder_external_auth" "provider_a" {
id = "provider-a"
}
data "coder_external_auth" "provider_b" {
id = "provider-b"
}
- Navigate to the "Create Workspace" page for that template
- Observe two auth buttons under "External Authentication"
- Click the "Login with..." button for the first provider
Expected Behavior
Only the clicked provider's button should show a loading spinner. The second provider's button should remain clickable so the user can authenticate with both providers without a page refresh.
Actual Behavior
Both buttons immediately show a loading spinner and become disabled. The second button cannot be clicked until the page is refreshed after the first auth completes.
Current Workaround
Users must:
- Click the first auth button and complete authentication
- Refresh the page
- Click the second auth button
This works because after refresh, the first provider is already authenticated, leaving only one unauthenticated button.
Root Cause Analysis
The externalAuthPollingState is a single shared state for all external auth providers. When any button is clicked, startPollingExternalAuth() sets the global state to "polling", and every ExternalAuthButton receives the same isLoading prop:
CreateWorkspacePageView.tsx (~line 536):
{externalAuth.map((auth) => (
<ExternalAuthButton
key={auth.id}
auth={auth}
isLoading={externalAuthPollingState === "polling"}
onStartPolling={startPollingExternalAuth}
displayRetry={externalAuthPollingState === "abandoned"}
/>
))}
CreateWorkspacePage.tsx (~line 365):
const useExternalAuth = (versionId: string | undefined) => {
const [externalAuthPollingState, setExternalAuthPollingState] =
useState<ExternalAuthPollingState>("idle");
// ... single state shared across ALL providers
};
Additionally, polling stops only when allSignedIn is true (every provider is authenticated), so even after the first auth succeeds, the polling state won't revert to "idle" until the second provider also authenticates — which the user can't trigger because its button is disabled.
Suggested Fix
Make the polling state per-provider instead of global, e.g., using a Map<string, ExternalAuthPollingState> keyed by auth provider ID, so each button independently tracks its own polling/loading/abandoned state.
Created on behalf of a user.
Description
When a template requires multiple
coder_external_authproviders (e.g., GitLab and an OIDC provider), clicking the "Login with..." button for one provider causes all other auth buttons to show a loading spinner and become disabled. The user cannot interact with the second auth button until they refresh the page after completing the first authentication.Environment
Steps to Reproduce
coder_external_authdata sources:Expected Behavior
Only the clicked provider's button should show a loading spinner. The second provider's button should remain clickable so the user can authenticate with both providers without a page refresh.
Actual Behavior
Both buttons immediately show a loading spinner and become disabled. The second button cannot be clicked until the page is refreshed after the first auth completes.
Current Workaround
Users must:
This works because after refresh, the first provider is already authenticated, leaving only one unauthenticated button.
Root Cause Analysis
The
externalAuthPollingStateis a single shared state for all external auth providers. When any button is clicked,startPollingExternalAuth()sets the global state to"polling", and everyExternalAuthButtonreceives the sameisLoadingprop:CreateWorkspacePageView.tsx(~line 536):CreateWorkspacePage.tsx(~line 365):Additionally, polling stops only when
allSignedInis true (every provider is authenticated), so even after the first auth succeeds, the polling state won't revert to"idle"until the second provider also authenticates — which the user can't trigger because its button is disabled.Suggested Fix
Make the polling state per-provider instead of global, e.g., using a
Map<string, ExternalAuthPollingState>keyed by auth provider ID, so each button independently tracks its own polling/loading/abandoned state.Created on behalf of a user.