flutter
/

Authentication with Bloc: Login, Logout, and Protected Routes

Last Sync: Today

On this page

10
0%
5 min read
Remaining
5 minleft

Click any section to jump β€” progress syncs automatically

flutter

Authentication with Bloc: Login, Logout, and Protected Routes

πŸ“Œ Summary (Featured Snippet) – Bloc authentication in Flutter is a structured approach to managing login, logout, token storage, and route protection using state management. It ensures secure, scalable, and testable authentication flows with persistent sessions and protected routes.

πŸš€ Quick Start – Need working code now? Jump to the Auth Bloc Implementation section for a complete, copy‑paste ready AuthBloc with login, logout, and token persistence. The full guide explains every piece in detail.

Flutter Bloc authentication flow diagram – unauthenticated state, login event, loading, authenticated state, logout, and token storage

Authentication is a core feature in most mobile apps. Using Bloc, you can build a clean, testable authentication flow that handles login, logout, token persistence, and route protection – without messy setState or callback hell. This guide walks you through building a complete authentication system with Bloc, from secure token storage to protecting routes with go_router. For a foundation in Bloc architecture, see <a href='/bloc-architecture'>Bloc Architecture</a> and <a href='/bloc-repository-pattern'>Repository Pattern</a>.

Authentication State Flow

The authentication state typically goes through these phases: initial check (maybe from persisted token), loading during login, success or failure, and logout. You also need to handle token refresh and session expiry in real‑world apps.

Setting Up the Auth Repository

Start by creating an authentication repository that abstracts the login, logout, and token storage logic. Use secure storage (e.g., flutter_secure_storage) to store tokens. For more on repository pattern, see <a href='/bloc-repository-pattern'>Repository Pattern with Bloc</a>.

YAMLRead-only
1
dependencies:
  flutter_secure_storage: ^9.0.0
DARTRead-only
1
// lib/features/auth/domain/repositories/auth_repository.dart
abstract class AuthRepository {
  Future<String> login(String email, String password);
  Future<void> logout();
  Future<String?> getToken();
  Future<void> saveToken(String token);
  Future<void> deleteToken();
  Future<bool> isAuthenticated();
}

// lib/features/auth/data/repositories/auth_repository_impl.dart
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../datasources/auth_remote_datasource.dart';

class AuthRepositoryImpl implements AuthRepository {
  final AuthRemoteDataSource remoteDataSource;
  final FlutterSecureStorage secureStorage;
  static const _tokenKey = 'auth_token';

  AuthRepositoryImpl(this.remoteDataSource, this.secureStorage);

  @override
  Future<String> login(String email, String password) async {
    final token = await remoteDataSource.login(email, password);
    await saveToken(token);
    return token;
  }

  @override
  Future<void> logout() async {
    await remoteDataSource.logout(); // optional: call logout API
    await deleteToken();
  }

  @override
  Future<void> saveToken(String token) async {
    await secureStorage.write(key: _tokenKey, value: token);
  }

  @override
  Future<String?> getToken() async {
    return await secureStorage.read(key: _tokenKey);
  }

  @override
  Future<void> deleteToken() async {
    await secureStorage.delete(key: _tokenKey);
  }

  @override
  Future<bool> isAuthenticated() async {
    final token = await getToken();
    return token != null && token.isNotEmpty;
  }
}

Defining Auth Bloc

DARTRead-only
1
// lib/features/auth/bloc/auth_event.dart
abstract class AuthEvent {}

class AuthLoginRequested extends AuthEvent {
  final String email;
  final String password;
  AuthLoginRequested(this.email, this.password);
}

class AuthLogoutRequested extends AuthEvent {}

class AuthStatusChecked extends AuthEvent {}
DARTRead-only
1
// lib/features/auth/bloc/auth_state.dart
abstract class AuthState {}

class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthAuthenticated extends AuthState {
  final String token;
  AuthAuthenticated(this.token);
}
class AuthUnauthenticated extends AuthState {}
class AuthFailure extends AuthState {
  final String message;
  AuthFailure(this.message);
}
DARTRead-only
1
// lib/features/auth/bloc/auth_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import '../domain/repositories/auth_repository.dart';

class AuthBloc extends Bloc<AuthEvent, AuthState> {
  final AuthRepository repository;

