flutter_secure_auth 🔒

pub package license WASM Compatible platform

A secure, lightweight, and offline-resilient authentication client for Flutter applications. It seamlessly handles OAuth2 PKCE authorization flows, REST sign-ins, secure token storage, and automatic credential refreshes with full multi-platform support (including WebAssembly/WASM).


✨ Key Features

  • 🔔 Reactive Auth State: authStateChanges stream for building reactive UIs with StreamBuilder — just like Firebase Auth.
  • 🌐 Drop-in HTTP Interceptor: AuthInterceptor auto-injects Authorization headers and handles 401 retries transparently.
  • 🏭 Quick Setup Factory: AuthService.create('https://api.example.com') — zero boilerplate for standard OAuth2 APIs.
  • 🔐 RFC 7636 OAuth2 PKCE Compliance: Safe code exchange using strictly compliant unreserved characters.
  • ⚡ 100% WASM Compatible: Fully migrated web storage to package:web/dart:js_interop for next-gen Flutter Web builds.
  • 🔌 Offline Resiliency: Smart refresh scheduling. Won't prematurely wipe credentials during network loss or 5xx server issues — only logs out on definitive auth errors (400/401/403).
  • 🩹 Self-Healing Storage: Corrupt secure storage inputs are silently discarded and healed, returning a clean login state rather than crashing.
  • 🏷️ Typed Error Codes: AuthErrorCode enum makes catch blocks readable and avoids magic number comparisons.
  • 📋 copyWith() on Tokens: Update specific token fields without boilerplate.
  • 🔒 Biometric / Guard Hook: readGuard callback on TokenStorage for plugging in Face ID / Touch ID before token retrieval.

🚀 Installation

Add the package to your pubspec.yaml:

dependencies:
  flutter_secure_auth: ^1.1.0

Then run:

flutter pub get

🧩 Usage Guide

1. Initialize the Auth Service

Option A — Quick factory (recommended for standard REST APIs):

import 'package:flutter_secure_auth/flutter_secure_auth.dart';

// Automatically builds tokenEndpoint and revokeEndpoint from a base URL
final authService = AuthService.create('https://api.yourdomain.com');

Option B — Full manual configuration:

final authService = AuthService(
  tokenEndpoint: Uri.parse('https://api.yourdomain.com/oauth/token'),
  revokeEndpoint: Uri.parse('https://api.yourdomain.com/oauth/revoke'),
);

2. Reactive Auth State (New in v1.1.0)

Listen to authStateChanges to rebuild your UI whenever the session changes:

StreamBuilder<AuthState>(
  stream: authService.authStateChanges,
  builder: (context, snapshot) {
    final state = snapshot.data ?? const AuthState.unauthenticated();
    if (state.isAuthenticated) {
      return HomeScreen();
    }
    return LoginScreen();
  },
);

Or check auth status programmatically:

if (await authService.isAuthenticated) {
  navigateTo(HomeScreen());
}

3. Sign In with Password

try {
  final tokens = await authService.signInWithPassword(
    endpoint: Uri.parse('https://api.yourdomain.com/auth/login'),
    username: 'user@example.com',
    password: 'securePassword123',
  );
  print('Signed in! Token expires in: ${tokens.timeToExpiry.inMinutes} minutes');
} on AuthException catch (e) {
  if (e.errorCode == AuthErrorCode.unauthorized) {
    showSnackBar('Invalid credentials. Please try again.');
  } else if (e.errorCode == AuthErrorCode.serverError) {
    showSnackBar('Server error. Please try later.');
  }
}

4. OAuth2 PKCE Flow

Generate a cryptographically secure verifier/challenge pair (conforming to RFC 7636) and initiate the authorization flow:

// 1. Create the PKCE pair
final pkce = createPkcePair(length: 64);

// 2. Build your authorization URL
final authUrl = Uri.parse('https://auth.yourdomain.com/authorize').replace(
  queryParameters: {
    'response_type': 'code',
    'client_id': 'your-client-id',
    'redirect_uri': 'com.yourapp:/oauthredirect',
    'scope': 'openid profile offline_access',
    'code_challenge': pkce.codeChallenge,
    'code_challenge_method': 'S256',
    'state': pkce.state,
  },
);

// 3. After redirect & code capture, perform secure exchange:
final tokens = await authService.exchangeAuthorizationCode(
  code: 'returned_auth_code_from_redirect',
  codeVerifier: pkce.codeVerifier,
  redirectUri: Uri.parse('com.yourapp:/oauthredirect'),
);

5. Send Authorized HTTP Requests (New: AuthInterceptor)

Option A — Drop-in HTTP client (recommended):

import 'package:flutter_secure_auth/flutter_secure_auth.dart';

// Wrap your client once — all requests are automatically authorized
final client = AuthInterceptor(authService: authService);

// Authorization: Bearer <token> is injected automatically
// If token is expired it refreshes first; if 401 is received it retries once
final response = await client.get(
  Uri.parse('https://api.yourdomain.com/profile'),
);
print(response.body);

// Close when done (e.g., in dispose())
client.close();

Option B — Manual per-request injection:

import 'package:http/http.dart' as http;

final rawRequest = http.Request('GET', Uri.parse('https://api.yourdomain.com/profile'));
final authorizedRequest = await authService.authorizedRequest(rawRequest);
final response = await authorizedRequest.send();

6. Typed Error Handling (New in v1.1.0)

} on AuthException catch (e) {
  switch (e.errorCode) {
    case AuthErrorCode.unauthorized:
      navigateToLogin();
    case AuthErrorCode.forbidden:
      showError('You do not have permission to perform this action.');
    case AuthErrorCode.serverError:
      showRetryButton();
    case AuthErrorCode.networkError:
      showOfflineBanner();
    default:
      showError(e.message);
  }
}

7. Token copyWith() (New in v1.1.0)

// Update a specific field without recreating the whole object
final refreshed = tokens.copyWith(
  accessToken: 'newToken',
  expiry: DateTime.now().add(const Duration(hours: 1)),
);

8. Biometric Guard on Token Retrieval

Plug in Face ID, Touch ID, or any custom security check before tokens are read:

import 'package:local_auth/local_auth.dart';

final storage = TokenStorage(
  readGuard: () async {
    final localAuth = LocalAuthentication();
    return await localAuth.authenticate(
      localizedReason: 'Authenticate to access your account',
    );
  },
);

final authService = AuthService(
  tokenEndpoint: Uri.parse('https://api.yourdomain.com/oauth/token'),
  storage: storage,
);

9. Sign Out

// Revokes refresh token server-side (if revokeEndpoint is set) and wipes local storage
await authService.signOut(revokeServerSide: true);

10. Dispose

@override
void dispose() {
  authService.dispose(); // cancels timers and closes authStateChanges stream
  super.dispose();
}

🎮 Interactive Example App

The package includes a full demo app illustrating:

  • Reactive auth state with StreamBuilder<AuthState>
  • Sign in / sign out flows
  • AuthInterceptor for authorized API requests
  • PKCE code generation
  • Typed error handling with AuthErrorCode

To run the demo:

cd example
flutter run

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.