Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auth example #433

Closed
Chojecki opened this issue Mar 27, 2021 · 16 comments
Closed

Auth example #433

Chojecki opened this issue Mar 27, 2021 · 16 comments

Comments

@Chojecki
Copy link

Chojecki commented Mar 27, 2021

Hi, firstly the lib is awsome and thank you all contributors for working on it.

I have a bad times to understand what proper auth config should like like. In navigator 1.0 I just had a simple "Splash" Widget which checked the app state and switched the widget tree base on it: login/register is state isn't authenticated, app routes if state is.

With new version it seems to be impossible. I see the docs about Guard Routes but the use case is unreal (mocked auth state in auth guard) and I cant convert it to our use case.

I've seen this topic #290

Here the author switch routers in MaterialApp, but looks a little like a hack.

Is this possible to share a little more real example with Auth Service etc.

@Milad-Akarie
Copy link
Owner

Hey @Chojecki Are you providing or injecting your AuthService?

@Chojecki
Copy link
Author

Chojecki commented Mar 28, 2021

Hi @Milad-Akarie
I'm using standard domain-infrastructure-application-presentation architecture, so to answer your question with full view:

At Infrastructure layer, I have a facade for authentication (using Auth0).

At application layer (which is mostly about state management) I'm using Riverpod+freezed, so I have there a AuthStateController (StateNotifier), I'm injecting my auth facade to this AuthStateController and base on action, this Controller returns AuthState of initial/unauthenticated/authenticated. I'm providing/deliver this AuthController and it AuthState to presentation layer by Riverpod provider.

So at presentation layer I have all UI (screens, widgets etc.) and of course router.dart and app.dart where MaterialApp lives.

Normally (with navigator 1.0 and previous auto router), at presentation layer I had a SplashScreen and this Splash was under "/" path (so initial) and in this Splash, I mapped an AuthState and switched WidgetTree base on it: HomeScreen for authenticated, LoginScreen for unauthenticated, Loading indicator on initial.

@Milad-Akarie
Copy link
Owner

Milad-Akarie commented Mar 28, 2021

@Chojecki I believe there are more than one approach to this, your approach is still doable using auto_route 1.0.0.
The approach I use in my real Apps includes Native splash screen + AutoRouteGuard.
this's how you can implement a native splash
https://flutter.dev/docs/development/ui/advanced/splash-screen
i'd await for the auth service so the moment I run the App I know whether my User is authenticated or not and from there I can either use an AutoRouteGuard to guard the home page (to do the redirecting) or simply pass either HomeRoute or LoginRoute as initialRoutes to the router delegate.

void main() async {
  var isAuthenticated = await authService.state;
  // native splash screen is showing until runApp is called
  runApp(MyApp(authenticated: isAuthenticated));
}

class MyApp extends StatelessWidget {
  final _appRouter = AppRouter();

  final bool authenticated;

  MyApp({Key? key, required this.authenticated}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      theme: ThemeData.dark(),
      routerDelegate: _appRouter.delegate(
        initialRoutes: [if (authenticated) HomeRoute() else LoginRoute()],
      ),
}

@Chojecki
Copy link
Author

Thanks, will try. I think the initialRoutes approach is ok, but how to react on logOut then? I mean, how to display LoginRoute in reaction on tap a logout button or for example the scenario of expired access token which will change AuthState to Unauthenticated. I suppose the answer is AuthGuard, but how to provide AuthState to AuthGaurd ? This part is hard to catch for me

@AscentionOne
Copy link

@Chojecki I believe there are more than one approach to this, your approach is still doable using auto_route 1.0.0.

The approach I use in my real Apps includes Native splash screen + AutoRouteGuard.

this's how you can implement a native splash

https://flutter.dev/docs/development/ui/advanced/splash-screen

i'd await for the auth service so the moment I run the App I know whether my User is authenticated or not and from there I can either use an AutoRouteGuard to guard the home page (to do the redirecting) or simply pass either HomeRoute or LoginRoute as initialRoutes to the router delegate.

void main() async {

  var isAuthenticated = await authService.state;

  // native splash screen is showing until runApp is called

  runApp(MyApp(authenticated: isAuthenticated));

}



class MyApp extends StatelessWidget {

  final _appRouter = AppRouter();



  final bool authenticated;



  MyApp({Key? key, required this.authenticated}) : super(key: key);



  @override

