Flutter web google_sign_in: How to retrieve refreshToken - flutter

google_sign_in does not return refreshToken. Is there a way to sign in with Google and receive a refresh token which can be sent to the API for further access to the user's data?
Refresh token could be also obtained with serverAuthCode which is always null at the moment. There are multiple issues created already describing this issue:
https://github.com/flutter/flutter/issues/45847
https://github.com/flutter/flutter/issues/57712
https://github.com/flutter/flutter/issues/15796
https://github.com/flutter/flutter/issues/62474
Is there any way to authenticate with Google Sign In and receive either refreshToken or serverAuthCode?

Google Sign In is based on oAuth2, one can create own implementation of the process.
Here's a code snippet of a google sign in service which can be used to retrieve refreshToken:
import 'dart:async';
import 'dart:html' as html;
import 'package:oauth2/oauth2.dart' as oauth2;
class GoogleSignInService {
final authorizationEndpoint =
Uri.parse('https://accounts.google.com/o/oauth2/v2/auth');
final tokenEndpoint = Uri.parse('https://oauth2.googleapis.com/token');
final String identifier;
final String secret;
final String baseUrl;
final List<String> scopes;
_SignInSession? _signInSession;
Uri get redirectUrl => Uri.parse('$baseUrl/callback.html');
GoogleSignInService({
required this.identifier,
required this.secret,
required this.baseUrl,
required this.scopes,
}) {
html.window.addEventListener('message', _eventListener);
}
void _eventListener(html.Event event) {
_signInSession?.completeWithCode((event as html.MessageEvent).data);
}
Future<GoogleSignInUser?> signIn() async {
if (_signInSession != null) {
return null;
}
final grant = oauth2.AuthorizationCodeGrant(
identifier,
authorizationEndpoint,
tokenEndpoint,
secret: secret,
);
var authorizationUrl = grant.getAuthorizationUrl(
redirectUrl,
scopes: scopes,
);
final url =
'${authorizationUrl.toString()}&access_type=offline&prompt=select_account+consent';
_signInSession = _SignInSession(url);
final code = await _signInSession!.codeCompleter.future;
if (code != null) {
final client = await grant.handleAuthorizationResponse({'code': code});
return GoogleSignInUser(
accessToken: client.credentials.accessToken,
refreshToken: client.credentials.refreshToken,
idToken: client.credentials.idToken!,
);
} else {
return null;
}
}
}
class GoogleSignInUser {
final String accessToken;
final String? refreshToken;
final String idToken;
const GoogleSignInUser({
required this.accessToken,
required this.refreshToken,
required this.idToken,
});
#override
String toString() {
return 'GoogleSignInUser{accessToken: $accessToken, refreshToken: $refreshToken, idToken: $idToken}';
}
}
class _SignInSession {
final codeCompleter = Completer<String?>();
late final html.WindowBase _window;
late final Timer _timer;
bool get isClosed => codeCompleter.isCompleted;
_SignInSession(String url) {
_window =
html.window.open(url, '_blank', 'location=yes,width=550,height=600');
_timer = Timer.periodic(const Duration(milliseconds: 500), (timer) {
if (_window.closed == true) {
if (!isClosed) {
codeCompleter.complete(null);
}
_timer.cancel();
}
});
}
void completeWithCode(String code) {
if (!isClosed) {
codeCompleter.complete(code);
}
}
}
Make sure to also create web/callback.html file:
<html>
<body>
</body>
<script>
function findGetParameter(parameterName) {
var result = null,
tmp = [];
location.search
.substr(1)
.split("&")
.forEach(function (item) {
tmp = item.split("=");
if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]);
});
return result;
}
let code = findGetParameter('code');
window.opener.postMessage(code, "http://localhost:5000");
window.close();
</script>
</html>
Change http://localhost:5000 to whatever domain you're using in production.
Exemplary usage:
final googleSignIn = GoogleSignInService(
identifier: 'CLIENT_ID',
secret: 'CLIENT_SECRET',
baseUrl: 'http://localhost:5000',
scopes: [
'email',
],
),
final user = await googleSignIn.signIn();

Related

Sign in with token with google_sign_in

