// auth.jsx — wired to real AWS Cognito // Uses amazon-cognito-identity-js loaded from CDN in index.html // Flow: signup → verify email → signin → JWT stored in localStorage // ── Cognito config ──────────────────────────────────────────────────────── const COGNITO_USER_POOL_ID = 'eu-central-1_IDzeVSllu'; const COGNITO_CLIENT_ID = '7nffjheavp985k7b4bgug45jf4'; const COGNITO_REGION = 'eu-central-1'; // ── Token storage helpers ───────────────────────────────────────────────── // Tokens are stored in localStorage under these keys. // The API_BASE fetch calls in upload.jsx read FOSSYL_ID_TOKEN automatically. const TOKEN_KEYS = { idToken: 'fossyl_id_token', accessToken: 'fossyl_access_token', refreshToken: 'fossyl_refresh_token', email: 'fossyl_email', expiry: 'fossyl_token_expiry', }; function saveTokens(authResult, email) { localStorage.setItem(TOKEN_KEYS.idToken, authResult.idToken.jwtToken); localStorage.setItem(TOKEN_KEYS.accessToken, authResult.accessToken.jwtToken); localStorage.setItem(TOKEN_KEYS.refreshToken, authResult.refreshToken.token); localStorage.setItem(TOKEN_KEYS.email, email); localStorage.setItem(TOKEN_KEYS.expiry, String(Date.now() + 55 * 60 * 1000)); } function clearTokens() { Object.values(TOKEN_KEYS).forEach(k => localStorage.removeItem(k)); // Remove Cognito auth tokens to prevent focus-based checkToken from re-authing. // Intentionally KEEP: LastAuthUser, device keys, fossyl_cognito_sub_* — // all needed so getCognitoUser resolves the UUID on the next sign-in. const _cp = `CognitoIdentityServiceProvider.${COGNITO_CLIENT_ID}`; const _lu = localStorage.getItem(`${_cp}.LastAuthUser`); if (_lu) { ['idToken', 'accessToken', 'refreshToken'].forEach(k => localStorage.removeItem(`${_cp}.${_lu}.${k}`) ); // LastAuthUser intentionally preserved } } function getIdToken() { return localStorage.getItem(TOKEN_KEYS.idToken) || ''; } // Expose globally so upload.jsx can read it without prop drilling window.getIdToken = getIdToken; // ── Device trust — skip TOTP on recognized devices, re-verify every 30 days ── // // Mechanism: Cognito's native device tracking (pool DeviceConfiguration already // set: ChallengeRequiredOnNewDevice=false, DeviceOnlyRememberedOnUserPrompt=true). // // • After successful TOTP: call setDeviceStatusRemembered() so Cognito marks // this browser as trusted. On success, store a timestamp locally. // • On next login: Cognito's authenticateUser detects the stored device key and // skips the SOFTWARE_TOKEN_MFA challenge → onSuccess fires without totpRequired. // • 30-day enforcement: before each login we check the local timestamp. If it // has expired we delete the Cognito device keys from localStorage, so the pool // treats this browser as an unrecognized device and fires totpRequired again. // • The timestamp is ONLY stored after setDeviceStatusRemembered confirms // Cognito actually remembered the device — never optimistically. // ───────────────────────────────────────────────────────────────────────────── const DEVICE_TRUST_MS = 30 * 24 * 60 * 60 * 1000; function _deviceTrustKey(email) { return `fossyl_device_trust_${email.toLowerCase()}`; } function _isDeviceTrusted(email) { const ts = Number(localStorage.getItem(_deviceTrustKey(email))) || 0; return ts > 0 && (Date.now() - ts) < DEVICE_TRUST_MS; } function _markDeviceTrusted(cognitoUser, email) { console.log('[fossyl-auth] _markDeviceTrusted: username=', cognitoUser.username, '| deviceKey=', cognitoUser.deviceKey, '| session valid=', !!cognitoUser.signInUserSession); cognitoUser.setDeviceStatusRemembered({ onSuccess: () => { localStorage.setItem(_deviceTrustKey(email), String(Date.now())); // Cognito stores device keys under the user's sub UUID, NOT the email. // Cache the UUID so getCognitoUser() can use it on every subsequent login. localStorage.setItem(`fossyl_cognito_sub_${email.toLowerCase()}`, cognitoUser.username); console.log('[fossyl-auth] setDeviceStatusRemembered SUCCESS — sub cached as', cognitoUser.username); }, onFailure: (err) => { console.error('[fossyl-auth] setDeviceStatusRemembered FAILED:', err); }, }); } function _expireDeviceTrust(email) { localStorage.removeItem(_deviceTrustKey(email)); const clientPrefix = `CognitoIdentityServiceProvider.${COGNITO_CLIENT_ID}`; // Use cached sub UUID — device keys live under UUID path, not email path const sub = localStorage.getItem(`fossyl_cognito_sub_${email.toLowerCase()}`); const storedUser = sub || localStorage.getItem(`${clientPrefix}.LastAuthUser`) || email; ['deviceKey', 'deviceGroupKey', 'randomPasswordKey'].forEach(k => localStorage.removeItem(`${clientPrefix}.${storedUser}.${k}`) ); console.log('[fossyl-auth] _expireDeviceTrust: cleared device keys for', storedUser); } // ── Cognito SDK helpers ─────────────────────────────────────────────────── function getCognitoUserPool() { const { CognitoUserPool } = window.AmazonCognitoIdentity; return new CognitoUserPool({ UserPoolId: COGNITO_USER_POOL_ID, ClientId: COGNITO_CLIENT_ID, }); } function getCognitoUser(email) { const { CognitoUser } = window.AmazonCognitoIdentity; // Cognito stores device keys under the user's sub UUID (not the email address). // If we have a cached UUID for this email, use it so authenticateUser finds // the device key in localStorage and includes it in the auth request. const sub = localStorage.getItem(`fossyl_cognito_sub_${email.toLowerCase()}`); return new CognitoUser({ Username: sub || email, Pool: getCognitoUserPool(), }); } // Sign up — returns Promise // On success Cognito sends a verification email automatically. function cognitoSignUp(email, password, name, organisation) { const { CognitoUserAttribute } = window.AmazonCognitoIdentity; const pool = getCognitoUserPool(); const attributes = [ new CognitoUserAttribute({ Name: 'email', Value: email }), new CognitoUserAttribute({ Name: 'name', Value: name || email }), new CognitoUserAttribute({ Name: 'custom:organisation', Value: organisation || '' }), ]; return new Promise((resolve, reject) => { pool.signUp(email, password, attributes, null, (err, result) => { if (err) reject(err); else resolve(result); }); }); } // Confirm signup with the 6-digit code from email function cognitoConfirm(email, code) { const user = getCognitoUser(email); return new Promise((resolve, reject) => { user.confirmRegistration(code, true, (err, result) => { if (err) reject(err); else resolve(result); }); }); } // Resend verification code function cognitoResend(email) { const user = getCognitoUser(email); return new Promise((resolve, reject) => { user.resendConfirmationCode((err, result) => { if (err) reject(err); else resolve(result); }); }); } // Sign in — resolves {result, user} on success. // When the device is trusted Cognito's authenticateUser skips totpRequired entirely // and resolves directly via onSuccess. When it isn't, totpRequired fires and the // caller is responsible for collecting the code and calling cognitoSubmitTotp. function cognitoSignIn(email, password, onTotpRequired) { const { AuthenticationDetails } = window.AmazonCognitoIdentity; const user = getCognitoUser(email); const details = new AuthenticationDetails({ Username: email, Password: password }); return new Promise((resolve, reject) => { user.authenticateUser(details, { onSuccess: (result) => { console.log('[fossyl-auth] authenticateUser onSuccess — TOTP was NOT required (device trusted by Cognito)'); resolve({ result, user }); }, onFailure: reject, newPasswordRequired: () => reject(new Error('Password reset required — contact support.')), totpRequired: () => { console.log('[fossyl-auth] authenticateUser totpRequired — Cognito does not recognise this device'); if (onTotpRequired) onTotpRequired(user); else reject(new Error('Two-factor authentication required.')); }, }); }); } // Submit TOTP code — resolves {result, user} so caller can call setDeviceStatusRemembered. function cognitoSubmitTotp(user, code) { return new Promise((resolve, reject) => { user.sendMFACode(code, { onSuccess: (result) => resolve({ result, user }), onFailure: reject, }, 'SOFTWARE_TOKEN_MFA'); }); } // Forgot password — sends reset code function cognitoForgotPassword(email) { const user = getCognitoUser(email); return new Promise((resolve, reject) => { user.forgotPassword({ onSuccess: resolve, onFailure: reject, }); }); } // Confirm forgot password with code + new password function cognitoConfirmNewPassword(email, code, newPassword) { const user = getCognitoUser(email); return new Promise((resolve, reject) => { user.confirmPassword(code, newPassword, { onSuccess: resolve, onFailure: reject, }); }); } // ── AuthPage component ──────────────────────────────────────────────────── // Stages: 'form' | 'verify' | 'forgot' | 'forgot_confirm' function AuthPage({ mode = 'signin' }) { const { go } = useNav(); const { signIn: ctxSignIn, signUp: ctxSignUp, paid } = useAuth(); const { toast } = useToast(); const [tab, setTab] = useState(mode); // 'signin' | 'signup' const [stage, setStage] = useState('form'); // 'form' | 'verify' | 'forgot' | 'forgot_confirm' const [loading, setLoading] = useState(false); const [pendingEmail, setPendingEmail] = useState(''); const [termsAccepted, setTermsAccepted] = useState(false); const [showTerms, setShowTerms] = useState(false); // Form field refs (uncontrolled — preserves design inputs exactly) const nameRef = useRef(); const orgRef = useRef(); const emailRef = useRef(); const passwordRef = useRef(); const codeRef = useRef(); const newPwRef = useRef(); // TOTP challenge state (set when Cognito requires MFA after password auth) const [totpUser, setTotpUser] = useState(null); const totpRef = useRef(); const switchTab = (t) => { setTab(t); setStage('form'); }; // ── Sign in ────────────────────────────────────────────────────────── const handleSignIn = async (e) => { e && e.preventDefault(); const email = emailRef.current?.value?.trim(); const password = passwordRef.current?.value; if (!email || !password) { toast('Email and password required', { tone: 'error' }); return; } const trusted = _isDeviceTrusted(email); const _sub = localStorage.getItem(`fossyl_cognito_sub_${email.toLowerCase()}`); const _cp = `CognitoIdentityServiceProvider.${COGNITO_CLIENT_ID}`; const _lu = _sub || localStorage.getItem(`${_cp}.LastAuthUser`) || email; console.log('[fossyl-auth] handleSignIn: trusted=', trusted, '| sub=', _sub, '| deviceKey=', localStorage.getItem(`${_cp}.${_lu}.deviceKey`)); if (!trusted) _expireDeviceTrust(email); setLoading(true); try { const auth = await cognitoSignIn(email, password, (user) => { setPendingEmail(email); setTotpUser(user); setStage('totp'); setLoading(false); }); if (!auth) return; // TOTP challenge — loading cleared above // Auth succeeded without MFA (Cognito recognized a trusted device) saveTokens(auth.result, email); ctxSignIn(); go(paid ? 'upload' : 'pricing'); } catch (err) { if (err.code === 'UserNotConfirmedException') { setPendingEmail(email); setStage('verify'); toast('Please verify your email first', { tone: 'error' }); } else { toast(err.message || 'Sign in failed', { tone: 'error' }); } } finally { setLoading(false); } }; // ── Sign up ────────────────────────────────────────────────────────── const handleSignUp = async (e) => { e && e.preventDefault(); const email = emailRef.current?.value?.trim(); const password = passwordRef.current?.value; const name = nameRef.current?.value?.trim(); const org = orgRef.current?.value?.trim(); if (!email || !password) { toast('Email and password required', { tone: 'error' }); return; } if (password.length < 8) { toast('Password must be at least 8 characters', { tone: 'error' }); return; } if (!termsAccepted) { setShowTerms(true); return; } setLoading(true); try { await cognitoSignUp(email, password, name, org); setPendingEmail(email); setStage('verify'); toast('Verification code sent to ' + email, { tone: 'success' }); } catch (err) { if (err.code === 'UsernameExistsException') { toast('Account already exists — sign in instead', { tone: 'error' }); switchTab('signin'); } else { toast(err.message || 'Sign up failed', { tone: 'error' }); } } finally { setLoading(false); } }; // ── Verify email ───────────────────────────────────────────────────── const handleVerify = async (e) => { e && e.preventDefault(); const code = codeRef.current?.value?.trim(); if (!code) { toast('Enter the 6-digit code', { tone: 'error' }); return; } setLoading(true); try { await cognitoConfirm(pendingEmail, code); toast('Email verified — signing you in…', { tone: 'success' }); // Auto sign in after verification if we have the password const password = passwordRef.current?.value; if (password) { const auth = await cognitoSignIn(pendingEmail, password); saveTokens(auth.result, pendingEmail); ctxSignUp(); go('pricing'); } else { setStage('form'); setTab('signin'); } } catch (err) { toast(err.message || 'Verification failed', { tone: 'error' }); } finally { setLoading(false); } }; const handleResend = async () => { try { await cognitoResend(pendingEmail); toast('New code sent to ' + pendingEmail, { tone: 'success' }); } catch (err) { toast(err.message || 'Could not resend code', { tone: 'error' }); } }; // ── Forgot password ─────────────────────────────────────────────────── const handleForgot = async () => { const email = emailRef.current?.value?.trim(); if (!email) { toast('Enter your email first', { tone: 'error' }); return; } setLoading(true); try { await cognitoForgotPassword(email); setPendingEmail(email); setStage('forgot_confirm'); toast('Reset code sent to ' + email, { tone: 'success' }); } catch (err) { toast(err.message || 'Could not send reset code', { tone: 'error' }); } finally { setLoading(false); } }; const handleForgotConfirm = async (e) => { e && e.preventDefault(); const code = codeRef.current?.value?.trim(); const newPw = newPwRef.current?.value; if (!code || !newPw) { toast('Code and new password required', { tone: 'error' }); return; } setLoading(true); try { await cognitoConfirmNewPassword(pendingEmail, code, newPw); toast('Password updated — sign in now', { tone: 'success' }); setStage('form'); setTab('signin'); } catch (err) { toast(err.message || 'Reset failed', { tone: 'error' }); } finally { setLoading(false); } }; // ── TOTP challenge ──────────────────────────────────────────────────── const handleTotpConfirm = async (e) => { e && e.preventDefault(); const code = totpRef.current?.value?.trim(); if (!code || code.length !== 6) { toast('Enter the 6-digit code from your app', { tone: 'error' }); return; } setLoading(true); try { const auth = await cognitoSubmitTotp(totpUser, code); saveTokens(auth.result, pendingEmail); // Mark this device as trusted for the next 30 days. // Future logins from this browser will skip TOTP until the window expires. _markDeviceTrusted(auth.user, pendingEmail); ctxSignIn(); go(paid ? 'upload' : 'pricing'); } catch (err) { toast(err.message || 'Verification failed', { tone: 'error' }); } finally { setLoading(false); } }; // ── Layout — left brand panel is always the same ────────────────────── return (
{/* Left: brand panel */}
go('landing')} style={{ cursor: 'pointer' }}> getfossyl.dev / {tab === 'signin' ? 'sign in' : stage === 'verify' ? 'verify' : 'sign up'}
Specimen access

