Skip to main content

Authentication Methods

Scute provides a comprehensive set of authentication methods designed to handle various user scenarios while maintaining security and user experience. This guide explains the different authentication methods, their use cases, and how to implement them effectively.

Authentication Flow Overview

Scute's authentication system follows a hierarchical approach:

  1. WebAuthn/Passkeys (highest security, best UX when available)
  2. Magic Links (secure, no password required)
  3. OTP (One-Time Password) (secure, works on all devices)
  4. OAuth/Social Login (convenient for users with existing accounts)

Core Authentication Methods

signInOrUp(identifier, options?)

The recommended unified authentication method that intelligently handles both sign-in and sign-up scenarios in a single call. This method provides the most seamless user experience by automatically selecting the best available authentication method.

Authentication Flow:

  1. WebAuthn Check: If supported and user has registered devices, attempts passkey authentication
  2. User Detection: Checks if the identifier exists in the system
  3. Smart Routing:
    • Existing user → Sign-in flow
    • New user → Sign-up flow
  4. Method Selection: Based on identifier type and app configuration:
    • Email: Magic link (default) or OTP based on email_auth_type
    • Phone: Always OTP

Use Cases:

  • Universal "Continue with Email/Phone" buttons
  • Simplified onboarding flows
  • Applications wanting to minimize user friction
  • Progressive web apps supporting multiple auth methods

Examples:

// Basic usage with email
const { data, error } = await scuteClient.signInOrUp('user@example.com');

if (error) {
console.error('Authentication failed:', error.message);
return;
}

if (!data) {
// User authenticated with passkey - they're signed in immediately
console.log('User signed in with passkey');
navigate('/dashboard');
} else {
// Additional verification needed (magic link or OTP sent)
console.log('Verification sent via:', data.type); // 'magic_link' or 'otp'
showVerificationUI(data.id); // Use data.id to check status
}

// Phone number authentication
const { data, error } = await scuteClient.signInOrUp('+1234567890');

// With options to disable WebAuthn
const { data, error } = await scuteClient.signInOrUp('user@example.com', {
webauthn: 'disabled'
});

Response Handling:

interface SignInOrUpResponse {
data: {
type: 'magic_link' | 'otp';
id: string; // Use this ID to check verification status
} | null;
error: ScuteError | null;
}

// Complete example with error handling
const handleUniversalAuth = async (identifier: string) => {
try {
const { data, error } = await scuteClient.signInOrUp(identifier);

if (error) {
// Handle specific error types
if (error instanceof TechnicalError) {
showErrorMessage('Something went wrong. Please try again.');
} else {
showErrorMessage(error.message);
}
return;
}

if (!data) {
// WebAuthn success - user is already signed in
onAuthenticationSuccess();
} else {
// Show appropriate verification UI
if (data.type === 'magic_link') {
showMagicLinkSentMessage(identifier);
} else {
showOTPInputForm(identifier, data.id);
}
}
} catch (err) {
console.error('Unexpected error:', err);
showErrorMessage('An unexpected error occurred.');
}
};

signIn(identifier, options?)

For existing users only. This method will fail if the user doesn't exist in the system, making it ideal for dedicated sign-in flows where you want to ensure only existing users can authenticate.

Key Features:

  • Requires user to exist in the system
  • Returns IdentifierNotRecognizedError if user doesn't exist
  • Still performs WebAuthn check first for registered users
  • Respects app configuration for fallback methods

Use Cases:

  • Dedicated "Sign In" forms separate from registration
  • Applications with distinct sign-in/sign-up flows
  • When you want to validate user existence before authentication
  • Member-only areas or private applications

Examples:

// Basic sign-in
const { data, error } = await scuteClient.signIn('user@example.com');

if (error) {
if (error instanceof IdentifierNotRecognizedError) {
showMessage('No account found with this email. Would you like to sign up?');
// Redirect to sign-up flow
} else {
showErrorMessage(error.message);
}
return;
}

// Handle successful authentication or verification needed
handleAuthResponse(data);

// Sign-in with WebAuthn disabled
const { data, error } = await scuteClient.signIn('user@example.com', {
webauthn: 'disabled'
});

