0

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.

Meggy
  • 1,491
  • 3
  • 28
  • 63

3 Answers3

1

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

Terblanche Daniel
  • 1,169
  • 2
  • 12
  • 18
  • This makes sense. But I'm getting this error - The argument type 'Future Function(RequestOptions)' can't be assigned to the parameter type 'void Function(RequestOptions, RequestInterceptorHandler)?'. – Meggy Jul 07 '21 at 11:22
  • If you could elaborate more on which step this is happening I can assist, but make sure you have an await where the requests are made, (Also the retry request), and that the method surrounding it is async – Terblanche Daniel Jul 07 '21 at 11:30
  • I think it's a null check error at RequestInterceptorHandler)? but not sure how to fix it. – Meggy Jul 07 '21 at 11:32
  • Well I got rid of the nullcheck error with // @dart=2.9 but still got a redline error under all of this - onRequest: (RequestOptions options) async { return await _addAuthenticationToken(options); }, onResponse: (Response response) async { if (response.statusCode == 401) return await refreshToken(response); return response; – Meggy Jul 07 '21 at 11:36
  • Is there any advantage to using DIO over just simply - else if (response.statusCode == 401) { refreshToken(); return downloadMainJSON(); } – Meggy Jul 07 '21 at 11:57
1

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.

Einzeln
  • 517
  • 4
  • 14
0

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.

Meggy
  • 1,491
  • 3
  • 28
  • 63