From ecdadfa07b878ebc357e9133cdf27ee11aa70967 Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Tue, 26 May 2026 23:25:20 -0400 Subject: [PATCH] feat: redact sensitive information --- README.md | 10 +++++++-- src/AuthProvider.tsx | 6 +++--- src/client/createSeamlessAuthClient.ts | 7 ++---- src/client/webauthnPrf.ts | 11 +++++----- src/components/AuthFallbackOptions.tsx | 4 +--- src/fetchWithAuth.ts | 2 +- src/scopedRoles.ts | 4 +--- src/utils.ts | 4 ++-- src/views/EmailRegistration.tsx | 7 +++--- src/views/Login.tsx | 30 +++++++++++--------------- src/views/PassKeyRegistration.tsx | 4 ++-- src/views/PhoneRegistration.tsx | 13 +++-------- src/views/VerifyMagicLink.tsx | 6 +++--- tests/PhoneRegistration.test.tsx | 4 ++-- tests/RegisterPassKey.test.tsx | 4 +++- tests/createSeamlessAuthClient.test.ts | 10 ++++++--- tests/login.test.tsx | 1 - tests/utils.test.ts | 3 +-- tests/webauthnPrf.test.ts | 10 +++++---- 19 files changed, 66 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index 504db10..11dcaa8 100644 --- a/README.md +++ b/README.md @@ -213,6 +213,9 @@ WebAuthn PRF lets a compatible passkey and browser derive local key material dur Browser and authenticator support is not universal. Call `isPasskeyPrfSupported()` before offering PRF-required flows, and keep a fallback for passkeys that authenticate successfully without returning PRF output. +Treat PRF salts as sensitive in client logs. PRF output is browser-local key material; keep it in +memory only as long as your application needs it and do not send it to Seamless Auth or your own API. + ```ts import { createSeamlessAuthClient } from '@seamless-auth/react'; @@ -275,7 +278,7 @@ function OAuthButtons() { const [providers, setProviders] = useState([]); useEffect(() => { - void listOAuthProviders().then((result) => setProviders(result.providers)); + void listOAuthProviders().then(result => setProviders(result.providers)); }, [listOAuthProviders]); async function signIn(providerId: string) { @@ -290,7 +293,7 @@ function OAuthButtons() { return (
- {providers.map((provider) => ( + {providers.map(provider => ( @@ -345,6 +348,9 @@ OAuth must be enabled on the Seamless Auth API with `LOGIN_METHODS` including `o one configured `oauth_providers` entry. Provider client secrets live on the server and are referenced by environment variable name; they are never passed through this SDK. +The built-in views avoid logging OTPs, magic-link tokens, bootstrap tokens, PRF salts, or raw +exception payloads that may contain sensitive request URLs. + ## Headless Client For custom auth UIs, use the exported client directly: diff --git a/src/AuthProvider.tsx b/src/AuthProvider.tsx index 7b0d01d..33c0ba7 100644 --- a/src/AuthProvider.tsx +++ b/src/AuthProvider.tsx @@ -130,7 +130,7 @@ export const AuthProvider: React.FC = ({ return true; } - console.error('Passkey login failed:', result.message); + console.error('Passkey login failed.'); return false; }; @@ -164,8 +164,8 @@ export const AuthProvider: React.FC = ({ } else { throw new Error('Could not delete user.'); } - } catch (error) { - console.error('Something went wrong deleting user:', error); + } catch { + console.error('Something went wrong deleting user.'); throw new Error('Could not delete user.'); } }; diff --git a/src/client/createSeamlessAuthClient.ts b/src/client/createSeamlessAuthClient.ts index 6daa346..fb23deb 100644 --- a/src/client/createSeamlessAuthClient.ts +++ b/src/client/createSeamlessAuthClient.ts @@ -229,10 +229,7 @@ export interface SeamlessAuthClient { userId: string, input: OrganizationMemberUpdateInput ) => Promise; - removeOrganizationMember: ( - organizationId: string, - userId: string - ) => Promise; + removeOrganizationMember: (organizationId: string, userId: string) => Promise; } const staleStepUpResult = (message: string): StepUpVerificationResult => ({ @@ -544,7 +541,7 @@ export const createSeamlessAuthClient = ( }; } - console.error('Passkey registration error:', error); + console.error('Passkey registration error.'); return { success: false, message: 'Passkey registration failed.', diff --git a/src/client/webauthnPrf.ts b/src/client/webauthnPrf.ts index eb7791e..caa6b87 100644 --- a/src/client/webauthnPrf.ts +++ b/src/client/webauthnPrf.ts @@ -112,7 +112,7 @@ export async function isPasskeyPrfSupported(): Promise { } export function preparePrfRequestOptions( - optionsJSON: PublicKeyCredentialRequestOptionsJSON, + optionsJSON: PublicKeyCredentialRequestOptionsJSON ): PublicKeyCredentialRequestOptionsJSON { const options = optionsJSON as PrfOptionsJSON; const prfEval = options.extensions?.prf?.eval; @@ -138,7 +138,7 @@ export function preparePrfRequestOptions( } export function extractPasskeyPrfResult( - credential: AuthenticationResponseJSON, + credential: AuthenticationResponseJSON ): PasskeyPrfResult | null { const extensionResults = credential.clientExtensionResults as unknown as PrfClientExtensionResults; @@ -158,7 +158,7 @@ export function extractPasskeyPrfResult( } export function stripPrfResultsFromAssertion( - credential: AuthenticationResponseJSON, + credential: AuthenticationResponseJSON ): AuthenticationResponseJSON { const extensionResults = credential.clientExtensionResults as unknown as PrfClientExtensionResults; @@ -182,8 +182,9 @@ export function stripPrfResultsFromAssertion( export function getRegistrationPrfCapable(attestationResponse: { clientExtensionResults?: unknown; }) { - const extensionResults = - attestationResponse.clientExtensionResults as PrfClientExtensionResults | undefined; + const extensionResults = attestationResponse.clientExtensionResults as + | PrfClientExtensionResults + | undefined; return extensionResults?.prf?.enabled === true; } diff --git a/src/components/AuthFallbackOptions.tsx b/src/components/AuthFallbackOptions.tsx index 2bb7eb9..fb7e24b 100644 --- a/src/components/AuthFallbackOptions.tsx +++ b/src/components/AuthFallbackOptions.tsx @@ -67,9 +67,7 @@ const AuthFallbackOptions: React.FC = ({ onClick={onEmailOtp} > Email Code - - Receive a one-time code by email - + Receive a one-time code by email )} diff --git a/src/fetchWithAuth.ts b/src/fetchWithAuth.ts index b142620..4d6f55c 100644 --- a/src/fetchWithAuth.ts +++ b/src/fetchWithAuth.ts @@ -36,7 +36,7 @@ export const createFetchWithAuth = (opts: FetchWithAuthOptions) => { const response = await fetch(url, requestInit); if (!response.ok) { - console.warn('Auth fetch failed:', response.status, url); + console.warn('Auth fetch failed:', response.status); } return response; diff --git a/src/scopedRoles.ts b/src/scopedRoles.ts index 7be66e8..e5d6e05 100644 --- a/src/scopedRoles.ts +++ b/src/scopedRoles.ts @@ -52,9 +52,7 @@ export function hasScopedRole( return false; } - const granted = grantedRoles.filter( - (role): role is string => typeof role === 'string' - ); + const granted = grantedRoles.filter((role): role is string => typeof role === 'string'); const required = Array.isArray(requiredRoles) ? requiredRoles : [requiredRoles]; return required.some(requiredRole => diff --git a/src/utils.ts b/src/utils.ts index c211ec8..9a3fece 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -77,8 +77,8 @@ export async function isPasskeySupported(): Promise { ) { try { return await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); - } catch (error) { - console.error('Error checking passkey support:', error); + } catch { + console.error('Error checking passkey support.'); return false; } } diff --git a/src/views/EmailRegistration.tsx b/src/views/EmailRegistration.tsx index 7c74067..7bc18f6 100644 --- a/src/views/EmailRegistration.tsx +++ b/src/views/EmailRegistration.tsx @@ -19,8 +19,7 @@ const EmailRegistration: React.FC = () => { const { refreshSession } = useAuth(); const authClient = useAuthClient(); const { passkeySupported } = usePasskeySupport(); - const isLoginFlow = - (location.state as { flow?: string } | null)?.flow === 'login'; + const isLoginFlow = (location.state as { flow?: string } | null)?.flow === 'login'; const [loading, setLoading] = useState(false); const [emailOtp, setEmailOtp] = useState(''); @@ -79,8 +78,8 @@ const EmailRegistration: React.FC = () => { await refreshSession(); navigate('/'); } - } catch (err) { - console.error(err); + } catch { + console.error('Email OTP verification failed.'); setError('Verification failed.'); } finally { setLoading(false); diff --git a/src/views/Login.tsx b/src/views/Login.tsx index ddfcc87..ef9d7a9 100644 --- a/src/views/Login.tsx +++ b/src/views/Login.tsx @@ -45,8 +45,7 @@ const Login: React.FC = () => { const [emailError, setEmailError] = useState(''); const [identifierError, setIdentifierError] = useState(''); const [showFallbackOptions, setShowFallbackOptions] = useState(false); - const [loginMethods, setLoginMethods] = - useState(DEFAULT_LOGIN_METHODS); + const [loginMethods, setLoginMethods] = useState(DEFAULT_LOGIN_METHODS); const [bootstrapToken, setBootstrapToken] = useState(null); useEffect(() => { @@ -59,7 +58,6 @@ const Login: React.FC = () => { if (token && token.length > 10) { setBootstrapToken(token); - console.log('Bootstrap token detected in URL'); } }, [hasSignedInBefore]); @@ -105,8 +103,8 @@ const Login: React.FC = () => { setFormErrors( 'An unexpected error occurred. Try again. If the problem persists, try resetting your password.' ); - } catch (err) { - console.error('Unexpected login error', err); + } catch { + console.error('Unexpected login error.'); setFormErrors( 'An unexpected error occurred. Try again. If the problem persists, try resetting your password.' ); @@ -123,8 +121,8 @@ const Login: React.FC = () => { } navigate('/magiclinks-sent', { state: { identifier } }); - } catch (err) { - console.error(err); + } catch { + console.error('Failed to send magic link.'); setFormErrors('Failed to send magic link.'); } }; @@ -139,8 +137,8 @@ const Login: React.FC = () => { } navigate('/verifyPhoneOTP', { state: { flow: 'login' } }); - } catch (err) { - console.error(err); + } catch { + console.error('Failed to send phone OTP.'); setFormErrors('Failed to send OTP.'); } }; @@ -155,8 +153,8 @@ const Login: React.FC = () => { } navigate('/verifyEmailOTP', { state: { flow: 'login' } }); - } catch (err) { - console.error(err); + } catch { + console.error('Failed to send email OTP.'); setFormErrors('Failed to send email code.'); } }; @@ -175,11 +173,7 @@ const Login: React.FC = () => { : DEFAULT_LOGIN_METHODS; setLoginMethods(availableMethods); - if ( - loginRes?.ok && - passkeySupported && - availableMethods.includes('passkey') - ) { + if (loginRes?.ok && passkeySupported && availableMethods.includes('passkey')) { const passkeyResult = await handlePasskeyLogin(); if (passkeyResult) { navigate('/'); @@ -205,8 +199,8 @@ const Login: React.FC = () => { if (mode === 'register') { await register(); } - } catch (err) { - console.error(err); + } catch { + console.error('Failed to continue sign-in.'); setFormErrors('Failed to continue sign-in. Please try again.'); } }; diff --git a/src/views/PassKeyRegistration.tsx b/src/views/PassKeyRegistration.tsx index 0199716..7ddfd19 100644 --- a/src/views/PassKeyRegistration.tsx +++ b/src/views/PassKeyRegistration.tsx @@ -59,8 +59,8 @@ const PasskeyRegistration: React.FC = () => { setStatus('success'); setMessage(result.message); navigate('/'); - } catch (error) { - console.error(error); + } catch { + console.error('Passkey registration failed.'); setStatus('error'); setMessage('Error registering passkey.'); } finally { diff --git a/src/views/PhoneRegistration.tsx b/src/views/PhoneRegistration.tsx index 8eb64a5..2f73b1f 100644 --- a/src/views/PhoneRegistration.tsx +++ b/src/views/PhoneRegistration.tsx @@ -16,8 +16,7 @@ const PhoneRegistration: React.FC = () => { const navigate = useNavigate(); const location = useLocation(); const { refreshSession } = useAuth(); - const isLoginFlow = - (location.state as { flow?: string } | null)?.flow === 'login'; + const isLoginFlow = (location.state as { flow?: string } | null)?.flow === 'login'; const [phoneOtp, setPhoneOtp] = useState(''); const [phoneVerified, setPhoneVerified] = useState(null); @@ -67,8 +66,8 @@ const PhoneRegistration: React.FC = () => { setPhoneVerified(true); } } - } catch (error: unknown) { - console.error(error); + } catch { + console.error('Phone OTP verification failed.'); setError('Verification failed.'); } @@ -81,18 +80,12 @@ const PhoneRegistration: React.FC = () => { ? await authClient.requestLoginPhoneOtp() : await authClient.requestPhoneOtp(); - const data = await response.json(); - if (!response.ok) { setError( 'Failed to send SMS code. If this persists, refresh the page and try again.' ); return; } else { - if (data.token) { - // Set a new token - localStorage.setItem('token', data.token); - } setResendMsg('Verification SMS has been resent.'); } }; diff --git a/src/views/VerifyMagicLink.tsx b/src/views/VerifyMagicLink.tsx index 63fbba3..09bb53e 100644 --- a/src/views/VerifyMagicLink.tsx +++ b/src/views/VerifyMagicLink.tsx @@ -30,7 +30,7 @@ const VerifyMagicLink: React.FC = () => { if (mounted) { setError('Missing token for verification.'); } - console.error('No token found', token); + console.error('No magic-link token found.'); return; } @@ -44,8 +44,8 @@ const VerifyMagicLink: React.FC = () => { } return; } - } catch (error) { - console.error(error); + } catch { + console.error('Failed to verify magic-link token.'); } if (!mounted) { diff --git a/tests/PhoneRegistration.test.tsx b/tests/PhoneRegistration.test.tsx index a9aa91b..b578720 100644 --- a/tests/PhoneRegistration.test.tsx +++ b/tests/PhoneRegistration.test.tsx @@ -107,7 +107,7 @@ describe('PhoneRegistration', () => { expect(mockAuthClient.requestPhoneOtp).toHaveBeenCalled(); }); - test('resend stores token if returned', async () => { + test('resend does not persist returned tokens', async () => { mockAuthClient.requestPhoneOtp.mockResolvedValueOnce({ ok: true, json: async () => ({ token: 'abc123' }), @@ -119,7 +119,7 @@ describe('PhoneRegistration', () => { fireEvent.click(screen.getByRole('button', { name: /resend code to phone/i })); }); - expect(localStorage.getItem('token')).toBe('abc123'); + expect(localStorage.getItem('token')).toBeNull(); }); test('timer counts down', () => { diff --git a/tests/RegisterPassKey.test.tsx b/tests/RegisterPassKey.test.tsx index be53599..d078e8a 100644 --- a/tests/RegisterPassKey.test.tsx +++ b/tests/RegisterPassKey.test.tsx @@ -161,6 +161,8 @@ describe('RegisterPasskey', () => { render(); - expect(screen.getByText(/passkeys are not supported on this device/i)).toBeInTheDocument(); + expect( + screen.getByText(/passkeys are not supported on this device/i) + ).toBeInTheDocument(); }); }); diff --git a/tests/createSeamlessAuthClient.test.ts b/tests/createSeamlessAuthClient.test.ts index aaa0ba1..4912cad 100644 --- a/tests/createSeamlessAuthClient.test.ts +++ b/tests/createSeamlessAuthClient.test.ts @@ -78,9 +78,13 @@ describe('createSeamlessAuthClient', () => { await expect(client.requestLoginPhoneOtp()).resolves.toBe(response); await expect(client.verifyLoginPhoneOtp('123456')).resolves.toBe(response); - expect(mockFetchWithAuth).toHaveBeenNthCalledWith(1, '/otp/generate-login-phone-otp', { - method: 'GET', - }); + expect(mockFetchWithAuth).toHaveBeenNthCalledWith( + 1, + '/otp/generate-login-phone-otp', + { + method: 'GET', + } + ); expect(mockFetchWithAuth).toHaveBeenNthCalledWith( 2, '/otp/verify-login-phone-otp', diff --git a/tests/login.test.tsx b/tests/login.test.tsx index dd78cad..732d852 100644 --- a/tests/login.test.tsx +++ b/tests/login.test.tsx @@ -298,5 +298,4 @@ describe('Login', () => { expect(navigate).toHaveBeenCalledWith('/verifyPhoneOTP'); }); - }); diff --git a/tests/utils.test.ts b/tests/utils.test.ts index dd14ed2..b3c3d7d 100644 --- a/tests/utils.test.ts +++ b/tests/utils.test.ts @@ -103,8 +103,7 @@ describe('isPasskeySupported', () => { const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); await expect(isPasskeySupported()).resolves.toBe(false); expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining('Error checking passkey support:'), - expect.any(Error) + expect.stringContaining('Error checking passkey support.') ); consoleSpy.mockRestore(); }); diff --git a/tests/webauthnPrf.test.ts b/tests/webauthnPrf.test.ts index 66d32e6..fe8d35f 100644 --- a/tests/webauthnPrf.test.ts +++ b/tests/webauthnPrf.test.ts @@ -38,9 +38,9 @@ describe('webauthnPrf helpers', () => { }); expect((options.extensions as any).prf.eval.first).toBeInstanceOf(ArrayBuffer); - expect(Array.from(new Uint8Array((options.extensions as any).prf.eval.first))).toEqual([ - 1, 2, 3, 4, - ]); + expect( + Array.from(new Uint8Array((options.extensions as any).prf.eval.first)) + ).toEqual([1, 2, 3, 4]); }); it('extracts PRF output and strips it from assertion payloads', () => { @@ -67,7 +67,9 @@ describe('webauthnPrf helpers', () => { output: Uint8Array.from([5, 6, 7, 8]), outputBase64url: 'BQYHCA', }); - expect(JSON.stringify(stripPrfResultsFromAssertion(assertion))).not.toContain('BQYHCA'); + expect(JSON.stringify(stripPrfResultsFromAssertion(assertion))).not.toContain( + 'BQYHCA' + ); expect( (stripPrfResultsFromAssertion(assertion).clientExtensionResults as any).prf.results ).toBeUndefined();