// Phone number sign-in
const { data, error } = await scuteClient.signIn('+1234567890');

WebAuthn Options:

type ScuteWebauthnOption = 'strict' | 'optional' | 'disabled';

// Strict: Require WebAuthn if user has it enabled (fails if WebAuthn fails)
await scuteClient.signIn('user@example.com', { webauthn: 'strict' });

// Optional: Try WebAuthn first, fallback to other methods (default)
await scuteClient.signIn('user@example.com', { webauthn: 'optional' });

// Disabled: Skip WebAuthn entirely
await scuteClient.signIn('user@example.com', { webauthn: 'disabled' });

signUp(identifier, options?)

For new users only. This method creates new user accounts and will fail if the user already exists, making it perfect for dedicated registration flows.

Key Features:

  • Only creates new users
  • Returns IdentifierAlreadyExistsError if user exists
  • Accepts additional user metadata during registration
  • No WebAuthn check (since it's a new user)
  • Sends verification based on identifier type and app config

Use Cases:

  • Dedicated registration/sign-up forms
  • Onboarding flows with user data collection
  • Invitation-based registration
  • Applications requiring explicit user consent for account creation

Examples:

// Basic sign-up
const { data, error } = await scuteClient.signUp('newuser@example.com');

if (error) {
if (error instanceof IdentifierAlreadyExistsError) {
showMessage('An account with this email already exists. Sign in instead?');
// Redirect to sign-in flow
} else {
showErrorMessage(error.message);
}
return;
}

// Sign-up with user metadata
const { data, error } = await scuteClient.signUp('newuser@example.com', {
userMeta: {
name: 'John Doe',
company: 'Acme Inc',
role: 'developer',
agreed_to_terms: true,
marketing_consent: false
}
});

// Phone number sign-up
const { data, error } = await scuteClient.signUp('+1234567890', {
userMeta: {
name: 'Jane Smith',
preferred_language: 'es'
}
});

User Metadata Schema:

interface UserMeta {
// String fields
name?: string;
company?: string;
role?: string;

// Boolean fields
agreed_to_terms?: boolean;
marketing_consent?: boolean;
newsletter_subscription?: boolean;

// Number fields
age?: number;

// Any other custom fields defined in your app config
[key: string]: string | number | boolean | undefined;
}

Complete Registration Flow:

const handleSignUp = async (identifier: string, userData: UserMeta) => {
// Validate required fields first
if (!userData.name || !userData.agreed_to_terms) {
showErrorMessage('Please fill in all required fields');
return;
}

const { data, error } = await scuteClient.signUp(identifier, {
userMeta: userData
});

if (error) {
if (error instanceof IdentifierAlreadyExistsError) {
// Offer to sign in instead
const shouldSignIn = await showConfirmDialog(
'An account already exists with this email. Sign in instead?'
);

if (shouldSignIn) {
return handleSignIn(identifier);
}
} else {
showErrorMessage(error.message);
}
return;
}

// Show verification UI
if (data.type === 'magic_link') {
showMessage(`We've sent a verification link to ${identifier}`);
} else {
showOTPInput(identifier, data.id);
}
};

Direct Authentication Methods

sendLoginMagicLink(identifier, webauthnEnabled?)

Explicitly sends a magic link for authentication, bypassing any passkey checks. This method gives you direct control over the authentication flow when you specifically want email-based authentication.

Key Features:

  • Bypasses WebAuthn/passkey authentication
  • Works for both existing and new users
  • Sends email with secure, time-limited authentication link
  • Returns polling data to check verification status

Use Cases:

  • "Continue with Email" buttons
  • Users who prefer email-based authentication
  • Fallback when WebAuthn fails or is unavailable
  • Shared device scenarios where passkeys aren't appropriate

Examples:

// Send magic link for existing or new user
const { data, error } = await scuteClient.sendLoginMagicLink('user@example.com');

if (error) {
showErrorMessage('Failed to send magic link');
return;
}

// Show user-friendly message
showMessage('Check your email for a sign-in link');

// Optional: Poll for verification status
pollForMagicLinkVerification(data.id);

// Disable WebAuthn in the magic link token
const { data, error } = await scuteClient.sendLoginMagicLink(
'user@example.com',
false // webauthnEnabled = false
);
// Poll for magic link verification
const pollForMagicLinkVerification = async (magicLinkId: string) => {
const maxAttempts = 30; // 5 minutes with 10-second intervals
let attempts = 0;

const checkStatus = async () => {
try {
const { data, error } = await scuteClient.getMagicLinkStatus(magicLinkId);

if (!error && data) {
// Magic link was clicked - sign in the user
await scuteClient.signInWithTokenPayload(data);
onAuthenticationSuccess();
return;
}
} catch (err) {
// Continue polling
}

attempts++;
if (attempts < maxAttempts) {
setTimeout(checkStatus, 10000); // Check every 10 seconds
} else {
showMessage('Magic link expired. Please try again.');
}
};

checkStatus();
};

// Handle magic link from URL (when user clicks the link)
const handleMagicLinkFromURL = async () => {
const token = scuteClient.getMagicLinkToken(); // Gets token from current URL

if (token) {
const { error } = await scuteClient.signInWithMagicLinkToken(token);

if (error) {
showErrorMessage('Invalid or expired magic link');
} else {
onAuthenticationSuccess();
}
}
};

sendLoginOtp(identifier, webauthnEnabled?)

Explicitly sends a one-time password (OTP) for authentication. This method is ideal for phone-based authentication or when you need a more immediate verification method.

Key Features:

  • Bypasses WebAuthn/passkey authentication
  • Sends SMS or email-based OTP
  • Works for both existing and new users
  • 6-digit numeric code with time expiration

Use Cases:

  • Phone number authentication
  • Regions where SMS is preferred over email
  • Two-factor authentication flows
  • Users without reliable email access
  • High-security applications

Examples:

// Send OTP to phone number
const { data, error } = await scuteClient.sendLoginOtp('+1234567890');

if (error) {
showErrorMessage('Failed to send verification code');
return;
}

showMessage('Enter the 6-digit code sent to your phone');
showOTPInput(data.id);

// Send OTP to email (if app is configured for email OTP)
const { data, error } = await scuteClient.sendLoginOtp('user@example.com');

OTP Verification:

// Complete OTP verification flow
const handleOTPVerification = async (otp: string, identifier: string) => {
if (!/^\d{6}$/.test(otp)) {
showErrorMessage('Please enter a valid 6-digit code');
return;
}

try {
const { data, error } = await scuteClient.verifyOtp(otp, identifier);

if (error) {
showErrorMessage('Invalid or expired code. Please try again.');
return;
}

// OTP verified - sign in the user
await scuteClient.signInWithTokenPayload(data.authPayload);
onAuthenticationSuccess();

} catch (err) {
showErrorMessage('Verification failed. Please try again.');
}
};

// Resend OTP functionality
const resendOTP = async (identifier: string) => {
const { data, error } = await scuteClient.sendLoginOtp(identifier);

if (!error) {
showMessage('New verification code sent');
return data.id; // New OTP session ID
} else {
showErrorMessage('Failed to resend code');
return null;
}
};

signInWithOAuthProvider(provider)

Initiates OAuth authentication with external providers like Google, GitHub, or other configured OAuth services.

Key Features:

  • Redirects to OAuth provider for authentication
  • Handles federated identity management
  • Supports multiple OAuth providers
  • Automatic account linking for existing users

Use Cases:

  • Social login (Google, Facebook, GitHub, etc.)
  • Enterprise SSO integration
  • Reducing password fatigue
  • Accessing provider-specific APIs
  • Streamlined onboarding for users with existing accounts

Examples:

// Redirect to Google OAuth
await scuteClient.signInWithOAuthProvider('google');

// Redirect to GitHub OAuth
await scuteClient.signInWithOAuthProvider('github');

// Get OAuth URL without redirecting (useful for custom handling)
const googleUrl = scuteClient.getOAuthUrl('google');
window.open(googleUrl, '_blank'); // Open in new tab

// Check available OAuth providers
const { data: appData } = await scuteClient.getAppData();
const providers = appData.oauth_providers || [];

providers.forEach(provider => {
console.log(`Available: ${provider.name} (${provider.provider})`);
});

OAuth Configuration Example:

// Display OAuth providers dynamically
const renderOAuthButtons = (providers: ScuteOAuthProviderConfig[]) => {
return providers.map(provider => (
<button
key={provider.provider}
onClick={() => scuteClient.signInWithOAuthProvider(provider.provider)}
style={{ backgroundColor: provider.color }}
>
<img src={provider.icon} alt={provider.name} />
Continue with {provider.name}
</button>
));
};

// Handle OAuth callback (in your callback page)
const handleOAuthCallback = async () => {
try {
const { error } = await scuteClient.signInWithMagicLink();

if (error) {
showErrorMessage('OAuth authentication failed');
navigate('/login');
} else {
navigate('/dashboard');
}
} catch (err) {
console.error('OAuth callback error:', err);
navigate('/login');
}
};

Token-Based Authentication Methods

signInWithTokenPayload(payload)

Internal method used after successful verification of magic links, OTP, or OAuth to establish the user session. This method completes the authentication flow by setting up the user's session.

Key Features:

  • Establishes authenticated session
  • Triggers authentication state change events
  • Handles user data fetching
  • Sets up session persistence
  • Remembers user identifier for future logins

Use Cases:

  • Completing magic link verification
  • Completing OTP verification
  • Handling OAuth callbacks
  • Custom authentication flows
  • Testing and development scenarios

Examples:

// This method is typically called internally, but can be used for custom flows
const completeAuthentication = async (tokenPayload: ScuteTokenPayload) => {
const { error } = await scuteClient.signInWithTokenPayload(tokenPayload);

if (error) {
showErrorMessage('Authentication failed');
} else {
// User is now signed in
navigate('/dashboard');
}
};

// Common usage patterns (these methods call signInWithTokenPayload internally):

// 1. Magic link verification
const { data, error } = await scuteClient.verifyMagicLinkToken(token);
if (!error) {
await scuteClient.signInWithTokenPayload(data.authPayload);
}

// 2. OTP verification
const { data, error } = await scuteClient.verifyOtp(otp, identifier);
if (!error) {
await scuteClient.signInWithTokenPayload(data.authPayload);
}

// 3. Magic link from URL
const { error } = await scuteClient.signInWithMagicLink(); // Handles token extraction and signInWithTokenPayload

Token Payload Structure:

interface ScuteTokenPayload {
access: string; // JWT access token
access_expires_at: string; // ISO timestamp
refresh?: string | null; // Refresh token (if enabled)
refresh_expires_at?: string | null; // ISO timestamp
}

signOut()

Securely ends the user session and cleans up all authentication state. This method ensures complete logout across all stored data.

Key Features:

  • Revokes refresh token on server
  • Clears local session storage
  • Removes stored credentials
  • Triggers sign-out event for UI updates
  • Works across browser tabs (broadcast channel)

Use Cases:

  • User-initiated logout
  • Session timeout handling
  • Security-sensitive operations
  • Switching between accounts
  • Admin-initiated session termination

Examples:

// Basic sign out
const handleSignOut = async () => {
const success = await scuteClient.signOut();

if (success) {
showMessage('Signed out successfully');
navigate('/login');
} else {
// Still clears local state even if server call fails
showMessage('Signed out (offline)');
navigate('/login');
}
};

// Sign out with confirmation
const handleSignOutWithConfirmation = async () => {
const confirmed = await showConfirmDialog(
'Are you sure you want to sign out?'
);

if (confirmed) {
await handleSignOut();
}
};

// Listen for sign-out events (useful for global state management)
scuteClient.onAuthStateChange((event, session, user) => {
if (event === 'signed_out') {
// Clear application state
clearUserData();
clearLocalCache();
navigate('/login');
}
});

// Automatic sign out on session expiration
scuteClient.onAuthStateChange((event, session, user) => {
if (event === 'session_expired') {
showMessage('Your session has expired. Please sign in again.');
navigate('/login');
}
});

Sign Out Return Value:

// signOut() returns a boolean indicating server-side success
const success = await scuteClient.signOut();

if (success) {
// Refresh token was successfully revoked on server
} else {
// Local state was cleared, but server revocation failed
// This can happen due to network issues or already-expired tokens
// User is still effectively signed out locally
}

WebAuthn/Passkey Methods

addDevice()

Registers a new WebAuthn device/passkey for the currently authenticated user. This enhances security and provides passwordless authentication for future logins.

Key Features:

  • Requires active authentication session
  • Triggers browser's WebAuthn UI
  • Supports various authenticator types (platform, roaming)
  • Stores credential for future authentication
  • Emits registration events for UI feedback

Examples:

// Add device after user signs in
const handleAddPasskey = async () => {
try {
const { data, error } = await scuteClient.addDevice();

if (error) {
if (error.message.includes('cancelled')) {
showMessage('Passkey setup was cancelled');
} else {
showErrorMessage('Failed to set up passkey');
}
return;
}

showMessage('Passkey added successfully! You can now sign in faster.');

} catch (err) {
showErrorMessage('Passkey not supported on this device');
}
};

// Check if WebAuthn is supported before showing the option
const shouldShowPasskeyOption = () => {
return scuteClient.isWebauthnSupported();
};

// Listen for passkey registration events
scuteClient.onAuthStateChange((event, session, user) => {
switch (event) {
case 'webauthn_register_start':
showMessage('Setting up your passkey...');
break;
case 'webauthn_register_success':
showMessage('Passkey set up successfully!');
break;
}
});

isWebauthnSupported()

Checks if WebAuthn is supported on the current device and browser.

const isSupported = scuteClient.isWebauthnSupported();

if (isSupported) {
// Show passkey options in UI
} else {
// Hide passkey features
}

isAnyDeviceRegistered()

Checks if the current user has any registered WebAuthn devices on this browser/device.

// Requires authentication
try {
const hasDevices = await scuteClient.isAnyDeviceRegistered();

if (hasDevices) {
showMessage('You can sign in with your passkey');
} else {
showPasskeySetupPrompt();
}
} catch (error) {
// User not authenticated
}

Authentication State Management

onAuthStateChange(callback)

Listen to authentication state changes to keep your UI synchronized with the user's authentication status. This is the primary method for handling authentication events in your application.

Authentication Events:

  • signed_in - User successfully authenticated
  • signed_out - User signed out or session ended
  • initial_session - Initial session check completed
  • session_refetch - Session data refreshed
  • session_expired - Session expired and needs renewal
  • token_refreshed - Access token automatically refreshed
  • magic_pending - Magic link or OTP sent, waiting for verification
  • magic_new_device_pending - Magic link sent due to new device detection
  • otp_pending - OTP sent, waiting for verification
  • otp_new_device_pending - OTP sent due to new device detection
  • webauthn_register_start - WebAuthn registration started
  • webauthn_register_success - WebAuthn registration completed
  • webauthn_verify_start - WebAuthn verification started
  • webauthn_verify_success - WebAuthn verification completed

Examples:

// Basic authentication state handling
const unsubscribe = scuteClient.onAuthStateChange((event, session, user) => {
console.log('Auth event:', event);

switch (event) {
case 'signed_in':
console.log('User signed in:', user?.email);
navigate('/dashboard');
break;

case 'signed_out':
console.log('User signed out');
navigate('/login');
break;

case 'session_expired':
showMessage('Your session has expired. Please sign in again.');
navigate('/login');
break;

case 'magic_pending':
showMessage('Check your email for a sign-in link');
break;

case 'otp_pending':
showOTPInput();
break;

case 'webauthn_register_start':
showMessage('Setting up your passkey...');
break;
}
});

// Clean up listener when component unmounts
return () => unsubscribe();

Advanced State Management:

// React hook for authentication state
const useAuth = () => {
const [session, setSession] = useState(null);
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
const unsubscribe = scuteClient.onAuthStateChange((event, session, user) => {
setSession(session);
setUser(user);

if (event === 'initial_session') {
setLoading(false);
}
});

return unsubscribe;
}, []);