{stage === 'verify' ? 'Check your inbox.' : stage === 'forgot_confirm' ? 'Set a new password.' : stage === 'totp' ? 'One more step.' : tab === 'signin' ? Fossyl. : 'Begin the excavation.'}

{stage === 'verify' ? `A 6-digit code was sent to ${pendingEmail}. Enter it to activate your account.` : 'Your reports stay in your account for 30 days. After that they are purged — Fossyl does not retain uploaded source beyond a scan\'s lifetime.'}

{showTerms && ( { setTermsAccepted(true); setShowTerms(false); }} onDecline={() => { setTermsAccepted(false); setShowTerms(false); }} /> )} {/* Right: form panel */}
{/* ── Stage: TOTP challenge ───────────────────────────────── */} {stage === 'totp' && (
Two-factor authentication

Enter the 6-digit code from your authenticator app.

Authenticator code { e.target.value = e.target.value.replace(/\D/g, ''); }}/>
{ setStage('form'); setTotpUser(null); }}>← Back to sign in
)} {/* ── Stage: verify email ─────────────────────────────────── */} {stage === 'verify' && (
Verify email

Enter the 6-digit code sent to {pendingEmail}

Verification code
)} {/* ── Stage: forgot password confirm ──────────────────────── */} {stage === 'forgot_confirm' && (
Reset password

Enter the code sent to {pendingEmail} and your new password.

Reset code
New password
setStage('form')}>← Back to sign in
)} {/* ── Stage: main form (signin / signup) ──────────────────── */} {(stage === 'form' || stage === 'forgot') && (
switchTab('signin')}>Sign in switchTab('signup')}>Create account
{tab === 'signup' && (
Full name
Organisation
)}
Work email
Password {tab === 'signin' && ( { e.preventDefault(); handleForgot(); }}> forgot? )}
{tab === 'signup' && ( )}
or

F-auth · session expires after 30 days of inactivity

)}
); } function AuthTab({ active, onClick, children }) { return ( ); } Object.assign(window, { AuthPage, getIdToken, clearTokens });