I'm trying to implement a "silent" login for my application, currently, after the application restarts, the user is signed out, and needs to signin again, but I do store the token.
How can I sign in the user automatically using the token (or am I doing something wrong?) when the application starts?
current code:
import 'package:get/get.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:syncornot/controllers/cache_manager.dart';
class GoogleAuthController extends GetxController with CacheManager {
var _googleSignIn = GoogleSignIn(scopes: ['email']);
var googleAcc = Rx<GoogleSignInAccount?>(null);
var isSignedIn = false.obs;
var googleToken;
#override
void onInit() {
checkLoginStatus();
super.onInit();
}
void signInWithGoogle() async {
try {
googleAcc.value = await _googleSignIn.signIn();
isSignedIn.value = true;
googleToken = await googleAcc.value?.authentication;
update(); // <-- without this the isSignedin value is not updated.
saveToken(googleToken.accessToken);
} catch (e) {
Get.snackbar(
'Error occured!',
e.toString(),
snackPosition: SnackPosition.BOTTOM,
);
}
}
void login() async {
isSignedIn.value = true;
await saveToken(googleToken);
}
void silentLogin() async {
googleAcc.value = await _googleSignIn.signIn();
isSignedIn.value = true;
await saveToken(googleToken);
}
void logOut() {
isSignedIn.value = false;
saveToken(googleToken);
}
void checkLoginStatus() {
final token = getToken();
if (token != null) {
silentLogin();
isSignedIn.value = true;
}
}
}
and the CacheManager:
import 'package:get_storage/get_storage.dart';
mixin CacheManager {
Future<bool> saveToken(String? token) async {
final box = GetStorage();
await box.write(CacheManagerKey.TOKEN.toString(), token);
return true;
}
String? getToken() {
final box = GetStorage();
return box.read(CacheManagerKey.TOKEN.toString());
}
Future<void> removeToken() async {
final box = GetStorage();
await box.remove(CacheManagerKey.TOKEN.toString());
}
}
enum CacheManagerKey { TOKEN }
I want some silent signing by using the credentials or something..
Thanks

I need your help for an error I encounter in flutter dart

