Skip to main content

Flutter & WASM Integration

Use the official Flutter package

For new projects, use awesome_node_auth_flutter β€” the official Flutter/Dart client for awesome-node-auth.

It handles:

  • Cookie + CSRF automatically on web/WASM
  • Bearer token automatically on native (iOS, Android, Desktop)
  • Concurrent-refresh deduplication, session revocation, reactive state stream
  • Zero token-management boilerplate

This page documents the official package. For the raw manual approach (no package), see the Advanced / Bearer Token guide.

Platform behaviour​

PlatformAuth modeToken managementCSRFWASM
WebCookie (HttpOnly)AutomaticAuto-injectedβœ…
WASMCookie (HttpOnly)AutomaticAuto-injectedβœ…
iOSBearerPluggable TokenStoragen/an/a
AndroidBearerPluggable TokenStoragen/an/a
DesktopBearerPluggable TokenStoragen/an/a

The package detects the platform at runtime β€” you do not choose the mode.

Python backend

awesome_node_auth_flutter works with both the Node.js (awesome-node-auth) and the Python (awesome-python-auth) backends. Set apiPrefix to match your server's api_prefix (default /api/auth in Python vs /auth in Node.js). See the Python / FastAPI guide for server-side setup.


Installation​

# pubspec.yaml
dependencies:
awesome_node_auth_flutter: ^1.9.4
flutter pub get

Setup​

Create an AuthClient with AuthOptions:

import 'package:awesome_node_auth_flutter/awesome_node_auth_flutter.dart';

final auth = AuthClient(
AuthOptions(
apiPrefix: 'http://localhost:3000/auth',
loginUrl: '/login', // redirect after logout (headless: false)
headless: true, // true β†’ you control routing manually
initializeOnStartup: true, // calls checkSession() before first frame
),
);

initializeOnStartup: true calls checkSession() automatically during initialization so the reactive state stream emits the current user before your first widget build.


Reactive state β€” StreamBuilder​

auth.state.userStream is a broadcast stream that replays the current value to new subscribers, making it safe to use in StreamBuilder without a flash of unauthenticated UI:

StreamBuilder<AuthUser?>(
stream: auth.state.userStream,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.data != null) {
return HomeScreen(user: snapshot.data!);
}
return const LoginScreen();
},
);

Events​

Subscribe to auth.events to react to auth lifecycle changes. This is the recommended integration point for GoRouter / AutoRoute redirects:

auth.events.listen((event) {
switch (event.type) {
case AuthEventType.loggedIn:
router.go('/dashboard');
case AuthEventType.loggedOut:
router.go('/login');
case AuthEventType.sessionExpired:
router.go('/login');
case AuthEventType.sessionRevoked:
router.go('/login');
case AuthEventType.tokenRefreshed:
// optional β€” tokens refreshed silently
break;
case AuthEventType.userUpdated:
// profile changed
break;
}
});

Login + 2FA flow​

// Simple login
final result = await auth.login(email, password);

if (result.success && !result.requires2fa) {
// Logged in β€” auth.state.userStream emits the user automatically
context.go('/dashboard');
} else if (result.requires2fa) {
// 2FA required β€” navigate to the 2FA screen
context.go('/2fa', extra: result);
} else if (result.requires2FASetup) {
// 2FA enrollment required
context.go('/2fa/setup', extra: result);
} else {
showError(result.error ?? 'Login failed');
}

On the 2FA screen, result.tempToken and result.availableMethods are available to drive the UI.


2FA β€” TOTP​

// ── Setup (generate QR code) ──────────────────────────────────────────────
final setup = await auth.setupTotp();
// setup.qrCodeUrl β†’ display in a QR widget
// setup.secret β†’ show as text fallback

// ── Verify during enrollment ──────────────────────────────────────────────
await auth.verifyTotp(totpCode);

// ── Validate during login 2FA step ───────────────────────────────────────
final loginResult = await auth.validate2fa(
result.tempToken!,
totpCode,
);

2FA β€” SMS OTP​

// ── SMS login (passwordless) ──────────────────────────────────────────────
await auth.sendSmsLogin(email);

// Verify OTP code sent to the user's phone
final loginResult = await auth.verifySmsLogin(userId, otpCode);

// ── SMS 2FA (second factor during login) ─────────────────────────────────
// 1. First receive result.tempToken from auth.login()
await auth.send2faSms(result.tempToken!);

// 2. User receives OTP on their phone
final loginResult = await auth.validate2fa(
result.tempToken!,
otpCode,
method: 'sms',
);

// ── Request magic link ────────────────────────────────────────────────────
await auth.sendMagicLink(email);

// ── Validate token from the link (deep link handler) ─────────────────────
final loginResult = await auth.verifyMagicLink(token);

Password​

// Change password (authenticated)
await auth.changePassword(currentPassword, newPassword);

// Request password reset (unauthenticated)
await auth.forgotPassword(email);

// Complete reset with token from email link
await auth.resetPassword(newPassword, resetToken);

Email​

// Request email change (authenticated)
await auth.requestEmailChange(newEmail);

// Confirm with token from email link
await auth.confirmEmailChange(confirmationToken);

// Resend verification email
await auth.resendVerificationEmail();

// Verify email address with token from email link
await auth.verifyEmail(verificationToken);

Profile​

// Get current user
final user = auth.state.currentUser; // sync, from cache

// Refresh from server
final user = await auth.fetchProfile();

// Update profile fields
await auth.updateProfile({'displayName': 'Alice', 'avatarUrl': '...'});

Sessions​

