277 JavaScript Frontend Developer Interview Questions & Answers

133 top • 30 Amazon • 15 Apple • 29 Google • 18 Meta • 21 Microsoft • 20 Netflix • 11 NVIDIA

JavaScript Frontend Developer icon

Questions with Detailed ExplanationsWith Detailed Explanations

(Last Updated: August 28, 2026)

131. How would you respond to a compromised frontend dependency in production?SecurityHard

Question Details

A transitive package in the build has been confirmed to exfiltrate form data from released bundles. Define containment, affected-version and route identification, build and artifact verification, traffic blocking, credential and session response, removal or pinning, clean rebuild, cache and service-worker invalidation, monitoring, user communication, and forensic preservation. Then design provenance, lockfile, review, integrity, sandboxing, secret-minimization, and rollout controls that reduce recurrence without claiming scanners alone prevent supply-chain attacks.

Short Interview Answer (30-60 seconds)

I would treat it as an active incident: contain exposure, identify every affected release and route, preserve evidence, restrict exfiltration where possible, protect credentials and sessions, remove the package, rebuild from trusted inputs, invalidate stale delivery paths, verify artifacts, monitor recovery, communicate appropriately, and strengthen supply-chain controls.

Detailed Explanation

A harmful piece of software used by the website has already reached users and is secretly sending information away. The job is to stop further harm quickly, find every website version and page that contains it, protect people whose information may have been exposed, replace the bad software safely, and make sure old copies cannot keep running. I would also keep evidence so the cause can be investigated. Finally, I would improve how outside software is selected, checked, released, and monitored so a similar problem is less likely to reach users again.

Useful Questions to Ask the Interviewer
  1. Which production releases, deployment regions, routes, and asset versions are currently suspected or confirmed to contain the compromised package?
  2. What data was the malicious package observed collecting, and which external destinations or domains received it?
  3. Do we have immutable build records, lockfiles, source commits, artifact hashes, dependency manifests, CDN logs, and deployment history for the affected releases?
  4. Does the application use service workers, long-lived CDN caching, or offline assets that could continue serving the compromised bundle after a normal deployment?
  5. Which credentials, sessions, tokens, or user actions could have been exposed through the affected forms?
  6. What incident-response, legal, privacy, and customer-notification processes already exist?
How would you respond to a compromised frontend dependency in production? diagram
How to Explain It in an Interview

I would treat the confirmed dependency compromise as an active supply-chain incident. Released JavaScript executes in the user's browser with the capabilities available to the application origin, so malicious code that is already exfiltrating form data requires immediate containment before a complete root-cause investigation.

1. Contain the incident immediately

I would stop deploying builds that contain the dependency and freeze unrelated production changes so the response remains traceable. If a known-safe prior release exists, I would consider an emergency rollback. Otherwise, I would prepare the smallest safe replacement deployment that removes the malicious code.

I would disable an affected feature or separately loaded third-party script immediately when that can reduce exposure safely. If the package has already been bundled into first-party JavaScript, however, disabling a package registry version does not remove code from bundles users already downloaded. Those deployed assets must be replaced.

For traffic blocking, I would distinguish controls we actually have. If exfiltration goes through our own API, proxy, CDN, or other infrastructure, I can block the corresponding requests there. If the malicious JavaScript sends directly from users' browsers to an external attacker-controlled domain, our normal server edge cannot generally intercept that outbound browser traffic. I can remove the malicious code and send a stricter Content Security Policy, such as a restricted connect-src directive, on subsequent document loads. Corporate DNS or network-proxy blocking is useful only when we control the clients' network. I would not claim any of these controls can recall JavaScript that is already executing in an open tab.

2. Identify every affected version, bundle, and route

I would map the compromised transitive package version to exact lockfile entries, build records, source commits, release IDs, chunk hashes, and deployment timestamps. Because it is transitive, I would also identify the direct dependency that introduced it and the complete dependency path.

Then I would determine which production routes actually load affected chunks. Route-level code splitting means one compromised package can appear only on certain pages or interactions. I would use build manifests, securely retained source maps if available, asset manifests, CDN logs, release metadata, and application telemetry to establish which users and routes could have executed the code.

I would not use today's local node_modules tree as evidence for an older release. Historical production artifacts must be traced from their own build records.

3. Preserve forensic evidence before cleanup

Before replacing evidence, I would preserve affected bundles, lockfiles, package metadata, package tarball identifiers where available, build logs, CI logs, dependency manifests, source commits, artifact hashes, deployment metadata, relevant CSP reports, CDN records, and network or application logs.

I would record timestamps and chain-of-custody information according to the organization's incident process. Logs should contain useful release IDs, route identifiers, asset hashes, and security-event metadata without storing passwords, authorization tokens, full session identifiers, or sensitive form contents.

Preserving evidence matters because immediately deleting workspaces, caches, or build artifacts can destroy information needed to establish how the compromise entered production.

4. Verify released artifacts

I would calculate cryptographic hashes of affected production assets and compare them with trusted build records or previously recorded artifact hashes where such records exist. This helps distinguish a malicious dependency that entered during the normal build from a later unauthorized artifact modification.

I would also inspect the generated bundles or dependency metadata to verify that the malicious package code is actually present in the releases we classify as affected. Package-manager state alone does not prove which code was shipped to users.

5. Determine data, credential, and session exposure

I would identify what the malicious JavaScript could read or cause the browser to send. It may access form fields, DOM data, JavaScript-readable storage, client configuration, and tokens deliberately exposed to JavaScript.

HttpOnly cookies are different: JavaScript cannot directly read their values. However, malicious same-origin JavaScript may still issue authenticated requests because the browser can attach eligible cookies automatically. Therefore, HttpOnly reduces token theft but does not make an authenticated session harmless after arbitrary first-party script execution.

If passwords, reusable tokens, API credentials, payment information, or other sensitive data were exposed, I would coordinate appropriate reset or rotation. If an attacker could abuse affected authenticated sessions, I would revoke those sessions or require reauthentication according to the demonstrated exposure and business risk.

Authentication and authorization are separate. Authentication establishes identity. Authorization decides what that identity may do. The trusted server must enforce authorization for every protected operation; frontend state must never be treated as an authorization boundary.

6. Remove or pin the compromised dependency

I would remove the dependency if the application can function safely without it. If it is required, I would move to a version independently confirmed to be safe.

For a transitive package, a package-manager override or resolution mechanism may be an appropriate emergency mitigation to force a reviewed safe version. I would make that change explicit, code-reviewed, tested, and documented rather than silently altering dependency resolution.

I would commit the corrected lockfile and review the dependency diff. A lockfile improves deterministic dependency resolution, but it does not prove that the locked package is trustworthy. A malicious release can be locked perfectly.

7. Rebuild in a clean, trusted environment

I would avoid rebuilding from an existing developer workspace, dependency directory, package cache, or CI worker that might itself contain compromised material unless that environment has been investigated and cleared.

I would use a clean trusted builder, the reviewed source commit, the corrected lockfile, and trusted package inputs. Where the package manager supports strict lockfile installation, I would use it so dependency resolution cannot drift silently during the emergency rebuild.

The build should produce immutable artifacts. I would record the source commit, dependency metadata, builder identity or attestation where supported, artifact hashes, and provenance information so we can show what inputs produced the replacement release.

Where reproducible builds are practical, reproducing the output independently provides additional confidence. I would not assume every frontend toolchain is perfectly reproducible without testing that property.

8. Invalidate stale CDN, HTML, and service-worker delivery paths

