Token Exfiltration Defence

How oidc-spa mitigates the risks of token exposure

oidc-spa implements a comprehensive, defense-in-depth strategy to protect against token exfiltration during a successful XSS or supply-chain attack.

The objective is to achieve, in a purely client-side architecture, a level of token safety comparable to traditional backend-based authentication (session cookies). The concerns raised in the talk below no longer apply when the exfiltration defence is enabled.

With the defence enabled, an attacker cannot read or request valid tokens. This is the same property provided by backend session cookies.

Enabling the Exfiltration Defence

Enabling the defence is simply a matter of flipping a switch:

vite.config.ts
import { oidcSpa } from "oidc-spa/vite-plugin";

export default {
  plugins: [
    // ...
    oidcSpa({
      enableTokenExfiltrationDefense: true,
      // If you access external resource servers, (other than you own server APIs)
      // you must declare them.
      //resourceServersAllowedHostnames: ["vault.my-company.com", "s3.my-company.com"]
    })
  ]
};

Understanding the Security Guarantees (and Their Limits)

Supply-Chain Attacks

If an NPM dependency is compromised, the damage remains extremely limited:

  • The attacker cannot exfiltrate valid tokens

  • This blocks the most common and impactful class of supply-chain attacks • Most real-world supply-chain malware is opportunistic, not targeted

An attacker could theoretically act on behalf of the user during the active compromise, but:

  • This requires a targeted attack specifically against your build

  • This is realistic only for massive, high-value open-source systems

  • Even then, oidc-spa makes it very difficult

Why? Because unlike session-cookie auth, where any fetch() automatically includes credentials, here the attacker must obtain a reference to your fetchWithAuth() or getOidc() functions.

These functions usually live inside hashed static assets (example: assets/KcAdminUi-BV3D797K.js). The hash will likely differ between the moment the attacker crafts the exploit and the moment the compromised dependency lands in your build.

Additionally, oidc-spa blocks the discovery of the module graph[^1].

Bottom line: For supply-chain attacks, oidc-spa offers stronger protection than traditional session cookies.

XSS Attacks

XSS remains dangerous. XSS is always targeted and assumes full knowledge of your application, including your module graph.

An attacker can:

  • import your fetchWithAuth() implementation

  • perform any action the current user is allowed to perform

This is exactly the same situation as cookie-based auth. Cookies don’t help here either, if anything, they make it easier.

So, this does not mean oidc-spa is less secure than cookie auth. Both are equally vulnerable to XSS.

The good news is that XSS can be very effectively blocked with strict Content-Security-Policy (CSP). And you should absolutely enable one.

Bottom line: XSS attacks can still allow the attacker to act on behalf of the user. Token exfiltration is prevented, but XSS must still be mitigated with CSP.

CSP Configuration

Compromised Browser Extensions

This is the one scenario where cookie-based auth has an advantage.

If a user installs a malicious browser extension, it can inspect outgoing network traffic and see the substituted tokens.

This affects only the user with the compromised extension—and it affects all SPAs using client-side auth, not just your app.

How oidc-spa Achieves This

The entire strategy relies on the fact that, thanks to the Vite plugin or oidcSpaEarlyInit, oidc-spa gets a guaranteed window of execution before any other JavaScript runs.

During that window, it can:

  • Harden the environment by preventing monkey-patching of fetch, XHR, WebSocket, Promise, String, and other critical built-ins

  • Safely extract the authorization response from the URL and store it in memory

  • Register a message listener that cannot be unregistered, ensuring silent-signin integrity • Enforce restrictions on service worker registration

  • And most importantly:

Tokens are never exposed to the application layer

The tokens your app sees are structurally valid JWTs, but the signature segment is replaced. Such tokens cannot be used to authenticate requests.

Before any request leaves the app (fetch, XHR, WebSocket, beacon), the real tokens are restored inside a hardened, sandboxed pre-network interceptor created during early init.

The only way to see the real token is to inspect network traffic.

These protections have zero impact on DX or performance. The only requirement is to avoid libraries that monkey-patch critical built-ins.


Last updated

Was this helpful?