return { session, user, loading };
};

// Global state management (Redux/Zustand example)
const authSlice = createSlice({
name: 'auth',
initialState: {
user: null,
session: null,
loading: true
},
reducers: {
setAuth: (state, action) => {
state.user = action.payload.user;
state.session = action.payload.session;
state.loading = false;
},
clearAuth: (state) => {
state.user = null;
state.session = null;
state.loading = false;
}
}
});

// Set up listener
scuteClient.onAuthStateChange((event, session, user) => {
if (event === 'signed_in' || event === 'initial_session') {
store.dispatch(authSlice.actions.setAuth({ user, session }));
} else if (event === 'signed_out') {
store.dispatch(authSlice.actions.clearAuth());
}
});

getUser(accessToken?)

Get the current authenticated user's data or verify a specific access token.

Examples:

// Get current user (uses stored session)
const { data, error } = await scuteClient.getUser();

if (error) {
console.log('User not authenticated');
} else {
console.log('Current user:', data.user);
}

// Verify specific access token
const { data, error } = await scuteClient.getUser(accessToken);

getAuthToken()

Get the current access token from the stored session.

const { data, error } = await scuteClient.getAuthToken();

if (!error) {
console.log('Access token:', data.access);
console.log('Expires at:', data.access_expires_at);
}

