Plugin.Maui.SecureSession
1.0.0
See the version list below for details.
dotnet add package Plugin.Maui.SecureSession --version 1.0.0
NuGet\Install-Package Plugin.Maui.SecureSession -Version 1.0.0
<PackageReference Include="Plugin.Maui.SecureSession" Version="1.0.0" />
<PackageVersion Include="Plugin.Maui.SecureSession" Version="1.0.0" />
<PackageReference Include="Plugin.Maui.SecureSession" />
paket add Plugin.Maui.SecureSession --version 1.0.0
#r "nuget: Plugin.Maui.SecureSession, 1.0.0"
#:package Plugin.Maui.SecureSession@1.0.0
#addin nuget:?package=Plugin.Maui.SecureSession&version=1.0.0
#tool nuget:?package=Plugin.Maui.SecureSession&version=1.0.0
Plugin.Maui.SecureSession
Mobile authentication and session management for .NET MAUI on iOS and Android.
This package sits one level above SecureStoragePlus. Tokens are persisted there. The session owns login, refresh, expiry, logout, multi-device control, and biometric unlock.
await session.LoginAsync("ada", "maui");
var token = await session.GetAccessTokenAsync();
Automatically:
API request
↓
401
↓
Refresh token
↓
New access token
↓
Retry
Features
| Feature | What it does |
|---|---|
| Access token | Issued on login, attached as Bearer on HttpClient |
| Refresh token | Exchanged before expiry or after a 401 |
| Rotating refresh | New refresh token replaces the previous one; reuse expires the session |
| Token expiry | Uses expires_at or a JWT exp claim, with a refresh skew |
| Automatic refresh | Single-flight refresh shared by concurrent callers |
| Logout | This device or every device |
| Session expiry | Absolute lifetime and idle timeout |
| Multi-device | List and revoke other sessions |
| Biometric unlock | Face ID / fingerprint gate after lock or process death |
| Secure persistence | AES-256-GCM via Plugin.Maui.SecureStoragePlus |
Install
dotnet add package Plugin.Maui.SecureSession
Target frameworks: net10.0, net10.0-android, net10.0-ios.
Quick start
using Plugin.Maui.SecureSession;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.Services.AddSingleton<IAuthGateway, ShopAuthGateway>();
builder.Services
.AddHttpClient<IShopApi, ShopApi>(client =>
{
client.BaseAddress = new Uri("https://api.shop");
})
.AddSecureSession();
builder
.UseMauiApp<App>()
.UseSecureSession(options =>
{
options.AccessTokenRefreshSkew = TimeSpan.FromSeconds(60);
options.IdleTimeout = TimeSpan.FromMinutes(15);
options.AbsoluteSessionLifetime = TimeSpan.FromDays(14);
options.RequireBiometricUnlock = true;
options.LockOnBackground = true;
});
return builder.Build();
}
}
await session.LoginAsync("ada", "secret");
var token = await session.GetAccessTokenAsync();
Resolve ISecureSession from DI, or use SecureSession.Current.
Auth gateway
The plugin does not talk to a specific identity server. Your app implements IAuthGateway (or sets delegates on SecureSessionOptions).
public sealed class ShopAuthGateway : IAuthGateway
{
public bool CanRefresh => true;
public async Task<AuthResponse> LoginAsync(LoginRequest request, DeviceContext device, CancellationToken ct)
{
var tokens = await api.LoginAsync(request.Username, request.Password, device, ct);
return new AuthResponse { Tokens = tokens };
}
public async Task<AuthResponse> RefreshAsync(RefreshRequest request, DeviceContext device, CancellationToken ct)
{
var tokens = await api.RefreshAsync(request.RefreshToken, device, ct);
return new AuthResponse { Tokens = tokens, RefreshTokenRotated = true };
}
public Task LogoutAsync(LogoutRequest request, CancellationToken ct) =>
api.LogoutAsync(request, ct);
public Task<IReadOnlyList<RemoteSession>> GetSessionsAsync(string accessToken, CancellationToken ct) =>
api.GetSessionsAsync(accessToken, ct);
public Task RevokeSessionAsync(string accessToken, string sessionId, CancellationToken ct) =>
api.RevokeAsync(accessToken, sessionId, ct);
}
Already have tokens from a browser OAuth flow?
await session.LoginAsync(new TokenBundle
{
AccessToken = access,
RefreshToken = refresh,
AccessTokenExpiresAt = expiresAt,
UserId = userId
});
If the access token is a JWT and AccessTokenExpiresAt is omitted, the plugin reads the exp claim.
When the server rotates refresh tokens, throw RefreshFailedException with RefreshFailureKind.RefreshTokenReused if a retired token is presented again. The local session is cleared.
Automatic 401 retry
AddSecureSession() wraps HttpClient with SecureSessionHandler:
- Attach the current access token
- Send the request
- On 401, refresh once (shared across concurrent calls)
- Retry the original request with the new token
builder.Services
.AddHttpClient("shop", client => client.BaseAddress = new Uri("https://api.shop"))
.AddSecureSession();
Without the generic host:
using var client = SecureSessionHttp.CreateClient(session);
GetAccessTokenAsync() also refreshes proactively when the access token is inside AccessTokenRefreshSkew.
Logout and expiry
await session.LogoutAsync(); // this device
await session.LogoutAsync(LogoutScope.AllDevices); // every device
A session also ends when:
- The access token expires and refresh is impossible or rejected
- A rotated refresh token is reused
AbsoluteSessionLifetimeelapsesIdleTimeoutelapses with noTouchAsync/ token use
Subscribe to SessionExpired to send the user back to login.
Multi-device sessions
Each login carries a stable DeviceId (persisted in SecureStoragePlus) and a new SessionId. The gateway can register that pair.
var devices = await session.GetSessionsAsync();
await session.RevokeSessionAsync(other.SessionId);
Revoking the current session signs out locally.
Biometric unlock
Tokens stay encrypted at rest. The lock is an in-memory gate: after process death or LockAsync(), GetAccessTokenAsync() fails with SessionLockedException until UnlockAsync().
if (await session.GetBiometricAvailabilityAsync() == BiometricAvailability.Available)
{
await session.EnableBiometricUnlockAsync();
}
await session.LockAsync();
await session.UnlockAsync(); // Face ID / fingerprint
LockOnBackground locks when the app pauses (Android) or enters the background (iOS), if biometric unlock is enabled.
Without the generic host
var session = SecureSession.Create(new SecureSessionOptions
{
RefreshAsync = (request, device, ct) => auth.RefreshAsync(request, device, ct),
LoginAsync = (request, device, ct) => auth.LoginAsync(request, device, ct)
});
await session.RestoreAsync();
Platform notes
iOS — Face ID needs a usage string in Info.plist:
<key>NSFaceIDUsageDescription</key>
<string>Unlock your session</string>
SecureStoragePlus needs a Keychain entitlement. In Entitlements.plist:
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
</array>
Android — BiometricPrompt needs USE_BIOMETRIC (declare it on the app) and a minimum of API 23. Enroll a fingerprint or face on the emulator before testing unlock.
| Android | iOS | net10.0 |
|
|---|---|---|---|
| Login / refresh / logout | Yes | Yes | Yes (tests) |
| 401 refresh + retry | Yes | Yes | Yes |
| SecureStoragePlus persistence | Yes | Yes | Yes |
| Biometric unlock | BiometricPrompt | LocalAuthentication | Stub |
| Lock on background | OnPause |
DidEnterBackground |
Call NotifyBackground |
Sample
samples/Plugin.Maui.SecureSession.Sample signs in against an in-memory auth server (password maui), forces a 401, refreshes, lists devices, and exercises biometric lock.
dotnet build src/Plugin.Maui.SecureSession/Plugin.Maui.SecureSession.csproj
dotnet pack src/Plugin.Maui.SecureSession/Plugin.Maui.SecureSession.csproj -c Release -o artifacts
dotnet test tests/Plugin.Maui.SecureSession.Tests/Plugin.Maui.SecureSession.Tests.csproj
dotnet build samples/Plugin.Maui.SecureSession.Sample/Plugin.Maui.SecureSession.Sample.csproj -f net10.0-android
Pack from source
dotnet pack src/Plugin.Maui.SecureSession/Plugin.Maui.SecureSession.csproj -c Release -o artifacts
The .nupkg is written to artifacts/Plugin.Maui.SecureSession.1.0.0.nupkg.
License
MIT
Support
If this plugin saved you a weekend of native plumbing, consider buying me a coffee. Your support keeps it maintained, documented, and free.
This library stays open source. A coffee helps cover time for bug fixes, new features, and docs.
| Product | Versions 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. |
-
net10.0
- Microsoft.Extensions.Http (>= 10.0.0)
- Microsoft.Maui.Controls (>= 10.0.20)
- Plugin.Maui.SecureStoragePlus (>= 1.0.1)
-
net10.0-android36.0
- Microsoft.Extensions.Http (>= 10.0.0)
- Microsoft.Maui.Controls (>= 10.0.20)
- Plugin.Maui.SecureStoragePlus (>= 1.0.1)
- Xamarin.AndroidX.Biometric (>= 1.1.0.33)
-
net10.0-ios26.0
- Microsoft.Extensions.Http (>= 10.0.0)
- Microsoft.Maui.Controls (>= 10.0.20)
- Plugin.Maui.SecureStoragePlus (>= 1.0.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Initial release. Access/refresh tokens, rotating refresh, automatic 401 retry, logout, session expiry, multi-device sessions, biometric unlock, and SecureStoragePlus persistence for .NET MAUI on iOS and Android.