Deploying a safe bundle is not sufficient if old content still points users to compromised assets.

I would purge affected CDN entries that we control, especially HTML documents, manifests, bootstrap files, or non-content-hashed assets that can reference compromised chunks. With content-hashed filenames, the safe rebuild should normally produce new asset URLs, but stale HTML can still reference an old compromised URL.

A normal web application cannot directly erase every browser's ordinary HTTP cache on command. Instead, I would ensure new documents reference safe content-hashed assets and use appropriate cache policies for mutable entry documents so clients receive the corrected references.

If a service worker cached the compromised assets, I would release a known-safe service-worker version that updates its cache names or manifest, removes obsolete compromised entries during activation where appropriate, and serves only approved assets. In a severe incident, an emergency service-worker strategy may use skipWaiting and clients.claim when the resulting lifecycle behavior has been reviewed carefully. Those APIs accelerate activation but can also change running pages underneath existing clients, so they are a tradeoff rather than a universal default.

I would test both a fresh browser and an existing profile already controlled by the old service worker. Offline clients that do not reconnect cannot receive remediation until they reconnect, so monitoring must account for that limitation.

9. Verify the remediation before declaring recovery

I would verify that production HTML, manifests, and route chunks reference approved assets only. I would compare deployed artifact hashes with the trusted clean build and confirm that the compromised package or code is absent from the replacement dependency graph and bundles.

I would exercise every affected route and interaction and inspect browser network activity to confirm the observed exfiltration behavior has stopped. I would verify the replacement service worker, CDN behavior, session flows, CSP behavior, and functionality previously supplied by the removed package.

Safe failure matters. If a risky third-party capability cannot be restored safely, I would disable that feature rather than silently loading an unverified fallback.

10. Monitor after deployment

I would monitor for requests to confirmed attacker-controlled destinations where telemetry can observe them, continued requests for old compromised asset hashes, stale service-worker versions, unexpected network destinations visible through CSP reporting or application telemetry, suspicious authenticated behavior, and API patterns associated with the compromise.

I would keep monitoring through the period in which stale clients can reasonably remain active. A deployment completing successfully does not mean every browser instantly stops running previously downloaded code.

I would avoid collecting sensitive form values merely to prove that exfiltration stopped. Monitoring should use metadata and controlled test accounts where possible.

11. Communicate confirmed impact

I would maintain an incident timeline that separates confirmed facts from assumptions. Engineering, security, incident response, privacy, legal, product, and support teams should share a consistent understanding of affected releases, exposure windows, mitigations, user impact, and remaining uncertainty.

If affected users need to reset passwords, reauthenticate, monitor accounts, or take another protective action, the communication should clearly explain what is known, who is affected when that can be determined, what has been fixed, and what users should do next. I would follow the organization's legal and regulatory notification process rather than inventing notification thresholds myself.

12. Complete root-cause analysis

After containment, I would determine how the malicious package reached the build. Investigation areas might include a compromised upstream maintainer, malicious package release, dependency confusion, registry-account compromise, unsafe installation scripts, CI compromise, unauthorized lockfile changes, or another supply-chain path.

Those are hypotheses, not conclusions. I would name the cause only when the evidence supports it.

The investigation should establish the first malicious package version, dependency path, first affected build, first affected deployment, affected routes, data-access behavior, exfiltration destination, exposure period, and why existing controls did not prevent or detect the incident earlier.

13. Strengthen provenance controls

Provenance means evidence showing where software came from and how it was built. Where ecosystem support exists, I would prefer packages and internal artifacts with verifiable publishing or build provenance.

For our own releases, I would retain the source commit, lockfile, dependency manifest, builder identity or attestation, artifact hashes, and deployment identity. Cryptographically verifiable provenance can make unauthorized substitutions easier to detect and gives incident responders stronger evidence about artifact origin.

Provenance does not prove that source code is safe. A legitimately published package can still contain malicious code.

14. Make dependency changes deterministic and reviewable

I would keep lockfiles committed and require dependency and lockfile changes to go through review. Automated update tools are useful for producing small, visible upgrades, but high-risk dependency changes should still receive human judgment.

For important dependencies, I would review publisher or ownership changes, unexpected new install scripts, unusual release activity, new transitive packages, significant permission or capability changes, and large unexplained code differences where practical.

I would reduce unnecessary dependencies because every dependency adds maintenance and supply-chain exposure. I would not blindly rewrite mature libraries merely to reduce package count, because a poorly implemented replacement can introduce different vulnerabilities.

15. Use integrity controls for the threats they actually address

For separately hosted third-party scripts, Subresource Integrity can allow the browser to verify that a fetched script or stylesheet matches an expected cryptographic hash. For cross-origin resources, the resource must also satisfy the browser's CORS requirements for SRI validation.

SRI does not protect an application bundle that already incorporated malicious dependency code during the build. It also does not tell us whether the expected bytes themselves are benign.

Package-manager lockfiles can contain integrity metadata that helps verify downloaded package bytes against the recorded package artifact. Again, matching expected bytes is an integrity property, not a malware guarantee.

16. Isolate risky third-party browser code where possible

If a third-party capability does not require direct access to the main application's DOM or JavaScript context, I would consider placing it in a sandboxed iframe with only the minimum sandbox permissions necessary.

The exact sandbox flags matter. For example, combining powerful permissions carelessly can undermine the intended isolation, especially for same-origin content. I would design the iframe's origin and permissions deliberately and expose only a narrow validated message interface when cross-context communication is required.

This approach cannot isolate ordinary npm libraries that must execute directly inside the application bundle, so sandboxing is useful only for suitable features rather than being a universal dependency defense.

17. Minimize secrets and sensitive data in frontend code

I would never ship server secrets, private signing keys, database credentials, or long-lived privileged credentials in frontend JavaScript. Anything delivered to a browser should be treated as observable and potentially reachable by compromised first-party JavaScript.

For cookie-based sessions, Secure, HttpOnly, and an appropriate SameSite policy provide useful protections. HttpOnly prevents direct JavaScript reads, Secure restricts cookie transmission to secure transport, and SameSite can reduce some cross-site request risks. None of these makes arbitrary malicious same-origin JavaScript safe.

I would minimize sensitive values placed in the DOM, JavaScript-accessible browser storage, client configuration, and logs. Data that the browser does not need should remain on the trusted server.

18. Use browser security controls as layers, not guarantees

A restrictive CSP can limit script sources and network destinations. Trusted Types can reduce dangerous DOM-based injection paths by requiring trusted values for supported DOM sinks. For untrusted text, I would prefer textContent, createTextNode, safe DOM APIs, or normal framework escaping instead of innerHTML. If the product intentionally accepts HTML, I would use a well-maintained sanitizer appropriate for that HTML context rather than treating generic input filtering as complete protection.

These controls are valuable for XSS and limiting some consequences of injected or third-party code, but they do not make a malicious first-party dependency trustworthy. Code legitimately executing in the application's own bundle can often use capabilities the application itself requires.

The same distinction applies to other browser controls. Same-origin policy and CORS primarily restrict cross-origin access; they do not protect data from malicious JavaScript already executing within the application's origin. CSRF defenses address cross-site request forgery and generally cannot be relied upon to stop arbitrary JavaScript already executing in the trusted page, because that script may access the same tokens and application APIs available to legitimate code.

Clickjacking defenses such as frame-ancestors or X-Frame-Options may still be appropriate for the application, but they do not directly solve this dependency compromise. I would keep the response focused on controls that change the supply-chain incident's risk.