I have an application and I created a legin and logout page... and when I click on my application's logout button, I get an error like this " Null check operator used on a null value"*and when I point to the error, it tells me [1] :
https://i.stack.imgur.com/n0uJ8.pngentrez
import 'dart:async';
import 'dart:convert';
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:logger/logger.dart';
import '../db/db_auth_shared_preference.dart';
import '../network/app_urls.dart';
import '../models/auth.dart';
enum Status {
notLoggedIn,
loggedIn,
authenticating,
loggedOut,
notReet,
reseted,
resetting
}
//Help display the logs
var logger = Logger();
class AuthProvider with ChangeNotifier {
Auth? _auth;
Auth get auth => _auth!;
void setAuth(Auth auth) {
_auth = auth;
notifyListeners();
}
bool isAuth() {
if (_auth == null || auth.token == '') {
return false;
}
return true;
}
// Time before the token expires
Timer? _authTimer;
DateTime? _expiryDate;
String? username;
String? password;
// Set the status of the user to Not Logged In at the start of the app
Status _status = Status.notLoggedIn;
Status get status => _status;
// Change the status of the user
set status(Status value) {
_status = value;
notifyListeners();
}
// Log In the user
Future<Map<String, dynamic>> login(String email, String password) async {
Map<String, Object> results;
final Map<String, dynamic> loginData = {
'email': email,
'password': password
};
status = Status.authenticating;
logger.d("--- Authentication ---");
try {
Response response = await post(
Uri.parse(
"${AppUrl.login}? username=${loginData['email']}&password=${loginData['password']}"
),
);
logger.d('Login response : ${response.statusCode}');
// The Request Succeded
if (response.statusCode == 200) {
final Map<String, dynamic> responseData =
json.decode(utf8.decode(response.bodyBytes));
var requestStatus = responseData["status"];
if (requestStatus != 0) {
status = Status.notLoggedIn;
results = {'status': false, 'message': "La Connexion a échoué"};
} else {
// Get the status code of the request
Map<String, dynamic> authData = responseData["utilisateurJson"];
logger.d(authData);
_expiryDate = DateTime.now().add(const Duration(seconds: 3500));
//store user shared pref
Auth authUser = Auth.fromMap(authData,
timeToExpire: _expiryDate,
username: loginData['email'],
password: loginData['password']);
_expiryDate = authUser.expiryDate;
logger.wtf(_expiryDate);
//clear session data
AuthPreferences().removeAuth();
//store User session
AuthPreferences().saveAuth(authUser);
setAuth(authUser);
status = Status.loggedIn;
username = loginData["email"];
password = loginData["password"];
results = {
'status': true,
'message': 'Successful login',
'auth': authUser,
};
autoLogOut();
}
} else {
status = Status.notLoggedIn;
results = {'status': false, 'message': 'La Connexion a échoué'};
}
return results;
} catch (e) {
logger.e(e);
status = Status.notLoggedIn;
results = {
'status': false,
'message': "La Connexion avec le serveur a échoué"
};
return results;
} }
void autoLogOut() {
if (_authTimer != null) {
_authTimer!.cancel();
}
final timeToExpiry = _expiryDate!.difference(DateTime.now()).inSeconds;
_authTimer = Timer(Duration(seconds: timeToExpiry),
() async => await login(username!, password!));
}
// Log Out the User
void logOut() {
logger.d("--- User Logging Out ---");
AuthPreferences().removeAuth();
status = Status.loggedOut;
_expiryDate = null;
_auth = null;
logger.d("--- User Logged Out ---");
}
Future<Auth?> tryAutoLogin() async {
final authSession = await AuthPreferences().getAuth();
if (authSession == null) {
return null;
}
logger.d("The expiry time is : ${authSession.expiryDate}");
if (authSession.expiryDate.isBefore(DateTime.now())) {
login(authSession.username, authSession.password);
return authSession;
}
_expiryDate = authSession.expiryDate;
setAuth(authSession);
logger.d("SETTING THE USER");
autoLogOut();
return authSession;
}
}
Error Explanation: Bang operator(!) means that in flutter, when you use this operator, you are completely sure that variable is not going to be null in any case.
There are two ways to resolve it -
Use if conditional to confirm that variable is not null
Use null-aware or if-null operator ?? like
Auth get auth => _auth ?? Auth();
Since you didn't provide any error logs; based on attached image and as your cursor on line no 29, _auth variable is null. So before using ! make sure your variable is not null.

GraphQL notification in flutter - how to catch result?

