Skip to content

Route Guards

Understand how to protect routes with authentication and authorization checks.


What is it?

Route guards are mechanisms that control access to specific routes based on certain conditions like authentication status, user roles, or other business logic. They act as gatekeepers that determine whether a user can navigate to a particular screen, redirecting them to appropriate destinations if they don't meet the requirements.


Why does it exist?

Route guards exist to:

  • Protect routes from unauthorized access
  • Implement authentication requirements
  • Enforce role-based access control
  • Redirect users to appropriate screens
  • Handle session validation
  • Prevent route access in invalid states
  • Implement authorization logic

Authentication Guards

Protecting routes with authentication checks.

// Authentication guard with GoRouter
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';

// Auth state management
class AuthState extends ChangeNotifier {
  bool _isAuthenticated = false;
  String? _userToken;

  bool get isAuthenticated => _isAuthenticated;
  String? get token => _userToken;

  Future<void> login(String username, String password) async {
    // Simulate login
    await Future.delayed(const Duration(seconds: 1));
    _isAuthenticated = true;
    _userToken = 'fake-token-123';
    notifyListeners();
  }

  Future<void> logout() async {
    _isAuthenticated = false;
    _userToken = null;
    notifyListeners();
  }
}

class AuthGuardApp extends StatelessWidget {
  const AuthGuardApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => AuthState(),
      child: MaterialApp.router(
        routerConfig: _authGuardRouter,
      ),
    );
  }
}

final _authGuardRouter = GoRouter(
  initialLocation: '/',
  // Global redirect for authentication
  redirect: (context, state) {
    final auth = Provider.of<AuthState>(context);
    final isAuthenticated = auth.isAuthenticated;
    final isLoginRoute = state.matchedLocation == '/login';

    // If not authenticated and not on login, go to login
    if (!isAuthenticated && !isLoginRoute) {
      return '/login';
    }

    // If authenticated and on login, go to home
    if (isAuthenticated && isLoginRoute) {
      return '/';
    }

    return null; // No redirect
  },
  routes: [
    // Public routes
    GoRoute(
      path: '/login',
      builder: (context, state) => const LoginScreen(),
    ),

    // Protected routes (requires auth)
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
      routes: [
        GoRoute(
          path: 'profile',
          builder: (context, state) => const ProfileScreen(),
        ),
        GoRoute(
          path: 'settings',
          builder: (context, state) => const SettingsScreen(),
        ),
        GoRoute(
          path: 'dashboard',
          builder: (context, state) => const DashboardScreen(),
        ),
      ],
    ),
  ],
  errorBuilder: (context, state) => const ErrorScreen(),
);

What's happening here? - AuthState manages authentication - Global redirect checks auth status - Login route is public - All other routes require auth - Redirects to login if not authenticated


Role-Based Guards

Controlling access based on user roles.

// Role-based access control
class UserState extends ChangeNotifier {
  String _role = 'guest';

  String get role => _role;

  bool get isAdmin => _role == 'admin';
  bool get isManager => _role == 'manager';
  bool get isUser => _role == 'user';

  void setRole(String role) {
    _role = role;
    notifyListeners();
  }
}

class RoleGuardApp extends StatelessWidget {
  const RoleGuardApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => UserState(),
      child: MaterialApp.router(
        routerConfig: _roleGuardRouter,
      ),
    );
  }
}

