How to request refresh token called once on multiple api calls - flutter

I have a function that refreshes a token if the previous API response returns an error code of 1000. However, when multiple API calls are made at the same time, it results in multiple refresh token requests. I want to ensure that the refresh token is only called once.
Here is my code
requestGet(String endPoint, Map<String, dynamic> params, [bool isUsingToken = false]) async {
String sign = getSign(timestamp + jsonEncode(params));
String deviceId = await SharedPrefsService().getDeviceId();
String token = await SharedPrefsService().getToken();;
final response = await httpGet(endPoint, params, sign, token, deviceId, isUsingToken);
dynamic result = response;
var isRenewed = await renewTokenIfNeeded(deviceId, result, endPoint);
if (isRenewed) {
token = await SharedPrefsService().getToken();
final renewedResponse = await httpGet(endPoint, params, sign, token, deviceId, isUsingToken);
result = renewedResponse;
}
return result;
}
Future<bool> renewTokenIfNeeded(String deviceId, result) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
bool renewingToken = prefs.getBool('renewingToken') ?? false;
if (result['error_code'] == '1000') {
prefs.setBool('renewingToken', true);
try {
if (renewingToken) {
return true;
}
var isRenewed = await requestRenewToken(deviceId);
if (isRenewed) {
prefs.setBool('renewingToken', false);
return true;
}
} finally {
prefs.setBool('renewingToken', false);
}
}
return false;
}
requestRenewToken(String deviceId) async {
var refresh = await AuthenticationService().refreshToken();
if (refresh.errorCode == '9999') {
SharedPrefsService().clearAllData();
return false; // then back to sign in
}
if (refresh.errorCode == '0000') {
SharedPrefsService().saveTokenData(refresh.token!, refresh.userName!, deviceId);
return true;
}
return false;
}
I have tried using synchronized and mutex packages, but they do not seem to work and I prefer to minimize the use of external packages. Can you please suggest a solution? Thank you!

Related

how to redirect the user to the login page if the token has expired

hello I have a case where when the user token expires the user does not switch to the loginPage page, even though I have set it here.
how do i solve this problem thanks.
i set it on splashscreen if token is not null then go to main page and if token is null then go to login page.
but when the token expires it still remains on the main page
Future<void> toLogin() async {
Timer(
const Duration(seconds: 3),
() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? token = prefs.getString(Constant.token);
Navigator.pushReplacementNamed(
context,
token != null ? AppRoute.mainRoute : AppRoute.loginRoute,
arguments: token,
);
},
);
}
and function when user login
CustomButtonFilled(
title: 'Login',
onPressed: () async {
final prefs =
await SharedPreferences.getInstance();
prefs.setString(Constant.token, '');
if (nimController.text.isEmpty ||
passwordController.text.isEmpty) {
showError('NIM/Password harus diisi');
} else {
setState(() {
isLoading = true;
});
User? user = await userProvider.login(
nimController.text,
passwordController.text);
setState(() {
isLoading = false;
});
if (user == null) {
showError('NIM/Password tidak sesuai!');
} else {
userProvider.user = user;
Navigator.pushNamedAndRemoveUntil(
context,
'/main',
(route) => false,
);
}
}
},
),
and this call api
Future<User?> login(String nim, String password) async {
String url = Constant.baseURL;
try {
var body = {
'username': nim,
'password': password,
};
var response = await http.post(
Uri.parse(
'$url/login_mhs',
),
body: body,
);
if (response.statusCode == 200) {
final token = jsonDecode(response.body)['data']['access_token'];
//Ini mulai nyimpen token
await UtilSharedPreferences.setToken(token);
print(token);
// print(await UtilSharedPreferences.getToken());
return User.fromJson(jsonDecode(response.body));
} else {
return null;
}
} catch (e) {
print(e);
throw Exception();
}
}
you can just make your own HTTP client using Dio and add Interceptor to automatically regenerate idToken if expired using the refreshToken given.
Http client gives an error if the refreshToken also gets expired.
In that case, just navigate to the login screen.
Full code for adding interceptor and making own HTTP client is given below
import 'package:dio/dio.dart';
import '../utils/shared_preference.dart';
class Api {
static Dio? _client;
static Dio clientInstance() {
if (_client == null) {
_client = Dio();
_client!.interceptors
.add(InterceptorsWrapper(onRequest: (options, handler) async {
if (!options.path.contains('http')) {
options.path = 'your-server' + options.path;
}
options.headers['Authorization'] =
'Bearer ${PreferenceUtils.getString('IdToken')}';
return handler.next(options);
}, onError: (DioError error, handler) async {
if ((error.response?.statusCode == 401 &&
error.response?.data['message'] == "Invalid JWT")) {
if (PreferenceUtils.exists('refreshToken')) {
await _refreshToken();
return handler.resolve(await _retry(error.requestOptions));
}
}
return handler.next(error);
}));
}
return _client!;
}
static Future<void> _refreshToken() async {
final refreshToken = PreferenceUtils.getString('refreshToken');
final response = await _client!
.post('/auth/refresh', data: {'refreshToken': refreshToken});
if (response.statusCode == 201) {
// successfully got the new access token
PreferenceUtils.setString('accessToken', response.data);
} else {
// refresh token is wrong so log out user.
PreferenceUtils.deleteAll();
}
}
static Future<Response<dynamic>> _retry(RequestOptions requestOptions) async {
final options = Options(
method: requestOptions.method,
headers: requestOptions.headers,
);
return _client!.request<dynamic>(requestOptions.path,
data: requestOptions.data,
queryParameters: requestOptions.queryParameters,
options: options);
}
}
Dio client = Api.clientInstance();
var resposne = (hit any request);
if(error in response is 401){
//it is sure that 401 is because of expired refresh token as we
//already handled idTokoen expiry case in 401 error while
//adding interceptor.
navigate to login screen for logging in again.
}
Please accept the solution if it solves your problem.
If your session expire feature has some predefine interval or logic than you have to implement it in splash screen and based on that you can navigate user further. Otherwise you want to handle it in API response only you have add condition for statusCode 401.
checkSessionExpire(BuildContext context)
if (response.statusCode == 200) {
//SuccessWork
} else if (response.statusCode == 401) {
//SessionExpire
} else {
return null
}
}