I subscribe to a graphql document:
// Dart imports:
import 'dart:async';
// Package imports:
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:phoenix_socket/phoenix_socket.dart';
// Project imports:
import 'package:core/util/confirmations/phoenix_link.dart';
class SubscriptionChannel {
PhoenixSocket? socket;
PhoenixChannel? channel;
GraphQLClient? client;
final StreamController<Map> _onMessageController = StreamController<Map>();
Stream<Map> get onMessage => _onMessageController.stream;
Future<void> connect(
String phoenixHttpLinkEndpoint, String websocketUriEndpoint) async {
final HttpLink phoenixHttpLink = HttpLink(
phoenixHttpLinkEndpoint,
);
channel =
await PhoenixLink.createChannel(websocketUri: websocketUriEndpoint);
final phoenixLink = PhoenixLink(
channel: channel!,
);
var link = Link.split(
(request) => request.isSubscription, phoenixLink, phoenixHttpLink);
client = GraphQLClient(
link: link,
cache: GraphQLCache(),
);
}
void addSubscriptionTransactionConfirmed(
String address, Function(QueryResult) function) {
final subscriptionDocument = gql(
'subscription { transactionConfirmed(address: "$address") { nbConfirmations } }',
);
Stream<QueryResult> subscription = client!.subscribe(
SubscriptionOptions(document: subscriptionDocument),
);
subscription.listen(function);
}
Future<Message> onPushReply(Push push) async {
final Completer<Message> completer = Completer<Message>();
final Message result = await channel!.onPushReply(push.replyEvent);
completer.complete(result);
return completer.future;
}
void close() {
_onMessageController.close();
if (socket != null) {
socket!.close();
}
}
}
The goal is, after a api request, to wait a notification with a nb of confirmations:
{
...
await subscriptionChannel.connect(
'https://mainnet.archethic.net/socket/websocket',
'ws://mainnet.archethic.net/socket/websocket');
subscriptionChannel.addSubscriptionTransactionConfirmed(
transaction.address!, waitConfirmations);
transactionStatus = sendTx(signedTx); // API Request
...
}
void waitConfirmations(QueryResult event) {
if (event.data != null &&
event.data!['transactionConfirmed'] != null &&
event.data!['transactionConfirmed']['nbConfirmations'] != null) {
EventTaxiImpl.singleton().fire(TransactionSendEvent(
response: 'ok',
nbConfirmations: event.data!['transactionConfirmed']
['nbConfirmations']));
} else {
EventTaxiImpl.singleton().fire(
TransactionSendEvent(nbConfirmations: 0, response: 'ko'),
);
}
subscriptionChannel.close();
}
My code works in a StatefulWidget but doesn't work in a class
Have you got some examples in a class where you subscribe to a grapqhql notification please to understand how to code this in a class
NB: i'm using Phoenix link
// Dart imports:
import 'dart:async';
// Package imports:
import 'package:gql_exec/gql_exec.dart';
import 'package:gql_link/gql_link.dart';
import 'package:phoenix_socket/phoenix_socket.dart';
/// a link for subscriptions (or also mutations/queries) over phoenix channels
class PhoenixLink extends Link {
/// the underlying phoenix channel
final PhoenixChannel channel;
final RequestSerializer _serializer;
final ResponseParser _parser;
/// create a new [PhoenixLink] using an established PhoenixChannel [channel].
/// You can use the static [createChannel] method to create a [PhoenixChannel]
/// from a websocket URI and optional parameters (e.g. for authentication)
PhoenixLink(
{required PhoenixChannel channel,
ResponseParser parser = const ResponseParser(),
RequestSerializer serializer = const RequestSerializer()})
: channel = channel,
_serializer = serializer,
_parser = parser;
/// create a new phoenix socket from the given websocketUri,
/// connect to it, and create a channel, and join it
static Future<PhoenixChannel> createChannel(
{required String websocketUri, Map<String, String>? params}) async {
final socket = PhoenixSocket(websocketUri,
socketOptions: PhoenixSocketOptions(params: params));
await socket.connect();
final channel = socket.addChannel(topic: '__absinthe__:control');
final push = channel.join();
await push.future;
return channel;
}
#override
Stream<Response> request(Request request, [NextLink? forward]) async* {
assert(forward == null, '$this does not support a NextLink (got $forward)');
final payload = _serializer.serializeRequest(request);
String? phoenixSubscriptionId;
StreamSubscription<Response>? websocketSubscription;
StreamController<Response>? streamController;
final push = channel.push('doc', payload);
try {
final pushResponse = await push.future;
//set the subscription id in order to cancel the subscription later
phoenixSubscriptionId =
pushResponse.response['subscriptionId'] as String?;
if (phoenixSubscriptionId != null) {
//yield all messages for this subscription
streamController = StreamController();
websocketSubscription = channel.socket
.streamForTopic(phoenixSubscriptionId)
.map((event) => _parser.parseResponse(
event.payload!['result'] as Map<String, dynamic>))
.listen(streamController.add, onError: streamController.addError);
yield* streamController.stream;
} else if (pushResponse.isOk) {
yield _parser
.parseResponse(pushResponse.response as Map<String, dynamic>);
} else if (pushResponse.isError) {
throw _parser.parseError(pushResponse.response as Map<String, dynamic>);
}
} finally {
await websocketSubscription?.cancel();
await streamController?.close();
//this will be called once the caller stops listening to the stream
// (yield* stops if there is no one listening)
if (phoenixSubscriptionId != null) {
channel.push('unsubscribe', {'subscriptionId': phoenixSubscriptionId});
}
}
}
}

Flutter - Refresh page if data isn't loaded on screen

