Flutter redirect when encountered http 401 - flutter

I have a navigation like this for now:
main > app > account > address
right now i encountered issue if user fetching address and their session expires how can I redirect them back to login page? Still new to flutter, can't find the esolution how to send unauthenticated status to main so it can redirect it.
I'm using dio and MobX package.
dio.interceptors
.add(InterceptorsWrapper(onRequest: (RequestOptions options) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String token = prefs.getString('access_token');
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
options.headers['Accept'] = 'application/json';
return options; //continue
}, onResponse: (Response response) {
final int statusCode = response.statusCode;
var results = {};
if (statusCode == 200 || statusCode == 201 || statusCode == 204) {
final dynamic decodeResponse = this.decodeResponse(response);
bool responseIsList = decodeResponse is List;
if (!responseIsList && decodeResponse['token'] != null) {
final token = decodeResponse['token'];
setAuthorizationToken(token['access_token'], token['refresh_token']);
}
if (responseIsList) {
return decodeResponse;
} else {
final resultToAdd = decodeResponse;
results.addAll(resultToAdd);
return results;
}
}
return response;
}, onError: (DioError e) async {
final r = e.response;
if (r.statusCode == 401) {
AuthStore().unauthorize(prefs: _sharedPreferences);
}
if (r != null) {
return {"has_error": true, ...r.data};
}
// Do something with response error
return e;
}));
I've tried to put Observer on my main.dart to keep checking on the status
#override
Widget build(BuildContext context) {
return Observer(builder: (_) {
if (store.statusAuth == 401) {
Navigator.pushReplacementNamed(context, '/'); // this is error
}
// other material app function
});
Any solution?
The solution only I can think of for now is checking every time API is called (every store). if 401 will automatically send it back to login page. but this approach will be very redundant.

I have the same issue. To resolve this issue, we need a Navigation without context to can navigate on interceptors.
Api service is initializated before MaterialApp. MaterialApp is allowed pass the navigatorKey for top navigator.
I create GlobalKey when define interceptors of apiservice and use it for return MaterialApp.
final GlobalKey<NavigatorState> navigator;//Create a key for navigator
and create apiService
void _configAuthApiService() {
...
if (error.response?.statusCode == 401) {
navigator.currentState.pushReplacementNamed('/login');
}
...
}
When create material app:
MaterialApp(
...
navigatorKey: navigator,
...
)

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
}
}

Retry to get a new access token after dio QueuedInterceptor returns 401

