Flutter/Dart: Call a Function Within itself to repeatedly Trigger a Timer to Refresh a JWT Token?

827 Views Asked by At

Instead of checking whether my JWT Token has expired on every query, I'd like to check it only upon the first initialization of the app in main and then automatically refresh it every 55 minutes.

Here's my refresh function which I invoke at the top of my Widget tree ;

 void main() async {
    await refreshToken()
 };

And here's the refreshToken code;

  Future<String?> refreshToken() async {

  String? refreshToken =  await getCryptRefresh(); //gets refresh token from an encrypted box
      final http.Response response = 
      await http.post(Uri.parse('http://example.com/refresh'),
         headers: <String, String>{'Content-Type': 'application/json; charset=UTF-8'},
         body: jsonEncode(<String?, dynamic>{
        'email': currentemail, 
        'id': currentuserid,
        'refreshToken': refreshToken
        }),
      );

    if (response.body.contains('token')) {
       Map<String, dynamic> refreshMap = json.decode(response.body);
         String token = refreshMap['token'];
           putCryptJWT(token); // stores new token in encrypted Hive box
               print("token renewed = " + token);
               
                 Timer(Duration(minutes: 55), () {
                     refreshToken();
                 });
          
               return token;
    } else {
            String noresponse = 'responsebody doesnt contain token';
                print(noresponse);
    }
}

Android studio first gave me a null error with a red line under refreshToken()and suggested a nullcheck !. But once I did that I gave me this error;

The expression doesn't evaluate to a function, so it can't be invoked.

It suggested I remove the parenthesis in the getter invocation. So I removed both the parenthesis and the nullcheck to just simply refreshToken;

But it doesn't run every 55 minutes as I hoped.

I'd rather not check the expiry upon every query. Both my JWTToken and its RefreshToken are stored in an encrypted Hive box. Hence checking every query seems a bit intensive.

3

There are 3 best solutions below

0
On

I don't know if this is preferable to using Dio, but I simply did this for all HTTP post requests;

else if (response.body.contains('TokenExpiredError')) {
     await refreshToken();
       return downloadMainJSON(
        );

It seems to work well.

0
On

In some cases that you can't get response back from backend which is my case. I add the counter in the app since I've starting getting token and keep counting. Your way is reply on CPU clocking which I tested it before it's not precise, some devices might count faster, some even slower. My way will use Date time now and check the different date time now with the time stamp that we save in the beginning every 15 seconds instead so it's still not exactly precise but at least the error shouldn't be more than 15 seconds

DateTime _timeoutAt;

start() {
   _timeoutAt = DateTime.now().add(Duration(minutes: 55);
   Timer _timerCheckIsTimeout = Timer.periodic(Duration(seconds: 15), (timer) {
   final isTimeout = DateTime.now().isAfter(_timeoutAt)
   if (isTimeout) {
      //Call back from here
      await refreshToken();
      timer.cancel();
   }
}

I know that it's not the best practise but people has different conditions and they can't bring out the best practise. Please use above example if you can control backend to response it back.

5
On

I would not suggest doing it that way. You are creating a lot of work for yourself. A better approach would be to intercept the response of your request. Check if the response code is 401 (Meaning unauthorized) which I guess is what your backend would return. Then refresh the token, and fire the original request again. This is a much more seamless way of working, so there are no unnecessary token expiration checks, and the user experience is still seamless. You can easily do this with the Dio, package. You can do something like this.

var _http;
void initMethod(){
_http = Dio();
//set some headers
_http.options.contentType = "application/json";
_http.options.headers['Content-Type'] = "application/json";
//Now add interceptors for both request and response to add a token on outgoing request and to handle refresh on failed response.
_http.interceptors
        .add(InterceptorsWrapper(onRequest: (RequestOptions options) async {
      return await _addAuthenticationToken(options);
    }, onResponse: (Response response) async {
      if (response.statusCode == 401) return await refreshToken(response);
return response;
    }));
}

Future<Response> refreshtoken(Response response){
//Get your new token here
//Once you have the token, retry the original failed request. (After you set the token where ever you store it etc)
return await _retry(response);
}
Future<RequestOptions> _addAuthenticationToken(RequestOptions options) async {
    
    if (tokenPair != null)
      options.headers['Authorization'] =
          "Bearer " + "yout token goes here";
    return options;
  }

Future<Response<dynamic>> _retry(RequestOptions requestOptions) async {
    final options = new Options(
      method: requestOptions.method,
      headers: requestOptions.headers,
    );
    return await this._http.request<dynamic>(requestOptions.path,
        data: requestOptions.data,
        queryParameters: requestOptions.queryParameters,
        options: options);
  }

You might need to experiment to check that you get the desired result, but that should do the trick