Skip to content

How It Works

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

The PIMActivation Portal is a single-page application. It has no backend, no proxy, and no server-side session. Everything runs in your browser — including authentication, policy resolution, the batch engine, and progressive rendering. This page walks through the architecture from boot to bulk activation.

Top-level layout

Portal/
├── index.html
├── css/portal.css
├── js/
│   ├── app.js                 # Bootstrap and event wiring
│   ├── auth.js                # MSAL wrapper (redirect flow, claims threading)
│   ├── msal-config.js         # Client / tenant ID — injected at deploy time
│   ├── policy-cache.js        # 30-minute in-memory policy cache
│   ├── profiles.js            # IndexedDB profile manager
│   ├── roles.js               # Progressive rendering and expiry timers
│   └── api/
│       ├── arm-client.js      # ARM activate / deactivate
│       ├── batch-client.js    # Bulk engine (Graph $batch + ARM concurrency)
│       └── graph-client.js    # Graph + $batch with 429 retry
├── manifest.json
└── staticwebapp.config.json   # SPA routing, CSP, security headers

No bundler, no framework — vanilla JavaScript loaded as <script> tags from index.html in dependency order.

Boot sequence

  1. index.html loads MSAL.js v5 from cdn.jsdelivr.net (the only allowed third-party origin under the CSP), then loads msal-config.js, the API clients, and finally app.js.
  2. app.js constructs an MSAL PublicClientApplication from window.msalConfig, processes any pending redirect response, and either renders the signed-in shell or shows the sign-in CTA.
  3. On sign-in, the portal acquires tokens for Graph and ARM scopes (defined in msal-config.js) silently and starts fetching roles in parallel.

Authentication

auth.js wraps MSAL with three responsibilities:

  • Sign-in / sign-out via loginRedirect / logoutRedirect. The redirect flow is used (not popup) so token acquisition survives strict popup blockers and works on mobile.
  • Token acquisition via acquireTokenSilent with fallback to acquireTokenRedirect on interaction_required.
  • Claims threading — when an API call fails with a Conditional Access claims challenge, the portal stores the pending operation, calls acquireTokenRedirect with the decoded claims parameter, and then threads the same claims into every subsequent token acquisition for the operation.

Tokens are cached in sessionStorage only. There is no refresh-token persistence.

API clients

Graph client (api/graph-client.js)

  • Issues GET requests for eligibility / assignment schedules and policies.
  • Bundles bulk POSTs into Microsoft Graph $batch requests, chunked at 20 (the Graph batch limit).
  • Detects 429 responses, honors Retry-After headers, and applies exponential backoff.
  • Detects 401 with WWW-Authenticate: insufficient_claims (or claims-bearing JSON body) and surfaces a ClaimsChallengeError to the orchestrator.

ARM client (api/arm-client.js)

  • Discovers Azure Resource role eligibilities and assignments via tenant-root asTarget() queries — no per-subscription enumeration.
  • PUTs activation requests against roleAssignmentScheduleRequests.
  • Surfaces the same claims-challenge errors so the orchestrator can step up.

Batch engine (api/batch-client.js)

The orchestrator that powers bulk activation:

  • Splits the selected roles by plane.
  • Routes Entra and Group operations through Graph $batch (20 per request).
  • Runs Azure Resource operations as concurrent ARM calls under a small concurrency limit, using Promise.allSettled so one failure does not abort the batch.
  • Aggregates per-role outcomes into the activity history.
  • On a claims challenge from any plane, pauses the batch, performs the step-up, then resumes with the new claims threaded in.

Policy enrichment

policy-cache.js fetches role policies in bulk:

  • For Entra, policies are fetched via tenant-root role-management policy assignment queries to avoid per-Administrative-Unit permission issues.
  • For Groups and Azure Resources, per-policy fetches are made on first use.
  • Results are cached in memory for 30 minutes keyed by tenant.

The policy object exposes the matrix the UI renders: requiresJustification, requiresTicket, requiresMfa, requiresAuthContext (with authContextId), requiresApproval, and maxDurationHours.

Roles, rendering, and timers

roles.js is the rendering brain:

  • Holds an in-memory map of roles keyed by a stable uid so selection state survives refreshes.
  • Renders eligible and active tables progressively as plane fetches resolve — Entra and Group results appear first; Azure usually arrives shortly after.
  • Runs a 30-second tick to update the live countdown on active roles and recompute colour-coded urgency.
  • De-duplicates Azure roles when the same role is inherited from multiple ancestor scopes (the closest ancestor wins).

Activation profiles (IndexedDB)

profiles.js manages a single IndexedDB object store. Each profile carries an id, name, an array of role uids, optional pre-filled justification / ticket / duration override, an optional tenant id (when tenant-scoped profiles are enabled), and createdAt / lastUsedAt timestamps.

Profiles are listed by last use so frequent rotations stay at the top.

Storage map

Store What it holds Cleared when
sessionStorage MSAL tokens, pending activation state during step-up, preferred tenant for the session Tab closes
localStorage Feature flags, theme, role cache, saved filters Cleared by user / Disconnect flow
IndexedDB Activation profiles Cleared by user / browser tools
In-memory Policy cache (30 min TTL), role render state, batch state Page reloads

Routing and CSP

staticwebapp.config.json defines:

  • A SPA fallback that rewrites unmatched routes to /index.html.
  • A strict Content Security Policy (see Security and Privacy for the full breakdown).
  • Cache-control headers that keep /js/*, /css/*, /favicons/*, and /manifest.json fresh.

Why no backend?

A backend would need to store or proxy access tokens, which fundamentally changes the security model. Keeping every privileged call browser-direct means:

  • No second place where tokens could leak.
  • No data residency questions — your tokens never leave Microsoft's auth surface plus your browser.
  • No service to run, scale, or patch.
  • Self-hosting collapses to "drop the static files into Azure Static Web Apps" with a Bicep template.

Clone this wiki locally