I have a Future.delayed in my initState function that gets jwt cache and uses it to get the logged in user's details. The initState function is:
#override
void initState() {
super.initState();
Future.delayed(Duration.zero, () async {
final token = await CacheService().readCache(key: "jwt");
if (token != null) {
await Provider.of<ProfileNotifier>(context, listen: false)
.decodeUserData(
context: context,
token: token,
option: 'home',
);
}
});
}
Now it does work and I do get the data but not on the first run. I have to either hot reload the emulator or navigate to another page and come back for the page to rebuild itself and show the data on screen. I don't understand why it doesn't show the data on the first run itself.
I tried to add conditional to build method to run setState and initState again if data is not there.
#override
Widget build(BuildContext context) {
ProfileModel profile =
Provider.of<ProfileNotifier>(context, listen: false).profile;
if (profile.profileName.isEmpty) {
print('reloading to get data');
initState();
setState(() {});
} ....
And it doesn't run coz the data is there but somehow it doesn't show up on the screen till the page is refreshed. I can't seem to figure out the problem here. Please help.
EDIT: ProfileNotifier class:
class ProfileNotifier extends ChangeNotifier {
final ProfileAPI _profileAPI = ProfileAPI();
final CacheService _cacheService = CacheService();
ProfileModel _profile = ProfileModel(
profileImage: "",
profileName: "",
profileBio: "",
);
AccountModel _account = AccountModel(
userId: "",
userEmail: "",
userPassword: "",
);
ProfileModel get profile => _profile;
AccountModel get account => _account;
Future decodeUserData({
required BuildContext context,
required String token,
required String option,
}) async {
try {
_profileAPI.decodeUserData(token: token).then((value) async {
final Map<String, dynamic> parsedData = await jsonDecode(value);
var userData = parsedData['data'];
if (userData != null) {
List<String>? userProfileData = await _cacheService.readProfileCache(
key: userData['userData']['id'],
);
if (userProfileData == null) {
final isProfileAvailable =
await Provider.of<ProfileNotifier>(context, listen: false)
.getProfile(
context: context,
userEmail: userData['userData']['userEmail'],
);
if (isProfileAvailable is ProfileModel) {
_profile = isProfileAvailable;
} else {
_account = AccountModel(
userId: userData['userData']['id'],
userEmail: userData['userData']['userEmail'],
userPassword: userData['userData']['userPassword'],
);
_profile = ProfileModel(
profileImage: '',
profileName: '',
);
}
if (option != 'profileCreation' && isProfileAvailable == false) {
Navigator.of(context).pushReplacementNamed(ProfileCreationRoute);
}
} else {
_account = AccountModel(
userId: userData['userData']['id'],
userEmail: userData['userData']['userEmail'],
userPassword: userData['userData']['userPassword'],
);
_profile = ProfileModel(
profileName: userProfileData[3],
profileImage: userProfileData[4],
profileBio: userProfileData[5],
);
}
} else {
Navigator.of(context).pushReplacementNamed(AuthRoute);
}
notifyListeners();
});
} catch (e) {
debugPrint('account/profileNotifier decode error: ' + e.toString());
}
}
Future getProfile({
required BuildContext context,
required String userEmail,
}) async {
try {
var getProfileData = await _profileAPI.getProfile(
userEmail: userEmail,
);
final Map<String, dynamic> parsedProfileData =
await jsonDecode(getProfileData);
bool isReceived = parsedProfileData["received"];
dynamic profileData = parsedProfileData["data"];
if (isReceived && profileData != 'Fill some info') {
Map<String, dynamic> data = {
'id': (profileData['account']['id']).toString(),
'userEmail': profileData['account']['userEmail'],
'userPassword': profileData['account']['userPassword'],
'profile': {
'profileName': profileData['profileName'],
'profileImage': profileData['profileImage'],
'profileBio': profileData['profileBio'],
}
};
AccountModel accountModel = AccountModel.fromJson(
map: data,
);
return accountModel;
} else {
return false;
}
} catch (e) {
debugPrint('profileNotifier getProfile error: ' + e.toString());
}
}
Future setProfile({
required String profileName,
required String profileImage,
required String profileBio,
}) async {
_profile.profileName = profileName;
_profile.profileImage = profileImage;
_profile.profileBio = profileBio;
await _cacheService.writeProfileCache(
key: _account.userId,
value: [
_account.userId,
_account.userEmail,
_account.userPassword as String,
profileName,
profileImage,
profileBio,
],
);
notifyListeners();
}
}
I've removed the create profile, update profile and profile image upload methods in the notifier as they are not involved here.
The CacheService class using shared_preferences package is:
class CacheService {
Future<String?> readCache({
required String key,
}) async {
final SharedPreferences sharedPreferences =
await SharedPreferences.getInstance();
String? cache = await sharedPreferences.getString(key);
return cache;
}
Future<List<String>?> readProfileCache({
required String key,
}) async {
final SharedPreferences sharedPreferences =
await SharedPreferences.getInstance();
List<String>? cachedData = await sharedPreferences.getStringList(key);
return cachedData;
}
Future writeCache({required String key, required String value}) async {
final SharedPreferences sharedPreferences =
await SharedPreferences.getInstance();
await sharedPreferences.setString(key, value);
}
Future writeProfileCache(
{required String key, required List<String> value}) async {
final SharedPreferences sharedPreferences =
await SharedPreferences.getInstance();
await sharedPreferences.setStringList(key, value);
}
Future deleteCache({
required BuildContext context,
required String key,
}) async {
final SharedPreferences sharedPreferences =
await SharedPreferences.getInstance();
await sharedPreferences.remove(key).whenComplete(() {
Navigator.of(context).pushReplacementNamed(AuthRoute);
});
}
}
I'm not sure if the code is completely optimized. Any improvement is welcome. Thanks.

Flutter: How to refresh token when token expires with ferry (graphql) client?

How to retrieve a new token with a refresh token in flutter in a ferry (graphql) client?
The response after a mutation looks like this:
{
"data": {
"auth_login": {
"access_token": "ey...",
"refresh_token": "Ua...",
"expires": 900000
}
}
}
I tried to accomplish it with fresh_graphql, but it does not work. The authenticationStatus is always unauthenticated but the token was always legit.
Implementation:
import 'dart:math';
import 'package:ferry/ferry.dart';
import 'package:ferry_hive_store/ferry_hive_store.dart';
import 'package:fresh_graphql/fresh_graphql.dart';
import 'package:gql_http_link/gql_http_link.dart';
import 'package:hive/hive.dart';
Future<Client> initClient(String? accessToken, String? refreshToken) async {
Hive.init('hive_data');
final box = await Hive.openBox<Map<String, dynamic>>('graphql');
await box.clear();
final store = HiveStore(box);
final cache = Cache(store: store);
final freshLink = await setFreshLink(accessToken ?? '', refreshToken);
final link = Link.from(
[freshLink, HttpLink('https://.../graphql/')]);
final client = Client(
link: link,
cache: cache,
);
return client;
}
Future<FreshLink> setFreshLink(String accessToken, String? refreshToken) async {
final freshLink = FreshLink<dynamic>(
tokenStorage: InMemoryTokenStorage<dynamic>(),
refreshToken: (dynamic token, client) async {
print('refreshing token!');
await Future<void>.delayed(const Duration(seconds: 1));
if (Random().nextInt(1) == 0) {
throw RevokeTokenException();
}
return OAuth2Token(
accessToken: 'top_secret_refreshed',
);
},
shouldRefresh: (_) => Random().nextInt(2) == 0,
)..authenticationStatus.listen(print);
print(freshLink.token);
print(freshLink.authenticationStatus);
await freshLink
.setToken(OAuth2Token(tokenType: 'Bearer', accessToken: accessToken));
return freshLink;
}
Any solution, even without fresh_graphql, would be appreciated!
The way I initialize my ferry client is as follows.
Create a CustomAuthLink that inherits from AuthLink.
import 'package:gql_http_link/gql_http_link.dart';
class _CustomAuthLink extends AuthLink {
_CustomAuthLink() : super(
getToken: () {
// ...
// Call your api to refresh the token and return it
// ...
String token = await ... // api refresh call
return "Bearer $token"
}
);
}
Use this custom auth link to initialise your client.
...
final link = Link.from([freshLink, HttpLink('https://.../graphql/')]);
...
Client(
link: _CustomAuthLink().concat(link),
)
...
I am not sure if you still going to need freshLink anymore. You might wanna remove it and pass HttpLink(...) directly into the .concat(...) method.