StorageAccess
How an app running inside a third-party iframe asks for its own cookies and storage back, now that browsers partition both by the embedding site.
@inject Bit.Butil.StorageAccess storageAccessMDN reference
HasAccess answers true and Request is unnecessary. This API is for the
case where your Blazor app is embedded in someone else's site, where the browser hands
it a fresh, empty storage jar per embedding site and any auth cookie set on its own domain is
invisible.
HasAccess answers true on engines that don't partition storage at all, since there is nothing to be denied - which means a false genuinely means 'ask'. Check it before prompting: browsers count an unnecessary prompt against you.
@inject Bit.Butil.StorageAccess storageAccess
var supported = await storageAccess.IsSupported();
if (await storageAccess.HasAccess() is false)
{
// show a button - Request needs a user gesture
}Must be called from a user-gesture handler, and browsers generally only grant it for a site the user has already interacted with at the top level. False covers every refusal - user declined, no gesture, embedder's permissions policy forbids it - because the spec deliberately doesn't say which. A grant lasts for the rest of this document's lifetime, not permanently, so ask again after a reload.
Bit.Butil.Cookie cookie
Bit.Butil.StorageAccess storageAccess
@* Must be a real user gesture: without one the request is refused, and the refusal is remembered. *@
<button @onclick="Ask">Continue</button>
{
private async Task Ask()
{
var granted = await storageAccess.Request();
if (granted)
{
// unpartitioned cookies and storage are now readable
var token = await cookie.GetValue("auth");
}
else
{
// fall back: an in-frame sign-in, or a popup to the top-level origin
}
}
}Once access is granted, nothing else changes: Cookie, LocalStorage, SessionStorage and IndexedDb all start seeing the unpartitioned jar. The Storage Access API is a gate in front of the storage APIs, not a replacement for them.
Bit.Butil.LocalStorage localStorage
Bit.Butil.StorageAccess storageAccess
if (_needsGesture)
{
<button @onclick="Ask">Continue</button>
}
{
private bool _needsGesture;
// The usual pattern for an embedded app.
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender is false) return;
if (await storageAccess.IsSupported() && await storageAccess.HasAccess() is false)
{
// Render a "Continue" button rather than calling Request on load - without a gesture it
// is refused, and the refusal is remembered.
_needsGesture = true;
StateHasChanged();
return;
}
// from here on, the ordinary storage wrappers see the real jar:
var session = await localStorage.GetItem("session");
}
private async Task Ask() => _needsGesture = await storageAccess.Request() is false;
}Request outside a user gesture is refused, and some engines treat a refused
request as a signal not to prompt again for a while. Render a button, and call it from the
click.
API reference
ValueTask<bool> IsSupported()ValueTask<bool> HasAccess()ValueTask<bool> Request()ValueTask<bool> IsRequestForSupported()ValueTask<bool> RequestFor(string origin)