// List active sessions
final sessions = await auth.getActiveSessions();

// Revoke a specific session
await auth.revokeSession(sessionHandle);

Account linking​

// Initiate linking by sending an email to the other account
await auth.requestLinkingEmail(otherEmail, provider);

// Confirm the link using the token from the email
await auth.verifyLinkingToken(token, provider);

// List all linked providers
final accounts = await auth.getLinkedAccounts();

// Unlink a specific provider account
await auth.unlinkAccount(provider, providerAccountId);

UI Config​

Load backend feature flags to drive adaptive UI (e.g., hide magic-link button when the server has it disabled):

final config = await auth.loadUiConfig();
if (config?.magicLinkEnabled == true) {
// Show magic-link option in the login screen
}

Authenticated HTTP calls​

auth.httpClient is a pre-configured HTTP client that automatically attaches the correct auth credentials (cookies on web, Authorization: Bearer on native) and transparently refreshes tokens on 401 responses:

// GET with automatic auth β€” no manual token handling needed
final response = await auth.httpClient.get(
Uri.parse('https://api.example.com/todos'),
);

// POST with body
final response = await auth.httpClient.post(
Uri.parse('https://api.example.com/todos'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'title': 'Buy milk'}),
);

Custom TokenStorage​

On native platforms the package stores tokens in memory by default. For persistence across restarts, provide a TokenStorage implementation backed by flutter_secure_storage:

note

flutter_secure_storage is not a dependency of awesome_node_auth_flutter. Add it yourself if you need persistent token storage on native.

# pubspec.yaml β€” add only for native persistence
dependencies:
awesome_node_auth_flutter: ^1.9.4
flutter_secure_storage: ^9.0.0
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:awesome_node_auth_flutter/awesome_node_auth_flutter.dart';

class SecureTokenStorage implements TokenStorage {
final _storage = const FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
);

@override
Future<String?> read(String key) => _storage.read(key: key);

@override
Future<void> write(String key, String value) =>
_storage.write(key: key, value: value);

@override
Future<void> delete(String key) => _storage.delete(key: key);
}

// Pass to AuthOptions
final auth = AuthClient(
AuthOptions(
apiPrefix: 'http://localhost:3000/auth',
tokenStorage: SecureTokenStorage(),
headless: true,
),
);

Comportamento interno β€” refresh e deduplicazione​

  • Deduplicazione concorrente: se piΓΉ chiamate API ricevono un 401 contemporaneamente, il package esegue un solo refresh e mette in coda le altre fino al completamento.
  • SESSION_REVOKED: se il server risponde con { code: "SESSION_REVOKED" }, il package emette AuthEventType.sessionRevoked e non ritenta il refresh (evita loop infiniti).
  • Endpoint esclusi dal retry: /auth/login, /auth/register, /auth/refresh non vengono mai ritentati dopo un 401.
  • Web vs nativo: su web il refresh usa i cookie HttpOnly; su nativo invia il refreshToken nel corpo della richiesta.

Headless mode​

With headless: true, the package does not perform any automatic redirects. You control all navigation via the events stream β€” ideal with GoRouter or AutoRoute:

// main.dart
final auth = AuthClient(
AuthOptions(
apiPrefix: 'http://localhost:3000/auth',
headless: true,
initializeOnStartup: false, // call checkSession() manually
),
);

void main() async {
WidgetsFlutterBinding.ensureInitialized();
await auth.checkSession(); // restore session before first frame
runApp(const MyApp());
}

// GoRouter redirect
redirect: (context, state) {
final isAuth = auth.state.isAuthenticated;
final isInit = auth.state.isInitialized;
if (!isInit) return null; // still loading β€” don't redirect
if (!isAuth && !publicPaths.contains(state.matchedLocation)) {
return '/login';
}
return null;
},

WASM​

The package is fully WASM-compatible. Build with:

flutter build web --wasm

The package uses package:web and package:http exclusively β€” no dart:html or dart:io calls β€” so it compiles to WASM without modifications.


For larger apps, wrap AuthClient in a singleton service to decouple it from the widget tree:

// lib/services/auth_service.dart
import 'package:awesome_node_auth_flutter/awesome_node_auth_flutter.dart';

class AuthService {
AuthService._();
static final AuthService instance = AuthService._();

final AuthClient client = AuthClient(
AuthOptions(
apiPrefix: 'http://localhost:3000/auth',
headless: true,
),
);

AuthState get state => client.state;
Stream<AuthEvent> get events => client.events;
}
// main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await AuthService.instance.client.checkSession();
runApp(const MyApp());
}

Migration from manual integration (previous approach)

The previous version of this page documented a manual integration using flutter_secure_storage, the http package, and the X-Auth-Strategy: bearer header. Below is a quick before/after comparison.

Before (manual)After (awesome_node_auth_flutter)
http + manual X-Auth-Strategy: bearer headerHandled automatically by the package
flutter_secure_storage for token persistenceBuilt-in TokenStorage interface; bring your own storage
Manual 401 retry with refreshBuilt-in concurrent deduplication
No web/WASM supportFull web + WASM support via cookie mode
No reactive stateauth.state.userStream β€” replay stream
No event busauth.events β€” AuthEventType enum

To migrate:

  1. Add awesome_node_auth_flutter: ^1.9.4 to pubspec.yaml.
  2. Remove http and the manual AuthService if they were only used for auth.
  3. Replace all _bearerHeaders / _authedRequest calls with AuthClient methods.
  4. If you need persistent tokens on native, implement TokenStorage with flutter_secure_storage (see Custom TokenStorage above).