19. Improve rollout and rollback controls

For dependency changes whose risk justifies it, I would use staged or canary rollout instead of immediately exposing every user. I would observe functional failures, unexpected asset behavior, security telemetry, CSP reports, and new network destinations before widening the release.

Promotion should use immutable artifacts so the exact artifact validated in an earlier stage is the one promoted to a larger population rather than rebuilding it differently for each environment.

I would maintain a fast rollback mechanism, but a rollback artifact must be independently known to be safe. Rolling back to an older release that contains the same compromised transitive dependency merely reintroduces the incident.

20. Do not claim scanners prevent supply-chain attacks

Dependency scanners are useful for known vulnerabilities, known malicious packages, suspicious package metadata, and other detectable signals. They should run continuously and their findings should be reviewed according to risk.

But scanners cannot guarantee detection of a newly compromised maintainer, a previously unknown malicious release, a poisoned build process, a malicious package that has not yet been classified, or an authorized package version containing intentionally harmful behavior.

I would therefore use scanners as one layer among provenance, deterministic dependency resolution, dependency review, trusted clean builds, artifact verification, least privilege, appropriate isolation, secret minimization, browser defenses, staged rollout, monitoring, and incident-response readiness.

The practical sequence is: contain exposure, scope the exact released artifacts and routes, preserve evidence, protect affected users, remove the malicious dependency, rebuild from trusted inputs, eliminate stale delivery paths, verify production behavior, monitor recovery, communicate confirmed impact, complete root-cause analysis, and strengthen controls so the next dependency compromise is harder to ship and easier to detect.

Technical Approach
  1. Declare an active security incident and freeze unsafe releases.
  2. Stop or reduce exposure by disabling affected functionality, replacing affected assets, or rolling back only to a release proven safe.
  3. Restrict confirmed exfiltration paths using controls that actually apply: block requests in infrastructure we control, update CSP for new document loads, and use DNS or proxy blocking only for managed client networks.
  4. Trace the transitive package to exact lockfiles, builds, releases, chunks, routes, and exposure timestamps.
  5. Preserve compromised bundles, package metadata, hashes, logs, and deployment records before cleanup.
  6. Determine which user data, credentials, tokens, sessions, or authenticated actions were exposed and apply proportionate resets, rotations, or revocation.
  7. Remove the dependency or pin or override it to an independently verified safe version.
  8. Rebuild in a clean trusted environment using reviewed source and dependency inputs.
  9. Record and verify artifact hashes and provenance.
  10. Deploy new approved assets and invalidate stale CDN, HTML, manifest, and service-worker delivery paths.
  11. Test both fresh and previously cached browser states across every affected route.
  12. Monitor old asset hashes, stale service workers, attacker destinations, suspicious sessions, and related API behavior.
  13. Communicate confirmed impact and required user actions.
  14. Complete root-cause analysis.
  15. Strengthen provenance, lockfile review, dependency review, integrity controls, isolation, secret minimization, staged rollout, and safe rollback.
Practical Insights

The main cost is operational rather than algorithmic. Engineers may need to inspect many dependency trees, historical builds, bundles, routes, environments, caches, service workers, user sessions, and logs. Work grows with the number of releases and users that could be affected. Clean rebuilds, cache purges, session revocation, and staged releases can temporarily slow delivery or inconvenience users. Stronger provenance, review, integrity checks, immutable artifacts, and canary rollouts add ongoing CI and maintenance work, but they reduce uncertainty and make future compromises easier to contain, verify, and investigate. Browser memory usage is not the important cost in this incident; operational investigation and safe deployment dominate.

Why Interviewers Ask This

This question tests whether the candidate can manage a real frontend supply-chain compromise instead of treating dependency security as only a scanner problem. The interviewer is evaluating containment, release and artifact tracing, browser-specific cache behavior, credential and session response, trusted rebuild practices, forensic preservation, monitoring, user communication, and long-term controls such as provenance, lockfiles, integrity verification, sandboxing, secret minimization, and safe rollout.

Common interview mistakes

Common mistakes are treating the incident as solved after changing package.json; checking only the current dependency tree instead of historical released artifacts; assuming a package-registry takedown removes code already bundled into production; claiming the application's server edge can generally block direct browser traffic to an external attacker domain; deleting evidence before preserving it; rebuilding on a potentially contaminated workspace or CI worker; forgetting stale HTML, lazy-loaded chunks, CDN entries, and service-worker caches; claiming every browser cache can be remotely erased; assuming HttpOnly cookies make authenticated sessions completely safe; rotating credentials without considering actual exposure; relying only on CSP, CORS, SRI, lockfiles, or dependency scanners; treating provenance as proof that source code is benign; rolling back to another affected release; logging sensitive form data or tokens during investigation; and failing to test browsers that already contain old cached assets or service workers.

Interview tip

Present the response in incident order: contain, scope, preserve evidence, protect users, remove the dependency, rebuild cleanly, invalidate every stale delivery path you control, verify, monitor, communicate, and then prevent recurrence. Be precise about browser limitations: you cannot recall JavaScript already executing in an open tab, remotely erase every normal browser cache, or rely on scanners and lockfiles to prove a dependency is safe.

Interviewer may ask next
How would you handle users whose browsers may still be controlled by an old service worker containing the compromised bundle?

I would ship a known-safe service-worker version that stops serving the compromised assets, uses reviewed cache-versioning logic, and deletes obsolete affected caches when the new worker activates. If the incident severity justifies faster takeover, skipWaiting and clients.claim can be considered, but I would review the lifecycle tradeoff because they can cause a new worker to control pages that were loaded under an older version. I would also purge stale HTML and CDN references so clients cannot reacquire compromised assets. Then I would test fresh browsers and browsers already controlled by the old worker, and monitor continued requests for old asset hashes or old worker versions. Offline clients cannot receive the fix until they reconnect.

Why are lockfiles, integrity checks, provenance, and vulnerability scanners not enough to prevent this type of supply-chain attack?

Each control answers a different question. A lockfile makes dependency resolution repeatable, but it can repeatedly install a malicious version. Integrity hashes show that bytes match the expected artifact, but the expected artifact itself can be malicious. Provenance can provide evidence about who published or built an artifact and from which source or workflow, but authorized source can still contain harmful code. Vulnerability and malware scanners find known issues and some suspicious signals, but a new compromise may have no signature or advisory. I would combine these controls with dependency review, trusted clean builds, immutable artifacts, minimized privileges and secrets, appropriate isolation, browser defenses, staged rollout, monitoring, and tested incident response.

132. Design an OAuth authorization-code flow with PKCE for a public browser client.SecurityHard

Question Details

A single-page application redirects to an authorization server and receives an authorization code at its callback. Define generation and storage lifetime of state, nonce where applicable, code verifier and challenge, exact redirect URI, code exchange boundary, token audience and lifetime, refresh strategy, browser history cleanup, multi-tab behavior, logout, and error recovery. Identify which values are secrets versus public correlation values and how XSS, code interception, CSRF, and open redirects are addressed.

Short Interview Answer (30-60 seconds)

I would use authorization code with PKCE, fresh state per attempt, an exact registered redirect URI, and no browser client secret. I would validate state before exchanging the code, use the original verifier, keep tokens short-lived and audience-restricted, isolate tab transactions, clean callback history, and treat XSS as a critical remaining risk.

Detailed Explanation

