Enterprise SSO Testing Guide
How to Test SAML SSO with Playwright: Okta Integration Guide 2026
A scenario-by-scenario walkthrough of testing SAML SSO flows with Playwright and Okta. SP-initiated login, IdP-initiated login, SAMLResponse POST binding, RelayState preservation, certificate rotation, and the pitfalls that silently break enterprise SSO test suites in CI.
“Okta secures over 18,800 customers with identity management across enterprise and workforce applications, processing billions of authentications annually.”
Okta FY2025 10-K Filing
SAML SSO SP-Initiated Login Flow
1. Why Testing SAML SSO Is Harder Than It Looks
SAML (Security Assertion Markup Language) SSO is the dominant enterprise authentication protocol. When a user clicks “Sign in with SSO” in your application, the browser leaves your domain entirely, navigates to the identity provider (in this case Okta), authenticates there, and then returns to your application via an HTTP POST containing a signed XML document called the SAMLResponse. That cross-domain redirect chain is the first structural challenge for Playwright tests.
The complexity compounds in several ways. First, the SAMLRequest is typically sent via HTTP Redirect binding (a 302 with the request as a query parameter), but the SAMLResponse comes back via HTTP POST binding, meaning the IdP submits a hidden HTML form back to your SP's ACS (Assertion Consumer Service) URL. This POST submission is a browser navigation that Playwright must handle correctly. Second, the RelayState parameter preserves the user's original destination URL through the redirect chain. If your SP does not echo the RelayState correctly, the user lands on the wrong page after authentication. Third, the Okta Sign-In Widget is a JavaScript SPA that renders its own input fields with specific class names and data attributes that differ across Okta org configurations.
There are six structural reasons this flow resists reliable automation. First, the IdP redirect chain involves three or more domain transitions (your app, Okta, and sometimes a custom Okta domain), invalidating locators from previous domains. Second, the SAMLResponse POST binding creates a form submission that looks like a page navigation but is actually a cross-origin POST. Third, Okta's sign-in widget loads asynchronously and can present MFA challenges, password expiration dialogs, or consent screens depending on org policy. Fourth, SAML certificate rotation can silently break signature validation if your SP caches the old certificate. Fifth, SP-initiated and IdP-initiated flows have entirely different entry points and slightly different SAMLResponse structures. Sixth, SAML XML namespaces and canonicalization make response validation extremely sensitive to whitespace and encoding.
SAML SSO SP-Initiated Redirect Flow
Your App (SP)
User clicks SSO Login
302 Redirect
SAMLRequest in query string
Okta (IdP)
Sign-In Widget loads
User Authenticates
Username + password (+ MFA)
POST to ACS
SAMLResponse + RelayState
SP Validates
Signature + assertions
Dashboard
User is logged in
IdP-Initiated Login Flow
Okta Dashboard
User clicks app tile
Okta Generates
SAMLResponse (unsolicited)
POST to ACS
SAMLResponse without RelayState
SP Validates
Signature + assertions
Default Landing
SP-configured default URL
A comprehensive SAML SSO test suite must cover both SP-initiated and IdP-initiated flows, validate the SAMLResponse structure, confirm RelayState preservation, test error paths, and handle certificate edge cases. The following sections walk through each scenario with runnable Playwright TypeScript.
2. Setting Up a Reliable Test Environment
Before writing any test, you need a properly configured Okta developer org and a SAML-enabled service provider. Okta provides free developer organizations at developer.okta.com that support SAML 2.0 app integrations, custom sign-in policies, and MFA enrollment. Create a dedicated test org separate from production. Never run end-to-end tests against your production Okta org, because rate limits are strict and test user activity pollutes audit logs.
Okta SAML Test Environment Checklist
- Create a free Okta Developer org at developer.okta.com
- Create a SAML 2.0 application integration in Okta admin console
- Set Single Sign-On URL (ACS URL) to your test SP endpoint
- Set Audience URI (SP Entity ID) to match your SP configuration
- Download the IdP metadata XML and signing certificate
- Create dedicated test users with known passwords in Okta
- Assign test users to the SAML application
- Disable MFA policies for the test group (unless testing MFA scenarios)
Environment Variables
Okta API for Test User Management
Use the Okta Users API to create, reset, and clean up test users programmatically. This keeps your test suite idempotent and prevents stale user state from causing flaky failures.
Playwright Configuration for SAML SSO
SAML SSO flows involve cross-domain redirects between your SP and Okta. Playwright needs generous navigation timeouts to accommodate the redirect chain, especially in CI environments where network latency is higher. Configure the global setup to create fresh test users before the suite runs.
3. Scenario: SP-Initiated Login Happy Path
The SP-initiated flow is the most common SAML SSO pattern. Your application (the Service Provider) generates a SAMLRequest, encodes it in the query string or POST body, and redirects the browser to Okta's SSO URL. Okta renders the Sign-In Widget, the user authenticates, and Okta POSTs the signed SAMLResponse back to your ACS endpoint. Your SP validates the signature, extracts the assertions (user email, name, groups), creates a session, and redirects the user to the dashboard.
This is your smoke test. If this breaks, no enterprise user can log in. The key challenge is handling the domain transitions: your app redirects to Okta, and after authentication Okta POSTs back to your app. Playwright handles this well, but you must wait for the correct URL patterns at each stage.
SP-Initiated SAML Login Happy Path
ModerateGoal
Starting from your app's SSO login page, complete a full SAML authentication via Okta, land on the authenticated dashboard, and confirm the session contains the correct SAML attributes.
Preconditions
- App running at
APP_BASE_URLwith SAML SP configured - Test user exists and is assigned to the SAML app in Okta
- No MFA policies enabled for the test user group
Playwright Implementation
SP-Initiated SAML Login: Playwright vs Assrt
import { test, expect } from '@playwright/test';
test('SP-initiated SAML login: happy path', async ({ page }) => {
await page.goto('/login/sso');
await page.getByPlaceholder(/email|organization/i)
.fill('yourcompany.com');
await page.getByRole('button', { name: /sign in with sso/i })
.click();
await page.waitForURL(/\.okta\.com/, { timeout: 15_000 });
await page.getByLabel(/username|email/i)
.fill(process.env.TEST_OKTA_USERNAME!);
await page.getByLabel(/password/i)
.fill(process.env.TEST_OKTA_PASSWORD!);
await page.getByRole('button', { name: /sign in/i }).click();
await page.waitForURL(/\/dashboard/, { timeout: 30_000 });
await expect(
page.getByRole('heading', { name: /dashboard/i })
).toBeVisible();
await expect(
page.getByText(process.env.TEST_OKTA_USERNAME!)
).toBeVisible();
const cookies = await page.context().cookies();
const sessionCookie = cookies.find(
(c) => c.name === 'session'
);
expect(sessionCookie).toBeDefined();
});4. Scenario: IdP-Initiated Login from Okta Dashboard
IdP-initiated login is the reverse flow. Instead of starting from your application, the user logs into Okta first, then clicks your app's tile on the Okta dashboard. Okta generates an unsolicited SAMLResponse (there is no corresponding SAMLRequest) and POSTs it directly to your ACS URL. This flow is common in enterprises where users access multiple apps from a centralized Okta portal.
The tricky part for your SP is that IdP-initiated responses arrive without a prior SAMLRequest to correlate against. Many SAML libraries reject unsolicited responses by default because they are more susceptible to replay attacks. Your SP must be explicitly configured to accept IdP-initiated responses, and your test must verify this configuration works correctly.
IdP-Initiated Login from Okta Dashboard
ComplexPlaywright Implementation
IdP-Initiated Login: Playwright vs Assrt
test('IdP-initiated SAML login', async ({ page }) => {
await page.goto(process.env.OKTA_ORG_URL + '/login/login.htm');
await page.getByLabel(/username|email/i)
.fill(process.env.TEST_OKTA_USERNAME!);
await page.getByLabel(/password/i)
.fill(process.env.TEST_OKTA_PASSWORD!);
await page.getByRole('button', { name: /sign in/i }).click();
await page.waitForURL(/\.okta\.com\/app\/UserHome/i, {
timeout: 15_000,
});
await page.getByRole('link', { name: /your app name/i }).click();
await page.waitForURL(
new RegExp(process.env.APP_BASE_URL!),
{ timeout: 30_000 }
);
await expect(
page.getByRole('heading', { name: /dashboard/i })
).toBeVisible();
await expect(
page.getByText(process.env.TEST_OKTA_USERNAME!)
).toBeVisible();
});5. Scenario: RelayState Deep Link Preservation
RelayState is a SAML parameter that preserves the user's original destination URL through the authentication redirect chain. When a user tries to access /settings/billing without a session, your SP should encode that path into the RelayState parameter of the SAMLRequest. After Okta authenticates the user and POSTs the SAMLResponse back to your ACS URL, the RelayState parameter accompanies the response. Your SP then redirects the user to the original deep link instead of the default dashboard.
Testing RelayState is critical because broken deep linking is one of the most common SAML integration bugs. Users who bookmark internal pages or follow links from emails expect to land on that exact page after SSO, not a generic dashboard. The test must verify that the RelayState survives the entire redirect chain unmodified.
RelayState Deep Link Preservation
ModeratePlaywright Implementation
Assrt Equivalent
# scenarios/saml-relay-state.assrt
describe: RelayState preserves deep link through SAML flow
given:
- I navigate directly to /settings/billing (protected page)
- I am not logged in
steps:
- wait for the redirect to Okta
- fill username and password with test credentials
- click "Sign In"
expect:
- I am redirected to /settings/billing (not /dashboard)
- the billing page heading is visible
- my session is authenticated6. Scenario: Intercepting and Validating the SAMLResponse
Beyond testing that the login flow works, you should validate the actual SAMLResponse content. The SAMLResponse is a base64-encoded XML document containing the assertion (user identity), conditions (audience restriction, time validity), and the digital signature. Your test can intercept this response using Playwright's request interception and validate its structure without modifying the actual flow.
This scenario uses page.route() to intercept the POST to your ACS URL, extract the SAMLResponse parameter, decode and parse the XML, and run assertions against the SAML attributes. The request continues to your SP normally after interception, so the login flow completes as expected.
Intercepting and Validating SAMLResponse
ComplexPlaywright Implementation
What to Assert Beyond the UI
SAMLResponse Validation Checklist
- Response Status is urn:oasis:names:tc:SAML:2.0:status:Success
- Issuer matches the Okta Entity ID from IdP metadata
- Assertion NameID matches the authenticated user email
- Audience restriction matches your SP Entity ID
- NotBefore and NotOnOrAfter timestamps are valid
- Signature is present and uses SHA-256
- Custom attribute statements (groups, roles) are populated
7. Scenario: Error Handling and Invalid Credentials
Enterprise SSO failures are notoriously opaque. When a SAML assertion fails validation, most SPs show a generic error page with no actionable information. When Okta rejects credentials, the error message depends on the sign-in policy. Your test suite must cover the negative paths: wrong password, locked account, user not assigned to the SAML app, and expired SAML assertions.
Error Handling: Invalid Credentials and Unassigned User
StraightforwardPlaywright Implementation
Assrt Equivalent
# scenarios/saml-error-handling.assrt
describe: SAML SSO error handling scenarios
---
scenario: Wrong password shows Okta error
steps:
- navigate to SSO login
- enter domain "yourcompany.com"
- click "Sign in with SSO"
- fill username with test user email
- fill password with "WrongPassword123!"
- click "Sign In"
expect:
- an error message about invalid credentials is visible
- I am still on the Okta sign-in page
---
scenario: Unassigned user gets access denied
steps:
- navigate to SSO login and trigger SAML redirect
- fill username with "unassigned-user@yourcompany.com"
- fill password with a valid password
- click "Sign In"
expect:
- an error about app not assigned or access denied is visible8. Scenario: Bypassing SAML in CI with Saved Sessions
Running the full SAML SSO flow before every test is slow and fragile. The redirect chain to Okta and back adds three to five seconds per test, and Okta rate limits can throttle parallel workers. The solution is to authenticate once in a setup project, save the session using Playwright's storageState, and reuse it across all subsequent tests that need an authenticated session.
For even faster CI runs, you can bypass SAML entirely by generating a session token directly through your SP's test API. This approach creates a valid session without touching Okta at all, eliminating the external dependency for non-SSO-specific tests.
Session Persistence with storageState
ModeratePlaywright Implementation: Browser-Based Setup
API-Based Session Injection (Faster)
// test/helpers/session-bypass.ts
// For non-SSO tests, create a session directly via your SP's test API
// This bypasses Okta entirely and is 10x faster in CI
export async function createTestSession(
email: string
): Promise<string> {
const res = await fetch(
process.env.APP_BASE_URL + '/api/test/create-session',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email,
provider: 'saml',
attributes: {
firstName: 'E2E',
lastName: 'Test',
groups: ['Engineering', 'QA'],
},
}),
}
);
const { sessionToken } = await res.json();
return sessionToken;
}
// In global setup, inject the session:
// await context.addCookies([{
// name: 'session',
// value: sessionToken,
// domain: 'localhost',
// path: '/',
// }]);Session Reuse: Full SAML Flow vs API Bypass
// Full SAML flow in global setup
async function globalSetup(config: FullConfig) {
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
await page.goto(APP_BASE_URL + '/login/sso');
await page.getByPlaceholder(/email|domain/i)
.fill('yourcompany.com');
await page.getByRole('button', { name: /sign in with sso/i })
.click();
await page.waitForURL(/\.okta\.com/);
await page.getByLabel(/username|email/i)
.fill(TEST_OKTA_USERNAME);
await page.getByLabel(/password/i)
.fill(TEST_OKTA_PASSWORD);
await page.getByRole('button', { name: /sign in/i })
.click();
await page.waitForURL(/\/dashboard/);
await context.storageState({
path: './test/.auth/sso-session.json'
});
await browser.close();
}9. Common Pitfalls That Break SAML SSO Test Suites
Certificate Rotation Breaking Signature Validation
Okta rotates signing certificates periodically, and many organizations configure automatic key rotation. When Okta starts signing SAMLResponses with a new certificate, your SP's cached copy of the old certificate will reject every assertion. This is the single most common cause of “SSO suddenly stopped working” incidents. Your test suite should include a certificate validation check that fetches the current certificate from Okta's metadata endpoint and compares it against your SP's configured certificate. The Okta admin console shows the certificate expiration date under Applications, then your app, then Sign On, then SAML Signing Certificates.
Clock Skew Between SP and IdP
SAML assertions contain NotBefore and NotOnOrAfter timestamps. If your SP server clock is more than a few minutes out of sync with Okta's servers, valid assertions will be rejected as expired or not yet valid. This is especially common in Docker containers and CI environments where NTP synchronization may not be configured. Most SAML libraries accept a configurable clock skew tolerance (typically two to five minutes). Make sure your SP configuration includes this tolerance, and add a test that verifies your SP's system clock is within acceptable range.
ACS URL Mismatch
The Assertion Consumer Service URL configured in Okta must exactly match the endpoint your SP exposes. A trailing slash mismatch (/auth/saml/callback vs /auth/saml/callback/), an HTTP vs HTTPS difference, or a port number discrepancy will cause Okta to reject the SAMLRequest or your SP to reject the SAMLResponse. In CI environments with dynamic URLs (like Vercel preview deployments), you need to update the ACS URL in Okta for each environment or use a stable tunnel URL.
Okta Sign-In Widget Version Differences
Okta's Sign-In Widget has gone through multiple major versions with different DOM structures, class names, and element attributes. Version 7 introduced the Identity Engine architecture with new interaction flows. If your Okta org is on Classic Engine versus Identity Engine, the sign-in page will render differently. Selectors that work on one version may fail silently on another. Use role-based and label-based Playwright locators (like getByLabel('username')) rather than CSS class selectors to maximize compatibility across widget versions.
XML Canonicalization Issues
SAML signatures are computed over canonicalized XML, meaning the exact byte representation of the XML matters. If your SAML library applies a different canonicalization algorithm than what Okta used when signing, the signature will fail even though the logical content is identical. This commonly manifests when SPs parse and re-serialize the SAMLResponse XML before validating the signature, introducing whitespace or namespace changes. Always validate the signature on the raw base64-decoded XML before any parsing or transformation.
10. Writing These Scenarios in Plain English with Assrt
Each scenario above is 25 to 50 lines of Playwright TypeScript. Multiply that by the seven scenarios you actually need and you have a large test file that breaks the moment Okta updates the Sign-In Widget, renames a button label, or changes the MFA challenge flow. SAML SSO is particularly fragile because you are testing across two organizations (your SP and the Okta IdP), and either side can change independently. Assrt lets you describe the scenario in plain English, generates the equivalent Playwright code, and regenerates the selectors automatically when the underlying pages change.
The SAMLResponse interception scenario from Section 6 demonstrates the power of this approach. In raw Playwright, you need to know how to set up route interception, parse URL-encoded POST bodies, decode base64, parse XML with namespace handling, and navigate the SAML assertion tree. In Assrt, you describe what you want to validate and let the framework handle the mechanical details.
Assrt compiles each scenario block into the same Playwright TypeScript you saw in the preceding sections, committed to your repo as real tests you can read, run, and modify. When Okta rolls out a new Sign-In Widget version or your SP changes its ACS endpoint, Assrt detects the failure, analyzes the new DOM, and opens a pull request with the updated locators. Your scenario files stay untouched.
Start with the SP-initiated happy path. Once it is green in your CI, add the IdP-initiated flow, then RelayState validation, then SAMLResponse interception, then the error scenarios, then session bypass. In a single afternoon you can have comprehensive SAML SSO coverage that most enterprise applications never achieve by hand.
Related Guides
How to Test Auth0 Universal Login
A practical, scenario-by-scenario guide to testing Auth0 Universal Login with Playwright....
How to Test Azure AD Login
A practical, scenario-by-scenario guide to testing Azure AD (Entra ID) login with...
How to Test Clerk Sign-In
A practical, scenario-by-scenario guide to testing Clerk authentication with Playwright....
Ready to automate your testing?
Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.