final _roleGuardRouter = GoRouter(
  initialLocation: '/',
  redirect: (context, state) {
    final user = Provider.of<UserState>(context);

    // Check if route requires specific role
    final location = state.matchedLocation;

    // Admin only routes
    if (location.startsWith('/admin') && !user.isAdmin) {
      return '/unauthorized';
    }

    // Manager routes
    if (location.startsWith('/manager') && !user.isManager && !user.isAdmin) {
      return '/unauthorized';
    }

    // User routes (any authenticated user)
    if (location.startsWith('/dashboard') && user.role == 'guest') {
      return '/login';
    }

    return null;
  },
  routes: [
    // Public
    GoRoute(
      path: '/',
      builder: (context, state) => const PublicHomeScreen(),
    ),
    GoRoute(
      path: '/login',
      builder: (context, state) => const LoginScreen(),
    ),
    GoRoute(
      path: '/unauthorized',
      builder: (context, state) => const UnauthorizedScreen(),
    ),

    // User routes
    GoRoute(
      path: '/dashboard',
      builder: (context, state) => const DashboardScreen(),
    ),

    // Manager routes
    GoRoute(
      path: '/manager/reports',
      builder: (context, state) => const ReportsScreen(),
    ),

    // Admin routes
    GoRoute(
      path: '/admin/users',
      builder: (context, state) => const UsersManagementScreen(),
    ),
    GoRoute(
      path: '/admin/settings',
      builder: (context, state) => const AdminSettingsScreen(),
    ),
  ],
);

What's happening here? - UserState manages user role - Role-based redirect logic - Admin, manager, user roles - Unauthorized screen for access denied


Route-Level Guards

Protecting individual routes with guards.

// Route-level guards
class RouteGuardApp extends StatelessWidget {
  const RouteGuardApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => AuthState(),
      child: MaterialApp.router(
        routerConfig: _routeGuardRouter,
      ),
    );
  }
}

final _routeGuardRouter = GoRouter(
  initialLocation: '/',
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
    ),
    GoRoute(
      path: '/login',
      builder: (context, state) => const LoginScreen(),
    ),
    // Route with guard
    GoRoute(
      path: '/profile',
      builder: (context, state) => const ProfileScreen(),
      // Route-level redirect
      redirect: (context, state) {
        final auth = Provider.of<AuthState>(context);
        if (!auth.isAuthenticated) {
          return '/login';
        }
        return null;
      },
    ),
    // Route with specific conditions
    GoRoute(
      path: '/settings',
      builder: (context, state) => const SettingsScreen(),
      redirect: (context, state) {
        final auth = Provider.of<AuthState>(context);
        final user = Provider.of<UserState>(context);

        if (!auth.isAuthenticated) {
          return '/login';
        }
        if (!user.isAdmin) {
          return '/unauthorized';
        }
        return null;
      },
    ),
  ],
);

// Custom guard for specific features
class FeatureGuard {
  static String? checkAccess(BuildContext context, String feature) {
    final auth = Provider.of<AuthState>(context);
    final user = Provider.of<UserState>(context);

    if (!auth.isAuthenticated) {
      return '/login';
    }

    // Feature-specific checks
    if (feature == 'admin' && !user.isAdmin) {
      return '/unauthorized';
    }

    if (feature == 'premium' && !user.isPremium) {
      return '/upgrade';
    }

    return null;
  }
}

// Using custom guard
class FeatureRoutes {
  static final routes = [
    GoRoute(
      path: '/admin',
      builder: (context, state) => const AdminPanelScreen(),
      redirect: (context, state) {
        return FeatureGuard.checkAccess(context, 'admin');
      },
    ),
    GoRoute(
      path: '/premium',
      builder: (context, state) => const PremiumContentScreen(),
      redirect: (context, state) {
        return FeatureGuard.checkAccess(context, 'premium');
      },
    ),
  ];
}

What's happening here? - Route-level redirects - Multiple condition checks - Custom guard classes - Feature-based access control


Guard with Async Checks

Asynchronous route guards.

// Async route guards
class AsyncGuardApp extends StatelessWidget {
  const AsyncGuardApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => AsyncAuthState(),
      child: MaterialApp.router(
        routerConfig: _asyncGuardRouter,
      ),
    );
  }
}

class AsyncAuthState extends ChangeNotifier {
  bool _isLoading = true;
  bool _isAuthenticated = false;

  bool get isLoading => _isLoading;
  bool get isAuthenticated => _isAuthenticated;

  AsyncAuthState() {
    _checkAuth();
  }

  Future<void> _checkAuth() async {
    // Simulate async auth check
    await Future.delayed(const Duration(seconds: 2));
    _isAuthenticated = true;
    _isLoading = false;
    notifyListeners();
  }

