---
title: "MSAL.js v5 Vanilla JS PKCE (No Bundler)"
aliases: [msal-umd, msal-script-tag, msal-spa-pkce, msal-vanilla]
tags: [azure-ad, msal, pkce, spa, vanilla-js, auth]
sources:
  - "daily/2026-04-15.md"
created: 2026-04-15
updated: 2026-04-15
---

# MSAL.js v5 Vanilla JS PKCE (No Bundler)

MSAL.js v5 can be used in plain HTML/JS projects via its UMD build without a bundler. The library exposes `window.msal` from a `<script>` tag. This is appropriate for lightweight dashboards or static sites that don't use React/Next.js.

## Key Points

- MSAL v5 ships a UMD build (`msal-browser.min.js`) that works with a `<script>` tag — no npm/bundler required
- The Azure Portal app registration **platform type must be "Single-page application"** for PKCE — if set to "Web", Azure requires a client secret and the flow will fail silently
- PKCE flow requires no client secret — the code verifier/challenge pair handles the security
- Backend token validation can use Node.js built-in `crypto` module + a JWKS fetch from Azure with a 24h cache — no new npm dependencies needed
- `__SSO_ENABLED` flag in a config file allows disabling SSO without touching other files

## Details

### Script Tag Setup

Self-host the MSAL UMD build instead of loading from CDN to avoid external dependency:

```html
<script src="/assets/msal-browser.min.js"></script>
<script>
  const msalConfig = {
    auth: {
      clientId: CONFIG.AZURE_CLIENT_ID,
      authority: `https://login.microsoftonline.com/${CONFIG.AZURE_TENANT_ID}`,
      redirectUri: window.location.origin + "/login.html"
    },
    cache: { cacheLocation: "sessionStorage" }
  };
  const msalInstance = new window.msal.PublicClientApplication(msalConfig);
</script>
```

The `window.msal` namespace is the entry point — all MSAL classes are on it.

### Azure Portal Configuration

The app registration platform type is critical:
- **"Single-page application"** → PKCE flow, no client secret, correct for browser-only apps
- **"Web"** → authorization code flow, expects a client secret, wrong for pure SPAs

If the platform type is "Web", Azure will expect a client secret on the token endpoint. The PKCE flow will fail because the browser-side MSAL request doesn't send a secret. Switch to "Single-page application" in Azure Portal → Authentication → Platform configurations.

### Backend JWKS Validation (Node.js, No Extra Deps)

```js
// Fetch JWKS from Azure and cache for 24h
const JWKS_URL = `https://login.microsoftonline.com/${TENANT_ID}/discovery/v2.0/keys`;
let jwksCache = { keys: [], fetchedAt: 0 };

async function getSigningKey(kid) {
  if (Date.now() - jwksCache.fetchedAt > 86400000) {
    const res = await fetch(JWKS_URL);
    jwksCache = { keys: (await res.json()).keys, fetchedAt: Date.now() };
  }
  return jwksCache.keys.find(k => k.kid === kid);
}

// Validate with Node.js built-in crypto
const [header64, payload64, sig64] = token.split(".");
const key = await getSigningKey(JSON.parse(atob(header64)).kid);
// ... construct public key from JWK and verify signature with crypto.verify()
```

This approach adds zero npm dependencies — useful in projects where keeping the dependency footprint small matters.

### Deploy Pattern

For this project (`social-reporting-tool`):
- `setup.sh` — first-time only: creates directories, initial container setup
- `deploy.sh` — all subsequent deploys: copies frontend files, rebuilds image
- **`deploy.sh` does NOT restart the container with updated env vars** — after adding new env vars to `.env`, manually run: `docker compose down && docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d`

## Related Concepts

- [[wiki/tech-patterns/azure-ad-msal-auth]] — general MSAL patterns across Oliver projects
- [[wiki/concepts/librechat-openid-auth]] — Azure AD app registration used in LibreChat context
- [[wiki/concepts/shell-static-deploy-patterns]] — deploy.sh patterns for frontend projects

## Sources

- [[daily/2026-04-15.md]] — MSAL v5 SSO implementation for `optical-dev.oliver.solutions` social-reporting dashboard
