简体   繁体   中英

How to handle the authentication token and and GraphQL client with Riverpod?

We are using GraphQL to connect our backend with our mobil application. We are using Riverpod to, among other things, handle global state, dependency injection, etc.

We are using a provider to handle the GraphQL cliente without any authentication header at first while the user is not authentication (this will handle unauthenticated request like login or registering), after the user is authenticated, a token provider who provides the token is updated and it must update the client provider who is user for every repository on the application:

final StateProvider<String> tokenProvider =
    StateProvider<String>((_) => '', name: "Token Provider");
final graphQLClientProvider = Provider<GraphQLClient>(
  (ref) {
    final String token = ref.watch(tokenProvider).state;

    final Link _link = HttpLink(
        'http://192.168.1.36:1337/graphql',
        defaultHeaders: {
          if (token != '') 'Authorization': 'Bearer $token',
        });

    return GraphQLClient(
      cache: GraphQLCache(),
      link: _link,
    );
  },
  name: "GraphQL Client Provider",
);

There is too problems here:

  1. After updating the token, the client is changed, and from this, the function who updates the token does not finish in a proper way:
[ +141 ms] E/flutter ( 8361): [ERROR:flutter/lib/ui/ui_dart_state.cc(199)] Unhandled Exception: Bad state: Tried to use AuthNotifier after `dispose` was called.
[        ] E/flutter ( 8361): 
[        ] E/flutter ( 8361): Consider checking `mounted`.
[        ] E/flutter ( 8361): 
[        ] E/flutter ( 8361): #0      StateNotifier._debugIsMounted.<anonymous closure> (package:state_notifier/state_notifier.dart:128:9)
[        ] E/flutter ( 8361): #1      StateNotifier._debugIsMounted (package:state_notifier/state_notifier.dart:135:6)
[        ] E/flutter ( 8361): #2      StateNotifier.state= (package:state_notifier/state_notifier.dart:155:12)
[        ] E/flutter ( 8361): #3      AuthNotifier.signIn (package:thesis_cancer/features/auth/application/auth.notifier.dart:84:7)
[        ] E/flutter ( 8361): <asynchronous suspension>
[        ] E/flutter ( 8361): #4      _LoginCardState._submit (package:flutter_login/src/widgets/auth_card.dart:503:15)
[        ] E/flutter ( 8361): <asynchronous suspension>
[        ] E/flutter ( 8361): 

It means when the token is updated, the authentication notifier who receives the repository with depends on the provided client is updated/changes (following the chain):

final authRepositoryProvider = Provider<AuthRepository>(
  (ref) => GraphQLAuthRepository(client: ref.read(graphQLClientProvider)),
  name: 'Auth Repository Provider',
);

final authNotifierProvider = StateNotifierProvider<AuthNotifier, AuthState>(
  (ref) => AuthNotifier(
    authRepository: ref.watch(authRepositoryProvider),
    dataStore: ref.watch(dataStoreRepositoryProvider),
    profileRepository: ref.watch(profileRepositoryProvider),
    tokenController: ref.watch(tokenProvider.notifier),
    userController: ref.watch(userEntityProvider.notifier),
  ),
  name: "Authentication Notifier Provider",
);

The notification function who breaks at the conditional( if ):

Future<String?> signIn({
    required String username,
    required String password,
  }) async {
    try {
      final Map<String, dynamic> rawUser = await authRepository.signIn(
          identifier: username, password: password) as Map<String, dynamic>;
      final User sessionUser = User.fromJson(rawUser);
      if (sessionUser.confirmed != false) {
        tokenController.state = sessionUser.token!; <-- Bug begins here
      }
      await dataStore.writeUserProfile(sessionUser);
      userController.state = sessionUser;
      state = const AuthState.loggedIn();
    } on LogInFailureByBadRequest {
      return "E-posta veya şifre geçersiz.";
    } on LogInFailure catch (error) {
      return error.toString();
    }
  }
  1. While the client appears to be changed when the token is updated, the client itself does not reflect the change (it does not include the authentication header with the token) and thus breaks the GraphQL client: 在此处输入图像描述

Following the comments on the issue we opened , we added the ProviderReference to our notifier (following this question ) but it does not work:

 AuthNotifier(
    this._ref, {
    required this.authRepository,
    required this.profileRepository,
    required this.dataStore,
    required this.tokenController,
    required this.userController,
  }) : super(const AuthState.loading());
  final ProviderReference _ref;
  final ProfileRepository profileRepository;
  final AuthRepository authRepository;
  final DataStoreRepository dataStore;
  final StateController<User?> userController;
  final StateController<String> tokenController;

At this point, we don't understand what's the problem here.

UPDATE

We removed this changes both in the StateNotifier and its provider.

Getting the client at repository by read does not avoid the error: 在此处输入图像描述

What's the proper way to handle the authorization token and the GraphQL client with Riverpod ?

Thank you.

The problem is the authRepositoryProvider :

final authRepositoryProvider = Provider<AuthRepository>(
  (ref) => GraphQLAuthRepository(client: ref.read(graphQLClientProvider)),
  name: 'Auth Repository Provider',
);

In this fragment, the authRepositoryProvider is using a GraphQLClient without token. When the token is updated, the graphQLClientProvider state is well updated, but the authRepositoryProvider isn't because you are using ref.read avoiding the rebuild of the authRepositoryProvider state. This implies that the authRepositoryProvider knows a disposed (and unmounted) state of graphQLClientProvider and that's why the error message.

There are 2 solutions:

  1. Using ref.watch :
final authRepositoryProvider = Provider<AuthRepository>(
  (ref) => GraphQLAuthRepository(client: ref.watch(graphQLClientProvider)),
  name: 'Auth Repository Provider',
);
  1. Passing the ref.read as parameter:
final authRepositoryProvider = Provider<AuthRepository>(
  (ref) => GraphQLAuthRepository(client: ref.read),
  name: 'Auth Repository Provider',
);

class AuthRepository {
  AuthRepository(this.read)

  final Reader read;

  GraphQLClient get client => read(graphQLClientProvider);
}

Be care about using read and watch , the first one does not react to the changes but could be used anywhere, while the second one reacts to a provider's state update but only can be used in another provider constructor or in a widget that provides access to a watch or ref.watch depending on the version of Riverpod.

In addition, there is another problem in your code:

  final User sessionUser = User.fromJson(rawUser);
  if (sessionUser.confirmed != false) {
    // In this moment, the token is updated,
    // then the QrahpQLClient is updated,
    // then the AuthNotifier is recreated,
    // i.e the current is disposed
    // and now becomes the previous
    tokenController.state = sessionUser.token!; <-- Bug begins here
  }
  await dataStore.writeUserProfile(sessionUser);
  userController.state = sessionUser;

  // In this moment you are updating the state of a disposed notifier
  state = const AuthState.loggedIn();

To provide a solution it is necessary to define which elements will make rebuild the AuthNotifer.state instead of recreating the notifier.

You must consider access to another notifiers bases on the ref.read and manually create the subscriptions in the StateController constructor.

In the future, when riverpod 1.0 will be released, this issue will be easy with ref.listen but now you need to rewrite your code. You can take inspiration in the bloc-to-bloc communication gide: https://bloclibrary.dev/#/architecture?id=bloc-to-bloc-communication

i think the error is in your AuthNotifier:

  if (sessionUser.confirmed != false) {
    tokenController.state = sessionUser.token!; <-- you are updating a dependencie of your actual AuthNotifier object
  }
  await dataStore.writeUserProfile(sessionUser); <-- during the await a new object is created and the actual instance is disposed so this line will throw

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM