How to Screenshot a Web App Behind Microsoft Entra ID Login in Headless Chrome
Sign in through Microsoft Entra once, save Playwright browser state, then reuse it in headless Chrome to capture an authenticated page.
Direct answer: Complete the app’s real Microsoft Entra sign-in in a browser, save the authenticated browser state with Playwright, and load that state into a fresh headless Chromium context before navigating to the protected route. Verify that the app is signed in before taking the screenshot. The saved state may expire, be rejected by tenant policy, or contain credentials that could let someone impersonate the account, so protect it like a secret.
This method captures a rendered web page. A browserless token flow such as device code can be suitable for calling an API, but it does not by itself create a signed-in Chrome page to screenshot. Microsoft’s sample web app illustrates the browser redirect to Entra and back to the application after sign-in and consent (Microsoft’s web app sign-in quickstart; authentication and authorization samples).
1. Confirm the sign-in flow and authorization
Use an account and environment approved for automation. The right account, tenant settings, app registration, and login method depend on the application and organization. This workflow reuses a browser session; it does not bypass sign-in, MFA, Conditional Access, or app authorization.
Before writing automation, sign in to the target app manually and note what success looks like: a final application URL, a stable page heading, or an element that appears only after authentication. You will use that app-specific signal to avoid saving a state file while still on the login page or consent screen.
2. Install Playwright
The examples below use Node.js and Playwright’s Chromium browser. They save authentication state in a local file, then load it into a new browser context for the screenshot.
mkdir entra-screenshot
cd entra-screenshot
npm init -y
npm install --save-dev playwright
npx playwright install chromium
Use a current Node.js installation supported by your environment. Run these commands in a private working directory: the state file created in the next step is sensitive.
3. Sign in interactively and save browser state
Create save-auth.mjs. Replace the example URL and the success selector with values for your app. The script opens a visible browser so you can complete the actual Entra flow, including any required consent or challenge. It saves state only after the app-specific success element appears.
import { chromium } from 'playwright';
const appUrl = 'https://app.example.com/';
const successSelector = '[data-testid="signed-in-home"]';
const statePath = 'playwright/.auth/entra-user.json';
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext();
const page = await context.newPage();
try {
await page.goto(appUrl, { waitUntil: 'domcontentloaded' });
console.log('Complete sign-in in the browser, including any required challenge.');
await page.locator(successSelector).waitFor({ state: 'visible', timeout: 0 });
await context.storageState({ path: statePath, indexedDB: true });
console.log(`Saved authenticated browser state to ${statePath}`);
} finally {
await browser.close();
}
Playwright documents saving and reusing authenticated state, including cookies and local storage; its APIs and examples also cover IndexedDB state (Playwright authentication; preserving authenticated state with codegen). The indexedDB option is available in current Playwright versions; update Playwright if your installed version does not recognize it. Some apps use session storage or another app-specific mechanism, so a storage snapshot is not guaranteed to capture every sign-in implementation.
If your app does not have a useful test ID, wait for a stable heading or another reliable authenticated-only element, for example page.getByRole('heading', { name: 'Dashboard' }).waitFor(). Avoid relying solely on a URL if the app can display an error or loading page at that route.
4. Reuse state in headless Chromium and capture
Create screenshot.mjs. A new context is created with the saved state, then the script checks the app-specific success signal before taking a full-page PNG. Change the target route and selector to match your app.
import { chromium } from 'playwright';
const targetUrl = 'https://app.example.com/reports/monthly';
const statePath = 'playwright/.auth/entra-user.json';
const successSelector = '[data-testid="signed-in-home"]';
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ storageState: statePath });
const page = await context.newPage();
try {
await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
await page.locator(successSelector).waitFor({ state: 'visible', timeout: 30_000 });
await page.screenshot({ path: 'authenticated-page.png', fullPage: true });
console.log('Saved authenticated-page.png');
} catch (error) {
console.error('Capture failed. Check the URL, saved state, and authenticated-page signal.');
throw error;
} finally {
await browser.close();
}
Run the two scripts in order:
node save-auth.mjs
node screenshot.mjs
The capture script intentionally fails if the authenticated element never appears. That is safer than silently saving a screenshot of the sign-in page. In production, make the success check specific enough to distinguish the authenticated app from login, consent, loading, and error states.
5. Configure capture and readiness
Playwright’s screenshot options control the image, while browser-context options control the environment. Add only the settings the app needs; changing device, locale, timezone, or viewport can change layout or application behavior.
| Need | Playwright setting | Consideration |
|---|---|---|
| Full page | page.screenshot({ fullPage: true }) |
Very long pages can produce large images and take longer to render. |
| One component | page.locator('.report').screenshot({ path: 'report.png' }) |
Wait for the component to be visible and populated first. |
| Viewport dimensions | browser.newContext({ viewport: { width: 1440, height: 1000 } }) |
Choose dimensions that match the intended desktop or mobile layout. |
| Retina scale | deviceScaleFactor: 2 in newContext |
Increases pixel dimensions and output size. |
| Dark mode | colorScheme: 'dark' in newContext |
Only affects apps that respond to the browser color scheme. |
| Specific browser locale | locale: 'en-US' in newContext |
May change translated text, dates, and number formatting. |
| Timezone | timezoneId: 'America/New_York' in newContext |
Use a valid timezone identifier and keep it consistent across runs. |
| Hide an element | style: '.cookie-notice { visibility: hidden !important }' in screenshot options |
Use only when hiding that element is appropriate for the capture. |
| Animation | animations: 'disabled' in screenshot options |
Can make a capture more stable by suppressing supported animations. |
| JPEG or quality | type: 'jpeg', quality: 80 |
JPEG is lossy; quality applies to JPEG screenshots. |
For example, to capture a consistently sized dark desktop viewport:
const context = await browser.newContext({
storageState: statePath,
viewport: { width: 1440, height: 1000 },
deviceScaleFactor: 1,
colorScheme: 'dark',
locale: 'en-US',
timezoneId: 'UTC'
});
// After navigation and the authenticated-page check:
await page.screenshot({
path: 'authenticated-page.jpg',
type: 'jpeg',
quality: 85,
fullPage: true,
animations: 'disabled'
});
For dynamic pages, domcontentloaded is only an initial navigation milestone. Follow it with the app-specific condition that means the data you need is ready. A fixed delay can be useful for a known animation, but it is usually a weaker readiness check than waiting for the relevant element or content.
6. Protect and refresh the authentication state
Playwright warns that saved authentication state can contain cookies and headers that could be used to impersonate an account. Treat the file as a secret: exclude it from source control, limit filesystem access, do not print its contents, and delete it when it is no longer needed (Playwright authentication guidance).
mkdir -p playwright/.auth
printf '\nplaywright/.auth/\n' >> .gitignore
chmod 700 playwright/.auth
chmod 600 playwright/.auth/entra-user.json
Use your organization’s approved secret-storage and retention controls in CI. Do not upload the state file as a broadly accessible build artifact. When the app stops accepting it, repeat the authorized interactive sign-in and save fresh state. Do not assume a session has a fixed lifetime; expiration and reauthentication requirements depend on the app and tenant.
7. MFA, Conditional Access, and device code
Headless Chrome can reuse a valid browser session, but it should not be assumed to satisfy every MFA or passwordless challenge unattended. Microsoft documents Conditional Access policies that can require MFA, and Authenticator passwordless sign-in can ask a user to approve a request (Microsoft Entra MFA guidance; passwordless sign-in with Authenticator). Complete challenges through an authorized supported method or ask the tenant administrator about an approved test arrangement.
Device-code authentication is a separate flow for public clients and API access. Microsoft’s samples include browserless sign-in that obtains tokens for Microsoft Graph; those tokens do not automatically establish the cookies, local storage, and application state needed for a rendered signed-in web page. Use the actual browser sign-in flow when the deliverable is a screenshot of the web app.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Screenshot is the Entra login page | State was saved before sign-in completed, not loaded, or is no longer accepted. | Re-run interactive sign-in, wait for an authenticated-only app element, and verify it in the headless context before capture. |
| App redirects back to sign-in | Wrong app URL or account, cookie domain mismatch, expired state, or an app-specific storage dependency. | Start from the app’s normal entry URL; inspect redirects and verify the same authorized account and origin. Check whether the app uses storage beyond the saved state. |
| Browser stops at MFA or approval | Tenant policy or the account’s sign-in method requires user interaction. | Complete the challenge using an approved method. Ask the tenant administrator about a supported test setup if unattended capture is required. |
| “Timeout” waiting for the success selector | The selector is wrong, appears only after another action, or the page has not reached the authenticated state. | Inspect the app manually, choose a stable authenticated-only signal, and distinguish a selector timeout from navigation readiness. |
| State save reports unsupported IndexedDB option | Installed Playwright version does not support the option used by the example. | Update Playwright, or omit indexedDB: true if your app does not depend on IndexedDB. |
| Works locally, fails in CI | State was not securely provisioned, the browser differs, or CI cannot complete an interactive challenge. | Provision state using an approved process, keep it protected, and use the same expected browser/context configuration. Do not expect CI to approve MFA on a user’s behalf. |
| Screenshot is blank or missing data | Capture happened before app data rendered, or the route needs an additional app action. | Wait for the data-bearing element or a meaningful app readiness condition before taking the screenshot. |
| Images or layout vary between runs | Fonts, animations, viewport, data, or asynchronous content differ. | Keep browser and context settings consistent, wait for relevant content, and disable animations when appropriate. Exact pixel identity is not guaranteed by this workflow. |
9. Performance, reliability, and cost
The main work is launching Chromium, loading the app, waiting for authentication and page data, and rendering the requested area. Reuse one browser process for multiple captures where suitable, but create an isolated context per account or job and load only the state that job needs. Full-page screenshots of long or image-heavy pages consume more time and memory than a viewport capture. A larger device scale factor increases image dimensions and file size.
For reliable automation, make the authenticated-page check and content-readiness check explicit, fail closed when either is missing, and refresh state through the approved sign-in process after rejection. Do not treat a successful HTTP response or a completed navigation as proof that the user is authenticated. Protecting and provisioning the state file is part of the operational cost; an expired session can require a human sign-in depending on tenant policy.
Local Playwright capture runs in your own environment; browser execution, maintenance, and storage are your responsibility. If you prefer a hosted screenshot request, ScreenshotNeo is a website screenshot API and MCP server. A hosted screenshot call does not replace an Entra sign-in for a protected page: use it for pages the capture service can access, and do not send private authentication state unless your security requirements and the service’s supported authentication options permit it.
Or skip the browser setup
For a page accessible to the capture service, ScreenshotNeo takes a screenshot with one GET request. See the ScreenshotNeo API documentation.
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 cookie and consent banners like a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and responses identify the page verdict and billing status. Its MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. These capabilities do not make a private Entra-protected page accessible without a supported authorized access path.
Sign up free for 1,000 screenshots a month, with no card required.
FAQ
Can I take a screenshot of a page that requires Microsoft login?
Yes, if you complete the app’s authorized browser sign-in, save reusable browser state, and the app and tenant continue to accept that state. Confirm an authenticated app element before capturing.
Can headless Chrome handle Microsoft MFA?
It can use a session after a supported sign-in, but MFA and passwordless approval may require a person according to account and tenant policy. Do not assume the challenge can be completed unattended.
Does a device-code token let Chrome capture the signed-in app?
Not by itself. Device-code samples target token-based API access; a screenshot of the web app needs a browser session the app recognizes.
How do I keep Playwright logged in between runs?
Save the authenticated context state after the real sign-in, then pass its file as storageState when creating a new context. Protect and refresh the file as needed.
Why does the screenshot show the sign-in page?
The state may not have been saved or restored, may belong to another origin or account, may have expired, or the app may depend on state the snapshot did not capture. Check the app’s authenticated signal before saving the image.