I am trying to implement a JWT Access/Refresh token flow with flutter. After my access token expires, my QueuedInterceptor gets a new access token with the refresh token. Everything works fine, but it is not retrying to get the requested ressource and returns a 401. After a refresh of that page, the resource loads. How do I implement a retry with QueuedInterceptor ?
class AuthInterceptor extends QueuedInterceptor {
final Dio _dio;
AuthInterceptor(this._dio);
#override
void onRequest(
RequestOptions options, RequestInterceptorHandler handler) async {
final accessToken = await storage.read(key: "accessToken");
final refreshToken = await storage.read(key: "refreshToken");
if (accessToken == null || refreshToken == null) {
const AuthState.unauthenticated();
final error = DioError(requestOptions: options, type: DioErrorType.other);
return handler.reject(error);
}
final accessTokenHasExpired = JwtDecoder.isExpired(accessToken);
final refreshTokenHasExpired = JwtDecoder.isExpired(refreshToken);
var _refreshed = true;
if (refreshTokenHasExpired) {
const AuthState.unauthenticated();
final error = DioError(requestOptions: options, type: DioErrorType.other);
return handler.reject(error);
} else if (accessTokenHasExpired) {
// regenerate new access token
_refreshed = await _regenerateAccessToken();
}
if (_refreshed) {
options.headers["Authorization"] = "Bearer $accessToken";
return handler.next(options);
} else {
final error = DioError(requestOptions: options, type: DioErrorType.other);
return handler.reject(error);
}
}
Future<bool> _regenerateAccessToken() async {
try {
var dio = Dio();
final refreshToken = await storage.read(key: "refreshToken");
final response = await dio.post(
"https://localhost:7104/api/Login/Token/Refresh",
options: Options(headers: {"Authorization": "Bearer $refreshToken"}),
);
if (response.statusCode == 200 || response.statusCode == 201) {
final newAccessToken = response.data["accessToken"];
storage.write(key: "accessToken", value: newAccessToken);
return true;
} else if (response.statusCode == 401 || response.statusCode == 403) {
const AuthState.unauthenticated();
return false;
} else {
return false;
}
} on DioError {
return false;
} catch (e) {
return false;
}
}
}
This is how I create the request with the interceptor. It throws a 401 if my access token is expired:
final dio = Dio();
dio.options.baseUrl = authenticationBackend;
dio.interceptors.addAll([
AuthInterceptor(dio),
]);
var response = await dio.get('$host/animals');
class RefreshTokenInterceptor extends Interceptor {
final Dio dio;
RefreshTokenInterceptor({
required this.dio,
});
#override
void onError(DioError err, ErrorInterceptorHandler handler) async {
if (err.response == null) {
return;
}
if (err.response!.statusCode == 401) {
var res = await refreshToken();
if (res != null && res) {
await _retry(err.requestOptions);
}
}
return handler.next(err);
}
/// Api to get new token from refresh token
///
Future<bool?> refreshToken() async {
///call your refesh token api here
}
/// For retrying request with new token
///
Future<Response<dynamic>> _retry(RequestOptions requestOptions) async {
final options = Options(
method: requestOptions.method,
headers: requestOptions.headers,
);
return dio.request<dynamic>(requestOptions.path,
data: requestOptions.data,
queryParameters: requestOptions.queryParameters,
options: options);
}
}
And then use it
dio.interceptors.addAll(
[
/// interceptor for refreshing token
///
RefreshTokenInterceptor(dio: dio),
],
);

Empty map on flutter when initiating

Map user = {};
Future<void> getUser(String idProfile) async {
final response = await ac.getItem("/v2/users/:0", [idProfile]);
if (response.statusCode >= 200 && response.statusCode < 300) {
setState(() {
user = json.decode(response.body);
print(user);
});
}
}
#override
void initState() {
super.initState();
getUser(getCurrentUser());
print(user);
}
With the first print, it returns me the user. However, at the second doesn't. I need to get the user information. How could I do it?
getUser is a future method, you need to wait until it fetches data from API. While you are using StatefulWidget , you can show landing indication while it fetch data from API.
If it is inside Column widget,
if (user.isEmpty) Text("fetching data")
else LoadDataWidget(),
Also you can use ternary operator.
Map user = {};
//Return a user from the function
Future<Map<String, dynamic>> getUser(String idProfile) async {
final response = await ac.getItem("/v2/users/:0", [idProfile]);
if (response.statusCode >= 200 && response.statusCode < 300) {
user = json.decode(response.body) as Map<String, dynamic>;
return user
}
else {
throw Exception();
}
}
// Set the user value in initstate
#override
void initState() {
super.initState();
user = getUser(getCurrentUser());
print(user);
}

API call terminate when screen gets off (or app goes background) IOS Flutter