  AuthBloc(this.repository) : super(AuthInitial()) {
    on<AuthLoginRequested>(_onLoginRequested);
    on<AuthLogoutRequested>(_onLogoutRequested);
    on<AuthStatusChecked>(_onAuthStatusChecked);

    // Check initial auth status when bloc is created
    add(AuthStatusChecked());
  }

  Future<void> _onLoginRequested(
    AuthLoginRequested event,
    Emitter<AuthState> emit,
  ) async {
    emit(AuthLoading());
    try {
      final token = await repository.login(event.email, event.password);
      emit(AuthAuthenticated(token));
    } catch (e) {
      emit(AuthFailure(e.toString()));
    }
  }

  Future<void> _onLogoutRequested(
    AuthLogoutRequested event,
    Emitter<AuthState> emit,
  ) async {
    await repository.logout();
    emit(AuthUnauthenticated());
  }

  Future<void> _onAuthStatusChecked(
    AuthStatusChecked event,
    Emitter<AuthState> emit,
  ) async {
    final isAuthenticated = await repository.isAuthenticated();
    if (isAuthenticated) {
      final token = await repository.getToken();
      emit(AuthAuthenticated(token!));
    } else {
      emit(AuthUnauthenticated());
    }
  }
}

Integrating with the UI

DARTRead-only
1
// lib/main.dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // Setup DI (e.g., with get_it)
  await setup();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => getIt<AuthBloc>(), // or AuthBloc(AuthRepositoryImpl(...))
      child: MaterialApp(
        title: 'Auth Demo',
        home: const SplashPage(), // or use router
      ),
    );
  }
}
DARTRead-only
1
class LoginPage extends StatelessWidget {
  final TextEditingController emailController = TextEditingController();
  final TextEditingController passwordController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Login')),
      body: BlocConsumer<AuthBloc, AuthState>(
        listener: (context, state) {
          if (state is AuthAuthenticated) {
            Navigator.pushReplacementNamed(context, '/home');
          }
          if (state is AuthFailure) {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text(state.message)),
            );
          }
        },
        builder: (context, state) {
          if (state is AuthLoading) {
            return Center(child: CircularProgressIndicator());
          }
          return Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              children: [
                TextField(controller: emailController, decoration: InputDecoration(labelText: 'Email')),
                TextField(controller: passwordController, obscureText: true, decoration: InputDecoration(labelText: 'Password')),
                SizedBox(height: 20),
                ElevatedButton(
                  onPressed: () {
                    context.read<AuthBloc>().add(
                      AuthLoginRequested(emailController.text, passwordController.text),
                    );
                  },
                  child: Text('Login'),
                ),
              ],
            ),
          );
        },
      ),
    );
  }
}

Protecting Routes with GoRouter

A common pattern is to use go_router with a redirect function that checks the authentication state. The bloc provides the state, and the router redirects accordingly. For more navigation patterns, see <a href='/bloc-navigation'>Bloc Navigation</a>.

DARTRead-only
1
// lib/app/router.dart
import 'package:go_router/go_router.dart';

final router = GoRouter(
  initialLocation: '/',
  redirect: (context, state) {
    final authState = context.read<AuthBloc>().state;
    final isAuthenticated = authState is AuthAuthenticated;
    final isGoingToLogin = state.matchedLocation == '/login';

    if (!isAuthenticated && !isGoingToLogin) {
      return '/login';
    }
    if (isAuthenticated && isGoingToLogin) {
      return '/home';
    }
    return null;
  },
  routes: [
    GoRoute(path: '/', redirect: (_, __) => '/home'),
    GoRoute(path: '/login', builder: (context, state) => LoginPage()),
    GoRoute(path: '/home', builder: (context, state) => HomePage()),
    GoRoute(path: '/profile', builder: (context, state) => ProfilePage()),
  ],
);

// In MyApp:
MaterialApp.router(
  routerConfig: router,
  builder: (context, child) => BlocProvider<AuthBloc>(
    create: (_) => getIt<AuthBloc>(),
    child: child!,
  ),
);

Handling Token Expiry and Refresh

In real apps, tokens expire. You can implement a token refresh mechanism in the repository or in an HTTP interceptor. When a 401 response is received, you can trigger a refresh event in the auth bloc.

