You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(msal): persist the token cache on WebAssembly through IKeyValueStorage
Layer 3/4 of the MSAL authentication work (#3139). Before this the MSAL cache
was memory-only in the browser, so every page reload meant signing in again.
- MsalCacheHelper has no browser backend, so on WebAssembly the provider
registers SetBeforeAccessAsync/SetAfterAccessAsync callbacks that
serialize the cache through MsalTokenCacheStore into the host's default
IKeyValueStorage, keyed MsalCache_{ClientId}. Which store that is - and so
whether the cache survives a reload or a tab close - is the storage layer's
KeyValueStorageConfiguration:BrowserCacheLocation (localStorage by default);
this side only reads and writes the blob, so MemoryStorage yields the old
in-memory behavior for free
- MsalKeyValueStorage wraps the resolved default store for injection, since a
bare IKeyValueStorage registration resolves to the in-memory one
- Sign-out and ITokenCache.Cleared both delete the serialized cache, in a
finally and with CancellationToken.None, so a cancelled or failed sign-out
cannot leave refresh-token material behind
- A warning names the store whenever the cache is persisted unprotected: the
serialized cache holds the refresh token, and only an Entra `spa`
registration caps that token at 24 non-sliding hours
Tests: Given_MsalTokenCacheStore (unit) covers the round trip, key layout,
corrupt-blob handling and no-op writes; the UI suite gains the sign-out /
cancelled sign-out / Cleared purge cases and the "persisted only on
WebAssembly" guard, and the WebAssembly lane runs it. Docs: the browser cache
section of the how-to (store choice, the `spa` prerequisite and why it
matters) and the migration note. Spec 011 carries the design trail.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqfQh5ovrPEvFGcERLiLhH
| iOS | ✅ Web authentication session | ✅ Handled natively by MSAL — [keychain entitlement required](#ios-keychain-access-group)|
20
-
| WebAssembly | ✅ Popup |In-memory only (tokens don't survive a page reload) |
20
+
| WebAssembly | ✅ Popup |✅ Browser storage, `localStorage` by default — cleartext, see [below](#webassembly-token-cache)|
21
21
| Mac Catalyst | ❌ Not supported (`AddMsal` throws `PlatformNotSupportedException`) | — |
22
22
23
23
MSAL's own cache (refresh and ID tokens) is what the last column describes. The access token that `IAuthenticationService` hands to HTTP handlers is kept separately, in the host's default `IKeyValueStorage` — `KeyStore` / Keychain on native Android and iOS, but plain `ApplicationData` on Android and iOS heads built with `UnoFeatures=SkiaRenderer`, where the Uno SDK loads the storage package's plain `netX.0` build. See [Key-value storage](xref:Uno.Extensions.Storage.Overview#key-value-storage).
@@ -27,6 +27,10 @@ The set of identity scenarios (Microsoft accounts, work/school accounts, B2C, so
27
27
## Prerequisites
28
28
29
29
- An app registration on the Microsoft identity platform, with each platform's redirect URI registered — see [Redirect URIs](#4-redirect-uris) for the value the provider applies on each target.
30
+
-**If you target WebAssembly**, the browser redirect URI must be registered under the **`spa`** platform of the app registration, not as a public-client/native URI. This is not a formality:
31
+
- MSAL.NET issues the token request from the browser over `fetch`, which CORS only permits for a redirect URI registered as `spa`.
32
+
- A `spa` registration caps refresh tokens at **24 hours, non-sliding** — as opposed to the 90-day sliding tokens a public-client registration issues. That cap is what makes it acceptable to keep the token cache in browser storage at all, since browser storage is readable by any script on the origin. See [Refresh tokens](https://learn.microsoft.com/entra/identity-platform/refresh-tokens) and [the SPA cookie reference](https://learn.microsoft.com/entra/identity-platform/reference-third-party-cookies-spas).
33
+
- Consequence to design for: re-authentication has to happen in the top-level frame, and users sign in again at least daily.
30
34
31
35
## Step-by-step
32
36
@@ -425,6 +429,44 @@ With the fallback enabled, the provider logs a warning (including the fallback f
425
429
426
430
On Android and iOS the token cache is persisted natively by MSAL — no configuration is needed (or honored) on those platforms. On iOS it lands in a keychain access group the app has to grant first; see [iOS: keychain access group](#ios-keychain-access-group).
427
431
432
+
#### WebAssembly token cache
433
+
434
+
In the browser there is no protected store to write to, so the cache goes to browser storage in cleartext and what bounds the exposure is *lifetime*, not encryption.
435
+
436
+
The default is `localStorage` — the store WebAssembly already used for the token cache before this setting existed, so upgrading the package never relocates an app's data. It is **not** MSAL.js's default: msal-browser defaults to `sessionStorage`, and if you are starting fresh that is the tighter choice, because the serialized cache is dropped when the tab closes rather than persisting across browser restarts. Opt into it explicitly:
437
+
438
+
It is a **storage** setting, not an MSAL one — it selects the host's single default key-value store, which the token cache shares with everything else built on it:
439
+
440
+
```json
441
+
{
442
+
"KeyValueStorageConfiguration": {
443
+
"BrowserCacheLocation": "SessionStorage"
444
+
}
445
+
}
446
+
```
447
+
448
+
| Value | Behavior |
449
+
| --- | --- |
450
+
|`LocalStorage` (default) | Survives a page reload, closing the tab, and restarting the browser — the widest window in which a stolen cache stays usable. The default only because it is what WebAssembly already used. |
451
+
|`SessionStorage`| Survives a page reload; cleared when the tab closes. Matches MSAL.js and is the tighter choice for credentials. |
452
+
|`MemoryStorage`| Nothing is written to browser storage; the user signs in again after every reload. |
453
+
454
+
An invalid value throws while the host is being built rather than silently falling back.
455
+
456
+
One setting covers the Uno token cache (the access token) and the MSAL cache (the refresh and ID tokens) — splitting them would let the access token outlive both the tab and the refresh token. Because it belongs to storage rather than to a provider, it applies whatever you name your provider (`AddMsal(window, name: "MyMsal")`) and whichever provider you use. It is ignored on every other platform, where the platform's own protected store applies.
457
+
458
+
Signing out removes both: `LogoutAsync` removes every signed-in account and then deletes the serialized MSAL cache. Note this clears *our* storage, not the identity provider's session cookie — the next "Sign in" may complete without a prompt because the IdP still recognises the browser. Use the `end_session_endpoint` if you need a full sign-out. Clearing `ITokenCache` directly has the same storage effect.
459
+
460
+
> [!IMPORTANT]
461
+
> Whichever persistent option you pick, the serialized cache holds the **refresh token** in cleartext, readable by any script on the origin. What bounds the exposure is that your WebAssembly redirect URI must be registered under the Entra **`spa`** platform (see [Prerequisites](#prerequisites)), which caps refresh tokens at **24 hours, non-sliding**. A public-client registration issues a **90-day sliding** token instead, and nothing in the library can detect which one your tenant issued — the provider logs a warning naming the store on every unprotected persist, but the registration type is yours to get right. See [Refresh tokens in the Microsoft identity platform](https://learn.microsoft.com/entra/identity-platform/refresh-tokens).
462
+
463
+
For full control over the storage properties on desktop targets, use the `Storage()` extension method to configure the underlying `StorageCreationPropertiesBuilder`:
0 commit comments