zero_auth 0.3.0
zero_auth: ^0.3.0 copied to clipboard
Backend-agnostic auth state machine & session lifecycle for Dart/Flutter.
Zero Auth #
A backend-agnostic auth state machine & session lifecycle for Dart/Flutter: it models who is logged in, who they are, and how they logged in / out / recovered — a pure-Dart, headless core with zero native code, backend SDK, UI, or state-management framework.
🔔 Upgrade recommended:
0.3.0fixes the failure paths that silently broke real apps — a proactive refresh no longer leaks an unhandled error, a dead refresh token no longer leaves you "logged in" with a stale token, and an expired persisted session now heals itself at startup. It also addsloginWithfor third-party OAuth / magic links / passkeys,validAccessToken()for interceptors, andRefreshing/LoggingOutstates (⚠️ breaking: exhaustiveswitchmust handle them — preferstate.isAuthenticated). Pinzero_auth: ^0.3.0(or gitref: release/v0.3.0).
🌐 Official Website · 📦 View on pub.dev · 🔗 View on GitHub
Table of Contents #
Features #
- Backend-agnostic — a pure-Dart core; bring any backend by implementing
AuthStrategy(REST, gRPC, Firebase, your own RPC…). - Explicit state machine —
Unauthenticated,Authenticating,Authenticated,Refreshing,LoggingOutandAuthError, broadcast as a replay-last stream. Preferstate.isAuthenticated/state.isBusyoverstate is Authenticated, so a token renewal never unmounts your signed-in UI. - Silent restore & refresh — restores the persisted session at startup (refreshing it first when it has expired) and refreshes tokens transparently (single-flight, so concurrent callers share one call).
- Bring your own login flow —
loginWithadopts a session from any flow you drive yourself: third-party OAuth, magic links, passkeys or biometric unlock. - Never send an expired token —
validAccessToken()renews the session first when the token has expired; ideal for HTTP interceptors. - Typed auth exceptions —
InvalidCredentialsException,SessionExpiredException, and friends, mapped automatically from your strategy'sAuthException.code. - Configurable refresh failure handling —
refreshFailurePolicydecides whether a failed refresh signs the user out (default: yes for unrecoverable failures, no for transient ones). - Pluggable persistence —
TokenStoreis the only persistence boundary; the core shipsInMemoryTokenStore, production uses a secure store (seeexample/). - Unified errors — domain failures map to
AppException(from this package's error kernel); rawExceptions never cross the public surface. - Network-ready —
AuthTokenSourceis the extension point that lets Dio / GraphQL interceptors attachAuthorization: Bearerheaders. - Zero native code — no plugins, no
dart:io-only APIs; runs on server, CLI, and Flutter alike. - Strongly-typed session —
AuthSessioncarries access/refresh tokens, expiry, and raw claims. - Session (de)serialization —
AuthSession.toJson/AuthSession.fromJsonmake persistence a one-liner; a file-based reference store ships for server/CLI. - Proactive auto-refresh — pass
autoRefreshAheadtoAuthManagerand tokens renew before expiry (single-flight), so callers rarely hit an expired access token.
Installation #
Pub.dev (recommended) #
dependencies:
zero_auth: ^0.3.0
Git #
dependencies:
zero_auth:
git:
url: https://github.com/zero-labsco/zero_auth.git
ref: release/v0.3.0 # pin the release/vX.Y.Z branch (immutable per release)
Usage #
Quick start #
import 'package:zero_auth/zero_auth.dart';
final auth = AuthManager(strategy: MyAuthStrategy());
void main() async {
await auth.restore(); // restore a persisted session at app start
auth.state.listen((s) { // subscribe to state changes (replays last)
print(s);
});
await auth.login(Credentials(username: 'me', password: '••••'));
}
Wire your backend (AuthStrategy) #
class MyAuthStrategy implements AuthStrategy {
@override
Future<AuthSession> login(Credentials c) => api.login(c.username, c.password);
@override
Future<AuthSession> register(RegistrationInput i) => api.register(i);
@override
Future<void> logout(SessionHandle h) => api.logout(h.userId);
@override
Future<AuthSession> refresh(RefreshToken t) => api.refresh(t.value);
}
Persist the session (TokenStore) #
The core ships only InMemoryTokenStore. For production, inject a secure store — a flutter_secure_storage-backed reference implementation lives in example/lib/secure_token_store.dart:
final auth = AuthManager(
strategy: MyAuthStrategy(),
tokenStore: SecureTokenStore(), // from example/
);
Attach tokens to the network (AuthTokenSource) #
AuthManager is an AuthTokenSource. Hand it to a Dio interceptor (reference implementation in example/lib/dio_interceptor.dart):
dio.interceptors.add(AuthInterceptor(auth)); // adds `Authorization: Bearer <token>`
Example app & demo backend #
The repo ships two runnable pieces, so you can exercise the whole lifecycle end to end:
example/— a Flutter app drivingAuthManager(login / refresh / logout / call a protected endpoint).server/— a zero-dependencydart:iobackend for the example (nopub getrequired).
1. Start the demo backend
cd server
dart run bin/server.dart # listens on http://localhost:8080
| Method & path | Request | Response |
|---|---|---|
POST /login |
{ "username": "a", "password": "b" } |
200 tokens (expiresIn: 3600) · 401 invalid_credentials |
POST /refresh |
{ "refreshToken": "demo-refresh-token" } |
200 new tokens · 401 invalid_refresh_token |
POST /logout |
– | 200 { "ok": true } |
GET /me |
header Authorization: Bearer demo-access-token |
200 { "userId", "displayName" } · 401 unauthorized |
Any username works, but the password must be b — anything else returns 401, which is the easiest way to watch the AuthError path. CORS is enabled, so a Flutter Web build can call it directly.
2. Run the example app
cd example
flutter run
- The app talks to the real backend by default; flip the AppBar switch to fall back to the offline fake (
_DemoStrategy) when you don't want to run the server. - Log in with any username and password
b, then press Call /me to watchAuthInterceptorattachAuthorization: Bearer …and the backend echo the user back. - Stop the backend and log in again to see the mapped
network_unreachableerror instead of a rawDioException.
On an Android emulator use
http://10.0.2.2:8080instead oflocalhost(_baseUrlinexample/lib/main.dart).
API Reference #
| Type | Role |
|---|---|
AuthManager |
Orchestrates the state machine and session lifecycle; the main entry point. |
AuthState |
Sealed state: Unauthenticated / Authenticating / Authenticated / AuthError. |
AuthSession |
The active session: access/refresh tokens, expiry, display name, raw claims. |
AuthStrategy |
Backend boundary you implement (login / register / logout / refresh). |
TokenStore |
Persistence boundary for the active session (save / load / clear). |
AuthTokenSource |
Read-only access-token source for network layers. |
AppException |
The single public error type (from this package's error kernel). |
Result<T> |
Explicit Ok / Err success-failure wrapper. |
Architecture #
login/register success refresh fails
┌──────────────┐ ┌───────────────┐ ┌──────────────────┐
│ Unauthenticated │──▶│ Authenticating │──▶│ Authenticated │
└──────────────┘ └───────────────┘ └──────────────────┘
▲ │ │ ▲
│ AuthError ◀─────────┘ │ │ token near expiry
│ │ │ │ (single-flight refresh)
│ └───────────────────┘ ▼
└──────────────────────────────── logout / refresh failure ──┘
The manager holds no UI, backend, or native code. Wire your backend via AuthStrategy and your persistence via TokenStore; network layers depend only on AuthTokenSource.
Contributing #
Contributions are welcome! Please read the Contributing Guidelines before submitting issues or pull requests.
License #
Copyright (c) 2026 Zero Labs Co. (AmisKwok). This project is licensed under the Mozilla Public License 2.0 (MPL-2.0) — see the LICENSE file for details. The copyright notice and additional statements (no warranty, no endorsement) live in NOTICE.
- Commercial use is allowed. Use, modification and closed-source distribution are permitted.
- Modified the package? The files you modified must be published in source form under MPL-2.0. Your own app does not need to be open-sourced.
- Used it unmodified? No source disclosure is required.
- No endorsement. "Zero Labs Co.", "zero_auth", the logo / mascot artwork, and the author's name (AmisKwok) may not be used to endorse or promote derived products, or to imply sponsorship or affiliation, without prior written permission.
- No warranty, no liability. The copyright holder provides no warranty and accepts no liability for any modified or derivative version; modified versions must be clearly marked as modified.
This package is provided "as is", without warranty of any kind. The author assumes no responsibility or liability for the functionality, security, or any consequences arising from the use of modified versions or derivative projects.