This design lets a website send a person to a trusted sign-in service and safely bring them back after sign-in. The browser receives a short-lived one-time value instead of receiving the final permission immediately. Before leaving, the website creates random values that help prove the returning response belongs to the same sign-in attempt. When the person returns, the website checks those values before continuing. The design must also handle several browser tabs, failed sign-ins, signing out, removing temporary information from the address bar, and preventing malicious pages or scripts from stealing or misusing the sign-in result.

Useful Questions to Ask the Interviewer
  1. Is this a pure SPA that exchanges the code directly with the authorization server, or can we use a backend-for-frontend to keep OAuth tokens on a trusted server?
  2. Is OpenID Connect also being used for authentication, so an ID token and nonce are part of the flow?
  3. Does the authorization server support refresh tokens for public browser clients, refresh-token rotation, and revocation?
  4. Must the application support multiple simultaneous login attempts across different tabs or windows?
  5. What logout behavior is required: local application logout, authorization-server logout, refresh-token revocation, or all of them?
Design an OAuth authorization-code flow with PKCE for a public browser client. diagram
How to Explain It in an Interview
1. Define the trust boundary

A browser SPA is a public OAuth client. JavaScript delivered to a browser cannot securely keep a client secret because users can inspect the application and malicious script running in the page can access browser-visible data. Therefore, I use Authorization Code with PKCE and never embed a client secret or long-lived server credential in frontend code.

OAuth primarily delegates authorization to protected resources. If the application also needs user authentication, I use OpenID Connect on top of OAuth and validate its ID token according to the provider and protocol requirements. Authorization for API operations must always be enforced by the resource server or another trusted server. Hiding a button in the SPA is not authorization.

2. Create a fresh transaction for every authorization attempt

For each login attempt, generate independent cryptographically random values:

  • state: an unpredictable correlation value that binds the authorization response to the request and protects against CSRF-style authorization-response injection and related transaction mix-ups.
  • code_verifier: a high-entropy PKCE value retained only for the short authorization transaction.
  • nonce: when OpenID Connect is used, an unpredictable value placed in the authentication request and later validated against the ID token to reduce replay or token-substitution risks.

The code verifier is sensitive short-lived transaction material because someone who obtains both the authorization code and verifier can attempt redemption during the code's validity window. It is not a permanent application secret. state, nonce, and code_challenge are not client secrets, but state and nonce must still be unpredictable and correctly correlated.

Generate random bytes with the Web Crypto API, not Math.random(), timestamps, counters, or predictable identifiers. Create the PKCE challenge as the base64url-encoded SHA-256 digest of the verifier and send code_challenge_method=S256.

Give each transaction an explicit short expiration, normally only long enough for a user to complete sign-in. Remove it after success, terminal failure, cancellation, or expiry. Do not reuse state, nonce, or a verifier.

3. Handle storage lifetime and multiple tabs

Do not keep one global state and verifier under fixed storage keys. If two tabs start login, one attempt could overwrite the other.

Store one transaction record per authorization attempt. The record can contain the state, verifier, nonce when applicable, creation time, expiry time, expected redirect URI, and a validated local post-login route.

For a pure SPA, tab-scoped sessionStorage can be useful because it naturally separates most independent tab transactions and does not persist like localStorage. However, sessionStorage, localStorage, and ordinary in-memory JavaScript are all reachable by script that successfully executes in the same application origin. They are not protection against XSS.

A browser may also duplicate or restore tabs in ways that affect storage behavior, so the implementation must key and consume transactions uniquely rather than assuming there can be only one active login. If a callback can intentionally arrive in a different browsing context, design an explicit secure correlation mechanism instead of falling back to one shared long-lived credential store.

If a backend-for-frontend is allowed, I prefer keeping the OAuth transaction and tokens on that trusted server and giving the browser only an application session cookie. That reduces direct exposure of OAuth tokens to frontend JavaScript.

4. Build the authorization request

Redirect the browser to the authorization endpoint with the appropriate parameters, including:

  • response_type=code
  • the public client_id
  • the exact registered redirect_uri
  • code_challenge
  • code_challenge_method=S256
  • state
  • required scopes
  • nonce when OpenID Connect requires it

The OAuth redirect URI must be pre-registered and matched according to the authorization server's rules. For this design, use a fixed exact callback URI rather than accepting an arbitrary redirect target from user input.

If the SPA wants to return a user to a particular page after login, keep that destination separately in the transaction. Allow only a validated application-local path or another explicitly allowed destination. Never take an arbitrary returnUrl and redirect to it, because that can create an open redirect.

5. Process the callback defensively

Treat every callback parameter as untrusted input. The callback may contain an authorization code and state or OAuth error parameters.

If an authorization error is returned, handle it as an unauthenticated failure. Show a safe user message and log only useful non-sensitive metadata. Do not log authorization codes, PKCE verifiers, access tokens, refresh tokens, ID tokens, or complete URLs containing sensitive authorization parameters.

For a callback containing a code:

  1. Read the returned state.
  2. Locate exactly one live transaction that matches it.
  3. Reject missing, unknown, expired, consumed, or mismatched state.
  4. Recover the original verifier, expected redirect URI, and nonce if applicable from the stored transaction.
  5. Consume the transaction so that the response cannot be successfully processed twice.
  6. Exchange the authorization code using the original verifier.

Do not exchange the code first and validate state afterward. A state validation failure must stop the flow.

6. Keep the code-exchange boundary clear

For a pure public SPA, the browser can exchange the authorization code at an authorization server token endpoint that supports the required browser access, including the appropriate CORS behavior. The request sends the authorization code, the original code_verifier, the public client identifier where required, the correct grant type, and the same redirect URI used for the authorization request when required by the server. It does not send a client secret.

The authorization server checks that the supplied verifier produces the previously supplied challenge. The authorization code should also be short-lived, single-use, and bound by the authorization server to the appropriate client and redirect context. An attacker who steals only the authorization code should therefore be unable to redeem it without the verifier.

If a backend-for-frontend is available, the browser can instead send the callback result through that trusted application boundary and let the server perform the OAuth token exchange and hold the resulting tokens. That reduces token exposure in browser JavaScript.

7. Use tokens only for their intended purpose

An access token is intended for a particular protected resource or audience. The SPA must not assume that a token accepted by API A is valid for API B. The resource server must validate the token according to its format and deployment, including issuer, intended audience or resource, expiration, integrity, scopes or claims, and the authorization required for the requested operation.

Keep browser-visible access tokens short-lived. The exact lifetime is a deployment decision, so I would not invent one universal number. Shorter lifetimes reduce the useful window after theft but may increase refresh activity and operational complexity.

If OpenID Connect is used, an ID token represents authentication information for the client. It is not a general-purpose API access token. Validate its signature or other integrity mechanism, issuer, audience, expiration, nonce where required, and other protocol-required claims.

8. Refresh carefully

A browser client should not be given an indefinitely reusable credential. If refresh tokens are issued to the public browser client, use the authorization server's recommended public-client protections, including refresh-token rotation where supported. Rotation replaces the previous refresh token, and detected reuse of an invalidated token should trigger the provider's replay response, which may invalidate the token family or require new authorization.

Do not put refresh tokens in source code, URLs, analytics, logs, or unrelated persistent storage. Storing them in localStorage increases exposure to XSS because injected script can read them directly.

If a backend-for-frontend is available, keep OAuth refresh credentials on the trusted server and use a Secure, HttpOnly, appropriately configured SameSite application session cookie in the browser. An HttpOnly cookie prevents normal JavaScript from reading the cookie value, but authenticated requests can still be made by the browser, so the application must apply the appropriate CSRF and origin protections to state-changing operations.

9. Clean browser history

After the callback values have been copied and the transaction has been safely validated or failed, remove OAuth callback parameters from the visible URL with history.replaceState() or equivalent routing behavior.

The authorization code is short-lived and single-use, but leaving it and state values in browser history is unnecessary exposure. URLs can appear in browser history, screenshots, copied links, diagnostics, extensions, and surrounding logging systems.

Never place access tokens or refresh tokens in application query strings or URL fragments as part of this authorization-code design.

10. Address the major threats explicitly
Authorization-code interception

PKCE protects code redemption. The client keeps the verifier and sends only its derived SHA-256 challenge in the authorization request. The original verifier is required when redeeming the authorization code. Stealing the code alone should therefore not be enough.

CSRF and authorization-response injection

Use a fresh unpredictable state value for every attempt. Correlate the callback with the exact outstanding transaction and consume it once. Reject missing, stale, unexpected, or reused state.

The browser same-origin policy and CORS do not replace state. The same-origin policy limits how documents and scripts from different origins interact. CORS allows a server to relax selected cross-origin response-reading restrictions. Neither is the authorization check for the OAuth callback transaction.

Open redirects

Use the exact registered OAuth callback and do not derive it from attacker-controlled data. Treat post-login navigation separately and accept only validated local paths or explicitly allowed destinations.

XSS

XSS remains one of the most serious risks for a browser OAuth client. Malicious script running in the application's origin may read JavaScript-accessible state, verifiers, and tokens, or make authenticated requests using the user's active session.

Use textContent, safe DOM construction, or normal framework escaping when displaying untrusted strings. Do not insert untrusted data with innerHTML. If the product intentionally accepts HTML, sanitize it with a mature allowlist-based HTML sanitizer before using an HTML-rendering sink. Apply output encoding appropriate to the actual context rather than treating generic input filtering as complete XSS protection.

Use a restrictive Content Security Policy as defense in depth. Minimize permitted script sources and prefer nonce- or hash-based script authorization where practical. Trusted Types can further restrict dangerous DOM sinks in browsers that support the policy. Reduce unnecessary third-party scripts because script allowed to execute in the application's origin participates in the same security boundary. Review, update, and protect dependencies and the frontend build supply chain.

11. Apply other browser controls only where relevant

Use HTTPS for authorization, token exchange, APIs, and application sessions.

When using a backend session cookie, configure Secure, HttpOnly, and an appropriate SameSite value. Cookie attributes reduce particular browser risks but do not replace server-side authorization or all CSRF defenses.

Use CSP frame-ancestors when the application should not be embedded by untrusted sites. This reduces clickjacking risk for login-related and sensitive application actions.

Do not treat CORS as authentication or authorization. A resource server must authenticate and authorize requests regardless of whether a browser would allow another origin's JavaScript to read its response.

If the application previews user-uploaded files, avoid executing attacker-controlled active content in the privileged application origin. Depending on the file type, use downloads, sandboxing, validated safe formats, or a separate isolated origin.

Request only necessary scopes and claims. Do not unnecessarily copy identity attributes or token data into analytics, crash reports, local storage, monitoring events, or URLs.

12. Logout correctly

Logout can have several layers.

For local logout, remove application authentication state and outstanding OAuth transaction data. If the browser holds refresh tokens and the provider supports revocation, revoke them when required by the application's security model. If OpenID Connect or the authorization server provides a logout endpoint and single-sign-out behavior is required, follow that provider's defined logout flow and strictly validate or pre-register any post-logout redirect destination.

With a backend-for-frontend, invalidate the server-side application session and expire its browser session cookie. Clearing a JavaScript variable or deleting local storage does not itself revoke an access or refresh token that has already been issued.

13. Recover from errors safely

A failed flow should end in a known unauthenticated state. Delete the failed or expired transaction, clean authorization parameters from the URL when appropriate, and let the user begin a new authorization attempt with fresh state, nonce, verifier, and challenge.

Do not reuse a transaction after timeout, cancellation, token-exchange failure, replay detection, or state mismatch. Prevent redirect loops by distinguishing a normal unauthenticated page from an authorization attempt already in progress.

If token exchange fails after the transaction was consumed, start a new authorization transaction rather than replaying the same authorization code or verifier combination.

14. Verify the controls

I would test the design directly:

  • Change or remove state and verify that the callback is rejected before token exchange.
  • Replay an already consumed callback and verify that it fails.
  • Exchange a valid code with the wrong verifier and verify that the authorization server rejects it.
  • Start simultaneous login attempts in multiple tabs and verify that their transaction data does not overwrite or incorrectly satisfy another attempt.
  • Use expired transaction data and verify safe failure.
  • Attempt an unregistered redirect URI and verify authorization-server rejection.
  • Supply an external post-login return URL and verify that the application refuses it.
  • Verify that authorization codes, verifiers, access tokens, refresh tokens, and ID tokens do not appear in logs or analytics.
  • Verify that callback parameters are removed from browser history after processing.
  • Test common DOM-XSS sinks and verify framework escaping, sanitization where intentional HTML is allowed, CSP, and Trusted Types where deployed.
  • Verify that API authorization succeeds or fails independently of what the SPA UI displays.
  • Verify refresh-token rotation and replay handling when refresh tokens are used.
  • Verify logout invalidates the intended application session or refresh capability rather than only clearing UI state.

The important separation is: PKCE protects authorization-code redemption, state correlates and protects the authorization response transaction, exact redirect handling prevents redirect abuse, token audience and lifetime limit credential misuse, and trusted resource servers enforce authorization. None of these controls make XSS harmless, so minimizing browser-held credentials and preventing script injection remain central parts of the design.

Technical Approach
  1. Generate cryptographically random state, a PKCE verifier, and an OpenID Connect nonce when applicable.
  2. Derive the S256 PKCE challenge from the verifier.
  3. Store a short-lived per-attempt transaction containing the state, verifier, nonce when applicable, expiry, expected redirect URI, and validated local return path.
  4. Redirect to the authorization server with the challenge, state, exact redirect URI, requested scopes, and nonce when applicable.
  5. At callback, handle provider errors safely and validate state against exactly one unexpired, unconsumed transaction before exchanging any code.
  6. Consume the transaction and exchange the one-time authorization code with the original verifier and matching redirect URI.
  7. Validate OpenID Connect token properties when authentication is used and use access tokens only with their intended API audience.
  8. Keep browser-visible access tokens short-lived and use hardened refresh behavior such as rotation when refresh tokens are supported.
  9. Prefer a backend-for-frontend when stronger token isolation is available.
  10. Remove OAuth callback parameters from browser history.
  11. Keep concurrent tab transactions isolated.
  12. On logout, clear local transaction state and revoke or invalidate refresh credentials or server sessions when required.
  13. On failure, discard the transaction and create an entirely new authorization attempt.
  14. Test state mismatch, replay, wrong verifier, expiry, redirects, multi-tab behavior, token leakage, XSS defenses, refresh replay, and server-side authorization.
Practical Insights

The computational cost is very small. Each authorization attempt creates a few random values, performs one SHA-256 digest for PKCE, and reads or removes one small transaction record. For a properly keyed transaction store, these operations are effectively constant time for normal application use. Memory use is also small because only a few short-lived records are needed for active authorization attempts. The important cost is operational complexity: exact redirect registration, token and transaction lifetimes, refresh rotation, logout semantics, CSP, logging rules, multi-tab behavior, provider compatibility, and test coverage. A backend-for-frontend adds server infrastructure and session management, but it can materially reduce OAuth credential exposure to browser JavaScript.

