Authentication
Authentication
This example covers authentication patterns including login forms, session handling, OAuth flows, and maintaining authenticated state across steps.
Basic Login Flow
Simple username/password authentication:
import { Stepwright } from '@korvol/stepwright';
interface AuthData { email: string; password: string; isLoggedIn: boolean; userName: string;}
const script = Stepwright.create<AuthData>('Basic Login') .config({ headless: true, screenshotOnFailure: true, defaultTimeout: 30000, }) .data({ email: process.env.TEST_USER_EMAIL || 'test@example.com', password: process.env.TEST_USER_PASSWORD || 'testpass123', isLoggedIn: false, userName: '', })
.checkpoint('Login')
.step('Navigate to login page', async (ctx) => { await ctx.page.goto('https://example.com/login'); await ctx.page.waitForSelector('form#login-form'); ctx.log('Login page loaded'); })
.step('Enter email', async (ctx) => { await ctx.page.fill('#email', ctx.data.email); ctx.log('Email entered'); })
.step('Enter password', async (ctx) => { await ctx.page.fill('#password', ctx.data.password); // Don't log actual password ctx.log('Password entered'); })
.step('Submit login form', async (ctx) => { await ctx.page.click('#login-button');
// Wait for redirect to dashboard or error message await Promise.race([ ctx.page.waitForURL('**/dashboard**'), ctx.page.waitForSelector('.error-message'), ]);
// Check if login succeeded const currentUrl = ctx.page.url(); if (currentUrl.includes('/dashboard')) { ctx.data.isLoggedIn = true; ctx.log('Login successful'); } else { const errorMsg = await ctx.page.textContent('.error-message'); throw new Error(`Login failed: ${errorMsg}`); } })
.endCheckpoint()
.checkpoint('Verify Session')
.step('Verify user is logged in', async (ctx) => { // Check for logged-in indicators await ctx.page.waitForSelector('.user-menu');
const userName = await ctx.page.textContent('.user-name'); ctx.data.userName = userName || '';
ctx.log('Logged in as:', ctx.data.userName); await ctx.screenshot('logged-in-dashboard'); })
.step('Access protected page', async (ctx) => { await ctx.page.goto('https://example.com/profile');
// Verify we're not redirected to login const currentUrl = ctx.page.url(); if (currentUrl.includes('/login')) { throw new Error('Session not maintained - redirected to login'); }
ctx.log('Successfully accessed protected page'); })
.endCheckpoint();
const result = await script.run();
if (result.success) { console.log('User:', result.data.userName); console.log('Logged in:', result.data.isLoggedIn);}Login with “Remember Me”
Handle persistent sessions:
import { Stepwright } from '@korvol/stepwright';
interface RememberMeData { email: string; password: string; rememberMe: boolean;}
const script = Stepwright.create<RememberMeData>('Remember Me Login') .config({ headless: true, }) .data({ email: process.env.TEST_USER_EMAIL!, password: process.env.TEST_USER_PASSWORD!, rememberMe: true, })
.step('Navigate and login', async (ctx) => { await ctx.page.goto('https://example.com/login');
await ctx.page.fill('#email', ctx.data.email); await ctx.page.fill('#password', ctx.data.password);
// Check "Remember Me" checkbox if (ctx.data.rememberMe) { await ctx.page.check('#remember-me'); ctx.log('Remember me checked'); }
await ctx.page.click('#login-button'); await ctx.page.waitForURL('**/dashboard**'); })
.step('Verify persistent cookie', async (ctx) => { const context = ctx.page.context(); const cookies = await context.cookies();
// Find session cookie const sessionCookie = cookies.find((c) => c.name === 'session');
if (sessionCookie) { ctx.log('Session cookie expires:', new Date(sessionCookie.expires * 1000));
// Verify it's a persistent cookie (not session cookie) if (sessionCookie.expires === -1) { throw new Error('Expected persistent cookie, got session cookie'); } } });OAuth/SSO Login
Handle OAuth redirect flows:
import { Stepwright } from '@korvol/stepwright';
interface OAuthData { provider: 'google' | 'github' | 'microsoft'; oauthEmail: string; oauthPassword: string;}
const script = Stepwright.create<OAuthData>('OAuth Login') .config({ headless: true, defaultTimeout: 60000, // OAuth can be slow }) .data({ provider: 'google', oauthEmail: process.env.OAUTH_EMAIL!, oauthPassword: process.env.OAUTH_PASSWORD!, })
.checkpoint('Initiate OAuth')
.step('Navigate to login', async (ctx) => { await ctx.page.goto('https://example.com/login'); })
.step('Click OAuth provider button', async (ctx) => { // Click the appropriate OAuth button await ctx.page.click(`button.oauth-${ctx.data.provider}`);
// Wait for redirect to OAuth provider await ctx.page.waitForURL(/accounts\.google\.com|github\.com|login\.microsoftonline\.com/);
ctx.log('Redirected to OAuth provider'); })
.endCheckpoint()
.checkpoint('OAuth Provider Login')
.step('Enter OAuth credentials', async (ctx) => { // Handle Google OAuth (example) if (ctx.data.provider === 'google') { // Enter email await ctx.page.fill('input[type="email"]', ctx.data.oauthEmail); await ctx.page.click('#identifierNext');
// Wait for password screen await ctx.page.waitForSelector('input[type="password"]', { state: 'visible' });
// Enter password await ctx.page.fill('input[type="password"]', ctx.data.oauthPassword); await ctx.page.click('#passwordNext'); }
ctx.log('OAuth credentials entered'); })
.step('Handle consent screen', async (ctx) => { // Wait for consent or redirect back try { // Check if consent screen appears const consentButton = ctx.page.locator('button:has-text("Allow")'); if (await consentButton.isVisible({ timeout: 5000 })) { await consentButton.click(); ctx.log('Consent granted'); } } catch { // No consent screen, already authorized ctx.log('No consent needed'); } })
.endCheckpoint()
.checkpoint('Verify Login')
.step('Wait for redirect back to app', async (ctx) => { // Wait to be redirected back to our app await ctx.page.waitForURL('**/example.com/**');
ctx.log('Redirected back to app:', ctx.page.url()); })
.step('Verify logged in', async (ctx) => { await ctx.page.waitForSelector('.user-menu'); ctx.log('OAuth login successful'); await ctx.screenshot('oauth-logged-in'); })
.endCheckpoint();Two-Factor Authentication
Handle 2FA flows:
import { Stepwright } from '@korvol/stepwright';
interface TwoFactorData { email: string; password: string; totpSecret: string;}
// Helper to generate TOTP code (requires a TOTP library)function generateTOTP(secret: string): string { // In real implementation, use a library like 'otplib' // const { authenticator } = require('otplib'); // return authenticator.generate(secret); return '123456'; // Placeholder}
const script = Stepwright.create<TwoFactorData>('Two-Factor Authentication') .config({ headless: true, }) .data({ email: process.env.TEST_USER_EMAIL!, password: process.env.TEST_USER_PASSWORD!, totpSecret: process.env.TOTP_SECRET!, })
.checkpoint('Primary Authentication')
.step('Login with credentials', async (ctx) => { await ctx.page.goto('https://example.com/login');
await ctx.page.fill('#email', ctx.data.email); await ctx.page.fill('#password', ctx.data.password); await ctx.page.click('#login-button');
// Wait for 2FA prompt await ctx.page.waitForSelector('#totp-input'); ctx.log('2FA required'); })
.endCheckpoint()
.checkpoint('Second Factor')
.step('Enter TOTP code', async (ctx) => { // Generate current TOTP code const totpCode = generateTOTP(ctx.data.totpSecret);
await ctx.page.fill('#totp-input', totpCode); await ctx.page.click('#verify-totp');
ctx.log('TOTP code submitted'); })
.step('Handle backup code fallback', async (ctx) => { // If TOTP fails, try backup code const errorVisible = await ctx.page.locator('.totp-error').isVisible();
if (errorVisible) { ctx.log('TOTP failed, trying backup code');
await ctx.page.click('.use-backup-code'); await ctx.page.waitForSelector('#backup-code-input');
// Enter backup code const backupCode = process.env.BACKUP_CODE!; await ctx.page.fill('#backup-code-input', backupCode); await ctx.page.click('#verify-backup'); } })
.step('Verify 2FA success', async (ctx) => { await ctx.page.waitForURL('**/dashboard**'); ctx.log('2FA authentication successful'); })
.endCheckpoint();Session Persistence Across Steps
Maintain authentication throughout script:
import { Stepwright } from '@korvol/stepwright';
interface SessionData { userId: string; sessionToken: string;}
const script = Stepwright.create<SessionData>('Authenticated Session') .config({ headless: true, }) .data({ userId: '', sessionToken: '', })
.checkpoint('Login')
.step('Perform login', async (ctx) => { await ctx.page.goto('https://example.com/login'); await ctx.page.fill('#email', 'user@example.com'); await ctx.page.fill('#password', 'password123'); await ctx.page.click('#login'); await ctx.page.waitForURL('**/dashboard**');
// Store session info const cookies = await ctx.page.context().cookies(); const sessionCookie = cookies.find((c) => c.name === 'session'); ctx.data.sessionToken = sessionCookie?.value || '';
ctx.log('Logged in, session stored'); })
.endCheckpoint()
.checkpoint('Protected Actions')
.step('Access profile', async (ctx) => { await ctx.page.goto('https://example.com/profile');
// Session is automatically maintained const userId = await ctx.page.locator('#user-id').textContent(); ctx.data.userId = userId || '';
ctx.log('Accessed profile, user ID:', ctx.data.userId); })
.step('Update settings', async (ctx) => { await ctx.page.goto('https://example.com/settings'); await ctx.page.click('#toggle-notifications'); await ctx.page.click('#save-settings');
await ctx.page.waitForSelector('.settings-saved'); ctx.log('Settings updated while authenticated'); })
.step('Access billing', async (ctx) => { await ctx.page.goto('https://example.com/billing'); await ctx.page.waitForSelector('.billing-info');
ctx.log('Accessed billing page'); })
.endCheckpoint()
.checkpoint('Logout')
.step('Perform logout', async (ctx) => { await ctx.page.click('.user-menu'); await ctx.page.click('#logout');
await ctx.page.waitForURL('**/login**'); ctx.log('Logged out successfully'); })
.step('Verify session ended', async (ctx) => { // Try to access protected page await ctx.page.goto('https://example.com/profile');
// Should redirect to login if (!ctx.page.url().includes('/login')) { throw new Error('Session not properly ended'); }
ctx.log('Session properly terminated'); })
.endCheckpoint();Saving and Restoring Session State
Save cookies for reuse:
import { Stepwright } from '@korvol/stepwright';import fs from 'fs';
const SESSION_FILE = './session-state.json';
// Script to save session after loginconst saveSessionScript = Stepwright.create('Save Session') .step('Login', async (ctx) => { await ctx.page.goto('https://example.com/login'); await ctx.page.fill('#email', process.env.EMAIL!); await ctx.page.fill('#password', process.env.PASSWORD!); await ctx.page.click('#login'); await ctx.page.waitForURL('**/dashboard**'); })
.step('Save session state', async (ctx) => { const context = ctx.page.context();
// Get all storage const cookies = await context.cookies(); const localStorage = await ctx.page.evaluate(() => JSON.stringify(window.localStorage) ); const sessionStorage = await ctx.page.evaluate(() => JSON.stringify(window.sessionStorage) );
// Save to file const state = { cookies, localStorage, sessionStorage }; fs.writeFileSync(SESSION_FILE, JSON.stringify(state, null, 2));
ctx.log('Session saved to', SESSION_FILE); });
// Script to restore sessionconst restoreSessionScript = Stepwright.create('Restore Session') .step('Restore session state', async (ctx) => { const context = ctx.page.context();
// Load from file const state = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf-8'));
// Restore cookies await context.addCookies(state.cookies);
// Navigate to app await ctx.page.goto('https://example.com');
// Restore storage if (state.localStorage) { await ctx.page.evaluate((data) => { const items = JSON.parse(data); for (const [key, value] of Object.entries(items)) { window.localStorage.setItem(key, value as string); } }, state.localStorage); }
ctx.log('Session restored'); })
.step('Verify session valid', async (ctx) => { await ctx.page.goto('https://example.com/dashboard');
// Check if we're still logged in if (ctx.page.url().includes('/login')) { throw new Error('Restored session is invalid'); }
ctx.log('Session valid, user is logged in'); });Handling Session Expiry
Detect and handle expired sessions:
import { Stepwright } from '@korvol/stepwright';
const script = Stepwright.create('Session Expiry Handling') .config({ headless: true, })
.step('Perform long-running task', async (ctx) => { // Function to check if session is still valid const checkSession = async (): Promise<boolean> => { const response = await ctx.page.request.get('/api/session/check'); return response.status() === 200; };
// Function to re-authenticate const reAuthenticate = async () => { await ctx.page.goto('https://example.com/login'); await ctx.page.fill('#email', process.env.EMAIL!); await ctx.page.fill('#password', process.env.PASSWORD!); await ctx.page.click('#login'); await ctx.page.waitForURL('**/dashboard**'); ctx.log('Re-authenticated'); };
// Check session before important action if (!(await checkSession())) { ctx.log('Session expired, re-authenticating...'); await reAuthenticate(); }
// Perform the action await ctx.page.goto('https://example.com/important-action'); await ctx.page.click('#submit'); })
.step('Handle 401 responses', async (ctx) => { // Set up response handler ctx.page.on('response', async (response) => { if (response.status() === 401) { ctx.log('Received 401, session may be expired'); // Could trigger re-authentication here } });
// Continue with actions await ctx.page.goto('https://example.com/protected'); });Running the Examples
TEST_USER_EMAIL=test@example.com \TEST_USER_PASSWORD=secret123 \npx tsx scripts/basic-login.tsTEST_USER_EMAIL=test@example.comTEST_USER_PASSWORD=secret123
# Then runnpx tsx scripts/basic-login.tsSecurity Best Practices
- Never hardcode credentials - Use environment variables
- Never log passwords - Only log that password was entered
- Mask sensitive data in screenshots - Clear fields before capturing
- Use test accounts - Don’t use production credentials
- Rotate test credentials - Change passwords periodically
Next Steps
- Data Extraction - Extract data from authenticated pages
- Multi-Page Workflow - Complex authenticated flows
- Best Practices - Security recommendations