DARTRead-only
1
// AuthEvent extension
class AuthTokenExpired extends AuthEvent {}
class AuthTokenRefreshed extends AuthEvent {}

// In AuthBloc
on<AuthTokenExpired>((event, emit) async {
  try {
    final newToken = await repository.refreshToken();
    emit(AuthAuthenticated(newToken));
  } catch (e) {
    emit(AuthUnauthenticated());
  }
});

Testing Authentication

Use bloc_test to test the auth bloc with a mocked repository. Verify that the correct states are emitted. For more on testing, see <a href='/bloc-testing'>Bloc Testing</a>.

DARTRead-only
1
void main() {
  late MockAuthRepository repository;
  late AuthBloc bloc;

  setUp(() {
    repository = MockAuthRepository();
    bloc = AuthBloc(repository);
  });

  blocTest<AuthBloc, AuthState>(
    'emits [AuthLoading, AuthAuthenticated] on successful login',
    build: () => bloc,
    act: (bloc) {
      when(() => repository.login('test@example.com', 'password'))
          .thenAnswer((_) async => 'token');
      bloc.add(AuthLoginRequested('test@example.com', 'password'));
    },
    expect: () => [
      AuthLoading(),
      AuthAuthenticated('token'),
    ],
  );

  blocTest<AuthBloc, AuthState>(
    'emits [AuthUnauthenticated] on logout',
    build: () => bloc,
    seed: () => AuthAuthenticated('token'),
    act: (bloc) => bloc.add(AuthLogoutRequested()),
    expect: () => [AuthUnauthenticated()],
  );
}

Best Practices

  • Store tokens securely – Use flutter_secure_storage or encrypted_shared_preferences to store sensitive data.
  • Check auth status on app start – Read stored token and verify it (maybe with a ping endpoint) before showing the home screen.
  • Use BlocListener for navigation – Navigate based on state changes, not directly from bloc.
  • Handle logout everywhere – Clear all app state on logout. You might emit a global reset event.
  • Avoid storing passwords – Only store tokens; never store plain passwords.
  • Implement token refresh – Handle expired tokens gracefully without forcing the user to log in again.
  • Show loading indicators – Provide feedback during login requests.

Common Mistakes

  • ❌ Storing tokens in plain SharedPreferences – They are not secure; use secure storage.
  • ❌ Not handling initial authentication state – The app may briefly show an unauthenticated UI before reading the token.
  • ❌ Calling Navigator inside bloc – Breaks testability; use BlocListener instead.
  • ❌ Not resetting state on logout – The old authenticated state may linger in other blocs.
  • ❌ Not handling network errors – Provide meaningful error messages to the user.

What's Next?

Now that authentication is in place, you can integrate it with other features like protected API calls, and learn how to structure your app with modular architecture.

Next, explore <a href='/bloc-architecture'>Modular architecture with Bloc</a> and <a href='/bloc-testing'>Bloc testing</a>.

Test Your Knowledge

Q1
of 3

What is the best way to store authentication tokens securely in Flutter?

A
SharedPreferences
B
flutter_secure_storage
C
SQLite
D
File storage
Q2
of 3

Which widget should you use to navigate based on authentication state changes?

A
BlocBuilder
B
BlocListener
C
BlocProvider
D
BlocConsumer
Q3
of 3

What should you do on app start to restore the user's session?

A
Immediately navigate to home
B
Show login screen and check token later
C
Read stored token and emit appropriate state
D
Always require login

Frequently Asked Questions

How do I protect routes without go_router?

You can create a widget that listens to the auth state and conditionally shows either the desired screen or redirects to login. Use BlocBuilder in a root widget that decides which page to show.

Should I store the user object along with the token?

Yes, you can extend the AuthAuthenticated state to include a user object. Store the user data after login and retrieve it from the repository. But avoid storing too much sensitive data in secure storage; prefer the token and fetch user on demand.

How do I handle biometric authentication?

You can use packages like local_auth to verify the user's identity. After successful biometric authentication, you can either log in with stored credentials or simply mark the app as authenticated if a valid token already exists.

What about logging out from all tabs/screens?

When logout occurs, all blocs that depend on authentication should reset. You can use a global event bus or have a root AuthBloc that emits a reset event, and other blocs listen to it.

Previous

bloc form validation

Next

bloc pagination

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.