Why Interviewers Ask This

This question tests whether the candidate understands OAuth trust boundaries rather than only memorizing redirect steps. The interviewer is evaluating PKCE generation, transaction correlation, callback validation, token handling, refresh behavior, multi-tab safety, logout, failure recovery, browser storage, XSS, CSRF, authorization-code interception, redirect attacks, token audience, and the important fact that a public browser client cannot safely keep a client secret.

Common interview mistakes

Common mistakes include putting a client secret in the SPA; using predictable state, nonce, or PKCE values; using Math.random() for security values; using PKCE plain instead of S256; reusing a verifier or state; keeping one global transaction that concurrent tabs overwrite; validating state only after exchanging the code; accepting expired or already consumed state; forgetting nonce validation when OpenID Connect requires it; using an inconsistent redirect URI during the exchange; accepting attacker-controlled callback or post-login URLs; putting access or refresh tokens in URLs; storing long-lived refresh credentials in localStorage without addressing XSS exposure; assuming sessionStorage protects against XSS; treating CORS as CSRF protection or authorization; relying on frontend UI checks as API authorization; using an access token for the wrong audience; treating an ID token as an API access token; logging codes, verifiers, or tokens; leaving callback parameters in browser history; replaying failed transactions; assuming local UI logout revokes already issued credentials; and believing PKCE solves XSS. Another major mistake is inserting untrusted values with innerHTML or relying only on input filtering instead of safe DOM construction, context-appropriate output handling, sanitization only when HTML is intentionally allowed, CSP, Trusted Types where appropriate, and careful third-party dependency control.

Interview tip

Explain the design in trust-boundary order: public browser, authorization server, callback validation, token exchange, and protected API. State what state, nonce, verifier, and challenge each do. Then cover token audience and lifetime, refresh handling, browser storage, multiple tabs, URL cleanup, logout, safe failure, and verification. Explicitly say that PKCE does not solve XSS and that authorization must be enforced by the trusted resource server.

Interviewer may ask next
Why is PKCE still needed if the application already validates state?

They protect different parts of the flow. State correlates the authorization response with the browser's original authorization transaction and helps stop CSRF-style response injection and transaction mix-ups. PKCE protects authorization-code redemption by requiring the original verifier. If an attacker obtains only the authorization code, the attacker should not be able to redeem it without that verifier. A secure public-client authorization-code flow therefore normally uses both.

Would you store OAuth tokens in localStorage for this SPA?

I would avoid long-lived OAuth credentials in localStorage when possible because successful XSS in the application origin can read them directly. If a pure SPA must hold an access token, I would minimize its lifetime, scope, persistence, and exposure. When architecture allows, I prefer a backend-for-frontend that keeps OAuth access and refresh tokens on the trusted server and gives the browser a Secure, HttpOnly, appropriately SameSite application session cookie. That reduces direct token theft through JavaScript, although the cookie-based session still needs correct CSRF, origin, logout, and server-side authorization controls.

133. How would you make token refresh safe across multiple browser tabs?SecurityHard

Question Details

Several tabs share an authenticated session. When the access token expires, simultaneous requests can trigger multiple refresh attempts; token rotation invalidates older refresh material. Design coordination, single-flight behavior, result distribution, tab closure, failure and logout propagation, stale response rejection, storage or cookie boundaries, and protection from untrusted same-origin script. Include race tests, server-side rotation and revocation assumptions, and a fallback when cross-tab coordination APIs are unavailable.

Short Interview Answer (30-60 seconds)

I would single-flight refreshes inside each tab, use Web Locks for one cross-tab leader when available, and distribute new short-lived access state with BroadcastChannel. The refresh token stays in an HttpOnly cookie. The server atomically rotates credentials, handles duplicate refreshes safely, rejects stale state, and enforces authorization.

Detailed Explanation

See the Code while reading this explanation.

Several open pages may notice at almost the same time that a person's sign-in needs renewing. If every page tries to renew it separately, one renewal can make another renewal invalid. That can cause failed requests, accidental sign-outs, or an older result replacing a newer one. I would make the pages cooperate so normally only one performs the renewal while the others wait. I would also make the server safe when that cooperation fails, a page closes, messages arrive late, the network fails, or harmful page code is running.

Useful Questions to Ask the Interviewer
  1. Is the refresh credential already stored in an HttpOnly Secure cookie, or is JavaScript expected to hold it?
  2. Does the server use rotating refresh tokens, and does it expose a session generation, version, or equivalent freshness value?
  3. What replay policy should apply if two refresh requests present the same rotating credential at nearly the same time?
  4. Must all tabs share the same access token, or may they use separate short-lived access tokens for the same authenticated session?
  5. Which browser versions must be supported, especially for Web Locks and BroadcastChannel?
  6. What server response distinguishes an expired or revoked session from an expected duplicate refresh race or a temporary failure?
How would you make token refresh safe across multiple browser tabs? diagram
How to Explain It in an Interview

I would separate the design into two layers: browser coordination and trusted-server correctness.

Inside one tab, I would use a single-flight promise. Single-flight means that if several requests notice an expired access token together, they all wait for the same refresh operation instead of starting several refreshes.

Across tabs, I would prefer the Web Locks API. Every tab requests the same application-scoped refresh lock. Only the tab that acquires the lock may call the refresh endpoint. After acquiring the lock, it must check the current access-token state again because another tab may have refreshed while this tab was waiting. If the leader tab closes or crashes, the browser releases its lock, allowing another tab to continue.

After a successful refresh, the leader can publish the new short-lived access state through BroadcastChannel. I would include only information needed for coordination, such as the access token if the architecture requires sharing it, its expiry time, and a monotonically increasing server-issued generation or version. I would never broadcast the refresh token.

Every tab accepts only state newer than what it already has. This protects against delayed messages and out-of-order network responses. For example, if generation 42 has already been accepted, a delayed generation 41 result must be ignored. Client generation checks are defensive coordination logic; the server remains the authority for whether a token is valid.

The refresh credential should normally be unavailable to JavaScript. I would store it in an HttpOnly, Secure cookie with an appropriate SameSite policy and the narrowest practical Path and Domain. JavaScript can call the refresh endpoint with credentials, but it cannot directly read the cookie value. The access token should be short-lived and preferably kept in memory rather than persistent browser storage such as localStorage.

The server is the real security boundary. Refresh-token rotation must be atomic. A successful refresh consumes the currently valid refresh credential and produces the next valid state as one indivisible server-side operation. Two concurrent requests must never create two independent valid successor chains.

The exact duplicate-request policy must be designed deliberately. A strict server may accept the first refresh, reject later use of the consumed credential, and revoke the token family if reuse indicates likely theft. Another design may provide a very small, carefully implemented idempotency or retry window so an expected duplicate request can obtain the already-created successor state without creating a second chain. The browser must not guess which condition occurred from a generic status code.

That distinction matters for multi-tab races. If one tab successfully rotates the token and another nearly simultaneous request receives a duplicate or stale-refresh response, the second tab should not automatically broadcast logout unless the server says the session itself is invalid or revoked. Otherwise a harmless duplicate could destroy a valid session that another tab just refreshed.

Logout must also be coordinated. The trusted server revokes the session or refresh-token family. The initiating tab clears its in-memory access token and broadcasts a logout event. Other tabs clear their in-memory state and advance a local logout epoch or equivalent version. Any refresh operation that started before that logout must be rejected when it later completes, so an old in-flight response cannot silently sign the user back in.

