Monitor SaaS Login Flows: Protecting the Gateway to Your App
Authentication is the high-stakes foundation of any SaaS product. Learn to monitor email/password, OAuth (Google/GitHub), and MFA to ensure every user can access your platform 24/7.
If a user cannot log in, they cannot experience your product. Authentication is not just foundational; it is also one of the most fragile points in a modern application. Between Identity Providers (IdP), token sessions, and multi-factor authentication (MFA), a single misconfiguration can lock out your entire user base.
This guide provides a production-hardened blueprint for monitoring your Login Flow with supaguard's AI-native Playwright checks.
Why Monitor Login?
Authentication Failure Points
Login flows involve multiple systems that can fail:
- Identity provider - Auth0, Firebase, Cognito, or your own system
- Database - User lookup queries
- Session management - Cookie/token generation
- OAuth providers - Google, GitHub, Microsoft
- Email delivery - Magic links, verification emails
- MFA services - SMS, authenticator apps
The Impact of Login Failures
| Scenario | Impact |
|---|---|
| Login completely broken | No users can access the product |
| OAuth provider down | Users of that method locked out |
| Slow authentication | Frustrated users, support tickets |
| Session issues | Users randomly logged out |
Basic Email/Password Login Test
Start with the fundamental login flow:
import { test, expect } from "@playwright/test";
test("user can log in with email and password", async ({ page }) => {
await page.goto("https://app.example.com/login");
// Fill credentials
await page.getByLabel("Email").fill("synthetic-test@example.com");
await page.getByLabel("Password").fill("TestPassword123!");
// Submit
await page.getByRole("button", { name: "Sign In" }).click();
// Verify successful login
await expect(page).toHaveURL(/.*dashboard/);
await expect(page.getByText("Welcome back")).toBeVisible();
});OAuth Login Testing
Google OAuth
OAuth flows open a new window or redirect. Handle the flow:
test("user can log in with Google", async ({ page }) => {
await page.goto("https://app.example.com/login");
// Click Google sign-in button
const [popup] = await Promise.all([
page.waitForEvent("popup"),
page.getByRole("button", { name: "Continue with Google" }).click(),
]);
// Handle Google OAuth in popup
await popup.waitForLoadState();
await popup.getByLabel("Email or phone").fill("test@example.com");
await popup.getByRole("button", { name: "Next" }).click();
await popup.getByLabel("Password").fill("googlepassword");
await popup.getByRole("button", { name: "Next" }).click();
// Wait for redirect back to your app
await page.waitForURL("**/dashboard**");
await expect(page.getByText("Welcome")).toBeVisible();
});Important: Google OAuth testing in automated environments is tricky. Consider:
- Using Google's test accounts for automated testing
- Testing the "Continue with Google" button appears and is clickable
- Using a service account for API-level testing
GitHub OAuth
test("user can log in with GitHub", async ({ page }) => {
await page.goto("https://app.example.com/login");
const [popup] = await Promise.all([
page.waitForEvent("popup"),
page.getByRole("button", { name: "Continue with GitHub" }).click(),
]);
await popup.getByLabel("Username or email").fill("testuser");
await popup.getByLabel("Password").fill("githubpassword");
await popup.getByRole("button", { name: "Sign in" }).click();
// Authorize app if needed
if (await popup.getByRole("button", { name: "Authorize" }).isVisible()) {
await popup.getByRole("button", { name: "Authorize" }).click();
}
await page.waitForURL("**/dashboard**");
});Magic Link Login
Magic links require email access. Test the flow up to the point of email:
test("magic link request works", async ({ page }) => {
await page.goto("https://app.example.com/login");
// Request magic link
await page.getByRole("link", { name: "Sign in with email link" }).click();
await page.getByLabel("Email").fill("synthetic-test@example.com");
await page.getByRole("button", { name: "Send magic link" }).click();
// Verify confirmation
await expect(page.getByText("Check your email")).toBeVisible();
await expect(page.getByText("synthetic-test@example.com")).toBeVisible();
});For full magic link testing, you can:
- Use a test email service (like Mailosaur) to receive the email
- Extract the link programmatically
- Navigate to complete the flow
// Using Mailosaur for full magic link testing
import { MailosaurClient } from "mailosaur";
test("complete magic link flow", async ({ page }) => {
const mailosaur = new MailosaurClient("your-api-key");
const serverId = "your-server-id";
const testEmail = `test.${Date.now()}@${serverId}.mailosaur.net`;
// Request magic link
await page.goto("https://app.example.com/login");
await page.getByRole("link", { name: "Sign in with email link" }).click();
await page.getByLabel("Email").fill(testEmail);
await page.getByRole("button", { name: "Send magic link" }).click();
// Get email from Mailosaur
const email = await mailosaur.messages.get(serverId, {
sentTo: testEmail,
});
// Extract magic link from email
const magicLink = email.html.links.find((l) =>
l.href.includes("/auth/magic")
);
// Complete login
await page.goto(magicLink.href);
await expect(page).toHaveURL(/.*dashboard/);
});Multi-Factor Authentication (MFA)
TOTP (Authenticator App)
For TOTP testing, generate codes programmatically:
import { authenticator } from "otplib";
test("login with MFA", async ({ page }) => {
// The secret should be stored securely for your test account
const mfaSecret = "JBSWY3DPEHPK3PXP";
await page.goto("https://app.example.com/login");
await page.getByLabel("Email").fill("mfa-user@example.com");
await page.getByLabel("Password").fill("Password123!");
await page.getByRole("button", { name: "Sign In" }).click();
// MFA screen appears
await expect(page.getByText("Enter verification code")).toBeVisible();
// Generate TOTP code
const totpCode = authenticator.generate(mfaSecret);
await page.getByLabel("Verification code").fill(totpCode);
await page.getByRole("button", { name: "Verify" }).click();
await expect(page).toHaveURL(/.*dashboard/);
});SMS MFA
SMS MFA is harder to automate. Options:
- Test that the SMS request is sent (verify "Code sent" message)
- Use a virtual phone number service
- Maintain a test account without SMS MFA for automated testing
Session Persistence Test
Verify that sessions persist correctly:
test("session persists after page reload", async ({ page }) => {
// Log in
await page.goto("https://app.example.com/login");
await page.getByLabel("Email").fill("test@example.com");
await page.getByLabel("Password").fill("password");
await page.getByRole("button", { name: "Sign In" }).click();
await expect(page).toHaveURL(/.*dashboard/);
// Reload page
await page.reload();
// Should still be logged in
await expect(page.getByText("Welcome")).toBeVisible();
await expect(page).not.toHaveURL(/.*login/);
});Testing Error States
Don't just test the happy path. Verify error handling:
test("shows error for invalid credentials", async ({ page }) => {
await page.goto("https://app.example.com/login");
await page.getByLabel("Email").fill("wrong@example.com");
await page.getByLabel("Password").fill("wrongpassword");
await page.getByRole("button", { name: "Sign In" }).click();
// Should show error, not crash
await expect(page.getByText(/invalid|incorrect|wrong/i)).toBeVisible();
await expect(page).toHaveURL(/.*login/); // Still on login page
});Test Account Management
Creating a Synthetic Test Account
Your test account should:
- Have a recognizable email:
synthetic-monitor@yourcompany.com - Be excluded from marketing and analytics
- Have a strong, randomly-generated password
- Be flagged as a test account in your database
-- Example: Flag account as synthetic
UPDATE users
SET is_synthetic_test = true, exclude_from_analytics = true
WHERE email = 'synthetic-monitor@yourcompany.com';Storing Credentials Securely
Never hardcode credentials in test files. Use environment variables:
test("login flow", async ({ page }) => {
await page.goto("https://app.example.com/login");
await page.getByLabel("Email").fill(process.env.TEST_USER_EMAIL!);
await page.getByLabel("Password").fill(process.env.TEST_USER_PASSWORD!);
// ...
});In supaguard, configure these as check variables.
Monitoring Configuration
Recommended Frequencies
| Check Type | Frequency |
|---|---|
| Email/password login | Every 2-5 minutes |
| OAuth (button clickable) | Every 5 minutes |
| Full OAuth flow | Every 15-30 minutes |
| Password reset request | Every 15 minutes |
Multi-Region Testing
Run login tests from multiple regions to catch:
- Auth provider regional issues
- Latency problems
- Geo-blocking misconfigurations
Alert Configuration
Login failures should trigger immediate alerts:
- Route to PagerDuty for 24/7 response
- Also notify Slack for team visibility
Common Issues
CAPTCHA Blocking
If your login has CAPTCHA:
- Whitelist your test account from CAPTCHA
- Use CAPTCHA provider's testing mode
- Whitelist supaguard's IP ranges
Rate Limiting
Prevent lockouts:
- Whitelist test account from rate limits
- Reduce test frequency if needed
- Use multiple test accounts if necessary
Session Conflicts
If synthetic tests conflict with real usage:
- Use a dedicated test account
- Clear sessions before each test
Related Resources
- E-Commerce Checkout Monitoring — Monitor purchase flows
- Multi-Step Form Monitoring — Complex form flows
- Playwright Tips — Best practices
Multi-Step Form Monitoring: Test Complex Form Flows
Monitor multi-step forms, wizards, and onboarding flows with Playwright. Catch validation errors, step navigation issues, and submission failures before users encounter them.
Synthetic Monitoring for Startups: A Practical Playbook
Learn how startups can use synthetic monitoring to catch incidents early, protect activation and retention, and build reliability without an enterprise SRE budget.