supaguardsupaguardDocs
Use cases

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

ScenarioImpact
Login completely brokenNo users can access the product
OAuth provider downUsers of that method locked out
Slow authenticationFrustrated users, support tickets
Session issuesUsers 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 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:

  1. Use a test email service (like Mailosaur) to receive the email
  2. Extract the link programmatically
  3. 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

Check TypeFrequency
Email/password loginEvery 2-5 minutes
OAuth (button clickable)Every 5 minutes
Full OAuth flowEvery 15-30 minutes
Password reset requestEvery 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

On this page