Authentication and authorization are different responsibilities. Refresh establishes or renews authenticated session state. It does not decide what the user is allowed to do. Every protected API must enforce authorization on the trusted server for each operation.

Browser coordination APIs are not security boundaries. Web Locks, BroadcastChannel, localStorage, and JavaScript state all live within the origin. If an attacker gains same-origin script execution through XSS, that script can participate in those APIs and can make requests as the user. The design therefore has to remain safe even if a malicious script ignores the frontend lock entirely and sends refresh requests directly.

For that reason, I would reduce XSS impact separately. I would keep long-lived refresh material in an HttpOnly cookie, use short-lived access tokens, construct untrusted text with textContent or safe DOM APIs, rely on framework escaping where appropriate, never send untrusted data to innerHTML, sanitize only when the product intentionally permits HTML, use a restrictive Content Security Policy, enable Trusted Types where practical, tightly control third-party scripts, and review dependency and supply-chain risk.

If the refresh endpoint uses cookies, I would also design for CSRF. SameSite cookies are useful but may not be sufficient for every deployment. Depending on the application's cross-site requirements, the server can require an anti-CSRF token or another intentional CSRF defense for state-changing cookie-authenticated requests. CORS is not a substitute for CSRF protection, and the same-origin policy does not prevent every cross-site request from being sent.

If Web Locks is unavailable, I would degrade coordination rather than security. BroadcastChannel can still be used to announce refresh completion and logout, but simultaneous leaders remain possible. A storage event with a short-lived localStorage coordination record can be a further compatibility fallback. That record is only an advisory hint. It must contain no refresh token, server secret, long-lived credential, or authorization decision because any same-origin script can read or modify it.

If no usable cross-tab coordination API exists, each tab may independently attempt refresh. The server's atomic rotation, replay handling, revocation, expiry checks, and authorization must still keep the session correct. The fallback may cause extra requests or one tab to retry, but it must not create a security failure.

Failure handling should distinguish three cases. First, a definitive expired, revoked, or invalid session should fail closed: clear local authentication state, propagate logout, stop refresh loops, and require authentication again. Second, a server-defined duplicate or stale-refresh race should reconcile with the valid newer state rather than automatically logging out. Third, temporary network failures or 5xx responses should use bounded retry and backoff without erasing an otherwise valid server session unnecessarily.

Logging must never include access tokens, refresh tokens, cookies, authorization headers, or other secrets. Useful diagnostic fields include a correlation ID, a non-secret session-family identifier, refresh generation, result category, replay decision, and timing.

I would verify the design with race tests rather than only testing the happy path. I would start simultaneous protected requests from several tabs after access-token expiry and verify that normally only one browser refresh occurs when Web Locks works. Then I would deliberately bypass coordination and send two refreshes at the same time to prove that the server remains correct. I would close the leader while it holds the lock, delay BroadcastChannel messages, reorder refresh responses, deliver a stale generation after a newer generation, log out while refresh is in flight, simulate refresh-token replay, remove Web Locks and BroadcastChannel, and inject temporary network failures. The expected properties are no duplicate valid refresh chains, no stale-state overwrite, no resurrection after logout, no long-lived token exposure to JavaScript, deterministic handling of duplicate races, and server-enforced authorization throughout.

Key Insight / Why This Solution Works
  1. Keep the rotating refresh credential in an HttpOnly Secure cookie and keep short-lived access state in memory when practical.
  2. Deduplicate refresh calls inside each tab with one shared in-flight promise.
  3. When Web Locks exists, acquire one shared refresh lock across tabs.
  4. After acquiring the lock, re-check whether another tab already supplied a fresh access token.
  5. Call the trusted refresh endpoint only if refresh is still required.
  6. Have the server atomically validate, consume, rotate, expire, and revoke refresh state according to its replay policy.
  7. Return a server-issued freshness value such as a generation or session version with the new access state.
  8. Accept only a result newer than the current local generation and newer than the current logout epoch.
  9. Broadcast successful short-lived access state through BroadcastChannel without exposing the refresh token.
  10. On server-confirmed session revocation or expiry, clear local authentication state and propagate logout to every tab.
  11. Do not treat an expected duplicate-refresh race as logout unless the server explicitly says the session is invalid.
  12. Fall back to best-effort BroadcastChannel or storage-event coordination when Web Locks is missing, while relying on server-side atomic rotation for safety.
  13. Test simultaneous refresh, leader closure, delayed messages, stale responses, replay, logout races, network failures, and complete loss of cross-tab coordination.
Code
const authChannel =
  'BroadcastChannel' in globalThis ? new BroadcastChannel('auth-session-v1') : null;

let accessToken = null;
let expiresAt = 0;
let generation = 0;
let logoutEpoch = 0;
let refreshInFlight = null;

function tokenIsUsable() {
  // Keep a small expiry margin so a request is not started with a token that
  // is likely to expire while it is travelling to the trusted API.
  return Boolean(accessToken) && Date.now() + 10_000 < expiresAt;
}

function isValidTokenState(state) {
  // Data from the network or another same-origin tab is input, not an
  // authorization decision. Validate its shape before using it locally.
  return (
    Boolean(state) &&
    typeof state === 'object' &&
    typeof state.accessToken === 'string' &&
    Number.isFinite(state.expiresAt) &&
    Number.isInteger(state.generation) &&
    Number.isInteger(state.logoutEpoch)
  );
}

function adoptTokenState(state) {
  if (!isValidTokenState(state)) return false;

  // Never allow a refresh that began before the latest logout to restore the
  // session after logout has already propagated to this tab.
  if (state.logoutEpoch < logoutEpoch) return false;

  // Reject delayed or reordered results. Only a strictly newer server-issued
  // generation may replace the current access state.
  if (state.generation <= generation) return false;

  accessToken = state.accessToken;
  expiresAt = state.expiresAt;
  generation = state.generation;
  logoutEpoch = state.logoutEpoch;
  return true;
}

function clearLocalAuthentication(nextLogoutEpoch = logoutEpoch) {
  // Long-lived refresh material is not stored here. It is assumed to live in
  // an HttpOnly Secure cookie that JavaScript cannot directly read or copy.
  accessToken = null;
  expiresAt = 0;
  generation = 0;
  logoutEpoch = Math.max(logoutEpoch, nextLogoutEpoch);
}

authChannel?.addEventListener('message', (event) => {
  // BroadcastChannel is not a trust boundary. Compromised same-origin script
  // can send messages too, so these messages only coordinate local state.
  const message = event.data;
  if (!message || typeof message !== 'object') return;

  if (message.type === 'token') {
    adoptTokenState(message.state);
    return;
  }

  if (
    message.type === 'logout' &&
    Number.isInteger(message.logoutEpoch) &&
    message.logoutEpoch >= logoutEpoch
  ) {
    // Advance the logout epoch so older in-flight refresh results cannot
    // restore credentials after logout.
    clearLocalAuthentication(message.logoutEpoch);
  }
});

