|
| 1 | +# Browser Compatibility Map for MSAL Browser |
| 2 | + |
| 3 | +This document catalogs browser Web APIs that MSAL Browser depends on, their role in authentication flows, MSAL's fallback behavior, and known MSAL-specific restrictions. For general browser support data, refer to [MDN Web Docs](https://developer.mozilla.org/). |
| 4 | + |
| 5 | +> **Maintenance:** Update this file when a new browser API dependency is introduced or a browser change is discovered that affects MSAL flows. |
| 6 | +
|
| 7 | +## API Dependencies |
| 8 | + |
| 9 | +### Storage |
| 10 | + |
| 11 | +| API | MSAL Usage | Fallback | |
| 12 | +|-----|-----------|----------| |
| 13 | +| `sessionStorage` | Interaction status, PKCE verifier, redirect origin URL, redirect bridge response cache | `MemoryStorage` (response lost on navigation) | |
| 14 | +| `localStorage` | Persistent token cache (when `cacheLocation: "localStorage"`) | None if configured; not used by default | |
| 15 | +| `IndexedDB` | PoP token RSA keypairs | In-memory (keys lost on reload) | |
| 16 | +| `document.cookie` | Encryption key for localStorage cache | None — cache cannot be decrypted without it | |
| 17 | + |
| 18 | +**MSAL-specific restrictions:** |
| 19 | +- Safari PB: `sessionStorage.setItem()` immediately before `location.replace()` may lose data — affects redirect bridge (`handleRedirectPromise` returns `null`) |
| 20 | +- Safari ITP: 7-day cap on script-writable `localStorage`/cookies — tokens evicted without user interaction |
| 21 | +- Firefox PB: `indexedDB.open()` throws `SecurityError` — PoP falls back to memory |
| 22 | +- Chrome 115+ / Safari 16.1+: storage partitioned in cross-origin iframes — affects NAA and embedded apps |
| 23 | + |
| 24 | +### Crypto |
| 25 | + |
| 26 | +All `crypto.subtle` methods require HTTPS (secure context). On HTTP origins, `crypto.subtle` is `undefined`. |
| 27 | + |
| 28 | +| API | MSAL Usage | Fallback | |
| 29 | +|-----|-----------|----------| |
| 30 | +| `crypto.subtle.digest()` | PKCE `code_challenge` (SHA-256) | None — PKCE is mandatory | |
| 31 | +| `crypto.getRandomValues()` | PKCE verifier, state, nonce, correlation IDs | None | |
| 32 | +| `crypto.subtle.generateKey()` | PoP RSA keypairs, EAR AES keys | None for PoP/EAR | |
| 33 | +| `crypto.subtle.importKey()` | PoP signing, EAR decryption, localStorage encryption (HKDF → AES-GCM) | None | |
| 34 | +| `crypto.subtle.sign()` | PoP token signing | None | |
| 35 | +| `crypto.subtle.decrypt()` | EAR response decryption, localStorage decryption | None | |
| 36 | +| `crypto.subtle.deriveKey()` | HKDF key derivation for localStorage encryption | None | |
| 37 | + |
| 38 | +**MSAL-specific restriction:** `deriveKey()` with HKDF as base key requires Firefox 119+. |
| 39 | + |
| 40 | +### Messaging |
| 41 | + |
| 42 | +| API | MSAL Usage | Fallback | |
| 43 | +|-----|-----------|----------| |
| 44 | +| `BroadcastChannel` | Redirect bridge (popup/iframe → main frame), cross-tab cache sync | None for redirect bridge | |
| 45 | +| `postMessage` + `MessageChannel` | WAM browser extension communication | None for WAM path | |
| 46 | + |
| 47 | +**MSAL-specific restrictions:** |
| 48 | +- Chrome 115+ / Firefox TCP: `BroadcastChannel` partitioned by top-level site — breaks cross-origin iframe ↔ popup communication |
| 49 | +- Safari PB: cross-tab channels do not persist |
| 50 | + |
| 51 | +### Navigation & Window |
| 52 | + |
| 53 | +| API | MSAL Usage | Fallback | |
| 54 | +|-----|-----------|----------| |
| 55 | +| `window.location.assign()` / `.replace()` | Navigate to IdP | None | |
| 56 | +| `window.location.hash` / `.search` | Extract auth response from redirect URL | None | |
| 57 | +| `window.history.replaceState()` | Clean auth params from URL | Graceful no-op (URL stays dirty) | |
| 58 | +| `pageshow` event | Detect bfcache restoration to clear stale interaction state | Graceful — may cause `interaction_in_progress` error | |
| 59 | +| `window.open()` | Popup auth flows | None for popup | |
| 60 | +| Hidden iframe | Silent token renewal (`ssoSilent`, `acquireTokenSilent` fallback) | None for silent renewal | |
| 61 | +| `window.close()` | Close popup after auth | Graceful — popup stays open | |
| 62 | + |
| 63 | +**MSAL-specific restrictions:** |
| 64 | +- `window.open()` must be in a user-gesture call stack or browsers block it |
| 65 | +- COOP `same-origin` on AAD severs `window.opener` in popups — redirect bridge mitigates |
| 66 | +- Silent iframe requires IdP to allow framing and 3P cookies to be available; breaks under Safari ITP, Firefox TCP, and Chrome with 3P cookies disabled |
| 67 | +- Office.js sets `history.replaceState` to `null` — MSAL guards with `typeof` check |
| 68 | + |
| 69 | +### Network & DOM |
| 70 | + |
| 71 | +| API | MSAL Usage | Fallback | |
| 72 | +|-----|-----------|----------| |
| 73 | +| `fetch()` | All HTTP requests (token endpoint, discovery, custom auth) | None | |
| 74 | +| `TextEncoder` / `TextDecoder` | String ↔ binary for crypto | None | |
| 75 | +| `atob()` / `btoa()` | Base64 for JWK and token parsing | None | |
| 76 | +| Hidden form + `form.submit()` | POST-based `/authorize` (EAR, redirect POST, silent iframe POST) | None for POST flows | |
| 77 | +| `<link rel="preconnect">` | Early DNS/TLS to authority domain | Graceful — just slower | |
| 78 | + |
| 79 | +**MSAL-specific restrictions:** |
| 80 | +- CSP `connect-src` must allow authority/token endpoint domains |
| 81 | +- CSP `form-action` must allow authority domain for POST flows |
| 82 | +- CSP `frame-src` must allow authority domain for silent iframe |
| 83 | + |
| 84 | +## Privacy Restrictions Affecting MSAL |
| 85 | + |
| 86 | +| Restriction | Affected Browsers | Impact on MSAL | |
| 87 | +|-------------|-------------------|----------------| |
| 88 | +| 3P cookie blocking | Safari (ITP), Firefox (TCP), Chrome (user setting/Incognito) | Silent iframe renewal fails | |
| 89 | +| Storage partitioning in iframes | Chrome 115+, Safari 16.1+, Firefox TCP | `BroadcastChannel`/storage isolated — breaks embedded app scenarios | |
| 90 | +| Script-writable storage 7-day cap | Safari ITP | `localStorage` tokens evicted; cookie-based encryption key lost | |
| 91 | +| Private Browsing ephemeral storage | Safari, Firefox, Chrome | Tokens lost on tab close; IndexedDB blocked in Firefox PB | |
| 92 | +| Tracker domain blocking (Safari 17+ PB) | Safari | CDN-hosted redirect bridge scripts may fail to load | |
| 93 | + |
| 94 | +## Upcoming Changes |
| 95 | + |
| 96 | +| Change | Status | MSAL Impact | |
| 97 | +|--------|--------|-------------| |
| 98 | +| Chrome Storage Access API | Shipping | Potential fallback for 3P-cookie-blocked iframe renewal | |
| 99 | +| Safari tracking domain list expansion | Ongoing | More CDN domains may be blocked in PB | |
| 100 | +| Firefox `BroadcastChannel` partitioned under TCP | Active (Firefox 102+) | Breaks cross-origin iframe ↔ popup channel | |
| 101 | +| Platform authentication proposal | Early proposal | May enable native broker without extension | |
| 102 | + |
| 103 | +## API-to-Flow Matrix |
| 104 | + |
| 105 | +| API | `loginRedirect` | `loginPopup` | `ssoSilent` | `acquireTokenSilent` | NAA | WAM | |
| 106 | +|-----|:-:|:-:|:-:|:-:|:-:|:-:| |
| 107 | +| `sessionStorage` | ✅ | | | | | | |
| 108 | +| `localStorage` | ○ | ○ | ○ | ○ | | | |
| 109 | +| `IndexedDB` | ○ | ○ | ○ | ○ | | | |
| 110 | +| `BroadcastChannel` | ¹ | ✅ | ✅ | ✅² | | | |
| 111 | +| `window.open()` | | ✅ | | | | | |
| 112 | +| Hidden iframe | | | ✅ | ✅² | | | |
| 113 | +| `crypto.subtle` | ✅ | ✅ | ✅ | ✅ | | | |
| 114 | +| `fetch()` | ✅ | ✅ | ✅ | ✅ | | ✅ | |
| 115 | +| `postMessage` | | | | | ✅ | ✅ | |
| 116 | +| Form submit (POST) | ○ | ○ | ○ | | | | |
| 117 | +| Cookies | ○ | ○ | ³ | ³ | | | |
| 118 | + |
| 119 | +- ✅ = required | ○ = optional/configurable |
| 120 | +- ¹ redirect bridge popup/iframe only (not direct redirect) |
| 121 | +- ² when refresh token expired and MSAL falls back to hidden iframe |
| 122 | +- ³ IdP session cookie required in iframe; MSAL's own cookie for localStorage encryption only |
0 commit comments