fix(auth): show only known sign-in error codes

Sign-in errors arrive in the query string, so unknown codes get a fixed
message instead of being echoed into the banner.
This commit is contained in:
Alexa
2026-10-25 15:54:51 +02:00
parent 891ec89899
commit 5bfa463528
4 changed files with 13 additions and 6 deletions
+3 -2
View File
@@ -54,7 +54,7 @@ export function Login() {
const errorText = (e: unknown, codes: Record<string, React.ReactNode>): React.ReactNode => { const errorText = (e: unknown, codes: Record<string, React.ReactNode>): React.ReactNode => {
if (e instanceof ApiError && e.body && typeof e.body === 'object' && 'error' in e.body) { if (e instanceof ApiError && e.body && typeof e.body === 'object' && 'error' in e.body) {
const code = (e.body as { error: string }).error; const code = (e.body as { error: string }).error;
return codes[code] ?? e.message; return Object.hasOwn(codes, code) ? codes[code] : e.message;
} }
return e instanceof Error ? e.message : String(e); return e instanceof Error ? e.message : String(e);
}; };
@@ -75,7 +75,8 @@ export function Login() {
ssoHandled.current = true; ssoHandled.current = true;
setErr(signinError === 'not_linked' setErr(signinError === 'not_linked'
? <>This sign-in isn't linked to a DorkNet account yet. Link it on the <a className="underline" href={siteUrl('/api/auth/external/login?returnUrl=%2Faccount')}>website</a> first, then sign in here again.</> ? <>This sign-in isn't linked to a DorkNet account yet. Link it on the <a className="underline" href={siteUrl('/api/auth/external/login?returnUrl=%2Faccount')}>website</a> first, then sign in here again.</>
: SSO_ERRORS[signinError] ?? `Single sign-on failed (${signinError}).`); // Only known codes are shown: the query string is attacker-controllable.
: Object.hasOwn(SSO_ERRORS, signinError) ? SSO_ERRORS[signinError] : 'Single sign-on failed. Try again.');
setParams({}, { replace: true }); setParams({}, { replace: true });
return; return;
} }
@@ -2,7 +2,7 @@ import { NavLink, Link, useLocation, useNavigate, useSearchParams } from 'react-
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { BrandMark } from './BrandMark'; import { BrandMark } from './BrandMark';
import { getDownloads, SignInRequiredError } from '../lib/downloads'; import { getDownloads, SignInRequiredError } from '../lib/downloads';
import { getMe, signInUrl, signOut, SIGNIN_ERRORS, type AccountMe } from '../lib/account'; import { errorCopy, getMe, signInUrl, signOut, SIGNIN_ERRORS, type AccountMe } from '../lib/account';
const NAV = [ const NAV = [
{ to: '/', label: 'Home', exact: true }, { to: '/', label: 'Home', exact: true },
@@ -154,7 +154,7 @@ export function Header() {
{signinError && ( {signinError && (
<div className="border-t border-danger/30 bg-danger/10"> <div className="border-t border-danger/30 bg-danger/10">
<div className="mx-auto max-w-6xl px-4 sm:px-6 py-2 flex items-center justify-between gap-3 text-sm text-danger"> <div className="mx-auto max-w-6xl px-4 sm:px-6 py-2 flex items-center justify-between gap-3 text-sm text-danger">
<span>Sign-in didn't work: {SIGNIN_ERRORS[signinError] ?? signinError}</span> <span>Sign-in didn't work: {errorCopy(SIGNIN_ERRORS, signinError, 'something went wrong. Try again.')}</span>
<button type="button" className="btn-ghost !px-2 !py-1 text-xs" onClick={() => { params.delete('signin_error'); setParams(params, { replace: true }); }}>Dismiss</button> <button type="button" className="btn-ghost !px-2 !py-1 text-xs" onClick={() => { params.delete('signin_error'); setParams(params, { replace: true }); }}>Dismiss</button>
</div> </div>
</div> </div>
+6
View File
@@ -46,6 +46,12 @@ export const LINK_ERRORS: Record<string, string> = {
no_pending_identity: 'Your sign-in expired. Start again with "Sign in".', no_pending_identity: 'Your sign-in expired. Start again with "Sign in".',
}; };
/** Friendly copy for a known error code, else `fallback`. For codes read from
* the query string, pass a fixed fallback: anyone can put text in a link. */
export function errorCopy(map: Record<string, string>, code: string | null | undefined, fallback: string): string {
return code && Object.hasOwn(map, code) ? map[code] : fallback;
}
export const SIGNIN_ERRORS: Record<string, string> = { export const SIGNIN_ERRORS: Record<string, string> = {
account_unavailable: 'The account linked to this sign-in is unavailable (deleted or banned).', account_unavailable: 'The account linked to this sign-in is unavailable (deleted or banned).',
no_identity: "The sign-in provider didn't return an identity. Try again.", no_identity: "The sign-in provider didn't return an identity. Try again.",
+2 -2
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Link as RouterLink, useNavigate, useSearchParams } from 'react-router'; import { Link as RouterLink, useNavigate, useSearchParams } from 'react-router';
import { getMe, link, LINK_ERRORS, localPath, signInUrl, type AccountMe } from '../lib/account'; import { errorCopy, getMe, link, LINK_ERRORS, localPath, signInUrl, type AccountMe } from '../lib/account';
// After a first sign-in with the provider: prove which DorkNet account this // After a first sign-in with the provider: prove which DorkNet account this
// identity belongs to, once, with that account's password. // identity belongs to, once, with that account's password.
@@ -35,7 +35,7 @@ export function Link() {
window.location.assign(returnUrl); // the header re-reads who is signed in window.location.assign(returnUrl); // the header re-reads who is signed in
} catch (e) { } catch (e) {
const key = (e as Error).message; const key = (e as Error).message;
setErr(LINK_ERRORS[key] ?? key); setErr(errorCopy(LINK_ERRORS, key, key));
} finally { } finally {
setBusy(false); setBusy(false);
} }