Utility Methods

identifierExists(identifier)

Check if a user exists with the given email or phone number without initiating authentication.

try {
const user = await scuteClient.identifierExists('user@example.com');

if (user) {
console.log('User exists:', user.email);
console.log('Has passkey:', user.webauthn_enabled);
} else {
console.log('No user found with this identifier');
}
} catch (error) {
console.error('Failed to check identifier:', error);
}

getAppData(fresh?)

Get application configuration and settings to customize the authentication flow.

const { data, error } = await scuteClient.getAppData();

if (!error) {
console.log('App name:', data.name);
console.log('Email auth type:', data.email_auth_type); // 'magic' or 'otp'
console.log('OAuth providers:', data.oauth_providers);
console.log('Required identifiers:', data.required_identifiers);
console.log('User metadata schema:', data.user_meta_data_schema);
}

// Force fresh data from server
const { data } = await scuteClient.getAppData(true);

Session Management

listUserSessions()

Get all active sessions for the current user across all devices.

const { data, error } = await scuteClient.listUserSessions();

if (!error) {
data.sessions.forEach(session => {
console.log('Session:', session.display_name);
console.log('Type:', session.type); // 'webauthn', 'magic', 'oauth', etc.
console.log('Last used:', session.last_used_at);
console.log('Platform:', session.platform);
console.log('Browser:', session.browser);
});
}