  Future<void> login() async {
    await Future.delayed(const Duration(seconds: 1));
    _isAuthenticated = true;
    notifyListeners();
  }
}

final _asyncGuardRouter = GoRouter(
  initialLocation: '/',
  redirect: (context, state) {
    final auth = Provider.of<AsyncAuthState>(context);

    // Show loading while checking auth
    if (auth.isLoading) {
      return '/splash';
    }

    // Redirect based on auth status
    if (!auth.isAuthenticated) {
      return '/login';
    }

    return null;
  },
  routes: [
    GoRoute(
      path: '/splash',
      builder: (context, state) => const SplashScreen(),
    ),
    GoRoute(
      path: '/login',
      builder: (context, state) => const LoginScreen(),
    ),
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
    ),
  ],
);

// Custom AsyncGuard
class AsyncGuard {
  static Future<String?> check(
    BuildContext context,
    String route,
  ) async {
    final auth = Provider.of<AsyncAuthState>(context);

    // Simulate async validation
    await Future.delayed(const Duration(milliseconds: 500));

    if (!auth.isAuthenticated) {
      return '/login';
    }

    return null;
  }
}

What's happening here? - Async auth state checking - Loading screen during auth check - Async guard class - Simulated network delays


Real-World Examples

Common patterns with route guards.

// 1. Complete auth guard implementation
class AuthGuard {
  static String? check(BuildContext context, String route) {
    final auth = Provider.of<AuthState>(context);
    final user = Provider.of<UserState>(context);

    // Check authentication
    if (!auth.isAuthenticated) {
      return '/login';
    }

    // Check session timeout
    if (auth.isSessionExpired) {
      return '/session-expired';
    }

    // Check account status
    if (!user.isVerified) {
      return '/verify-email';
    }

    // Check subscription status
    if (route.startsWith('/premium') && !user.isSubscribed) {
      return '/pricing';
    }

    return null;
  }
}

// 2. Role-based guard with redirects
class RoleGuard {
  static String? check(
    BuildContext context,
    String route,
    List<String> allowedRoles,
  ) {
    final user = Provider.of<UserState>(context);

    if (route == '/admin') {
      if (user.isAdmin) return null;
      if (user.isManager) return '/manager/dashboard';
      return '/unauthorized';
    }

    if (!allowedRoles.contains(user.role)) {
      return '/unauthorized';
    }

    return null;
  }
}

// 3. Multi-factor guard
class MfaGuard {
  static String? check(BuildContext context, String route) {
    final auth = Provider.of<AuthState>(context);
    final mfa = Provider.of<MfaState>(context);

    if (!auth.isAuthenticated) {
      return '/login';
    }

    if (auth.requiresMfa && !mfa.isVerified) {
      return '/mfa-verify';
    }

    return null;
  }
}

// 4. Registration flow guard
class RegistrationFlowGuard {
  static String? check(BuildContext context, String route) {
    final flow = Provider.of<RegistrationState>(context);

    if (route == '/register/step1' && flow.step > 0) {
      return '/register/step${flow.step + 1}';
    }

    if (route == '/register/step2' && flow.step < 1) {
      return '/register/step1';
    }

    if (route == '/register/complete' && flow.step < 2) {
      return '/register/step${flow.step + 1}';
    }

    return null;
  }
}

