Skip to content

Security and Privacy

Sebastian F. Markdanner [MVP] edited this page May 11, 2026 · 2 revisions

The PIMActivation Portal is built around two non-negotiables:

  1. No backend. Every privileged call goes from your browser directly to Microsoft Graph or Azure Resource Manager.
  2. Tokens never leave your browser, and they don't survive the tab. MSAL.js caches them in sessionStorage only.

This page lays out what that means in practice, what the threat model is, and what mitigations are in place.

Data flow

┌──────────────────────────────┐
│  Your browser                │
│                              │
│  Portal SPA  ──────────────► login.microsoftonline.com (auth)
│              ──────────────► graph.microsoft.com         (Entra + Group PIM)
│              ──────────────► management.azure.com        (Azure Resource PIM)
└──────────────────────────────┘

There is no other arrow. No telemetry, no analytics, no third-party APIs. The Content Security Policy enforces that.

Content Security Policy

Portal/staticwebapp.config.json sets:

default-src 'self';
script-src  'self' https://cdn.jsdelivr.net;
connect-src 'self' https://login.microsoftonline.com
                   https://graph.microsoft.com
                   https://management.azure.com;
img-src     'self' data:;
style-src   'self' 'unsafe-inline';
frame-ancestors 'none';
upgrade-insecure-requests

What this gives you:

  • No inline scripts and no eval. Any <script> must come from the same origin (or, for MSAL.js, from cdn.jsdelivr.net).
  • No unauthorized network destinations. A bug or compromise that tries to exfiltrate data to an attacker-controlled host is blocked by the browser at the CSP layer.
  • No clickjacking. frame-ancestors 'none' and X-Frame-Options: DENY prevent the portal from being embedded.
  • HTTPS upgrade. Mixed-content requests are upgraded.

style-src 'unsafe-inline' is required for the theme-switching mechanism (CSS custom properties applied at runtime). It is mitigated by the absence of any inline <script> and by the strict script-src.

Global response headers

Header Value Why
Strict-Transport-Security max-age=31536000; includeSubDomains; preload Forces HTTPS for the entire eTLD+1
X-Content-Type-Options nosniff Stops MIME sniffing
X-Frame-Options DENY Belt-and-braces clickjacking defense
X-XSS-Protection 1; mode=block Legacy XSS auditor for older browsers
Referrer-Policy strict-origin-when-cross-origin Limits Referer leakage
Permissions-Policy camera=(), microphone=(), geolocation=(), payment=() Disables sensors the portal never needs

Authentication

  • MSAL.js v5 (loaded from the only allowed third-party origin, cdn.jsdelivr.net).
  • Authorization code flow with PKCE. No implicit grant.
  • Redirect flow for sign-in and sign-out (not popup) — survives strict popup blockers and works on mobile.
  • Token cache: sessionStorage only. Tokens disappear when the tab closes. There is no refresh-token persistence.

Conditional Access claims handling

When a Conditional Access policy issues a claims challenge, the portal:

  1. Decodes the challenge from a WWW-Authenticate: insufficient_claims header (or a JSON-encoded body).
  2. Persists the in-flight operation state to sessionStorage.
  3. Calls acquireTokenRedirect with the decoded claims parameter.
  4. Resumes the operation after sign-in returns.
  5. Threads the same claims into every subsequent token acquisition for the rest of the operation, so all Graph and ARM calls satisfy the requirement without prompting again.

For role policies that require an auth context proactively, the same flow runs before submit instead of in response to a 401.

What goes where

Store What When it's gone
sessionStorage MSAL access tokens, MSAL ID tokens, in-flight operation state during step-up, preferred tenant for the session Tab closes
localStorage Feature flags, theme, role cache (per tenant), saved filter pills Manual clear
IndexedDB Activation profiles Manual clear
In-memory Policy cache (30 min), batch state, role render state Page reloads
Cookies None set by the portal itself n/a
Telemetry None n/a

Threat model in short

Threat Mitigation
Server breach exfiltrating tokens No server. Tokens never reach a server we run.
XSS in the portal stealing tokens Strict CSP forbids inline / unsafe scripts and limits script origins to self and a single CDN. No eval. Tokens live in sessionStorage and are scoped to the tab.
Network exfiltration CSP connect-src allow-list blocks any network destination other than Microsoft auth, Graph, and ARM.
Clickjacking frame-ancestors 'none' and X-Frame-Options: DENY.
Subdomain takeover of the CDN MSAL.js is loaded over HTTPS from cdn.jsdelivr.net. SRI pinning is on the roadmap. If that origin were compromised, the CSP still prevents it from talking to anywhere except Microsoft endpoints.
Stolen browser session An attacker with full access to your browser session is already inside your trust boundary. Use OS-level protections (lock screen, FIDO2) and browser profile separation.
Compromised app registration (self-hosted) Use single-tenant registrations for self-hosted deployments and review the redirect URIs and consented scopes regularly. The portal does not require any client secret.

Reporting a vulnerability

For sensitive security reports, use GitHub's private security advisory flow rather than a public issue. Public issues are fine for non-sensitive hardening suggestions and CSP / header improvements.

Privacy

The portal stores no personal data outside your browser. It does not send any data to the maintainer or any third party. There is no telemetry, no analytics, and no logging endpoint. The only network destinations the portal contacts are:

  • login.microsoftonline.com (Microsoft authentication)
  • graph.microsoft.com (Microsoft Graph)
  • management.azure.com (Azure Resource Manager)
  • cdn.jsdelivr.net (MSAL.js bundle, no API surface, no observation)

Clone this wiki locally