revokeSession(sessionId, credentialId?)

Revoke a specific session from another device.

// Revoke a specific session
await scuteClient.revokeSession('session-id');

// Revoke session and associated WebAuthn credential
await scuteClient.revokeSession('session-id', 'credential-id');

removeDeviceCredential(credentialId)

Remove a WebAuthn credential without affecting the session.

await scuteClient.removeDeviceCredential('credential-id');

Error Handling

Scute provides specific error types for different authentication scenarios. For comprehensive error handling documentation, see the Error Handling Guide.

// Basic error handling
const { data, error } = await scuteClient.signInOrUp(identifier);

if (error) {
console.error('Authentication error:', error.message);
// Handle error based on type
return;
}

// Continue with success flow

Best Practices

1. Start with signInOrUp for Maximum Flexibility

// Recommended: Universal authentication
const handleAuth = async (identifier: string) => {
const { data, error } = await scuteClient.signInOrUp(identifier);

if (error) {
handleAuthError(error);
return;
}

// Handle success...
};

2. Implement Progressive Enhancement

// Check capabilities and adjust UI accordingly
const AuthComponent = () => {
const [webauthnSupported] = useState(scuteClient.isWebauthnSupported());
const [appData, setAppData] = useState(null);

useEffect(() => {
scuteClient.getAppData().then(({ data }) => setAppData(data));
}, []);

return (
<div>
{/* Always show primary authentication */}
<button onClick={() => handleAuth(identifier)}>
Continue with {identifier.includes('@') ? 'Email' : 'Phone'}
</button>

{/* Show OAuth if configured */}
{appData?.oauth_providers?.map(provider => (
<button
key={provider.provider}
onClick={() => scuteClient.signInWithOAuthProvider(provider.provider)}
>
Continue with {provider.name}
</button>
))}

{/* Show passkey option if supported */}
{webauthnSupported && (
<button onClick={handlePasskeyAuth}>
Sign in with Passkey
</button>
)}
</div>
);
};

