π 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>.
dependencies:
flutter_secure_storage: ^9.0.0
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();
}
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();
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
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 {}
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);
}
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);
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
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await setup();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => getIt<AuthBloc>(),
child: MaterialApp(
title: 'Auth Demo',
home: const SplashPage(),
),
);
}
}
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>.
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()),
],
);
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.
class AuthTokenExpired extends AuthEvent {}
class AuthTokenRefreshed extends AuthEvent {}
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>.
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>.