async function callRefreshEndpoint() {
  // The browser sends the HttpOnly refresh cookie automatically. JavaScript
  // never reads, logs, stores, or broadcasts the rotating refresh credential.
  const response = await fetch('/auth/refresh', {
    method: 'POST',
    credentials: 'same-origin',
    headers: {
      Accept: 'application/json',
    },
  });

  let body = null;
  if (response.headers.get('content-type')?.includes('application/json')) {
    body = await response.json();
  }

  if (!response.ok) {
    // The trusted server must distinguish a true invalid session from an
    // expected duplicate refresh race. A generic 401 alone is not enough for
    // the client to decide that every tab should be logged out.
    if (body?.code === 'SESSION_INVALID' || body?.code === 'SESSION_REVOKED') {
      return { kind: 'session-invalid' };
    }

    if (body?.code === 'REFRESH_DUPLICATE' || body?.code === 'REFRESH_STALE') {
      return { kind: 'duplicate' };
    }

    // Temporary failures fail safely without exposing secrets or entering an
    // unlimited retry loop. The caller may apply bounded backoff.
    throw new Error(`Temporary refresh failure: ${response.status}`);
  }

  // Validate the remote API boundary before accepting authentication state.
  // Authorization is still enforced independently by protected server APIs.
  if (
    !body ||
    typeof body.accessToken !== 'string' ||
    !Number.isFinite(body.expiresAt) ||
    !Number.isInteger(body.generation)
  ) {
    throw new Error('Invalid refresh response');
  }

  return {
    kind: 'success',
    state: {
      accessToken: body.accessToken,
      expiresAt: body.expiresAt,
      generation: body.generation,
      logoutEpoch,
    },
  };
}

async function refreshAsLeader() {
  // A waiting tab must re-check state after it becomes leader because another
  // tab may have completed refresh while this tab waited for the lock.
  if (tokenIsUsable()) return accessToken;

  const requestLogoutEpoch = logoutEpoch;
  const result = await callRefreshEndpoint();

  if (result.kind === 'session-invalid') {
    // Only a server-confirmed expired or revoked session triggers global
    // logout. This avoids turning a harmless duplicate refresh race into an
    // unnecessary sign-out of every tab.
    const nextEpoch = logoutEpoch + 1;
    clearLocalAuthentication(nextEpoch);
    authChannel?.postMessage({ type: 'logout', logoutEpoch: nextEpoch });
    throw new Error('Session is no longer valid');
  }

  if (result.kind === 'duplicate') {
    // Another tab may have won the rotation race. Do not manufacture new auth
    // state or log out automatically; wait for its broadcast when available.
    if (tokenIsUsable()) return accessToken;
    throw new Error('Refresh was superseded by another refresh');
  }

  // If logout happened while the refresh request was in flight, discard the
  // result even when its generation is otherwise newer.
  if (requestLogoutEpoch !== logoutEpoch) {
    throw new Error('Refresh result became stale after logout');
  }

  if (!adoptTokenState(result.state)) {
    // Safe failure is better than overwriting newer authentication state with
    // a delayed response.
    if (tokenIsUsable()) return accessToken;
    throw new Error('Stale refresh result rejected');
  }

  // Only short-lived access state is distributed. The rotating refresh token
  // remains inside the HttpOnly cookie and never crosses this JS channel.
  authChannel?.postMessage({ type: 'token', state: result.state });
  return accessToken;
}

async function coordinatedRefresh() {
  if (tokenIsUsable()) return accessToken;

  // Single-flight inside this tab makes concurrent local callers share one
  // refresh operation instead of creating their own races.
  if (refreshInFlight) return refreshInFlight;

  refreshInFlight = (async () => {
    try {
      if (navigator.locks?.request) {
        // Web Locks provides best-effort leadership across tabs. It reduces
        // duplicate work but is not trusted for security; the server must still
        // be correct if another script or browser context ignores the lock.
        return await navigator.locks.request('auth-refresh-v1', async () => {
          if (tokenIsUsable()) return accessToken;
          return refreshAsLeader();
        });
      }

      // Without Web Locks, duplicate refreshes are possible. Security therefore
      // depends on atomic server rotation and a defined duplicate/replay policy,
      // not on browser synchronization.
      return await refreshAsLeader();
    } finally {
      refreshInFlight = null;
    }
  })();

  return refreshInFlight;
}

async function logout() {
  // Server revocation is authoritative. Clearing browser memory alone does not
  // revoke refresh material that may exist on another tab or device.
  try {
    await fetch('/auth/logout', {
      method: 'POST',
      credentials: 'same-origin',
    });
  } finally {
    const nextEpoch = logoutEpoch + 1;
    clearLocalAuthentication(nextEpoch);
    authChannel?.postMessage({ type: 'logout', logoutEpoch: nextEpoch });
  }
}

export { coordinatedRefresh, logout };
Why Interviewers Ask This

This tests whether the candidate understands authentication races across browser tabs and knows that frontend coordination cannot be the security boundary. A strong answer combines browser concurrency control, short-lived token handling, stale-response rejection, logout propagation, XSS exposure limits, server-side atomic refresh-token rotation and revocation, safe failure behavior, and race-condition testing.

Common interview mistakes

Common mistakes are storing a long-lived refresh token in localStorage; allowing every tab to refresh independently without a defined server race policy; assuming Web Locks, BroadcastChannel, or localStorage are security boundaries; rotating refresh tokens only in frontend logic instead of atomically on the server; broadcasting the refresh token; accepting whichever response arrives last without a generation check; allowing an in-flight refresh to restore authentication after logout; treating every duplicate refresh failure as proof that the whole session is revoked; retrying indefinitely after permanent failure; treating authentication as authorization; logging tokens or cookies; assuming CORS prevents CSRF; and claiming cross-tab coordination protects against XSS. Another major mistake is testing only normal refresh and never testing leader closure, replay, delayed messages, reordered responses, logout races, temporary failures, or unavailable coordination APIs.

Interview tip

Explain the design in two layers: browser coordination reduces duplicate refreshes, while atomic server rotation, revocation, replay handling, and authorization provide security. Call out the hardest race explicitly: a losing duplicate refresh must not incorrectly log out a session that another tab just refreshed.

Interviewer may ask next
What happens if two tabs still send the same rotating refresh token to the server at exactly the same time?

The trusted server must resolve the race atomically. Only one request may consume the current refresh credential and establish the next valid state. The other request must receive a deterministic result under the server's replay policy. A strict design may reject reuse and revoke the token family when reuse is suspicious. A different design may use a tiny idempotency or duplicate-request window that returns the already-created successor state without creating another refresh chain. In either case, two valid independent successor chains must never be created, and the frontend must not assume that every losing duplicate means the whole session should be logged out.

How would you handle browsers where Web Locks or BroadcastChannel are unavailable?

I would degrade coordination without degrading security. Without Web Locks, BroadcastChannel can still announce refresh completion and logout, although two tabs may start refresh concurrently. Without BroadcastChannel, the storage event can carry a short-lived advisory coordination record, but that record must contain no refresh token, secret, or authorization decision because same-origin JavaScript can read or change it. If neither mechanism is usable, tabs may refresh independently. Atomic server rotation, explicit duplicate and replay handling, generation checks, expiry, revocation, and server-side authorization must still keep the session safe.

Disclaimer: This interview guide is for educational and informational purposes only. It is designed to help readers prepare, but it does not guarantee any interview result, hiring decision, offer, or outcome. Interview questions, hiring criteria, and preferred answers can vary by employer, interviewer, industry, location, and time. The examples and explanations reflect the authors' research and judgment, are provided without warranties of any kind, and should not be treated as the only correct approach. Diagrams are simplified illustrations intended to highlight the main components and their interactions; actual systems and implementations may be more complex. Alternative approaches may be equally valid or better suited to a particular question, context, or interviewer. To the fullest extent permitted by applicable law, the author, contributors, and publisher are not liable for decisions made, actions taken, or losses incurred based on this guide.