SyntaxCircus.Maui.TokenStorage 0.1.3

dotnet add package SyntaxCircus.Maui.TokenStorage --version 0.1.3
                    
NuGet\Install-Package SyntaxCircus.Maui.TokenStorage -Version 0.1.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="SyntaxCircus.Maui.TokenStorage" Version="0.1.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SyntaxCircus.Maui.TokenStorage" Version="0.1.3" />
                    
Directory.Packages.props
<PackageReference Include="SyntaxCircus.Maui.TokenStorage" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add SyntaxCircus.Maui.TokenStorage --version 0.1.3
                    
#r "nuget: SyntaxCircus.Maui.TokenStorage, 0.1.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package SyntaxCircus.Maui.TokenStorage@0.1.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=SyntaxCircus.Maui.TokenStorage&version=0.1.3
                    
Install as a Cake Addin
#tool nuget:?package=SyntaxCircus.Maui.TokenStorage&version=0.1.3
                    
Install as a Cake Tool

SyntaxCircus.Maui.TokenStorage

Build NuGet License: MIT

Secure auth token storage for MAUI apps: an injectable wrapper over ISecureStorage, and a session layer that splits sensitive tokens (secure storage) from non-sensitive metadata (IPreferences) so expiry checks don't need a keystore round-trip on every read. Ships with SessionTokenStore for OIDC-style access/refresh-token sessions, KeyedSessionStore<TSession> for any other session shape, and InstallationIdentityStore for per-device identity generation.

No support guaranteed. Published as-is and maintained on a best-effort basis. Issues and PRs are welcome, but there's no SLA — fork it or vendor what you need if that's not enough.

Targets

net10.0-android and net10.0-ios. Depends on Microsoft.Maui.Controls — as of current .NET MAUI there's no narrower package that exposes ISecureStorage/IPreferences on their own, so a storage-only library still pulls in the full Controls surface. If your app already references Microsoft.Maui.Controls (nearly all MAUI apps do), this adds nothing new.

Setup

// MauiProgram.cs
builder.Services.AddSecureTokenStorage();

AddSecureTokenStorage() registers ISecureTokenStorage and SessionTokenStore (also resolvable as ISessionTokenStore, same singleton instance). KeyedSessionStore<TSession> and InstallationIdentityStore (see below) are separate, opt-in registrations — AddKeyedSessionStore<TSession>(keyPrefix) and AddInstallationIdentityStore() — and can be combined with AddSecureTokenStorage() freely; all three share the same underlying ISecureTokenStorage/IPreferences registrations without duplicating them.

Choosing a store

Your session looks like... Use
An OIDC-style access token + optional refresh token SessionTokenStore (below)
Anything else with an expiry — e.g. an installation-credential session KeyedSessionStore<TSession>
A per-device identity (no session/expiry involved) InstallationIdentityStore

All three share the same underlying idea: sensitive/opaque data goes in ISecureTokenStorage (the keystore), and anything needed for a fast, frequent validity check goes in IPreferences instead, so that check never touches the keystore.

Usage

await sessionTokenStore.StoreAsync(new SessionTokens(
    AccessToken: response.AccessToken,
    RefreshToken: response.RefreshToken,
    ExpiresAt: response.ExpiresAt,
    UserId: response.UserId,
    Email: response.Email,
    IsAnonymous: false));

if (sessionTokenStore.HasValidAccessToken()) // synchronous — reads Preferences, not the keystore
{
    var accessToken = await sessionTokenStore.GetAccessTokenAsync();
}

await sessionTokenStore.ClearAsync(); // sign out

Custom session shapes — KeyedSessionStore<TSession>

For sessions that don't fit SessionTokens — for example an installation-credential model, where a device authenticates with a self-generated identity instead of an OIDC token pair:

// MauiProgram.cs
builder.Services.AddKeyedSessionStore<InstallationSession>("installation_session");
public sealed record InstallationSession(string AccessToken, DateTimeOffset ExpiresAt, Guid UserProfileId);

await installationSessionStore.StoreAsync(
    new InstallationSession(response.AccessToken, response.ExpiresAt, response.UserProfileId),
    response.ExpiresAt);

if (installationSessionStore.HasValidSession()) // synchronous — reads Preferences, not the keystore
{
    var session = await installationSessionStore.GetSessionAsync();
}

await installationSessionStore.ClearAsync();

TSession is serialized as a single JSON blob in secure storage; only its expiry is split out into IPreferences. Register one AddKeyedSessionStore<TSession>(keyPrefix, ...) per distinct session shape — the keyPrefix keeps its storage keys from colliding with SessionTokenStore's or another KeyedSessionStore<T>'s. On a trimmed/NativeAOT build, pass a source-generated JsonSerializerOptions via the optional jsonSerializerOptions parameter if reflection-based serialization isn't available for TSession.

Per-device identity — InstallationIdentityStore

For installation-credential auth, where the device itself needs a persistent random identity to authenticate with:

// MauiProgram.cs
builder.Services.AddInstallationIdentityStore();
var identity = await installationIdentityStore.GetOrCreateAsync(); // Guid Id + random base64 Credential
// first call generates and persists; later calls return the same identity

Why expiry lives in Preferences, not secure storage

HasValidAccessToken() (and KeyedSessionStore<TSession>.HasValidSession()) is called far more often than the token/session is actually read — every outgoing API call, typically — and a keystore round-trip on each of those is unnecessary overhead for a non-sensitive value. Expiry is also stored as a string, not a raw Unix-seconds integer: iOS's NSUserDefaults (what IPreferences maps to under the hood) stores integers as double, which silently loses precision for large epoch values. Storing the string form and parsing it back avoids that class of bug entirely. Both SessionTokenStore and KeyedSessionStore<TSession> use this same trick.