I have a login page which starts downloading base data for the app after user enters username and password, and its a long duration operation like 2 or 3 minutes
In IOS at the middle of downloading data if screen gets off and locked, the operations terminates.
Here is the code
LoginPage part:
var repository = GlobalRestRepository();
var db = BasicDB();
List<basicModel> notDownloaded = await db.selectByLoaded(false);
for (int i = 0; i < notDownloaded.length; i++) {
await repository.getBasic(notDownloaded.elementAt(i));
}
GlobalRestRepository part:
class GlobalRestRepository {
final HttpClient http = HttpClient();
Future<void> getBasic(basicModel model) async {
String url = "${Variables.mainUrl + basicModelUrl}";
var response = await http.postExtraToken(url);
.
.
.
}
}
HttpClient part:
import 'package:http/http.dart';
...
class HttpClient {
static final HttpClient _instance = HttpClient._privateConstructor();
factory HttpClient() {
return _instance;
}
Future<dynamic> postExtraToken(String path) async {
Response response;
try {
response = await post(Uri.parse(path),
headers: {"extra": Variables.extra, "token": Variables.token});
final statusCode = response.statusCode;
if (statusCode >= 200 && statusCode < 299) {
if (response.body.isEmpty) {
return [];
} else {
return jsonDecode(utf8.decode(response.bodyBytes));
}
} else if (statusCode >= 400 && statusCode < 500) {
throw ClientErrorException();
} else if (statusCode >= 500 && statusCode < 600) {
throw ServerErrorException();
} else {
throw UnknownException();
}
} on SocketException {
throw ConnectionException();
}
}
}
Can anyone help me with this?
Using Wakelock plugin, we can solve this problem, but I don't know this is the best solution or not.
Note: This plugin works for all the platforms except linux.
Add the below code in the screen where you are initiating the api request.
#override
void didChangeDependencies() {
super.didChangeDependencies();
checkAndEnableWakeLock();
}
void checkAndEnableWakeLock() async {
bool wakeLockEnabled = await Wakelock.enabled;
if (!wakeLockEnabled) {
Wakelock.enable();
}
}
Call disable method when all the api calls are completed.
Wakelock.disable();
Using above code you can avoid screen getting off issue.

Flutter dio cannot go to the interceptor when it call

I tried refreshing my tokens when my calling api goes to error 401 (token expires) but when it calls dio interceptor is not triggered.
api.dart is definition of my dio
Dio _createHttpClient() {
final api = Dio(
new BaseOptions(
baseUrl: environments.api,
contentType: Headers.jsonContentType,
responseType: ResponseType.json,
),
);
api
..interceptors.clear()
..interceptors.add(new ErrorDialogInterceptor())
..interceptors.add(new AuthTokenInterceptor(api));
return api;
}
final api = _createHttpClient();
profil_provider.dart is the call of my api
Future<ProfilePicture?> getPictureProfile(String id) async {
String url = '/v1/users/$id/profile-picture';
try {
final response = await api.get(
url,
options: Options(
responseType: ResponseType.bytes,
headers: {
ErrorDialogInterceptor.skipHeader: true,
},
),
);
Uint8List avatar = Uint8List.fromList(response.data);
return ProfilePicture(image: avatar);
} catch (e) {
print('e');
return null;
}
}
and it go to the catch error
auth_interceptor.dart is for manage my error and request of my api
class AuthTokenInterceptor extends Interceptor {
static const skipHeader = 'skipAuthToken';
Dio api;
AuthTokenInterceptor(this.api);
#override
onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
final context = applicationKey.currentContext;
print("test");
final repository = context?.read<AuthRepository>();
if (repository == null) {
return;
}
final accessToken = await repository.getAccessToken();
print("access: $accessToken");
if (accessToken != null) {
print(accessToken);
options.headers['Authorization'] = 'Bearer $accessToken';
}
return super.onRequest(options, handler);
}
#override
onError(DioError err, ErrorInterceptorHandler handler) async {
final context = applicationKey.currentContext;
if (context == null) {
return;
}
final response = err.response?.data;
if (response == null) {
return super.onError(err, handler);
}
final repository = context.read<AuthRepository>();
if (err.response?.statusCode == 401)
if (err.response?.statusCode == 401 &&
await repository.getRefreshToken() != null) {
api.interceptors.clear();
return _handlerRefreshToken(context, repository, err, handler);
}
return super.onError(err, handler);
}
and it not go to the auth interceptor with my debug print, I don't know why it not go there with the error 401 from the catch error
you can do.
// returns the http request
Future<Response<ProfilePicture>> getPictureProfile(String id) {
String url = '/v1/users/$id/profile-picture';
return api.get(url)
}
//...
// and use a try/catch outside the method
try{
Response<ProfilePicture> respose = await getPictureProfile(555)
print(respose.data)
}catch(e){
print(e)
}