// Router with all guards
final _fullGuardRouter = GoRouter(
  initialLocation: '/',
  routes: [
    // Public routes
    GoRoute(
      path: '/login',
      builder: (context, state) => const LoginScreen(),
    ),
    GoRoute(
      path: '/verify-email',
      builder: (context, state) => const VerifyEmailScreen(),
    ),
    GoRoute(
      path: '/mfa-verify',
      builder: (context, state) => const MfaVerifyScreen(),
    ),

    // Registration flow
    GoRoute(
      path: '/register/step1',
      builder: (context, state) => const RegisterStep1Screen(),
      redirect: (context, state) => RegistrationFlowGuard.check(context, state.matchedLocation),
    ),
    GoRoute(
      path: '/register/step2',
      builder: (context, state) => const RegisterStep2Screen(),
      redirect: (context, state) => RegistrationFlowGuard.check(context, state.matchedLocation),
    ),
    GoRoute(
      path: '/register/complete',
      builder: (context, state) => const RegisterCompleteScreen(),
      redirect: (context, state) => RegistrationFlowGuard.check(context, state.matchedLocation),
    ),

    // Protected routes
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
      redirect: (context, state) => AuthGuard.check(context, state.matchedLocation),
    ),
    GoRoute(
      path: '/profile',
      builder: (context, state) => const ProfileScreen(),
      redirect: (context, state) => AuthGuard.check(context, state.matchedLocation),
    ),
    GoRoute(
      path: '/admin',
      builder: (context, state) => const AdminScreen(),
      redirect: (context, state) => RoleGuard.check(context, state.matchedLocation, ['admin']),
    ),
    GoRoute(
      path: '/premium',
      builder: (context, state) => const PremiumScreen(),
      redirect: (context, state) => AuthGuard.check(context, state.matchedLocation),
    ),
  ],
);

What's happening here? - Complete auth guard implementation - Role-based guard with redirects - Multi-factor authentication guard - Registration flow guard - Multiple guards combined


Best Practices

Centralize Guard Logic

// Good - Centralized guards
class AppGuards {
  static String? authCheck(BuildContext context, String route) {
    final auth = Provider.of<AuthState>(context);
    if (!auth.isAuthenticated) return '/login';
    return null;
  }

  static String? adminCheck(BuildContext context, String route) {
    final user = Provider.of<UserState>(context);
    if (!user.isAdmin) return '/unauthorized';
    return null;
  }
}

// Bad - Scattered guard logic
GoRoute(
  path: '/profile',
  redirect: (context, state) {
    final auth = Provider.of<AuthState>(context);
    if (!auth.isAuthenticated) return '/login';
    return null;
  },
)

Use Redirects for Invalid States

// Good - Redirect to appropriate screens
redirect: (context, state) {
  if (!auth.isAuthenticated) return '/login';
  if (!user.isVerified) return '/verify-email';
  return null;
}

// Bad - Letting users access invalid states
redirect: (context, state) {
  if (!auth.isAuthenticated) return null; // Shows protected screen
  return null;
}

Provide Feedback for Guard Failures

// Good - Show why access was denied
redirect: (context, state) {
  if (!user.isAdmin) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('Admin access required')),
    );
    return '/unauthorized';
  }
  return null;
}

Common Mistakes

Not Handling Guard Failures

Wrong:

// Guard fails silently
redirect: (context, state) {
  if (!auth.isAuthenticated) {
    // User stays on protected route
    return null;
  }
  return null;
}

Correct:

// Redirect on failure
redirect: (context, state) {
  if (!auth.isAuthenticated) {
    return '/login';
  }
  return null;
}

Overcomplicating Guards

Wrong:

// Too complex
redirect: (context, state) {
  final auth = Provider.of<AuthState>(context);
  final user = Provider.of<UserState>(context);
  final settings = Provider.of<SettingsState>(context);
  // Too many conditions
  if (auth.isAuthenticated && user.isVerified && settings.featureEnabled) {
    return null;
  }
  // Complex logic
}

Correct:

// Simplified guards
redirect: (context, state) {
  return AuthGuard.check(context, state.matchedLocation);
}


Summary

Route guards protect routes from unauthorized access using authentication, authorization, and business logic checks. Use GoRouter's redirect functionality to implement guards, manage authentication state, and provide appropriate feedback to users when access is denied.


Next Steps


Did You Know?

  • Guards can be global or route-specific
  • Authentication guards protect routes
  • Role-based guards control permissions
  • Async guards handle network checks
  • Guards can redirect to login or error screens
  • Multiple guards can be combined
  • Guards provide user feedback
  • Guards improve app security