ScreenshotNeo

BlogHow-to

How to Capture a Page Behind an Okta Login with Playwright

Sign in through your app’s normal Okta flow, verify authentication, then capture the protected page with Playwright. Learn to reuse storage state safely and troubleshoot redirects.

By the ScreenshotNeo team4 October 20269 min read

To capture a page behind an Okta login with Playwright, use an authorized account to complete the application’s normal sign-in flow, verify that the app is authenticated, navigate to the protected page, and take the screenshot. For repeated captures, save the successful browser storage state and load it into later contexts. Treat that state like a credential: it may contain cookies or other data that can impersonate the account.

This guide uses Node.js with Playwright. It covers a user-facing sign-in flow, optional state reuse, screenshot options, troubleshooting, and security. Okta sign-in can use a hosted page or an embedded widget, and organization policy can require MFA or enrollment. Complete any required verification; do not try to bypass it.

1. Install Playwright and prepare an authorized test account

Use a test account and a target URL you are authorized to access. Confirm you know the app’s ordinary sign-in URL and a stable signal that appears only after successful authentication, such as a dashboard heading or account control.

mkdir okta-page-capture
cd okta-page-capture
npm init -y
npm install playwright
npx playwright install chromium

Playwright launches a real browser. The first sign-in may redirect to an Okta-hosted page; an embedded Sign-In Widget can instead appear within the application. Okta recommends the hosted widget for basic sign-in use cases. The exact fields and steps vary by app and policy. See Okta’s Sign-In Widget documentation.

2. Sign in, verify the app state, and capture the protected page

Save this as capture.mjs. Set the URLs and selectors for your app. The script opens a visible browser so you can complete MFA, enrollment, or other required prompts yourself. It waits for an app-specific authenticated element before opening the target and capturing it.

import { chromium } from 'playwright';

const loginUrl = process.env.LOGIN_URL;
const targetUrl = process.env.TARGET_URL;
const authenticatedSelector = process.env.AUTHENTICATED_SELECTOR;

if (!loginUrl || !targetUrl || !authenticatedSelector) {
  throw new Error('Set LOGIN_URL, TARGET_URL, and AUTHENTICATED_SELECTOR');
}

const browser = await chromium.launch({ headless: false });
const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
const page = await context.newPage();

try {
  await page.goto(loginUrl, { waitUntil: 'domcontentloaded' });
  console.log('Complete the normal Okta sign-in and any required verification in the browser.');

  // This is the proof of authentication. Pick an element only shown in the signed-in app.
  await page.locator(authenticatedSelector).waitFor({ state: 'visible', timeout: 180_000 });

  await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
  await page.locator(authenticatedSelector).waitFor({ state: 'visible', timeout: 60_000 });
  await page.screenshot({ path: 'capture.png', fullPage: true });
  console.log('Saved capture.png');
} finally {
  await context.close();
  await browser.close();
}

Run it with values appropriate for your application. For example, if the signed-in app always shows a navigation element with data-testid="account-menu":

LOGIN_URL='https://app.example.com/login' \
TARGET_URL='https://app.example.com/reports/monthly' \
AUTHENTICATED_SELECTOR='[data-testid="account-menu"]' \
node capture.mjs

Replace the example host, route, and selector. Do not rely on a successful click as proof of sign-in: redirects, MFA, or enrollment may still be in progress. Playwright’s authentication guidance recommends checking the final URL or a visible authenticated UI element. See Playwright authentication and the Page API.

Choose an authentication signal that is stable

  • Visible app element: Usually the best choice. Use a heading, account menu, or unique signed-in navigation control that is not present on the login screen.
  • Final URL: Useful when the app has a predictable post-login route. A URL alone can be misleading if the app renders an error or an intermediate page there.
  • App-specific content: For a target route, wait for a unique heading or data region before capture. This helps avoid screenshots taken while the page is still rendering.

If the app uses a single-page router, navigation might not trigger a full document load. In that case, wait for a route-specific element after navigation rather than relying only on a load event.

3. Save and reuse authenticated browser state

Interactive sign-in is a good fit when the session must be freshly established or policy requires a person to complete a prompt. For repeated captures, save state after verifying login, then use it to create later browser contexts. Cookies and local storage are included in the ordinary storage-state file. Playwright also documents optional IndexedDB capture for apps that keep authentication data there.

After the authenticated element appears in the first script, save state before closing the context:

await context.storageState({ path: 'playwright/.auth/user.json' });

For example, add these lines after the first successful authentication and before navigating to the target:

await page.locator(authenticatedSelector).waitFor({ state: 'visible', timeout: 180_000 });
await context.storageState({ path: 'playwright/.auth/user.json' });

Then a later run can load that state into a new context:

import { chromium } from 'playwright';

const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
  storageState: 'playwright/.auth/user.json',
  viewport: { width: 1440, height: 1000 },
});
const page = await context.newPage();

try {
  await page.goto('https://app.example.com/reports/monthly', {
    waitUntil: 'domcontentloaded',
  });
  await page.locator('[data-testid="account-menu"]').waitFor({
    state: 'visible',
    timeout: 60_000,
  });
  await page.screenshot({ path: 'capture.png', fullPage: true });
} finally {
  await context.close();
  await browser.close();
}