Dart the operator [] isn't defined for 'Future<Map<String,Object>> Function(String)'

I'm trying to set up Auth0 for my Flutter app from this demo, but I ran into this issue. I've only been using Flutter for a little while, and I fixed a few errors, but can't fix this one. I checked for other answers here, and tried the documentation too. The error message is;
The operator [] isn't defined for 'Future<Map<String,Object>> Function(String)'
This is the method with the error;
Future<void> loginAction() async {
setState(() {
isBusy = true;
errorMessage = '';
});
try {
final AuthorizationTokenResponse? result =
await appAuth.authorizeAndExchangeCode(
AuthorizationTokenRequest(
AUTH0_CLIENT_ID,
AUTH0_REDIRECT_URI,
issuer: 'https://$AUTH0_DOMAIN',
scopes: ['openid', 'profile', 'offline_access'],
promptValues: ['login']
),
);
Map<String, Object> parseIdToken(String idToken) {
final parts = idToken.split(r'.');
assert(parts.length == 3);
return jsonDecode(
utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))));
}
Future<Map<String, Object>> getUserDetails(String accessToken) async {
final url = 'https://$AUTH0_DOMAIN/userinfo';
final response = await http.get(Uri.parse(
url,
));
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
throw Exception('Failed to get user details');
}
}
await secureStorage.write(
key: 'refresh_token', value: result!.refreshToken);
setState(() {
isBusy = false;
isLoggedIn = true;
name = parseIdToken['name'];
picture = getUserDetails['picture'];
});
} catch (e, s) {
print('login error: $e - stack: $s');
setState(() {
isBusy = false;
isLoggedIn = false;
errorMessage = e.toString();
});
}
}
And the lines that are erroring are;
name = parseIdToken['name'];
picture = getUserDetails['picture'];
Can this be fixed?
getUserDetails is a function, and so it doesn't have a [] operator.
Instead, try: var userDetails = await getUserDetails("token..."); picture = userDetails["picture"];

How to refresh token and retry request on 401 error using Flutter

