How to Capture an Authenticated Page Screenshot After a Single Sign-On Redirect
Use Playwright to finish SSO, verify the protected page is ready, save reusable browser state securely, and capture a reliable screenshot.
To capture a page behind single sign-on (SSO), complete the permitted login flow in a Playwright browser context, wait until the final redirect or an authenticated-page condition confirms login, navigate to the protected page, verify its content is ready, and save a screenshot. If you need to reuse the login in another run, save the context’s storage state securely and load it into a new context.
Do not treat a successful sign-in click or a completed navigation as proof that the application is ready. SSO can set cookies across several redirects, and a page can finish navigating before its protected content has rendered. The target site’s exact selectors, SSO behavior, MFA requirements, and session lifetime are application-specific.
1. Set up Playwright
The runnable example below uses Node.js and the Chromium browser. It signs in through the application’s permitted UI, waits for an authenticated-page signal, saves browser state, then opens the protected page with that state and captures it. Replace the example URLs and selectors with those for your application. Use a test account and follow the site’s access and automation policies.
npm install playwright
npx playwright install chromium
Set credentials in your shell or a secrets manager; do not put them in source code. For example, in a POSIX shell:
export APP_LOGIN_URL='https://app.example.com/login'
export APP_TARGET_URL='https://app.example.com/reports'
export APP_USERNAME='your-test-user'
export APP_PASSWORD='your-test-password'
2. Complete SSO and save authenticated state
This example assumes the login form is on the starting page and that after submitting it, the browser returns to the application. If your identity provider uses a separate sign-in page, the same principle applies: interact with the permitted flow, then wait for a destination or authenticated UI condition that proves the application has accepted the session.
// save-auth.mjs
import { chromium } from 'playwright';
const loginUrl = process.env.APP_LOGIN_URL;
const username = process.env.APP_USERNAME;
const password = process.env.APP_PASSWORD;
if (!loginUrl || !username || !password) {
throw new Error('Set APP_LOGIN_URL, APP_USERNAME, and APP_PASSWORD');
}
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext();
const page = await context.newPage();
try {
await page.goto(loginUrl, { waitUntil: 'domcontentloaded' });
await page.getByLabel('Email').fill(username);
await page.getByLabel('Password').fill(password);
await page.getByRole('button', { name: /sign in|log in/i }).click();
// Prefer an application-specific authenticated condition when available.
await page.getByRole('heading', { name: 'Dashboard' }).waitFor({ state: 'visible' });
// Optionally also check the expected final app URL if it is stable.
// await page.waitForURL('https://app.example.com/**');
// Keep this file in an ignored, access-restricted directory.
await context.storageState({ path: 'playwright/.auth/user.json', indexedDB: true });
} finally {
await browser.close();
}
Create the parent directory before running the script, and add the state file and any containing auth directory to your ignore rules. Playwright recommends protecting authentication state because it can contain cookies and headers that can impersonate the account. Treat it like a credential: restrict access, do not commit or publish it, and refresh it when it expires.
mkdir -p playwright/.auth
printf '\nplaywright/.auth/\n' >> .gitignore
node save-auth.mjs
3. Reuse state, verify readiness, and capture
Load the saved state when creating a fresh browser context. Then navigate directly to the protected URL, wait for a meaningful page condition, and capture. A heading, report title, or known control is usually a stronger readiness check than waiting for a fixed number of seconds.
// capture.mjs
import { chromium } from 'playwright';
const targetUrl = process.env.APP_TARGET_URL;
if (!targetUrl) throw new Error('Set APP_TARGET_URL');
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
storageState: 'playwright/.auth/user.json',
});
const page = await context.newPage();
try {
await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
// Replace with a selector unique to the authenticated target page.
await page.getByRole('heading', { name: 'Reports' }).waitFor({ state: 'visible' });
await page.screenshot({ path: 'screenshot.png', fullPage: true });
} finally {
await browser.close();
}
Run the capture with node capture.mjs. If the expected heading is not unique or the application renders it dynamically, use a selector tied to a stable page landmark. You can also assert a logged-in control, account name, or known route. Do not use a generic element that also appears on the login or error page.
4. Choose the right login-completion signal
| Signal | What it confirms | Use it when |
|---|---|---|
| Final destination URL | The browser reached the expected route after redirects. | The app has a stable, predictable post-login URL. |
| Authenticated UI element | The application rendered a control or page content associated with a logged-in user. | The URL is dynamic, the app is a single-page application, or content readiness matters. |
| Both URL and UI | Navigation and application-level readiness. | A wrong route would be costly and the page has a reliable authenticated marker. |
Playwright navigation resolves with the response from the last redirect in a redirect chain. That does not by itself prove the application finished its own rendering or accepted all required authentication state. For this reason, wait for the final URL or an authenticated UI condition, then wait for the specific target content before taking the screenshot. See Playwright’s authentication guide and Page API.
5. Authentication state: what persists and what does not
Playwright storage state can preserve cookies, local storage, IndexedDB, and passkey authentication data. IndexedDB storage is opt-in in the API version shown above; if your application relies on it, include indexedDB: true. State files are tied to the application’s actual authentication design and can expire or be invalidated by logout, password changes, policy changes, or session limits.
Session storage is scoped to a page origin and is not included in Playwright’s built-in storage state. If the application requires session storage for authentication, Playwright documents saving and restoring it with an initialization script. Confirm that this is the app’s real requirement before adding custom restoration logic. Do not copy session data across unrelated origins.
If you do not need to repeat login, keep one browser context alive and capture multiple pages within it. If jobs run independently, save state and initialize a new context for each job. In either case, verify that the state remains valid by checking an authenticated UI condition after navigation.
6. Screenshot options and capture details
pathwrites the screenshot to a file; select an extension that matches the requested image format.fullPage: truecaptures the full scrollable page; omit it for only the current viewport.- Wait for a page-specific ready condition before capture. If images or client-rendered content are still loading, wait for those elements explicitly.
- For a consistent viewport, set the context viewport when creating the context, for example
await browser.newContext({ viewport: { width: 1440, height: 900 }, storageState: 'playwright/.auth/user.json' }). - See the current Playwright Page screenshot options for supported formats and options such as animation handling, masking, and clipping.
7. Other implementation choices
Use an authentication API when the application supports one
Playwright supports setting up authentication through an API where the app provides an appropriate endpoint, then saving the resulting state and using it in a browser context. This can be useful when a test does not need to exercise the interactive sign-in path. UI-driven login is more appropriate when the login experience itself is part of what you need to verify. Neither approach is universal; choose based on the application’s supported authentication mechanisms and the purpose of the capture.
Use a persistent context only when needed
A persistent browser profile can retain browser data between launches, but it also increases the amount of state that must be protected and cleaned up. For repeatable automation, an explicit storage-state file makes the authentication input visible in the code and easier to refresh deliberately. Avoid sharing a writable profile between concurrent jobs.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Redirect returns to login | Login was not completed, a required redirect/cookie was missed, or the saved session expired. | Wait for the app’s authenticated condition before saving state; rerun the login flow and confirm the state file is current. |
| Screenshot shows an IdP or login page | The target navigation did not establish an authenticated application session, or the target URL requires another redirect. | Check the final URL and wait for an application-specific heading or control before capture. Confirm the saved state belongs to the correct environment and account. |
| Timeout waiting for the heading | The selector differs from the real page, the page is still rendering, or authentication failed. | Inspect the final URL and page content in a headed run; use a stable selector actually present on the authenticated page, and distinguish an error state from a slow load. |
| Works locally but fails in CI | State may be expired, unavailable to the job, or dependent on a different environment, origin, or browser setup. | Provide the state securely to the job, regenerate it when invalid, and use the same site origin and compatible browser configuration. |
| Cookies persist but app still logs out | The app may rely on IndexedDB or another mechanism, or server-side policy may have invalidated the session. | Check the app’s auth design; enable IndexedDB state where needed, or perform a fresh supported login. Do not assume cookies alone are sufficient. |
| Session-storage login is missing | Session storage is not included in the built-in storage-state file. | Use a documented initialization-script approach only if the application truly depends on it, and scope restoration to the correct origin. |
| Screenshot is blank or incomplete | The page may not be ready, content may load after navigation, or the capture only includes the viewport. | Wait for the real content condition, wait for relevant images if needed, and set fullPage: true for a full document capture. |
| Authentication state appears in source control | The auth directory was not excluded or a file was committed before ignore rules were added. | Remove it from the repository history as appropriate, rotate affected credentials or sessions, and store future state in a restricted location. |
9. Performance, reliability, and cost
Reusing storage state avoids repeating the login interaction for each capture, but it does not remove the target page’s load and render time. Keep readiness checks specific so the script does not wait for unrelated background activity. A fixed sleep is simple but can be too short on a slow run and waste time on a fast one.
For reliability, create state through a supported flow, verify it against the protected target, and refresh it when the application expires it. Do not run parallel jobs against one mutable browser profile. Treat an authentication redirect, access-denied page, or bot check as a failed capture and inspect it before accepting the resulting image.
Playwright is browser automation software; its browser and compute costs depend on where and how you run it. The research sources provide no benchmark or fixed cost figure. Estimate based on your own environment, concurrency, browser runtime, and storage needs.
Or skip the browser setup
If the page is publicly accessible, ScreenshotNeo can return a website screenshot with one GET request. See the ScreenshotNeo API documentation. This does not replace a private SSO login: the endpoint takes a URL and the facts provided here do not establish that it can reuse your Playwright authentication state. Use the Playwright workflow above for protected pages that require your account.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
- Cookie and consent banners are accepted and removed before capture, along with 60+ known consent platforms, newsletter popups, and chat widgets; each cleanup step can be turned off.
- Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing; response headers report the page verdict and whether it was billed.
- An MCP server provides
take_screenshot,get_page_info, andcapture_pdftools for Claude, Cursor, and other MCP clients. - The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Every feature is on every plan.
Sign up free for 1,000 screenshots a month, with no card required.
FAQ
Can a screenshot API take a screenshot of a page behind my SSO?
This article’s ScreenshotNeo facts describe URL-based capture and do not establish support for importing Playwright’s authenticated browser state. For a page that requires your SSO session, use browser automation with the permitted login flow and protected state.
Should I wait for network idle after login?
Use a meaningful URL or authenticated-page condition. Network activity can continue for reasons unrelated to readiness, so an app-specific signal is often clearer.
Can I reuse the state file for multiple captures?
Yes, while it remains valid and the application accepts it. Protect it like a credential and refresh it when the site expires or invalidates the session.
Does a successful redirect prove login succeeded?
No. Confirm the expected final destination and an application-specific authenticated condition before capturing.