Testing

ISecureTokenStorage is the seam — substitute your own implementation (or a test double) instead of SecureTokenStorage to unit test code that depends on SessionTokenStore, KeyedSessionStore<TSession>, or InstallationIdentityStore without touching the platform keystore. IPreferences (also constructor-injected throughout) is the equivalent seam for expiry/metadata reads.

API reference

Every public type in the package, for quick scanning by humans and AI agents alike.

ISecureTokenStorage / SecureTokenStorage

Generic key/value secure storage, abstracted so it's constructor-injectable and testable.

Member Description
Task StoreAsync(string key, string value) Stores a value under key.
Task<string?> RetrieveAsync(string key) Returns the stored value, or null if nothing is stored.
Task RemoveAsync(string key) Removes the value stored under key.

SecureTokenStorage(ISecureStorage secureStorage) is the default implementation, wrapping Microsoft.Maui.Storage.ISecureStorage.

SessionTokens

record SessionTokens(string AccessToken, string? RefreshToken, DateTimeOffset ExpiresAt, string? UserId, string? Email, bool IsAnonymous) — the OIDC-style token set and session metadata SessionTokenStore persists.

ISessionTokenStore / SessionTokenStore

SessionTokenStore(ISecureTokenStorage secureTokenStorage, IPreferences preferences, ILogger<SessionTokenStore> logger) implements ISessionTokenStore.

Member Description
Task StoreAsync(SessionTokens tokens) Persists the token set: access/refresh tokens go to secure storage, everything else to IPreferences. Throws ArgumentNullException if tokens is null.
bool HasValidAccessToken() Synchronous. true if the stored expiry is more than 60 seconds in the future. Reads only IPreferences.
Task<string?> GetAccessTokenAsync() Returns the stored access token, or null (logged as a warning) if storage access fails.
Task<string?> GetRefreshTokenAsync() Returns the stored refresh token, or null (logged as a warning) if storage access fails or none was stored.
bool IsAnonymous() Defaults to true if nothing has been stored yet.
string? GetUserId() Returns the stored user id, or null if empty/unset.
string? GetEmail() Returns the stored email, or null if empty/unset.
DateTimeOffset? GetExpiresAt() Returns the stored access token's raw expiry, or null if nothing has been stored. No clock-skew margin (unlike HasValidAccessToken()).
Task ClearAsync() Removes all stored tokens and session metadata (sign-out).

KeyedSessionStore<TSession> where TSession : class

Generalizes SessionTokenStore's secure-storage/IPreferences split to an arbitrary caller-defined session payload. TSession is JSON-serialized as a single blob in secure storage; only its expiry goes into IPreferences.

KeyedSessionStore(ISecureTokenStorage secureTokenStorage, IPreferences preferences, ILogger<KeyedSessionStore<TSession>> logger, string keyPrefix, TimeSpan? clockSkewMargin = null, JsonSerializerOptions? jsonSerializerOptions = null)keyPrefix must be unique per store instance (see Custom session shapes above); clockSkewMargin defaults to 60 seconds.

Member Description
Task StoreAsync(TSession session, DateTimeOffset expiresAt) Serializes and stores session and its expiry. Throws ArgumentNullException if session is null.
bool HasValidSession() Synchronous. true if the stored expiry is beyond the clock-skew margin in the future. Reads only IPreferences, never secure storage.
Task<TSession?> GetSessionAsync() Returns the deserialized session, or null (logged as a warning) if nothing is stored, storage access fails, or the stored JSON can't be deserialized.
Task ClearAsync() Removes the stored session and its expiry.

InstallationIdentity / InstallationIdentityStore

For installation-credential auth models, where a device authenticates with a self-generated identity rather than an OIDC token.

record InstallationIdentity(Guid Id, string Credential)

InstallationIdentityStore(ISecureTokenStorage secureTokenStorage)

Member Description
Task<InstallationIdentity> GetOrCreateAsync() Returns the previously persisted identity, or generates (random Guid + random 32-byte base64 credential), persists, and returns a new one on first call. Safe under concurrent in-process calls.

TokenStorageServiceCollectionExtensions

Member Description
IServiceCollection AddSecureTokenStorage() Registers ISecureTokenStorage/SessionTokenStore (also resolvable as ISessionTokenStore, same singleton) and their ISecureStorage/IPreferences dependencies as singletons.
IServiceCollection AddInstallationIdentityStore() Registers InstallationIdentityStore as a singleton; idempotent alongside AddSecureTokenStorage().
IServiceCollection AddKeyedSessionStore<TSession>(string keyPrefix, TimeSpan? clockSkewMargin = null, JsonSerializerOptions? jsonSerializerOptions = null) Registers a KeyedSessionStore<TSession> singleton for keyPrefix; idempotent alongside the other two. Call once per distinct TSession.

Contributing

Issues and pull requests are welcome:

  • Keep changes focused, with a clear description of the behavior change.
  • Match the existing code style (see .editorconfig).
  • Call out any breaking changes to the public API in your PR description.

License

MIT — see LICENSE.txt.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-android36.0 is compatible.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-ios26.0 is compatible.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on SyntaxCircus.Maui.TokenStorage:

Package Downloads
SyntaxCircus.Maui.Environments

Runtime environment switching for MAUI apps that ship one binary across multiple backends (e.g. UAT/Production): a persisted environment selector with TTL-based default reversion, a switch coordinator that serializes app-supplied teardown/rebuild logic, environment-namespacing decorators for IPreferences/ISecureTokenStorage, and best-effort app-distribution-channel detection (TestFlight/Play/sideload).

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.3 125 8/17/2026
0.1.2 113 8/16/2026
0.1.1 104 8/16/2026