I try to refresh token and retry request on 401 error, but can not understand how to do it by right way
This is a recreation from what I remember so there can be typo and small errors maybe.
I hope you get an idea what I am trying to do here.
import 'package:http/http.dart' as http;
class APIUtility {
Uri uri;
String path, method;
var body;
var headers;
APIUtility({ #required this.path, #required this.method, this.body}) {
this.uri = Uri.parse("http://localhost:4000/api/${this.path}");
this.headers = {'Content-Type': 'application/json'};
}
Future request({ bool useToken = true }) async {
http.Response response;
if ( useToken ) { this.header['token'] = await getAccessToken(); }
try {
response = await // call api with http package with correct path, method and body
if ( useToken && response.statusCode == 401 ) return await _refreshTokenAndRequest();
else return jsonDecode(response.data);
}
catch (e) {
print(e);
return null;
}
}
_refreshTokenAndRequest() async {
String accessToken = await getAccessToken();
String refreshToken = await getRefreshToken();
var body = {'access_token': accessToken, 'refresh_token': refreshToken};
http.Response response = await http.post("${this.baseUrl}/api/auth/refresh", body: body);
if (response.statusCode == 200 || response.statusCode == 201) {
saveAccessToken(response.body['access_token']);
saveRefreshToken(response.body['refresh_token']);
return await request();
} else {
// Logout user from app
// Delete all database, token and all user info and show login screen;
return null;
}
}
}

flutter dio(4.0.0) handling token expiration (handling 401)

I have declared a class to make api requests using flutter Dio as follows.
class DioUtil {
static Dio _instance;
static Dio getInstance() {
if (_instance == null) {
_instance = createDio();
}
return _instance;
}
static Dio createDio() {
var dio = Dio();
dio.interceptors.add(InterceptorsWrapper(onRequest: (options, handler) {
// Do something before request is sent
return handler.next(options); //continue
}, onResponse: (response, handler) {
// Do something with response data
return handler.next(response); // continue
}, onError: (DioError e, handler) async {
if (e.response != null) {
if (e.response.statusCode == 401) {
var dio = DioUtil.getInstance();
dio.interceptors.requestLock.lock();
dio.interceptors.responseLock.lock();
RequestOptions requestOptions = e.requestOptions;
await refreshToken();
Repository repository = Repository();
var accessToken = await repository.readData("accessToken");
final opts = new Options(
method: requestOptions.method
);
dio.options.headers["Authorization"] = "Bearer " + accessToken;
dio.interceptors.requestLock.unlock();
dio.interceptors.responseLock.unlock();
dio.request(requestOptions.path,
options: opts,
data: requestOptions.data,
queryParameters: requestOptions.queryParameters);
}//TODO: handle else clause
}
}));
return dio;
}
static refreshToken() async {
Response response;
Repository repository = Repository();
var dio = Dio();
final Uri apiUrl = Uri.parse(BASE_PATH + "auth/reIssueAccessToken");
var refreshToken = await repository.readData("refreshToken");
dio.options.headers["Authorization"] = "Bearer " + refreshToken;
response = await dio.postUri(apiUrl);
if (response.statusCode == 200) {
LoginResponse loginResponse =
LoginResponse.fromJson(jsonDecode(response.toString()));
repository.addValue('accessToken', loginResponse.data.accessToken);
repository.addValue('refreshToken', loginResponse.data.refreshToken);
} else {
print(response.toString());
}
}
}
and I use flutter bloc pattern and my bloc is as follows.
class OurClassBloc extends Bloc<OurClassEvent, OurClassState> {
OurClassBloc(OurClassState initialState) : super(initialState);
Repository repository = Repository();
#override
Stream<OurClassState> mapEventToState(
OurClassEvent event,
) async* {
if (event is GetClasses) {
yield* _getClassCategories(event);
}
}
Stream<OurClassState> _getClassCategories(GetClasses event) async* {
Response response;
var dio = DioUtil.getInstance();
final String apiUrl = (BASE_PATH + "classCategories");
var accessToken = await repository.readData("accessToken");
Map<String, dynamic> map = {"active": event.active};
dio.options.headers["Authorization"] = "Bearer " + accessToken;
dio.options.headers["Accept"] = "*/*";
try {
response = await dio.get(apiUrl, queryParameters: map);
if (response.statusCode == 200) {
OurClassResponse loginResponse =
OurClassResponse.fromJson(jsonDecode(response.toString()));
yield OurClassSuccess(loginResponse);
}
if (response.statusCode >= 400) {
yield OurClassFailed();
}
} catch (e) {
yield OurClassFailed();
}
}
}
When I make the requests with valid access token, I get 200 status code in bloc class and api works fine.when the token is expired, the dio class correctly gets the new token, make the same api call with new token successfully and inside the below callback I get the correct response also.
onResponse: (response, handler) {
return handler.next(response);
}
but response doesn't comes to bloc class. Though it returned the response by calling return handler.next(response);,it is not coming to response variable inside _getClassCategories method.I expect the correct response should come to the response variable in bloc class for both scenarios:
makes the api call with valid token.
makes the api call with expired token.
but only scenario 1 is working in my code and hope someone here can help me to fix this.
EDIT- this works fine with dio previous version(3.0.10) - code
dio.request(requestOptions.path,
options: opts,
data: requestOptions.data,
queryParameters: requestOptions.queryParameters);
This line creates a new request with no relation to the original one. If the request succeeds, there is no code listening for a response. If you want the original caller to receive anything, you will need to forward the response to the original handler:
try {
final response = await dio.request(requestOptions.path,
options: opts,
data: requestOptions.data,
queryParameters: requestOptions.queryParameters);
handler.resolve(response);
} on DioError catch (error) {
handler.next(error); // or handler.reject(error);
}
Also, be sure to forward the error to the handler in non-401 cases as well. Dio 4.0.0 interceptors don't automatically forward anything.

Save local string from a function inside shared preferences and use it in other pages

I wish to save the userid string that am getting from a function that parses and decodes JWT token , and be able to use it in other pages in my Flutter app . I tried to save it inside shared preferences but doesn't seem to be working .This is my function and how I used shared preferences
String userName;
dynamic authenticator;
String _decodeBase64(String str) {
String output = str.replaceAll('-', '+').replaceAll('_', '/');
switch (output.length % 4) {
case 0:
break;
case 2:
output += '==';
break;
case 3:
output += '=';
break;
default:
throw Exception('Illegal base64url string!"');
}
return utf8.decode(base64Url.decode(output));
}
String _userid = '';
Map<String, dynamic> parseJwt(String token) {
final parts = token.split('.');
if (parts.length != 3) {
throw Exception('invalid token');
}
final payload = _decodeBase64(parts[1]);
final payloadMap = json.decode(payload);
if (payloadMap is! Map<String, dynamic>) {
throw Exception('invalid payload');
}
print(payload);
addStringToSF() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
_userid = payloadMap['user_id'];
prefs.setString('stringValue',_userid );
}
//print(payloadMap['user_id']);
return payloadMap;
}
getStringValuesSF() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
//Return String
String _userid = prefs.getString('userid');
print (_userid);
return _userid;
}
#override
void initState() {
super.initState();
getStringValuesSF();
}
authenticate() async {
// keyclock url : key-clock-url : example : http://localhost:8080
// my realm : name of your real.m
var uri = Uri.parse('http://169.254.105.22:8080/auth/realms/Clients');
// your client id
var clientId = 'helium';
var scopes = List<String>.of(['openid', 'profile']);
var port = 8080;
var issuer = await Issuer.discover(uri);
var client = new Client(issuer, clientId);
print(issuer.metadata);
urlLauncher(String url) async {
if (await canLaunch(url)) {
await launch(url, forceWebView: true);
} else {
throw 'Could not launch $url';
}
}
authenticator = new Authenticator(
client,
scopes: scopes,
port: port,
urlLancher: urlLauncher,
);
var c = await authenticator.authorize();
closeWebView();
var token = await c.getTokenResponse();
var userInformation = await c.getUserInfo();
setState(() {
userAccessToken = token.accessToken;
userName = userInformation.preferredUsername;
});
//print(token);
//return token;
parseJwt(userAccessToken);
}
I wish to use the userid variable here instead of the static string (id) am passing , in a way it dynamically reads the value from the function then use it inside the link to show the user's info :
final response = await http.get('http://169.254.105.22:8093/user/v1/users/d374169b-c61f-4a5a-b00a-2a2a8d9c4e19');
if (response.statusCode == 200) {
// If the server did return a 200 OK response,
// then parse the JSON.
return User.fromJson(jsonDecode(response.body));
} else {
// If the server did not return a 200 OK response,
// then throw an exception.
throw Exception('Failed to load user');
}
}
The second function is in another page (profile page), if anyone knows how I can save the userid from the function , then pass to another page (using sp or any other way) please don't hesitate to help thank you in advance
In my experience, when it comes to simple key/value storage, GetStorage is easier to use and less finicky than Shared Preferences. Try this:
Put this in your main before running your app.
await GetStorage.init();
Then your addStringToSF method would look like this:
addStringToSF() async {
final box = GetStorage();
_userid = payloadMap['user_id'];
box.write('stringValue', _userid);
}
Then from anywhere in your app access the stored value by
final box = GetStorage();
final newString = box.read('stringValue');
That should work for you.