Use the same Playwright project and a compatible browser setup when generating and loading state. State can expire or be invalidated by sign-out, policy, or app changes; when the authenticated signal does not appear, re-run the authorized sign-in flow and refresh the file. The state belongs to relevant origins, so make sure the app and its authentication setup match the saved context.

Ordinary storage state does not include session storage. If the application relies on session storage, follow Playwright’s documented save-and-restore approach. If tokens are held in IndexedDB, use the indexedDB storage-state option supported by the installed Playwright version. See BrowserContext storage-state options and Playwright’s session storage guidance.

4. Screenshot options and capture quality

The Page screenshot API supports a range of capture controls. The example uses fullPage: true to capture the full scrollable page. For a viewport-only image, omit that option or set it to false.

Need Playwright option or approach
Full document rather than current viewport fullPage: true
Specific output format Use a .png, .jpeg, or .webp path as supported by the installed version; JPEG quality can be set with quality.
Capture one component Locate it and call locator.screenshot({ path: 'panel.png' }).
Transparent PNG background Use omitBackground: true where supported by the Page screenshot API.
Use a consistent screen size Set viewport when creating the browser context.
Wait for page content Wait for an app-specific locator; avoid an arbitrary short delay where a reliable condition exists.

Check the Page screenshot API for exact option names and behavior in your installed Playwright version. For pages with lazy-loaded images, scrolling the page before capture can prompt content to load; verify the resulting image because the app may use its own loading behavior.

5. Troubleshooting Okta and Playwright capture failures

Symptom Likely cause Fix
Screenshot shows the Okta sign-in page Authentication did not finish, a policy prompt remains, or the state is expired. Use the ordinary app sign-in flow, complete required prompts, and wait for a signed-in app element before capture. Refresh saved state if it has expired.
Script times out waiting for the authenticated selector The selector is wrong, not unique, hidden, or absent in the current app state. Inspect the signed-in app manually and choose a stable visible selector. Confirm the test account has access and that the app returned from Okta.
Login succeeds locally but fails headless The flow may require interactive verification or depend on a session not present in the headless context. Complete sign-in in a visible browser, save the verified state, and load it into a later context. If policy requires new verification, use the approved interactive flow.
State file loads but the app redirects to login State expired, belongs to another origin, or the app depends on storage not included in the file. Sign in again for the correct app, check origins, and determine whether the app uses session storage or IndexedDB.
Screenshot is blank or incomplete The capture happened before app content rendered, or the page uses delayed/lazy loading. Wait for a route-specific content element, then capture. For lazy content, scroll as needed and confirm the finished screenshot.
Sign-in looks different from the expected form Okta can use a hosted redirect or an embedded widget, and policies vary by application. Use the real app login route and adapt to the visible flow. Do not assume a fixed form or fixed number of steps.

Okta documents policy-dependent authentication and MFA behavior in its Authentication API and MFA overview. A capture script should wait for permitted verification to finish rather than trying to circumvent it.

6. Security, reliability, performance, and cost

Protect saved state

Storage-state files may contain cookies and headers capable of impersonating the account. Keep them out of source control, restrict file access, use a dedicated authorized test account where appropriate, and remove stale state securely. Add the auth directory to .gitignore:

playwright/.auth/

Playwright explicitly warns that saved authentication state is sensitive and should not be committed. See Playwright’s authentication guide.

Make runs reliable

  • Wait for a visible authenticated signal and then a target-page signal, rather than timing screenshots from a fixed sleep.
  • Use selectors tied to stable app semantics or test IDs; avoid fragile positional selectors.
  • Give interactive authentication enough time for the organization’s verification steps, but keep the timeout bounded.
  • When a run fails, record the final URL and inspect a screenshot or trace locally without exposing credentials or state files.
  • Refresh state through the approved sign-in flow when it expires; do not assume a stored session lasts indefinitely.

Understand runtime and cost

A local Playwright capture has no per-screenshot API charge from Playwright, but it uses compute, browser installation, and maintenance time. Interactive login adds human time; state reuse reduces repeated setup but introduces session-expiry and secret-management work. Full-page screenshots can take longer and produce larger files than viewport captures. Reuse a browser process for multiple authorized captures when appropriate, while keeping separate contexts for session isolation.

7. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server for developers. A normal API capture is one GET request; see the ScreenshotNeo API documentation. For a page that requires Okta authentication, use Playwright with an authorized signed-in session as described above; this API call does not sign in to your Okta account.

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o shot.webp

ScreenshotNeo accepts and removes cookie/consent banners, newsletter popups, and chat widgets before capture, with each step configurable. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Learn more at ScreenshotNeo.

Sign up free for 1,000 screenshots a month, with no card required.

8. FAQ

Can Playwright complete Okta MFA automatically?

This guide leaves required verification to the authorized user. MFA and enrollment depend on organization policy and app context; use an approved test setup and complete the real flow.

Can I use saved state in CI?

Yes, if your organization permits it and you can securely provision and protect the state. Plan for expiry and use an approved way to refresh it.

Does storage state include every browser storage mechanism?

No. Ordinary state covers cookies and local storage; session storage needs a separate documented approach, and IndexedDB capture is an option in supported Playwright versions.

Why not call Okta’s authentication endpoint directly?

A screenshot task generally needs the app’s real user-facing flow, which handles redirects and application behavior. Direct authentication APIs have their own policy and rate-limit considerations; follow your organization’s approved integration.