  Widget build(BuildContext context) {

    return MaterialApp.router(

      theme: ThemeData.dark(),

      routerDelegate: _appRouter.delegate(

        initialRoutes: [if (authenticated) HomeRoute() else LoginRoute()],

      ),

}

Hi @Milad-Akarie I like this approach. I suppose that with this approach we don't have to set the route as initial with '/' in the router setting right?

I am also interested in how to implement AuthGuard when the token is expired. When user is navigating through the app. Which @Chojecki had mentioned.

Also, speaking of token expiration. Do you have any suggestion of when to check whether the token is expired? I am thinking of checking it before every request. However, will this decrease the performance of the app? Sorry I am kinda new of handling this problem. Thanks!

@theweiweiway
Copy link
Collaborator

The way I do token expiration, is like so:

I make a request to my server with my access token and the server checks if token is expired. If it is expired I return a 'bad-token' response to my client. Now, the client makes a request to the server to get a new token by sending a refresh token to the server. The server authenticates this long lived refresh token and returns a new access and refresh token to client, and the client makes the original request again.

I did this from scratch but it's way too much work- which is why I use firebase auth now.

@theweiweiway
Copy link
Collaborator

@AscentionOne @Chojecki

Thanks, will try. I think the initialRoutes approach is ok, but how to react on logOut then? I mean, how to display
LoginRoute in reaction on tap a logout button or for example the scenario of expired access token which will change > AuthState to Unauthenticated. I suppose the answer is AuthGuard, but how to provide AuthState to AuthGaurd ? This > part is hard to catch for me

Here are a list of solutions I can think of:

  1. Wherever you listen for auth state changes, you could have a function that fires when the user is logged out that pushes the correct Auth route. An example, would be Firebase Auth where you can do auth().onAuthStateChanged

  2. Do what I'm doing, which is using 2 routers: 1 for being logged out and 1 for being logged in. The correct router gets mounted depending on what the auth state is. Here's the code which I also posted in Access context inside Guard 1.0.0 #290

  final _appRouter = AppRouter();
  final _authRouter = AuthRouter();

  RootStackRouter _getRouter(AuthState state) {
    return state.maybeMap(
      signedIn: (_) => _appRouter,
      orElse: () => _authRouter,
    );
  }

...
MaterialApp.router(
    routerDelegate: _getRouter(state).delegate(),
    routeInformationParser:
        _getRouter(state).defaultRouteParser(),
)
  1. You could also use AuthGuard like you were saying, since you can access the auth state via router.navigatorKey.currentContext. This works because issue Access context inside Guard 1.0.0 #290 is only a problem in the case of initializing the app on page refresh of first app open - not when you are already in the app. I actually haven't tested this - but I think it should work.

@theweiweiway
Copy link
Collaborator

@AscentionOne

Hi @Milad-Akarie I like this approach. I suppose that with this approach we don't have to set the route as initial with
'/' in the router setting right?

I think it's good practice to set an initial route. Excluding an initial route might mess with deep-linking because of prefix matching i think?

_router.defaultRouteParser(includePrefixMatches: false), 

@Chojecki
Copy link
Author

Thanks for answers. Ye, about auth, we are using Auth0 because solutions like Firebase Auth are quite limited in case of users roles (admin, read, write etc.)

For now we ended with some imperative approach of ProviderListener + context.router.navigate but planning to refactor navigation during migration to Flutter 2.0. Already checked and this work (and by work I mean 'protect the route' but nothing else). Like name says AuthGuard is just guarding and not let user to navigate if conditions aren't ok. If token will expire and this will change AuthState to unauthenticated, the app stays on current (protected route) and because of that we have to rethink all navigation with AutoRoute.declarative

For now AuthGuard like this, works (without logout on token expire case):

// For now we are not using it. Things for future refactor to Navigator 2.0
class AuthGuard extends AutoRouteGuard {
  @override
  Future<bool> canNavigate(
      List<PageRouteInfo> pendingRoutes, StackRouter router) async {
    final context = router.navigatorKey.currentContext;
    final auth = context.read(authContollerProvider.state);

    return auth.map(
        initial: (_) => true,
        authenticated: (_) => true,
        authenticatedMultiTenant: (_) => true,
        unauthenticated: (_) {
          router.root.push(LoginRoute());
          return false;
        });
  }
}

@AscentionOne
Copy link

@theweiweiway Thank you for providing some examples and insight of AuthGaurd and handling token expiration problem. I will try it and see how it goes. For the FireBase, I like it integrates quite well with flutter. However, currently, our team is using server build on their own so... yeah... I will provide suggestions based on what you did before. Seems like a good solution also quite an amount of work 😂

@Chojecki Thank you for also provide some suggestion and how you solved it currently. I will try to implement it and see how it goes. Since I am also using Bloc as my state management solution. 👍

@theweiweiway
Copy link
Collaborator

If you guys using Bloc to handle auth, you could do

BlocListener<AuthBloc, AuthState>(
  listenWhen: (prevState, currState) {
    if (prevState is LoggedIn && currState is LoggedOut) {
      router.push(AuthRoute)
    }
  }
)

Now, whenever the user goes from being logged in to logged out, they get pushed to the authentication page

@AscentionOne
Copy link

AscentionOne commented Mar 31, 2021

If you guys using Bloc to handle auth, you could do

BlocListener<AuthBloc, AuthState>(
  listenWhen: (prevState, currState) {
    if (prevState is LoggedIn && currState is LoggedOut) {
      router.push(AuthRoute)
    }
  }
)

Now, whenever the user goes from being logged in to logged out, they get pushed to the authentication page

@theweiweiway Can I see the example of what you put in AuthRoute? My guess is just a SignIn screen route? Thank you!

To clarify, I mean what should I provide in AuthRouter in this comment. My guess is just a simple SignInRoute? I basically have all the pages in AppRouter. Thank you!

@theweiweiway
Copy link
Collaborator

it looks like this: Basically, all routes that show up when user is logged out will be defined ikn the auth router:

@MaterialAutoRouter(
  replaceInRouteName: 'Page,Route',
  routes: <AutoRoute>[
    RedirectRoute(path: "/", redirectTo: "/auth"),
    AutoRoute(
      /// These auth routes are declaratively rendered. See
      /// `AuthWrapperPage` to view how these routes are shown
      page: AuthWrapperPage,
      path: "/auth",
      children: [
        AutoRoute(path: "", page: AuthPage),
        AutoRoute(path: "email", page: AuthEmailPage),
        AutoRoute(path: "resend_email", page: AuthResendEmailPage),
        AutoRoute(path: "verify_email", page: AuthVerifyEmailPage),
        AutoRoute(path: "reenter_email", page: AuthReEnterEmailPage),
        AutoRoute(path: "create", page: AuthCreatePage),
        RedirectRoute(path: '*', redirectTo: ''),
      ],
    ),
    AutoRoute(page: InvitePage, path: "/invite/:invite_id"),
    RedirectRoute(path: '*', redirectTo: '/'),
  ],
)
class $AuthRouter {}

@AscentionOne
Copy link

it looks like this: Basically, all routes that show up when user is logged out will be defined ikn the auth router:

@MaterialAutoRouter(
  replaceInRouteName: 'Page,Route',
  routes: <AutoRoute>[
    RedirectRoute(path: "/", redirectTo: "/auth"),
    AutoRoute(
      /// These auth routes are declaratively rendered. See
      /// `AuthWrapperPage` to view how these routes are shown
      page: AuthWrapperPage,
      path: "/auth",
      children: [
        AutoRoute(path: "", page: AuthPage),
        AutoRoute(path: "email", page: AuthEmailPage),
        AutoRoute(path: "resend_email", page: AuthResendEmailPage),
        AutoRoute(path: "verify_email", page: AuthVerifyEmailPage),
        AutoRoute(path: "reenter_email", page: AuthReEnterEmailPage),
        AutoRoute(path: "create", page: AuthCreatePage),
        RedirectRoute(path: '*', redirectTo: ''),
      ],
    ),
    AutoRoute(page: InvitePage, path: "/invite/:invite_id"),
    RedirectRoute(path: '*', redirectTo: '/'),
  ],
)
class $AuthRouter {}

@theweiweiway Aesthetic 😘 Thank you!

@theweiweiway
Copy link
Collaborator

great, im gonna close this then!

@Dionnie123
Copy link

Can we see the routes of these 2 approuter? how do you setup the initial routes?

  final _appRouter = AppRouter();
  final _authRouter = AuthRouter();

  RootStackRouter _getRouter(AuthState state) {
    return state.maybeMap(
      signedIn: (_) => _appRouter,
      orElse: () => _authRouter,
    );
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants