Skip to content

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:

scripts/basic-login.ts
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:

scripts/remember-me-login.ts
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:

scripts/oauth-login.ts
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:

scripts/two-factor-auth.ts
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:

scripts/authenticated-session.ts
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:

scripts/save-session.ts
import { Stepwright } from '@korvol/stepwright';
import fs from 'fs';
const SESSION_FILE = './session-state.json';
// Script to save session after login
const 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 session
const 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:

scripts/session-expiry.ts
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

Terminal window
TEST_USER_EMAIL=test@example.com \
TEST_USER_PASSWORD=secret123 \
npx tsx scripts/basic-login.ts

Security Best Practices

  1. Never hardcode credentials - Use environment variables
  2. Never log passwords - Only log that password was entered
  3. Mask sensitive data in screenshots - Clear fields before capturing
  4. Use test accounts - Don’t use production credentials
  5. Rotate test credentials - Change passwords periodically

Next Steps