-
-
Notifications
You must be signed in to change notification settings - Fork 0
fix(auth): SSO auto-redirect + honest error page instead of the silent signup fallback #55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -14,7 +14,7 @@ | |||||||||||||||||||||||||||||
| import clsx from 'clsx'; | ||||||||||||||||||||||||||||||
| import { GoogleProvider } from '@gitroom/frontend/components/auth/providers/google.provider'; | ||||||||||||||||||||||||||||||
| import { AppleProvider } from '@gitroom/frontend/components/auth/providers/apple.provider'; | ||||||||||||||||||||||||||||||
| import { OauthProvider } from '@gitroom/frontend/components/auth/providers/oauth.provider'; | ||||||||||||||||||||||||||||||
| import { OauthProvider, DOS_OAUTH_RETRY_KEY } from '@gitroom/frontend/components/auth/providers/oauth.provider'; | ||||||||||||||||||||||||||||||
| import { useFireEvents } from '@gitroom/helpers/utils/use.fire.events'; | ||||||||||||||||||||||||||||||
| import { useVariables } from '@gitroom/react/helpers/variable.context'; | ||||||||||||||||||||||||||||||
| import { useTrack } from '@gitroom/react/helpers/use.track'; | ||||||||||||||||||||||||||||||
|
|
@@ -36,12 +36,17 @@ | |||||||||||||||||||||||||||||
| const [code, setCode] = useState(getQuery?.get('code') || ''); | ||||||||||||||||||||||||||||||
| const [state] = useState(getQuery?.get('state') || ''); | ||||||||||||||||||||||||||||||
| const [show, setShow] = useState(false); | ||||||||||||||||||||||||||||||
| const [error, setError] = useState<{ | ||||||||||||||||||||||||||||||
| status?: number; | ||||||||||||||||||||||||||||||
| message: string; | ||||||||||||||||||||||||||||||
| } | null>(null); | ||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||
| if (code) { | ||||||||||||||||||||||||||||||
| load(); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||||||||||
| const load = useCallback(async () => { | ||||||||||||||||||||||||||||||
| setError(null); | ||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||
| const response = await fetch( | ||||||||||||||||||||||||||||||
| `/auth/oauth/${provider?.toUpperCase() || 'GENERIC'}/exists`, | ||||||||||||||||||||||||||||||
|
|
@@ -54,21 +59,28 @@ | |||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| if (!response.ok) { | ||||||||||||||||||||||||||||||
| setShow(true); | ||||||||||||||||||||||||||||||
| // The exchange failed server-side. Never masquerade this failure as | ||||||||||||||||||||||||||||||
| // a fresh signup: surface it with a loop-guarded retry instead. | ||||||||||||||||||||||||||||||
| setError({ status: response.status, message: '' }); | ||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
61
to
66
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When the OAuth exchange fails server-side, the error message is set to an empty string (
Suggested change
|
||||||||||||||||||||||||||||||
| const data = await response.json(); | ||||||||||||||||||||||||||||||
| if (data?.token) { | ||||||||||||||||||||||||||||||
| window.sessionStorage.removeItem(DOS_OAUTH_RETRY_KEY); | ||||||||||||||||||||||||||||||
| setCode(data.token); | ||||||||||||||||||||||||||||||
| setShow(true); | ||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||
| window.sessionStorage.removeItem(DOS_OAUTH_RETRY_KEY); | ||||||||||||||||||||||||||||||
| window.location.href = '/'; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||
| console.error('Failed to verify oauth code:', e); | ||||||||||||||||||||||||||||||
| setShow(true); | ||||||||||||||||||||||||||||||
| setError({ message: (e as Error)?.message || '' }); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| }, [provider, code, state]); | ||||||||||||||||||||||||||||||
| if (error) { | ||||||||||||||||||||||||||||||
| return <AuthErrorState status={error.status} message={error.message} />; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| if (!code && !getQuery?.get('provider')) { | ||||||||||||||||||||||||||||||
| return <RegisterAfter token="" provider="LOCAL" />; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
@@ -79,6 +91,74 @@ | |||||||||||||||||||||||||||||
| <RegisterAfter token={code} provider={provider?.toUpperCase() || 'LOCAL'} /> | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // A failed OAuth exchange (state cookie mismatch, upstream token error, ...) | ||||||||||||||||||||||||||||||
| // used to fall through to the signup form, so a broken sign-in looked like a | ||||||||||||||||||||||||||||||
| // fresh registration - the exact confusion reported on 2026-09-21. This state | ||||||||||||||||||||||||||||||
| // shows what happened and offers a loop-guarded retry: up to RETRY_LIMIT | ||||||||||||||||||||||||||||||
| // automatic SSO restarts (a live id.dos.me session makes that one click), | ||||||||||||||||||||||||||||||
| // then a manual link so a persistent failure cannot ping-pong forever. | ||||||||||||||||||||||||||||||
| const RETRY_LIMIT = 2; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| function AuthErrorState({ | ||||||||||||||||||||||||||||||
| status, | ||||||||||||||||||||||||||||||
| message, | ||||||||||||||||||||||||||||||
| }: { | ||||||||||||||||||||||||||||||
| status?: number; | ||||||||||||||||||||||||||||||
| message: string; | ||||||||||||||||||||||||||||||
| }) { | ||||||||||||||||||||||||||||||
| const t = useT(); | ||||||||||||||||||||||||||||||
| const fetch = useFetch(); | ||||||||||||||||||||||||||||||
| const attempts = Number(window.sessionStorage.getItem(DOS_OAUTH_RETRY_KEY) || '0'); | ||||||||||||||||||||||||||||||
| const retry = useCallback(async () => { | ||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||
| window.sessionStorage.setItem(DOS_OAUTH_RETRY_KEY, String(attempts + 1)); | ||||||||||||||||||||||||||||||
| const response = await fetch('/auth/oauth/GENERIC'); | ||||||||||||||||||||||||||||||
| if (response.ok) { | ||||||||||||||||||||||||||||||
| window.location.href = await response.text(); | ||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||
| console.error('Failed to restart the SSO flow:', e); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| window.location.href = '/auth/login'; | ||||||||||||||||||||||||||||||
| }, [attempts]); | ||||||||||||||||||||||||||||||
|
Comment on lines
+113
to
+125
Comment on lines
+112
to
+125
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Accessing |
||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||
| <div className="flex flex-1 flex-col justify-center gap-[16px]"> | ||||||||||||||||||||||||||||||
| <h1 className="text-[40px] font-[500] -tracking-[0.8px] text-start"> | ||||||||||||||||||||||||||||||
| {t('sign_in_failed', 'Sign-in failed')} | ||||||||||||||||||||||||||||||
| </h1> | ||||||||||||||||||||||||||||||
| <p className="text-[14px] leading-relaxed text-zinc-400"> | ||||||||||||||||||||||||||||||
| {t( | ||||||||||||||||||||||||||||||
| 'sign_in_failed_body', | ||||||||||||||||||||||||||||||
| 'We could not complete your sign-in. This is usually temporary - try again below.' | ||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||
| {status ? ` (HTTP ${status})` : ''} | ||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||
| {!!message && ( | ||||||||||||||||||||||||||||||
| <p className="text-[12px] break-all text-red-400">{message}</p> | ||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||
| {attempts < RETRY_LIMIT ? ( | ||||||||||||||||||||||||||||||
| <Button type="button" onClick={retry} className="!h-[52px]"> | ||||||||||||||||||||||||||||||
| {t('try_again', 'Try again')} | ||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||
| <Link | ||||||||||||||||||||||||||||||
| href="/auth/login" | ||||||||||||||||||||||||||||||
| className="flex h-[52px] cursor-pointer items-center justify-center rounded-[10px] border border-fifth text-[15px] font-semibold" | ||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||
| {t('try_again', 'Try again')} | ||||||||||||||||||||||||||||||
| </Link> | ||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||
| <p className="text-center text-sm"> | ||||||||||||||||||||||||||||||
| {t('already_have_an_account', 'Already Have An Account?')} | ||||||||||||||||||||||||||||||
| <Link href="/auth/login" className="underline cursor-pointer"> | ||||||||||||||||||||||||||||||
| {t('sign_in', 'Sign In')} | ||||||||||||||||||||||||||||||
| </Link> | ||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| function getHelpfulReasonForRegistrationFailure(httpCode: number) { | ||||||||||||||||||||||||||||||
| switch (httpCode) { | ||||||||||||||||||||||||||||||
| case 400: | ||||||||||||||||||||||||||||||
|
|
@@ -172,7 +252,7 @@ | |||||||||||||||||||||||||||||
| <div className="flex flex-col text-[14px]"> | ||||||||||||||||||||||||||||||
| {!isAfterProvider && isGeneral && genericOauth ? ( | ||||||||||||||||||||||||||||||
| <div className="flex flex-col gap-4 mt-2"> | ||||||||||||||||||||||||||||||
| <OauthProvider /> | ||||||||||||||||||||||||||||||
| <OauthProvider autoStart /> | ||||||||||||||||||||||||||||||
| <p className="text-xs text-zinc-400 text-center mt-2 leading-relaxed"> | ||||||||||||||||||||||||||||||
| {t( | ||||||||||||||||||||||||||||||
| 'sso_description', | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the user exceeds the retry limit, they are redirected to
/auth/loginto try again manually. However, because/auth/loginhasautoStartenabled, it immediately triggers another redirect to the SSO provider, bypassing the loop guard and causing an infinite redirect loop. Checking the retry count inuseEffectbefore auto-starting prevents this loop.