Skip to main content

flutter_secure_auth 1.0.0 copy "flutter_secure_auth: ^1.0.0" to clipboard
flutter_secure_auth: ^1.0.0 copied to clipboard

A secure, lightweight authentication package for Flutter (REST + OAuth2 PKCE + token refresh) with secure local storage.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_secure_auth/flutter_secure_auth.dart';

// ---------------------------------------------------------------------------
// flutter_secure_auth v1.1.0 — Complete Example App
//
// Demonstrates:
//   • AuthService.create() quick factory
//   • authStateChanges stream + StreamBuilder<AuthState>
//   • isAuthenticated getter
//   • AuthInterceptor drop-in HTTP client
//   • signInWithPassword with typed AuthErrorCode handling
//   • exchangeAuthorizationCode (PKCE)
//   • createPkcePair()
//   • AuthTokens.copyWith()
//   • TokenStorage readGuard (biometric hook)
//   • signOut with dispose()
// ---------------------------------------------------------------------------

void main() {
  runApp(const FlutterSecureAuthExampleApp());
}

// ---------------------------------------------------------------------------
// 1. Create the AuthService once (at app level or via a provider)
// ---------------------------------------------------------------------------
final authService = AuthService.create(
  'https://api.example.com',
  // Override paths if your API uses non-standard endpoints:
  // tokenPath: '/auth/token',
  // revokePath: '/auth/logout',
);

// ---------------------------------------------------------------------------
// App root
// ---------------------------------------------------------------------------
class FlutterSecureAuthExampleApp extends StatelessWidget {
  const FlutterSecureAuthExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'flutter_secure_auth Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorSchemeSeed: Colors.indigo,
        useMaterial3: true,
      ),
      // ---------------------------------------------------------------------------
      // 2. Reactive auth routing with authStateChanges stream
      // ---------------------------------------------------------------------------
      home: StreamBuilder<AuthState>(
        stream: authService.authStateChanges,
        builder: (context, snapshot) {
          final state = snapshot.data ?? const AuthState.unauthenticated();
          if (state.isAuthenticated) {
            return HomeScreen(tokens: state.tokens!);
          }
          return const LoginScreen();
        },
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Login Screen
// ---------------------------------------------------------------------------
class LoginScreen extends StatefulWidget {
  const LoginScreen({super.key});

  @override
  State<LoginScreen> createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  final _emailController = TextEditingController(text: 'demo@example.com');
  final _passwordController = TextEditingController(text: 'password123');
  bool _loading = false;
  String? _errorMessage;

  // -------------------------------------------------------------------------
  // 3. Sign in with password + typed error handling via AuthErrorCode
  // -------------------------------------------------------------------------
  Future<void> _signIn() async {
    setState(() {
      _loading = true;
      _errorMessage = null;
    });

    try {
      await authService.signInWithPassword(
        endpoint: Uri.parse('https://api.example.com/auth/login'),
        username: _emailController.text.trim(),
        password: _passwordController.text,
      );
      // authStateChanges stream will automatically navigate to HomeScreen
    } on AuthException catch (e) {
      String message;
      switch (e.errorCode) {
        case AuthErrorCode.unauthorized:
          message = 'Invalid email or password. Please try again.';
        case AuthErrorCode.serverError:
          message = 'Server error. Please try again later.';
        case AuthErrorCode.networkError:
          message = 'No internet connection.';
        default:
          message = e.message;
      }
      setState(() => _errorMessage = message);
    } finally {
      setState(() => _loading = false);
    }
  }

  // -------------------------------------------------------------------------
  // 4. OAuth2 PKCE flow — generates code pair and exchanges it for tokens
  // -------------------------------------------------------------------------
  void _startPkceFlow() {
    final pkce = createPkcePair(length: 64);

    // In a real app you would launch this URL in a WebView / app browser:
    final authUrl = Uri.parse('https://auth.example.com/authorize').replace(
      queryParameters: {
        'response_type': 'code',
        'client_id': 'your-client-id',
        'redirect_uri': 'com.example.app:/callback',
        'scope': 'openid profile offline_access',
        'code_challenge': pkce.codeChallenge,
        'code_challenge_method': 'S256',
        'state': pkce.state,
      },
    );

    showDialog(
      context: context,
      builder: (_) => AlertDialog(
        title: const Text('PKCE Flow'),
        content: SelectableText(
          'Authorization URL (open in browser):\n\n$authUrl\n\n'
          'Code Verifier (stored securely):\n${pkce.codeVerifier}',
          style: const TextStyle(fontSize: 12),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('Close'),
          ),
          // After capturing the returned `code`, call:
          // await authService.exchangeAuthorizationCode(
          //   code: 'returned_code',
          //   codeVerifier: pkce.codeVerifier,
          //   redirectUri: Uri.parse('com.example.app:/callback'),
          // );
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('flutter_secure_auth Demo')),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            const Icon(Icons.lock_outline, size: 64, color: Colors.indigo),
            const SizedBox(height: 24),
            const Text(
              'flutter_secure_auth v1.1.0',
              textAlign: TextAlign.center,
              style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            const Text(
              'Secure OAuth2 + Token Lifecycle Management',
              textAlign: TextAlign.center,
              style: TextStyle(color: Colors.grey),
            ),
            const SizedBox(height: 32),
            TextField(
              controller: _emailController,
              decoration: const InputDecoration(
                labelText: 'Email',
                border: OutlineInputBorder(),
                prefixIcon: Icon(Icons.email_outlined),
              ),
              keyboardType: TextInputType.emailAddress,
            ),
            const SizedBox(height: 16),
            TextField(
              controller: _passwordController,
              decoration: const InputDecoration(
                labelText: 'Password',
                border: OutlineInputBorder(),
                prefixIcon: Icon(Icons.lock_outline),
              ),
              obscureText: true,
            ),
            if (_errorMessage != null) ...[
              const SizedBox(height: 12),
              Container(
                padding: const EdgeInsets.all(12),
                decoration: BoxDecoration(
                  color: Colors.red.shade50,
                  borderRadius: BorderRadius.circular(8),
                  border: Border.all(color: Colors.red.shade200),
                ),
                child: Text(
                  _errorMessage!,
                  style: TextStyle(color: Colors.red.shade700),
                ),
              ),
            ],
            const SizedBox(height: 24),
            FilledButton.icon(
              onPressed: _loading ? null : _signIn,
              icon: _loading
                  ? const SizedBox.square(
                      dimension: 18,
                      child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
                    )
                  : const Icon(Icons.login),
              label: Text(_loading ? 'Signing in…' : 'Sign In with Password'),
            ),
            const SizedBox(height: 12),
            OutlinedButton.icon(
              onPressed: _startPkceFlow,
              icon: const Icon(Icons.open_in_browser),
              label: const Text('Sign In with OAuth2 PKCE'),
            ),
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }
}

// ---------------------------------------------------------------------------
// Home Screen — shown when authenticated
// ---------------------------------------------------------------------------
class HomeScreen extends StatefulWidget {
  final AuthTokens tokens;

  const HomeScreen({super.key, required this.tokens});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  // -------------------------------------------------------------------------
  // 5. AuthInterceptor — drop-in HTTP client with automatic Authorization header
  // -------------------------------------------------------------------------
  late final AuthInterceptor _client = AuthInterceptor(authService: authService);
  String _apiResult = 'Tap "Fetch Profile" to make an authorized API request.';
  bool _loading = false;

  Future<void> _fetchProfile() async {
    setState(() {
      _loading = true;
      _apiResult = 'Fetching…';
    });

    try {
      // Authorization header is injected automatically.
      // Token is refreshed if expired. 401 triggers one refresh + retry.
      final response = await _client.get(
        Uri.parse('https://api.example.com/profile'),
      );
      setState(() => _apiResult = 'Status: ${response.statusCode}\n${response.body}');
    } on AuthException catch (e) {
      setState(() => _apiResult = 'Auth error: ${e.errorCode} — ${e.message}');
    } catch (e) {
      setState(() => _apiResult = 'Error: $e');
    } finally {
      setState(() => _loading = false);
    }
  }

  // -------------------------------------------------------------------------
  // 6. copyWith() — update a single field on existing tokens
  // -------------------------------------------------------------------------
  void _showCopyWith() {
    final updated = widget.tokens.copyWith(
      accessToken: 'simulatedNewToken_${DateTime.now().millisecondsSinceEpoch}',
    );
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('copyWith → ${updated.accessToken.substring(0, 32)}…')),
    );
  }

  Future<void> _signOut() async {
    await authService.signOut(revokeServerSide: true);
    // authStateChanges stream will automatically navigate to LoginScreen
  }

  @override
  Widget build(BuildContext context) {
    final expiry = widget.tokens.timeToExpiry;
    final expiryLabel = expiry.isNegative
        ? 'Expired'
        : 'Expires in ${expiry.inMinutes}m ${expiry.inSeconds.remainder(60)}s';

    return Scaffold(
      appBar: AppBar(
        title: const Text('Home — Authenticated'),
        actions: [
          IconButton(
            icon: const Icon(Icons.logout),
            tooltip: 'Sign Out',
            onPressed: _signOut,
          ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // Token info card
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text(
                      '🔑 Current Token',
                      style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
                    ),
                    const SizedBox(height: 8),
                    Text(
                      'Access: ${widget.tokens.accessToken.substring(0, 12)}…',
                      style: const TextStyle(fontFamily: 'monospace'),
                    ),
                    Text(
                      'Has Refresh Token: ${widget.tokens.hasRefreshToken}',
                    ),
                    Text(expiryLabel),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 16),
            // AuthInterceptor demo
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text(
                      '🌐 AuthInterceptor Demo',
                      style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
                    ),
                    const SizedBox(height: 8),
                    Text(
                      _apiResult,
                      style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
                    ),
                    const SizedBox(height: 12),
                    FilledButton.icon(
                      onPressed: _loading ? null : _fetchProfile,
                      icon: _loading
                          ? const SizedBox.square(
                              dimension: 16,
                              child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
                            )
                          : const Icon(Icons.cloud_download_outlined),
                      label: const Text('Fetch Profile'),
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 16),
            OutlinedButton.icon(
              onPressed: _showCopyWith,
              icon: const Icon(Icons.copy_outlined),
              label: const Text('Demo: tokens.copyWith()'),
            ),
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    _client.close();
    super.dispose();
  }
}
4
likes
140
points
224
downloads

Documentation

API reference

Publisher

verified publisherquinttechco.com

Weekly Downloads

A secure, lightweight authentication package for Flutter (REST + OAuth2 PKCE + token refresh) with secure local storage.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

crypto, flutter, flutter_secure_storage, http, json_annotation, lints, uuid

More

Packages that depend on flutter_secure_auth