Auth in Flutter 2 - Navigation and Guards with GoRouter
This is the second part of the Auth in Flutter series. In this article, we will discuss how to implement Authentication and Authorization in Flutter correctly. Our focus will be on Navigation and Guards.
Here's the link for the previous part in case you didn't read it:

Navigation
A few years ago, Flutter introduced a new declarative navigation approach that is now the recommended way to implement navigation. This approach allows for manual management of the page stack, providing endless opportunities. This guide will use the go_router and go_router_builder that implement this API.
In our application, there are two routes available: dashboard, which requires authenticated access, and login, the initial route. This is how configuration for them look like:
/// LoginRoute is a route for the login screen.
@TypedGoRoute<LoginRoute>(path: '/login')
class LoginRoute extends GoRouteData {
/// LoginRoute constructor
const LoginRoute();
@override
Widget build(
BuildContext context,
GoRouterState state,
) =>
const LoginScreen();
}
/// DashboardRoute is a route for the dashboard screen.
@TypedGoRoute<DashboardRoute>(path: '/dashboard')
class DashboardRoute extends GoRouteData {
/// DashboardRoute constructor
const DashboardRoute();
@override
Widget build(
BuildContext context,
GoRouterState state,
) =>
const DashboardScreen();
}
We can now access those routes by using GoRouter.go. Additionally, these routes are accessible through a deeplink. However, it is also important to note that we will not push routes imperatively. Instead, we will implement transitions from unauthorized to authorized screens in a declarative way using guards or, in the context of go_router, redirections.
Guards
The navigation guard is a rule that the router uses to determine whether to allow the user to proceed to a route. I will implement two guards - for authenticated and unauthenticated users.
For this purpose, go_router offers a redirect function that is triggered every time the path changes, whether it is the initial app opening or an update in runtime (e.g. via the web address bar or route push). I have developed a wrapper function to create dedicated guards in a more readable manner:
First of all, let's implement a guard that will redirect users from screens that require authenticated access to login:
/// Guard that navigates user from authorized routes to login
/// when their authentication status is unauthenticated.
final class RedirectIfUnauthenticatedGuard extends Guard {
// matches dashboard and settings routes
@override
Pattern get matchPattern => RegExp(r'^/(login|signup)$');
@override
bool get invertRedirect => true;
@override
String? redirect(BuildContext context, GoRouterState state) {
final auth = AuthScope.of(context);
if (auth.status == AuthenticationStatus.unauthenticated) {
return '/login';
}
return null;
}
}I have defined a matchPattern in this guard that matches two simple paths: /login and /signup. The invertRedirect property is set to true, which means that the redirect function will be invoked if the path does not match (for example, if an unauthenticated user tries to access the /dashboard route, this redirect will be called and the user will be sent to /login).
We can also create the opposite guard, redirecting authenticated users from the /login and /signup screens to /dashboard. Here's what it looks like:
/// Guard that navigates user from unauthorized routes to dashboard
/// if the user is authenticated.
final class RedirectIfAuthenticatedGuard extends Guard {
// matches login and signup routes
@override
Pattern get matchPattern => RegExp(r'^/(login|signup)$');
@override
String? redirect(BuildContext context, GoRouterState state) {
final auth = AuthScope.of(context);
if (auth.status == AuthenticationStatus.authenticated) {
return '/dashboard';
}
return null;
}
}These guards can now be passed to RedirectBuilder to be used as a redirect function:
GoRouter(
initialLocation: '/login',
routes: $appRoutes,
redirect: RedirectBuilder({
RedirectIfAuthenticatedGuard(),
RedirectIfUnauthenticatedGuard(),
}),
)Important note: Guards must be invoked each time the authentication status changes. For example, if the user logs out, the first guard must send them to the /signin route.
In our case, I used AuthScope.of which utilizes InheritedWidget under the hood and makes that context rebuild upon any authentication change. What is more, if you don't use InheritedWidgets, go_router provides refreshListenable that will recompute all the guards upon notification.
In combination with the components implemented in the previous guide, this configuration will automatically redirect the user when their authentication status changes. For example, after a successful login or logout.