axios interceptors to update auth - axios

I am using axios interceptors to check auth token on every request. This works fine. But when accessToken is null getToken() is fired twice. Is there a way to wait for the getToken to finish ? I just want getToken to fire once. The other requests needing a token should wait until getToken is fulfilled.
let isAlreadyFetchingAccessTokenRequest = false;
api.interceptors.request.use(
async config => {
let token = window.localStorage.getItem("accessToken");
if (!isAlreadyFetchingAccessTokenRequest && !token) {
isAlreadyFetchingAccessTokenRequest = true;
token = await getToken();
console.log("1. save token to local storage", token);
window.localStorage.setItem("accessToken", token);
}
config.headers.Authorization = `Bearer ${token}`;
return config;
},
function(error) {
return Promise.reject(error);
}
);

You are able to await a promise multiple times.
So could try something like this
let tokenPromise = null;
api.interceptors.request.use(
async config => {
let token = window.localStorage.getItem("accessToken");
if (!token) {
if (!tokenPromise) {
tokenPromise = getToken();
}
token = await tokenPromise;
console.log("1. save token to local storage", token);
window.localStorage.setItem("accessToken", token);
}
config.headers.Authorization = `Bearer ${token}`;
...

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

How to pass auth token to every request after login or signup - Dio flutter

This is an easy method to save the auth token from login and signup to pass to every other request. You just have to pass the token right in the initialization of "Dio". In my method I use Dio interceptor onResponse function to save the auth token to SharedPreference. Please do share other better methods.
class ApiSerivce {
late Dio dio;
late SharedPreferences prefs;
var token = '';
//
ApiSerivce() {
dio = Dio(BaseOptions(
baseUrl: BaseApi.baseUrl,
sendTimeout: 10000,
receiveTimeout: 10000,
));
tokenCheck();
intitialInterceptor();
}
tokenCheck() async {
prefs = await SharedPreferences.getInstance();
token = prefs.getString('token') ?? token;
}
intitialInterceptor() {
GlobalKey<NavigatorState>? navigator;
dio.interceptors.add(InterceptorsWrapper(
onError: (error, handler) {
print('dio error :::::: ' + error.message.toString());
if (error.response?.statusCode == 400) {
print('dio ::::::: auth not valid');
navigator?.currentState?.pushReplacementNamed('login');
}
print('dio error :::::: ' + error.response.toString());
return handler.next(error);
},
onRequest: (request, handler) {
request.headers['Cookie'] = "jwt=$token";
print('dio request path :::::: ' + request.headers.toString());
return handler.next(request);
},
onResponse: (response, handler) {
if (response.realUri.toString().contains('login') ||
response.realUri.toString().contains('signUp')) {
if (response.statusCode == 200 || response.statusCode == 201) {
token = response.data['token'];
setToken(response.data['token'].toString());
}
}
return handler.next(response);
},
));
}
}
setToken(String token) async {
final prefs = await SharedPreferences.getInstance();
prefs.setString('token', token);
}

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.

Pinterest Oauth2 Access Token Issue

I have a Flutter app that I'm trying to integrate with Pinterest, and I'm a little stuck.
I have code to request an access token and, while the code does get an access token, that token does not appear to be useful. Any API that I call with that token results in a 308, and if I go to the Pinterest developer site and debug the token, then it looks like this:
So, it's like the token has no scopes and was not issued for an actual application, which is very weird. The code I have looks like this:
Future<String> _login() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
accessToken = null;
if (accessToken == null) {
// accessToken = prefs.get(ACCESS_TOKEN_KEY);
//If we don't have an existing access token, get a new one.
if (accessToken == null) {
final appId = "myappid";
final secret =
"mysecret";
final url = Uri.https('api.pinterest.com', 'oauth', {
'response_type': 'code',
'client_id': appId,
'redirect_uri': "pdk<myappid>://",
'state': 'someBogusStuff',
'scope': 'read_public,write_public',
});
final result = await FlutterWebAuth.authenticate(
url: url.toString(), callbackUrlScheme: 'pdk<myappid>');
print(result);
final tokenEndpoint = Uri.https('api.pinterest.com', 'v1/oauth/token');
// Use the code to get an access token
final response = await http.post(tokenEndpoint, body: {
'client_id': appId,
'client_secret': secret,
'grant_type': 'authorization_code',
'code': Uri
.parse(result)
.queryParameters['code'],
});
if (response.statusCode != 200) {
return response.body;
}
var decodedResponse = jsonDecode(response.body);
print(decodedResponse);
accessToken = decodedResponse['access_token'];
//Save the access token
prefs.setString(ACCESS_TOKEN_KEY, accessToken);
}
}
return getMe(accessToken);
}
Future<String> getMe(String token) async {
final url =
Uri.https('api.pinterest.com', 'v1/me', {'access_token': token});
Completer<String> completer = Completer();
String result;
http.get(url, headers: {'User-Agent': 'PDK 1.0'}).then((response) {
print(response.statusCode);
result = response.body;
}).whenComplete(() => completer.complete(result));
return completer.future;
}
When I print out the result of the call to /oauth/token it looks like I got back a good token:
{access_token: AvtF3MxUy4gbujGGhN_KcYFExQVAFfmOZGmxYN5GkhE-iKDH6QpYADAAAzbHRpc4dD1gvFwAAAAA, token_type: bearer, scope: [read_write_all, read_public, write_public, read_private, write_private]}
But it doesn't work. What am I doing wrong here?

How to get the token from firebase_auth

I'd like to get the auth token from firebase (email and password auth) to authenticate in my firebase cloud function. It seems like the functions getIdToken() and getToken() are both not working for firebase_auth package.
is there an other function or is there even a better idea to make sure only authenticated users can trigger the cloud functions?
var token = await FirebaseAuth.instance.currentUser.getIdToken();
var response = await httpClient.get(url,headers: {'Authorization':"Bearer $token"});
I agree with #Doug on this one - callable wraps this for you and will be easier -, but my use case required me to make HTTPS calls (onRequest in Functions). Also, I think you're just in the correct path - but you're possibly not checking it in your Cloud Functions.
In your app, you'll call:
_httpsCall() async {
// Fetch the currentUser, and then get its id token
final user = await FirebaseAuth.instance.currentUser();
final idToken = await user.getIdToken();
final token = idToken.token;
// Create authorization header
final header = { "authorization": 'Bearer $token' };
get("http://YOUR_PROJECT_BASE_URL/httpsFunction", headers: header)
.then((response) {
final status = response.statusCode;
print('STATUS CODE: $status');
})
.catchError((e) {
print(e);
});
}
In your function, you'll check for the token:
export const httpsFunction = functions.https.onRequest((request, response) => {
const authorization = request.header("authorization")
if (authorization) {
const idToken = authorization.split('Bearer ')[1]
if (!idToken) {
response.status(400).send({ response: "Unauthenticated request!" })
return
}
return admin.auth().verifyIdToken(idToken)
.then(decodedToken => {
// You can check for your custom claims here as well
response.status(200).send({ response: "Authenticated request!" })
})
.catch(err => {
response.status(400).send({ response: "Unauthenticated request!" })
})
}
response.status(400).send({ response: "Unauthenticated request!" })
})
Keep in mind:
If I'm not mistaken, those tokens are valid for 1 hour, if you are going to store them somewhere, just be aware of this. I've tested locally and it takes around 200~500ms - every time - to get only the id token, which in most cases are not that big of overhead - but is significant.
It's going to be easiest for you to use a callable function, since that lets you:
Automatically send the current user's uid in the request.
Know very easily on the function side if a UID was provided in the request, and refuse service if none was provided.
The flutter plugin is here.
You should be able to do the equivalent work yourself, though, since callable functions are just a wrapper around normal HTTP connections. It's possible for you to get the ID token of the logged in user.
import 'package:firebase_messaging/firebase_messaging.dart';
.
.
.
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
#override
Future<void> initState() {
super.initState();
_firebaseMessaging.getToken().then((token) {
assert(token != null);
print("teken is: " + token);
});
}
Get your token from firebaseAuth and put in a string.
Future<Details> getDetails() async {
String bearer = await FirebaseAuth.instance.currentUser!.getIdToken();
print("Bearer: " + bearer.toString());
String token = "Bearer ${bearer}";
var apiUrl = Uri.parse('Your url here');
final response = await http.get(apiUrl, headers: {
'Authorization' : '${token}'
});
final responseJson = jsonDecode(response.body);
return Details.fromJson(responseJson);
}