3. Handle Network Connectivity

const handleAuthWithRetry = async (identifier: string, maxRetries = 3) => {
let attempts = 0;

while (attempts < maxRetries) {
try {
const result = await scuteClient.signInOrUp(identifier);
return result;
} catch (error) {
attempts++;

if (error.message.includes('network') && attempts < maxRetries) {
await new Promise(resolve => setTimeout(resolve, 1000 * attempts));
continue;
}

throw error;
}
}
};

4. Implement Proper Loading States

const AuthForm = () => {
const [loading, setLoading] = useState(false);
const [step, setStep] = useState('input'); // 'input', 'verification', 'success'

const handleSubmit = async (identifier: string) => {
setLoading(true);

try {
const { data, error } = await scuteClient.signInOrUp(identifier);

if (error) {
handleAuthError(error);
return;
}

if (!data) {
// WebAuthn success
setStep('success');
} else {
// Need verification
setStep('verification');
}
} finally {
setLoading(false);
}
};

return (
<div>
{step === 'input' && (
<form onSubmit={handleSubmit}>
<input type="email" placeholder="Enter your email" />
<button type="submit" disabled={loading}>
{loading ? 'Please wait...' : 'Continue'}
</button>
</form>
)}

{step === 'verification' && (
<VerificationStep />
)}

{step === 'success' && (
<div>Welcome back!</div>
)}
</div>
);
};

