Describe the bug
I am trying to create authentication for my app. If there are no user details present in the app (first time use) there is no token. I'm not too sure how to initialise my AuthLink when no token is present, but then change it once token is present (after login)
To Reproduce
Steps to reproduce the behavior:
graphQLClient = ValueNotifier(
GraphQLClient(
cache: InMemoryCache(),
link: AuthLink(getToken: () async {
if (store.state.authUser.accessToken == null) {
return "";
} else {
return "Bearer ${store.state.authUser.accessToken}";
}
}).concat(HttpLink(uri: "${store.state.serverURL}/graphql"))),
);
static const login = r"""
mutation Login($input: LoginInput!){
login(
data: $input
)
{
access_token
refresh_token
expires_in
token_type
user {
username
email
profile {
forename
surname
birthday
gender
}
}
}
}
""";
QueryResult result = await graphQLClient.value.mutate(MutationOptions(
document: Queries.login,
variables: {
"input": {
"username": value['username'],
"password": value['password'],
},
},
));
link_auth.dartNoSuchMethodError: The getter 'accessToken' was called on null.
Receiver: null
Tried calling: accessToken#0 Object.noSuchMethod (dart:core-patch/object_patch.dart:50:5)
#1 initialiseGlobals.<anonymous closure> (package:community/vendor/globals.dart:21:36)
<asynchronous suspension>
#2 new AuthLink.<anonymous closure>.onListen (package:graphql/src/link/auth/link_auth.dart:18:52)
<asynchronous suspension>
#3 _runGuarded (dart:async/stream_controller.dart:805:24)
#4 _StreamController._subscribe.<anonymous closure> (dart:async/stream_controller.dart:684:7)
#5 _BufferingStreamSubscription._guardCallback (dart:async/stream_impl.dart:414:13)
#6 _StreamController._subscribe (dart:async/stream_controller.dart:683:18)
#7 _ControllerStream._createSubscription (dart:async/stream_controller.dart:818:19)
#8 _StreamImpl.listen (dart:async/stream_impl.dart:472:9)
#9 Stream.first (dart:async/stream.dart:1188:25)
#10 QueryManager._resolveQueryOnNetwork (package:graphql/src/core/query_manager.dart:112:9)
<asynchronous suspension>
#11 QueryManager.fetchQueryAsMultiSourceResult (package:graphql/src/core/query_manager.dart:90:17)
#12 QueryManager.fetchQuery (package:graphql/src/core/query_manager.dart:68:9)
<asynchronous suspension>
#13 QueryManager.mutate (package:graphql/src/core/query_manager.dart:60:12)
#14 GraphQLClient.mutate (package:graphql/src/graphql_client.dart:50:25)
#15 AuthBloc.login (package:community/bloc/blocs/auth_bloc.dart:27:52)
<asynchronous suspension>
#16 AuthBloc.mapEventToState (package:community/bloc/blocs/auth_bloc.dart:16:14)
<asynchronous suspension>
#17 Bloc._bindStateSubject.<anonymous closure> (package:bloc/src/bloc.dart:127:14)
#18 Stream.asyncExpand.onListen.<anonymous closure> (dart:async/stream.dart:513:30)
#19 _rootRunUnary (dart:async/zone.dart:1132:38)
#20 _CustomZone.runUnary (dart:async/zone.dart:1029:19)
#21 _CustomZone.runUnaryGuarded (dart:async/zone.dart:931:7)
#22 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:336:11)
#23 _DelayedData.perform (dart:async/stream_impl.dart:591:14)
#24 _StreamImplEvents.handleNext (dart:async/stream_impl.dart:707:11)
#25 _PendingEvents.schedule.<anonymous closure> (dart:async/stream_impl.dart:667:7)
#26 _rootRun (dart:async/zone.dart:1120:38)
#27 _CustomZone.run (dart:async/zone.dart:1021:19)
#28 _CustomZone.runGuarded (dart:async/zone.dart:923:7)
#29 _CustomZone.bindCallbackGuarded.<anonymous closure> (dart:async/zone.dart:963:23)
#30 _rootRun (dart:async/zone.dart:1124:13)
#31 _CustomZone.run (dart:async/zone.dart:1021:19)
#32 _CustomZone.runGuarded (dart:async/zone.dart:923:7)
#33 _CustomZone.bindCallbackGuarded.<anonymous closure> (dart:async/zone.dart:963:23)
#34 _microtaskLoop (dart:async/schedule_microtask.dart:41:21)
#35 _startMicrotaskLoop (dart:async/schedule_microtask.dart:50:5)
Expected behavior
If there is no token present in the app, then use a normal Http Link. but once a token has been set, change to AuthLink (as all queries after login will require the header)
Desktop (please complete the following information):
Smartphone (please complete the following information):
I just realised that my accessToken is null because there is no data... i changed my if statement when producing the AuthLink to this
link: AuthLink(getToken: () async {
if (store.state.authUser == null) {
return "";
} else {
return "Bearer ${store.state.authUser.accessToken}";
}
}).concat(HttpLink(uri: "${store.state.serverURL}/graphql"))),
Can someone just tell me if this is the correct way?
EDIT
now its this.
return "Bearer ${store.state.authUser == null ? "" : store.state.authUser.accessToken ?? ""}";
TLDR: How to dynamically switch between AuthLink and normal link, depending on if the token is present
I'm facing the same problem as you are. The GraphQL client is setup when the app initializes which means there's no token to be passed into AuthLink until the user signs in/up.
This was my attempt at solving this problem (Inspired by an old issue):
final AuthLink authLink = AuthLink(
getToken: () async => 'Bearer $tokenFromSuccessfulSignIn',
);
final GraphQLClient client = GraphQLProvider.of(context).value;
client.link.concat(authLink);
I tried creating a new AuthLink that contains the token that was returned by my backend after a successful login. I then concatenate the new AuthLink into the existing GraphQLClient's link. Unfortunately though, this method doesn't work.
I'm hoping someone can shed some light on how to add an authorization header post-setup.
@terenceponce why not have a multi-route approach? You can group the routes, so that each group has a different GraphQLProvider widget, proving client with token or not based on use case. This should work for most apart from a few edge cases. For instance, login pages would have a GraphQLProvider provider of their and user account pages would have GraphQLProvider of their with an authentication token attached.
Am also thinking there could be another option, where you could use state management (Redux, Bloc etc.) to update the value of the client. Any changes to the client will notify listener and cause a rebuild with the new value. GraphQLProvider is a InheritedWidget and notifies any listeners such as Query and Mutation in case of any changes. So for instance, if authentication state changes, update the client and notify listeners that the client have changed. I hope i can get sometime this week to comb up a proof of concept for this.
@mainawycliffe I did the state management approach that you mentioned and it worked perfectly. Thanks for the idea!
Here's what I did in case anyone's curious:
final AuthenticationState authenticationState = Provider.of<AuthenticationState>(context);
Link link;
final HttpLink httpLink = HttpLink(
uri: 'https://someapi.com/graphql',
);
if (authenticationState.authenticationToken != null && authenticationState.authenticationToken.isNotEmpty) {
final AuthLink authLink = AuthLink(
getToken: () => 'Bearer ${authenticationState.authenticationToken}',
);
link = authLink.concat(httpLink as Link);
} else {
link = httpLink as Link;
}
final ValueNotifier<GraphQLClient> client = ValueNotifier<GraphQLClient>(
GraphQLClient(
cache: InMemoryCache(),
link: link,
),
);
For context, AuthenticationState is a class extending ChangeNotifier.
this works very well. thank you.
@dali546 You're welcome! May I suggest changing the title of this issue into something more search engine friendly like "How to add authorization header after client setup?"
I'm sure a lot of people will encounter this problem as well, so might as well make the solution easier for them to find.
@mainawycliffe I did the state management approach that you mentioned and it worked perfectly. Thanks for the idea!
Here's what I did in case anyone's curious:
final AuthenticationState authenticationState = Provider.of<AuthenticationState>(context); Link link; final HttpLink httpLink = HttpLink( uri: 'https://someapi.com/graphql, ); if (authenticationState.authenticationToken != null && authenticationState.authenticationToken.isNotEmpty) { final AuthLink authLink = AuthLink( getToken: () => 'Bearer ${authenticationState.authenticationToken}', ); link = authLink.concat(httpLink as Link); } else { link = httpLink as Link; } final ValueNotifier<GraphQLClient> client = ValueNotifier<GraphQLClient>( GraphQLClient( cache: InMemoryCache(), link: link, ), );For context,
AuthenticationStateis a class extendingChangeNotifier.
This may work in certain situations but I still want to add custom headers per Graphql query or mutation.
Can it be added to the package? Or if it was added already, how can I do that?
@terenceponce I still couldn't figure out how to use your solution, Would you please elaborate the solution because I am not able to get the AuthenticationState its seems something you wrote customise?
@terenceponce ditto what @abdulwahid24 said! Any chance there is a repo with the full example anywhere? Thanks
@abdulwahid24 It is custom code. It's not that hard to replicate though. In the sample code I posted, it's just a class that's under the influence of another class that extends ChangeNotifier meaning it has access to some state. I used that on the class that declares the httpLink variable so it can decided whether to add the auth header or not based on the value of the current state.
@terenceponce I have wrote this code. But this is not working. notifiyListener is not calling the widget. Can you please help me with this?
class MyApp extends StatelessWidget {
final _navigatorKey = GlobalKey<NavigatorState>();
@override
Widget build(BuildContext context) {
final AuthenticationState authenticationState = Provider.of<AuthenticationState>(context);
Link link;
final HttpLink httpLink = HttpLink(uri: graphqlEndpoint);
if (authenticationState.authenticationToken != null && authenticationState.authenticationToken.isNotEmpty){
final AuthLink authLink = AuthLink(
getToken: () => 'Bearer ${authenticationState.authenticationToken}',
);
link = authLink.concat(httpLink);
}else {
link = httpLink;
}
ValueNotifier<GraphQLClient> client = ValueNotifier(
GraphQLClient(
cache: cache,
link: link,
),
);
return GraphQLProvider(
client: client,
child: MaterialApp(
key: _navigatorKey,
title: '',
theme: ThemeData(
primarySwatch: Colors.orange,
fontFamily: 'Inter',
),
home: SplashScreen(),
routes: routes,
),
);
}
}
class AuthenticationState extends ChangeNotifier{
var _token = [];
dynamic get authenticationToken => _token;
void changeAuthState(value){
_token.add(value);
notifyListeners();
}
}
@Nik1308 hey there! Did you got it to working since then? I have the same problem and would be really glad to get some help! Basically I have used the same code as you with a lot of customizing around it but the base is the same.
@Nik1308 hey there! Did you got it to working since then? I have the same problem and would be really glad to get some help! Basically I have used the same code as you with a lot of customizing around it but the base is the same.
Any update? I'm with this problem in my app.
@silasrm @tibfox The above code is working, You just need to change this code
class AuthenticationState extends ChangeNotifier{
var _token = [];
dynamic get authenticationToken => _token;
void changeAuthState(value){
_token.add(value);
notifyListeners();
}
}
with
class AuthenticationState with ChangeNotifier{
var _token = [];
dynamic get authenticationToken => _token;
void isAuthenticated() {
_token.add("tokenAdded");
notifyListeners();
}
void isNotAuthenticated() {
_token.clear();
notifyListeners();
}
}
And then call the auth function
Provider.of<AuthenticationState>(context, listen: false).isAuthenticated()
wherever you are getting the auth token
@silasrm @tibfox The above code is working, You just need to change this code
class AuthenticationState extends ChangeNotifier{ var _token = []; dynamic get authenticationToken => _token; void changeAuthState(value){ _token.add(value); notifyListeners(); } }with
class AuthenticationState with ChangeNotifier{ var _token = []; dynamic get authenticationToken => _token; void isAuthenticated() { _token.add("tokenAdded"); notifyListeners(); } void isNotAuthenticated() { _token.clear(); notifyListeners(); } }And then call the auth function
Provider.of<AuthenticationState>(context, listen: false).isAuthenticated()
wherever you are getting the auth token
Hi @Nik1308,
I'm following your code, with this last modification, but throw this error:
The following ProviderNotFoundException was thrown building AppWidget(dirty):
[ ] I/flutter (19509): Error: Could not find the correct Provider<AuthenticationState> above this AppWidget Widget
[ ] I/flutter (19509):
[ ] I/flutter (19509): This likely happens because you used a `BuildContext` that does not include the provider
[ ] I/flutter (19509): of your choice. There are a few common scenarios:
[ ] I/flutter (19509):
[ ] I/flutter (19509): - The provider you are trying to read is in a different route.
AppWidget is my MyApp with other name. Any idea?
Hi,
My old code is 100% ok now. My error was where I'm put the ValueNotifier<GraphQLClient> client. I'm was putting this outside build() method. Now, is inside. :D
hey all,
i also getting Error to set token.
first when getToken or header both nulll but after registration i store token. its not set in Header so how to update Token and recall it
Most helpful comment
@dali546 You're welcome! May I suggest changing the title of this issue into something more search engine friendly like "How to add authorization header after client setup?"
I'm sure a lot of people will encounter this problem as well, so might as well make the solution easier for them to find.