juice_auth

Authentication lifecycle management for Juice applications. Provider-agnostic login, token refresh, session persistence, and reactive auth state.

pub package License: MIT

Features

  • Single Source of TruthauthBloc.state.isAuthenticated is synchronous, reactive, and always current
  • Provider AgnosticAuthProvider interface decouples from Firebase, Supabase, or any backend
  • Automatic Token Lifecycle — secure storage, silent refresh before expiry, singleflight refresh
  • Atomic Logout — one event clears tokens, storage, and session in deterministic order
  • Session Expiry DetectionsessionExpired status distinct from unauthenticated for proper UX
  • Login Rate Limiting — configurable max attempts with cooldown duration
  • Testable — mock AuthProvider, send events, assert AuthState with BlocTester

Installation

dependencies:
  juice_auth: ^0.2.0

Quick Start

1. Implement an Auth Provider

class MyApiAuthProvider extends AuthProvider {
  @override
  String get name => 'email';

  @override
  Future<AuthResult> authenticate(AuthCredentials credentials) async {
    final creds = credentials as EmailCredentials;
    final response = await dio.post('/auth/login', data: {
      'email': creds.email,
      'password': creds.password,
    });
    return AuthResult(
      accessToken: response.data['access_token'],
      refreshToken: response.data['refresh_token'],
      expiresAt: DateTime.parse(response.data['expires_at']),
      user: AuthUser(
        id: response.data['user']['id'],
        email: response.data['user']['email'],
        displayName: response.data['user']['name'],
      ),
    );
  }

  @override
  Future<AuthResult> refreshToken(String refreshToken) async {
    final response = await dio.post('/auth/refresh', data: {
      'refresh_token': refreshToken,
    });
    return AuthResult(
      accessToken: response.data['access_token'],
      refreshToken: response.data['refresh_token'],
      expiresAt: DateTime.parse(response.data['expires_at']),
      user: AuthUser(id: response.data['user']['id']),
    );
  }

  @override
  Future<void> revokeSession(AuthSession session) async {
    try {
      await dio.post('/auth/logout', data: {
        'refresh_token': session.refreshToken,
      });
    } catch (_) {
      // Best-effort — don't block logout
    }
  }
}

2. Register AuthBloc

void main() {
  // 1. Storage first (for token persistence)
  BlocScope.register<StorageBloc>(
    () => StorageBloc(),
    lifecycle: BlocLifecycle.permanent,
  );

  // 2. AuthBloc
  final storageBloc = BlocScope.get<StorageBloc>();

  BlocScope.register<AuthBloc>(
    () => AuthBloc.withConfig(
      AuthConfig(
        providers: {'email': MyApiAuthProvider()},
      ),
      storageBloc: storageBloc,
    ),
    lifecycle: BlocLifecycle.permanent,
  );

  runApp(MyApp());
}

3. Login

final authBloc = BlocScope.get<AuthBloc>();
authBloc.loginWithEmail('user@example.com', 'password');

4. React to Auth State

class AuthGate extends StatelessJuiceWidget<AuthBloc> {
  AuthGate({super.key}) : super(groups: {AuthGroups.status});

  @override
  Widget onBuild(BuildContext context, StreamStatus status) {
    switch (bloc.state.status) {
      case AuthStatus.unknown:
        return SplashScreen();
      case AuthStatus.unauthenticated:
        return LoginScreen();
      case AuthStatus.authenticated:
        return HomeScreen();
      case AuthStatus.sessionExpired:
        return SessionExpiredScreen();
    }
  }
}

5. Integrate with Route Guards

RouteConfig(
  path: '/profile',
  builder: (ctx) => ProfileScreen(),
  guards: [
    AuthGuard(
      isAuthenticated: () => BlocScope.get<AuthBloc>().state.isAuthenticated,
    ),
  ],
)

Rebuild Groups

Group Fires When
auth:status Login, logout, session expiry
auth:user User profile changes
auth:session Token refresh, session update
auth:error Auth error occurs

Integration

Package Integration
juice_routing Provide isAuthenticated callback to AuthGuard/GuestGuard/RoleGuard
juice_network Provide accessToken to AuthInterceptor, refreshToken to RefreshTokenInterceptor
juice_storage Tokens stored in secure storage via StorageBloc

Documentation

Libraries

juice_auth
Authentication lifecycle management for Juice applications.