5. Security Considerations

  • Always use HTTPS in production to protect tokens and user data
  • Validate user input before passing to authentication methods
  • Implement session timeout handling using auth state events
  • Use appropriate WebAuthn settings based on your security requirements

6. Testing Authentication Flows

// Example: Test helper for authentication flows
const createTestAuthClient = (config = {}) => {
return createClient({
appId: 'test-app-id',
baseUrl: 'http://localhost:3000',
debug: true,
...config
});
};

// Example: Mock authentication for testing
const mockSuccessfulAuth = () => {
jest.spyOn(scuteClient, 'signInOrUp').mockResolvedValue({
data: null,
error: null
});
};

Complete Implementation Example

Here's a comprehensive example showing how to implement a full authentication flow with all the best practices:

import { useState, useEffect } from 'react';
import { createClient, type ScuteError } from '@scute/auth';

const scuteClient = createClient({
appId: 'your-app-id',
baseUrl: 'https://api.scute.io'
});

export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
const unsubscribe = scuteClient.onAuthStateChange((event, session, user) => {
setUser(user);

if (event === 'initial_session') {
setLoading(false);
}
});

return unsubscribe;
}, []);

return (
<AuthContext.Provider value={{ user, loading, scuteClient }}>
{children}
</AuthContext.Provider>
);
};

export const LoginForm = () => {
const [identifier, setIdentifier] = useState('');
const [step, setStep] = useState('input');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');

const handleAuth = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');

const validationError = validateIdentifier(identifier);
if (validationError) {
setError(validationError);
setLoading(false);
return;
}

try {
const { data, error } = await scuteClient.signInOrUp(identifier);

if (error) {
setError(getErrorMessage(error));
} else if (!data) {
// WebAuthn success - user is signed in
setStep('success');
} else {
// Show verification step
setStep('verification');
}
} catch (err) {
setError('An unexpected error occurred');
} finally {
setLoading(false);
}
};

return (
<div className="auth-form">
{step === 'input' && (
<form onSubmit={handleAuth}>
<h2>Welcome</h2>
<input
type="text"
placeholder="Email or phone number"
value={identifier}
onChange={(e) => setIdentifier(e.target.value)}
disabled={loading}
/>
{error && <div className="error">{error}</div>}
<button type="submit" disabled={loading || !identifier}>
{loading ? 'Please wait...' : 'Continue'}
</button>
</form>
)}

{step === 'verification' && (
<VerificationStep identifier={identifier} />
)}

{step === 'success' && (
<div>Welcome! Redirecting...</div>
)}
</div>
);
};

This comprehensive guide covers all the authentication methods available in Scute, providing you with the knowledge and examples needed to implement secure, user-friendly authentication in your application.