Using Interceptor in Dio for Flutter to Refresh Token
Asked Answered
W

9

72

I am trying to use Interceptor with Dio in flutter, I have to handle Token expire. following is my code

Future<Dio> getApiClient() async {
    token = await storage.read(key: USER_TOKEN);
    _dio.interceptors.clear();
    _dio.interceptors
        .add(InterceptorsWrapper(onRequest: (RequestOptions options) {
      // Do something before request is sent
      options.headers["Authorization"] = "Bearer " + token;
      return options;
    },onResponse:(Response response) {
        // Do something with response data
        return response; // continue
    }, onError: (DioError error) async {
      // Do something with response error
      if (error.response?.statusCode == 403) {
        // update token and repeat
        // Lock to block the incoming request until the token updated

        _dio.interceptors.requestLock.lock();
        _dio.interceptors.responseLock.lock();
        RequestOptions options = error.response.request;
        FirebaseUser user = await FirebaseAuth.instance.currentUser();
        token = await user.getIdToken(refresh: true);
        await writeAuthKey(token);
        options.headers["Authorization"] = "Bearer " + token;

        _dio.interceptors.requestLock.unlock();
        _dio.interceptors.responseLock.unlock();
        _dio.request(options.path, options: options);
      } else {
        return error;
      }
    }));
    _dio.options.baseUrl = baseUrl;
    return _dio;
  }

problem is instead of repeating the network call with the new token, Dio is returning the error object to the calling method, which in turn is rendering the wrong widget, any leads on how to handle token refresh with dio?

Wriest answered 24/6, 2019 at 16:41 Comment(0)
W
53

I solved it using interceptors in following way :-

  Future<Dio> getApiClient() async {
    token = await storage.read(key: USER_TOKEN);
    _dio.interceptors.clear();
    _dio.interceptors
        .add(InterceptorsWrapper(onRequest: (RequestOptions options) {
      // Do something before request is sent
      options.headers["Authorization"] = "Bearer " + token;
      return options;
    },onResponse:(Response response) {
        // Do something with response data
        return response; // continue
    }, onError: (DioError error) async {
      // Do something with response error
      if (error.response?.statusCode == 403) {
        _dio.interceptors.requestLock.lock();
        _dio.interceptors.responseLock.lock();
        RequestOptions options = error.response.request;
        FirebaseUser user = await FirebaseAuth.instance.currentUser();
        token = await user.getIdToken(refresh: true);
        await writeAuthKey(token);
        options.headers["Authorization"] = "Bearer " + token;

        _dio.interceptors.requestLock.unlock();
        _dio.interceptors.responseLock.unlock();
        return _dio.request(options.path,options: options);
      } else {
        return error;
      }
    }));
    _dio.options.baseUrl = baseUrl;
    return _dio;
  }

Dio 4.0.0 Support

    dio.interceptors.add(
          InterceptorsWrapper(
            onRequest: (request, handler) {
              if (token != null && token != '')
                request.headers['Authorization'] = 'Bearer $token';
              return handler.next(request);
            },
            onError: (e, handler) async {
              if (e.response?.statusCode == 401) {
                try {
                  await dio
                      .post(
                          "https://refresh.api",
                          data: jsonEncode(
                              {"refresh_token": refreshtoken}))
                      .then((value) async {
                    if (value?.statusCode == 201) {
                      //get new tokens ...
                      print("access token" + token);
                      print("refresh token" + refreshtoken);
                      //set bearer
                      e.requestOptions.headers["Authorization"] =
                          "Bearer " + token;
                      //create request with new access token
                      final opts = new Options(
                          method: e.requestOptions.method,
                          headers: e.requestOptions.headers);
                      final cloneReq = await dio.request(e.requestOptions.path,
                          options: opts,
                          data: e.requestOptions.data,
                          queryParameters: e.requestOptions.queryParameters);
    
                      return handler.resolve(cloneReq);
                    }
                    return e;
                  });
                  return dio;
                } catch (e, st) {
                  
                }
              }
           },
        ),
    );
Wriest answered 19/10, 2019 at 7:57 Comment(14)
is this working for you? I intercept 401 and then send a request to get a new token if I lock requestLock and responseLock the refresh request is not completed, I had to remove those two linesGrettagreuze
@Grettagreuze see I am using firebase auth to generate token, your usecase might be different than mine.Wriest
@Grettagreuze i think in your case u can use a new instance to get the token, and keep using the older instance for you normal request/responseWriest
wouldn't you need and errorLock here as well, or else error responses will keep coming back?Motion
Risk of infinite loop if always returning the request againFlair
How, request and response are locked, after token generation, request will succeed.Wriest
@Wriest Do you have full example of refresh token? Maybe git repo or something to check how to write full fledge code OR how to use getApiClient() method.Mer
@Wriest if refresh token action is unsuccessfulFlair
If refresh token is unsuccessful then there is system failure at server side. Which is highly unlikely. And even if it happens it means that none of the other request will go through. Then you need to pass the error to the User to display.Wriest
But you have a valid point, let me write some tests for it and I'll update the code.Wriest
What if I want to use Dio to retrieve the refreshed token? Isn't lock gonna prevent it from requesting new token?Coccid
Does this support concurrent calls as well?Hying
please edit the answer to include version 4.0.0 supportOdell
The latest versioned code helped me..thanks a lot..Exotoxin
J
66

I have found a simple solution that looks like the following:

this.api = Dio();

    this.api.interceptors.add(InterceptorsWrapper(
       onError: (error) async {
          if (error.response?.statusCode == 403 ||
              error.response?.statusCode == 401) {
              await refreshToken();
              return _retry(error.request);
            }
            return error.response;
        }));

Basically what is going on is it checks to see if the error is a 401 or 403, which are common auth errors, and if so, it will refresh the token and retry the response. My implementation of refreshToken() looks like the following, but this may vary based on your api:

Future<void> refreshToken() async {
    final refreshToken = await this._storage.read(key: 'refreshToken');
    final response =
        await this.api.post('/users/refresh', data: {'token': refreshToken});

    if (response.statusCode == 200) {
      this.accessToken = response.data['accessToken'];
    }
  }

I use Flutter Sercure Storage to store the accessToken. My retry method looks like the following:

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

If you want to easily allows add the access_token to the request I suggest adding the following function when you declare your dio router with the onError callback:

onRequest: (options) async {
          options.headers['Authorization'] = 'Bearer: $accessToken';
          return options;
        },
Jacaranda answered 2/8, 2020 at 17:47 Comment(10)
This answer should be on top, Thanks broTwi
@Twi thanks, glad you found it helpful!Jacaranda
how do you update the refresh token when expired for example user closes the app and returns 4 hours later?Positivism
@Positivism the accessToken should expire roughly ~15 minutes. Your refreshToken shouldn't expire, and if it does it should expire once every few weeks or so. The refreshToken method gets a new accessToken, but if your refreshToken expires, you should log back in.Jacaranda
This is clean and clear, thanks, an update of the answer would be very cool with the latest Dio version.Duchess
Top answer Gabe, really helpful and clear 👌🏾Fuchsia
onError isn't even called ..... I hate Dio. Http package is the original and real thing which everyone should useRhearheba
when do you save the refreshToken?Prolix
@KwesiWelbred whenever you login the user you should get a refreshToken which you can save to Flutter Secure StorageJacaranda
If there are multiple requests fired at the same time which all get 403, it seems this would cause all of those requests to try to refresh, right?Vigorous
W
53

I solved it using interceptors in following way :-

  Future<Dio> getApiClient() async {
    token = await storage.read(key: USER_TOKEN);
    _dio.interceptors.clear();
    _dio.interceptors
        .add(InterceptorsWrapper(onRequest: (RequestOptions options) {
      // Do something before request is sent
      options.headers["Authorization"] = "Bearer " + token;
      return options;
    },onResponse:(Response response) {
        // Do something with response data
        return response; // continue
    }, onError: (DioError error) async {
      // Do something with response error
      if (error.response?.statusCode == 403) {
        _dio.interceptors.requestLock.lock();
        _dio.interceptors.responseLock.lock();
        RequestOptions options = error.response.request;
        FirebaseUser user = await FirebaseAuth.instance.currentUser();
        token = await user.getIdToken(refresh: true);
        await writeAuthKey(token);
        options.headers["Authorization"] = "Bearer " + token;

        _dio.interceptors.requestLock.unlock();
        _dio.interceptors.responseLock.unlock();
        return _dio.request(options.path,options: options);
      } else {
        return error;
      }
    }));
    _dio.options.baseUrl = baseUrl;
    return _dio;
  }

Dio 4.0.0 Support

    dio.interceptors.add(
          InterceptorsWrapper(
            onRequest: (request, handler) {
              if (token != null && token != '')
                request.headers['Authorization'] = 'Bearer $token';
              return handler.next(request);
            },
            onError: (e, handler) async {
              if (e.response?.statusCode == 401) {
                try {
                  await dio
                      .post(
                          "https://refresh.api",
                          data: jsonEncode(
                              {"refresh_token": refreshtoken}))
                      .then((value) async {
                    if (value?.statusCode == 201) {
                      //get new tokens ...
                      print("access token" + token);
                      print("refresh token" + refreshtoken);
                      //set bearer
                      e.requestOptions.headers["Authorization"] =
                          "Bearer " + token;
                      //create request with new access token
                      final opts = new Options(
                          method: e.requestOptions.method,
                          headers: e.requestOptions.headers);
                      final cloneReq = await dio.request(e.requestOptions.path,
                          options: opts,
                          data: e.requestOptions.data,
                          queryParameters: e.requestOptions.queryParameters);
    
                      return handler.resolve(cloneReq);
                    }
                    return e;
                  });
                  return dio;
                } catch (e, st) {
                  
                }
              }
           },
        ),
    );
Wriest answered 19/10, 2019 at 7:57 Comment(14)
is this working for you? I intercept 401 and then send a request to get a new token if I lock requestLock and responseLock the refresh request is not completed, I had to remove those two linesGrettagreuze
@Grettagreuze see I am using firebase auth to generate token, your usecase might be different than mine.Wriest
@Grettagreuze i think in your case u can use a new instance to get the token, and keep using the older instance for you normal request/responseWriest
wouldn't you need and errorLock here as well, or else error responses will keep coming back?Motion
Risk of infinite loop if always returning the request againFlair
How, request and response are locked, after token generation, request will succeed.Wriest
@Wriest Do you have full example of refresh token? Maybe git repo or something to check how to write full fledge code OR how to use getApiClient() method.Mer
@Wriest if refresh token action is unsuccessfulFlair
If refresh token is unsuccessful then there is system failure at server side. Which is highly unlikely. And even if it happens it means that none of the other request will go through. Then you need to pass the error to the User to display.Wriest
But you have a valid point, let me write some tests for it and I'll update the code.Wriest
What if I want to use Dio to retrieve the refreshed token? Isn't lock gonna prevent it from requesting new token?Coccid
Does this support concurrent calls as well?Hying
please edit the answer to include version 4.0.0 supportOdell
The latest versioned code helped me..thanks a lot..Exotoxin
K
23

I modify John Anderton's answer. I agree that it is better approach to check the token(s) before you actually make the request. we have to check if the tokens are expired or not, instead of making request and check the error 401 and 403.

I modify it to add some functionalities, so this interceptor can be used

  1. to add access token to the header if it is still valid
  2. to regenerate access token if it has expired
  3. to navigate back to Login Page if refresh token has expired
  4. to navigate back to Login Page if there is an error because of invalidated token (for example, revoked by the backend)

and it also work for multiple concurrent requests, and if you don't need to add token to the header (like in login endpoint), this interceptor can handle it as well. here is the interceptor

class AuthInterceptor extends Interceptor {
  final Dio _dio;
  final _localStorage = LocalStorage.instance; // helper class to access your local storage

  AuthInterceptor(this._dio);

  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
    if (options.headers["requiresToken"] == false) {
      // if the request doesn't need token, then just continue to the next interceptor
      options.headers.remove("requiresToken"); //remove the auxiliary header
      return handler.next(options);
    }

    // get tokens from local storage, you can use Hive or flutter_secure_storage
    final accessToken = _localStorage.getAccessToken();
    final refreshToken = _localStorage.getRefreshToken();

    if (accessToken == null || refreshToken == null) {
      _performLogout(_dio);

      // create custom dio error
      options.extra["tokenErrorType"] = TokenErrorType.tokenNotFound; // I use enum type, you can chage it to string
      final error = DioError(requestOptions: options, type: DioErrorType.other);
      return handler.reject(error);
    }

    // check if tokens have already expired or not
    // I use jwt_decoder package
    // Note: ensure your tokens has "exp" claim
    final accessTokenHasExpired = JwtDecoder.isExpired(accessToken);
    final refreshTokenHasExpired = JwtDecoder.isExpired(refreshToken);

    var _refreshed = true;

    if (refreshTokenHasExpired) {
      _performLogout(_dio);

      // create custom dio error
      options.extra["tokenErrorType"] = TokenErrorType.refreshTokenHasExpired;
      final error = DioError(requestOptions: options, type: DioErrorType.other);

      return handler.reject(error);
    } else if (accessTokenHasExpired) {
      // regenerate access token
      _dio.interceptors.requestLock.lock();
      _refreshed = await _regenerateAccessToken();
      _dio.interceptors.requestLock.unlock();
    }

    if (_refreshed) {
      // add access token to the request header
      options.headers["Authorization"] = "Bearer $accessToken";
      return handler.next(options);
    } else {
      // create custom dio error
      options.extra["tokenErrorType"] = TokenErrorType.failedToRegenerateAccessToken;
      final error = DioError(requestOptions: options, type: DioErrorType.other);
      return handler.reject(error);
    }
  }

  @override
  void onError(DioError err, ErrorInterceptorHandler handler) {
    if (err.response?.statusCode == 403 || err.response?.statusCode == 401) {
      // for some reasons the token can be invalidated before it is expired by the backend.
      // then we should navigate the user back to login page

      _performLogout(_dio);

      // create custom dio error
      err.type = DioErrorType.other;
      err.requestOptions.extra["tokenErrorType"] = TokenErrorType.invalidAccessToken;
    }

    return handler.next(err);
  }

  void _performLogout(Dio dio) {
    _dio.interceptors.requestLock.clear();
    _dio.interceptors.requestLock.lock();

    _localStorage.removeTokens(); // remove token from local storage

    // back to login page without using context
    // check this https://mcmap.net/q/176105/-how-to-navigate-without-context-in-flutter-app
    navigatorKey.currentState?.pushReplacementNamed(LoginPage.routeName);

    _dio.interceptors.requestLock.unlock();
  }

  /// return true if it is successfully regenerate the access token
  Future<bool> _regenerateAccessToken() async {
    try {
      var dio = Dio(); // should create new dio instance because the request interceptor is being locked

      // get refresh token from local storage
      final refreshToken = _localStorage.getRefreshToken();

      // make request to server to get the new access token from server using refresh token
      final response = await dio.post(
        "https://yourDomain.com/api/refresh",
        options: Options(headers: {"Authorization": "Bearer $refreshToken"}),
      );

      if (response.statusCode == 200 || response.statusCode == 201) {
        final newAccessToken = response.data["accessToken"]; // parse data based on your JSON structure
        _localStorage.saveAccessToken(newAccessToken); // save to local storage
        return true;
      } else if (response.statusCode == 401 || response.statusCode == 403) {
        // it means your refresh token no longer valid now, it may be revoked by the backend
        _performLogout(_dio);
        return false;
      } else {
        print(response.statusCode);
        return false;
      }
    } on DioError {
      return false;
    } catch (e) {
      return false;
    }
  }
}

usage

  final dio = Dio();

  dio.options.baseUrl = "https://yourDomain.com/api";
  dio.interceptors.addAll([
      AuthInterceptor(dio), // add this line before LogInterceptor
      LogInterceptor(),
  ]);

if your request doesn't need token in the header (like in the login endpoint), then you should make request like this

await dio.post(
  "/login",
  data: loginData,
  options: Options(headers: {"requiresToken": false}), // add this line
);

otherwise, just make a regular request without adding token to the header option, the interceptor will automatically handle it.

await dio.get("/user", data: myData);
Kasiekask answered 28/9, 2021 at 9:22 Comment(2)
Is this solution able to work for multiple concurrent request that requires token?Nonobedience
@Nonobedience yes since I lock the request if the token expiredKasiekask
M
14

Dio 4.0.2 deprecates Interceptor locks. QueuedInterceptor should be used instead.

From the docs:

Locks of interceptors were originally designed to synchronize interceptor execution, but locks have a problem that once it becomes unlocked all of the requests run at once, rather than executing sequentially. Now QueuedInterceptor can do it better.

QueuedInterceptor provides a mechanism for sequential access(one by one) to interceptors.

An example of AuthInterceptor implemented using QueuedInterceptor:

/// Adds Authorization header with a non-expired bearer token.
///
/// Logic:
/// 1. Check if the endpoint requires authentication
///   - If not, bypass interceptor
/// 2. Get a non-expired access token
///   - AuthRepository takes care of refreshing the token if it is expired
/// 3. Make API call (attaching token in Authorization header)
/// 4. If response if 401 (e.g. a not expired access token that was revoked by backend),
///    force refresh access token and retry call.
///
/// For non-authenticated endpoints add the following header to bypass this interceptor:
/// `Authorization: None`
///
/// For endpoints with optional authentication provide the following header:
/// `Authorization: Optional`
/// - If user is not authenticated: the Authorization header will be removed
///   and the call will be performed without it.
/// - If the user is authenticated: the authentication token will be attached in the
///   Authorization header.
class AuthInterceptor extends QueuedInterceptor {
  AuthInterceptor({
    required this.dio,
    required this.authRepository,
    this.retries = 3,
  });

  /// The original dio
  final Dio dio;
  final AuthRepository authRepository;

  /// The number of retries in case of 401
  final int retries;

  @override
  Future<void> onRequest(
    final RequestOptions options,
    final RequestInterceptorHandler handler,
  ) async {
    // Non-authenticated endpoint -> bypass this interceptor
    if (options._requiresNoAuthentication()) {
      options._removeAuthenticationHeader();
      return handler.next(options);
    }
    // Get auth token
    final authTokenRes = await authRepository.getAuthToken();
    authTokenRes.fold(
      success: (final authToken) {
        // Add auth token in Authorization header
        options._setAuthenticationHeader(authToken.token);
        handler.next(options);
      },
      failure: (final e) async {
        // Skip authentication header if it is optional and user is not authenticated
        if (e is UserNoAuthenticatedException && options._hasOptionalAuthentication()) {
          options._removeAuthenticationHeader();
          return handler.next(options);
        }
        // Handle auth token errors
        await _onErrorRefreshingToken();
        final error = DioError(requestOptions: options, error: e);
        handler.reject(error);
      },
    );
  }

  @override
  Future<void> onError(final DioError err, final ErrorInterceptorHandler handler) async {
    if (err.response?.statusCode != 401) {
      return super.onError(err, handler);
    }
    // Check retry attempt
    final attempt = err.requestOptions._retryAttempt + 1;
    if (attempt > retries) {
      return super.onError(err, handler);
    }
    err.requestOptions._retryAttempt = attempt;
    await Future<void>.delayed(const Duration(seconds: 1));
    // Force refresh auth token
    final authTokenRes = await authRepository.getAuthToken(forceRefresh: true);
    authTokenRes.fold(
      success: (final authToken) async {
        // Add new auth token in Authorization header and retry call
        try {
          final options = err.requestOptions.._setAuthenticationHeader(authToken.token);
          final response = await dio.fetch<void>(options);
          handler.resolve(response);
        } on DioError catch (e) {
          if (e.response?.statusCode == 401) {
            await _onErrorRefreshingToken();
          }
          super.onError(e, handler);
        }
      },
      failure: (final e) async {
        // Handle auth token errors
        await _onErrorRefreshingToken();
        final error = DioError(requestOptions: err.requestOptions, error: authTokenRes.error);
        return handler.next(error);
      },
    );
  }

  Future<void> _onErrorRefreshingToken() async {
    await authRepository.signOut();
  }
}

extension AuthRequestOptionsX on RequestOptions {
  bool _requiresNoAuthentication() => headers['Authorization'] == 'None';

  bool _hasOptionalAuthentication() => headers['Authorization'] == 'Optional';

  void _setAuthenticationHeader(final String token) => headers['Authorization'] = 'Bearer $token';

  void _removeAuthenticationHeader() => headers.remove('Authorization');

  int get _retryAttempt => (extra['auth_retry_attempt'] as int?) ?? 0;

  set _retryAttempt(final int attempt) => extra['auth_retry_attempt'] = attempt;
}

Notes:

  • In my case AuthRepository is a wrapper of FirebaseAuth. The Firebase SDK takes care of providing a non-expired token when getAuthToken() is called.
  • AuthRepository.getAuthToken() returns a Future<Result<AuthToken, AuthException>>. My Result object is similar to the one provided in Result package.
Madera answered 28/6, 2022 at 18:23 Comment(1)
This seems like a great answer, one that I would very much like to adapt for my app, but without knowing the exact AuthRepository implementation, it's hard to understand how this precisely should be used. Also, it isn't intuitive for a Flutter developer to read Kotlin documentation for Result and understand how to put that in their code. If you have time, please consider adding your implementation for AuthRepository and consider rewriting the answer to use the standard try...catch operators.Akmolinsk
E
8

I think that a better approach is to check the token(s) before you actually make the request. That way you have less network traffic and the response is faster.

EDIT: Another important reason to follow this approach is because it is a safer one, as X.Y. pointed out in the comment section

In my example I use:

http: ^0.13.3
dio: ^4.0.0
flutter_secure_storage: ^4.2.0
jwt_decode: ^0.3.1
flutter_easyloading: ^3.0.0 

The idea is to first check the expiration of tokens (both access and refresh). If the refresh token is expired then clear the storage and redirect to LoginPage. If the access token is expired then (before submit the actual request) refresh it by using the refresh token, and then use the refreshed credentials to submit the original request. In that way you minimize the network traffic and you take the response way faster.

I did this:

AuthService appAuth = new AuthService();

class AuthService {
  Future<void> logout() async {
    token = '';
    refresh = '';

    await Future.delayed(Duration(milliseconds: 100));

    Navigator.of(cnt).pushAndRemoveUntil(
      MaterialPageRoute(builder: (context) => LoginPage()),
      (_) => false,
    );
  }

  Future<bool> login(String username, String password) async {
    var headers = {'Accept': 'application/json'};
    var request = http.MultipartRequest('POST', Uri.parse(baseURL + 'token/'));
    request.fields.addAll({'username': username, 'password': password});
    request.headers.addAll(headers);
    http.StreamedResponse response = await request.send();

    if (response.statusCode == 200) {
      var resp = await response.stream.bytesToString();
      final data = jsonDecode(resp);
      token = data['access'];
      refresh = data['refresh'];
      secStore.secureWrite('token', token);
      secStore.secureWrite('refresh', refresh);
      return true;
    } else {
      return (false);
    }
  }

  Future<bool> refreshToken() async {
    var headers = {'Accept': 'application/json'};
    var request =
        http.MultipartRequest('POST', Uri.parse(baseURL + 'token/refresh/'));
    request.fields.addAll({'refresh': refresh});

    request.headers.addAll(headers);

    http.StreamedResponse response = await request.send();

    if (response.statusCode == 200) {
      final data = jsonDecode(await response.stream.bytesToString());
      token = data['access'];
      refresh = data['refresh'];

      secStore.secureWrite('token', token);
      secStore.secureWrite('refresh', refresh);
      return true;
    } else {
      print(response.reasonPhrase);
      return false;
    }
  }
}

After that create the interceptor

import 'package:dio/dio.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import '../settings/globals.dart';


class AuthInterceptor extends Interceptor {
  static bool isRetryCall = false;

  @override
  void onRequest(
      RequestOptions options, RequestInterceptorHandler handler) async {
    bool _token = isTokenExpired(token);
    bool _refresh = isTokenExpired(refresh);
    bool _refreshed = true;

    if (_refresh) {
      appAuth.logout();
      EasyLoading.showInfo(
          'Expired session');
      DioError _err;
      handler.reject(_err);
    } else if (_token) {
      _refreshed = await appAuth.refreshToken();
    }
    if (_refreshed) {
      options.headers["Authorization"] = "Bearer " + token;
      options.headers["Accept"] = "application/json";

      handler.next(options);
    }
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) async {
    handler.next(response);
  }

  @override
  void onError(DioError err, ErrorInterceptorHandler handler) async {
    handler.next(err);
  }
}

The secure storage functionality is from:

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

SecureStorage secStore = new SecureStorage();

class SecureStorage {
  final _storage = FlutterSecureStorage();
  void addNewItem(String key, String value) async {
    await _storage.write(
      key: key,
      value: value,
      iOptions: _getIOSOptions(),
    );
  }

  IOSOptions _getIOSOptions() => IOSOptions(
        accountName: _getAccountName(),
      );

  String _getAccountName() => 'blah_blah_blah';

  Future<String> secureRead(String key) async {
    String value = await _storage.read(key: key);
    return value;
  }

  Future<void> secureDelete(String key) async {
    await _storage.delete(key: key);
  }

  Future<void> secureWrite(String key, String value) async {
    await _storage.write(key: key, value: value);
  }
}

check expiration with:

bool isTokenExpired(String _token) {
  DateTime expiryDate = Jwt.getExpiryDate(_token);
  bool isExpired = expiryDate.compareTo(DateTime.now()) < 0;
  return isExpired;
}

and then the original request

var dio = Dio();

Future<Null> getTasks() async {
EasyLoading.show(status: 'Wait ...');
    
Response response = await dio
    .get(baseURL + 'tasks/?task={"foo":"1","bar":"30"}');
    
if (response.statusCode == 200) {
    print('success');
} else {
    print(response?.statusCode);
}}

As you can see the Login and refreshToken request use http package (they don't need the interceptor). The getTasks use dio and it's interceptor in order to get its response in one and only request

Engrossing answered 16/6, 2021 at 12:21 Comment(7)
this will only work in the token is in JWT format, but the token format is not the same for everyone.Wriest
@Wriest you are correct. The principal though remains the same; if you can check the expiration of the token on the client this is a viable solutionEngrossing
Your answer is the correct one. The logic of checking on 401/403 and refreshing is just wrong. The backend can decide to invalidate tokens because of a data breach, the client will need to log out of the app once seeing 401/403.Katzman
this should be the accepted answerKasiekask
@JohnAnderton this is good, but I think you should also add codes to handle invalid token, I mean for some reasons the tokens can be invalidated from the backend before expired and we should segue the user back to login page. maybe we should handle it in the onError method on the AuthInterceptor by checking the status code (401 / 403)Kasiekask
err.response?.statusCode == 401Kasiekask
never tested it, but i guess that you can move appAuth.logout(); in AuthInterceptor onErrorEngrossing
S
8

Dio 4.0.0

dio.interceptors.clear();
dio.interceptors.add(
      InterceptorsWrapper(
        onRequest: (request, handler) {
          if (token != null && token != '')
            request.headers['Authorization'] = 'Bearer $token';
          return handler.next(request);
        },
        onError: (err, handler) async {
          if (err.response?.statusCode == 401) {
            try {
              await dio
                  .post(
                      "https://refresh.api",
                      data: jsonEncode(
                          {"refresh_token": refreshtoken}))
                  .then((value) async {
                if (value?.statusCode == 201) {
                  //get new tokens ...
                  print("acces token" + token);
                  print("refresh token" + refreshtoken);
                  //set bearer
                  err.requestOptions.headers["Authorization"] =
                      "Bearer " + token;
                  //create request with new access token
                  final opts = new Options(
                      method: err.requestOptions.method,
                      headers: err.requestOptions.headers);
                  final cloneReq = await dio.request(err.requestOptions.path,
                      options: opts,
                      data: err.requestOptions.data,
                      queryParameters: err.requestOptions.queryParameters);

                  return handler.resolve(cloneReq);
                }
                return err;
              });
              return dio;
            } catch (err, st) {
              
            }
          }
       },
    ),
);
Spar answered 16/7, 2021 at 13:3 Comment(0)
I
3

You would get a response status code as 401 for token expiration. In order to request new access token, you need to use post method along with form data and required Dio's options (content-type and headers). Below is the code shows how to request new token.

After successful request, if you get the response status code as 200, then you will get new access token value along with refresh token value and save them in any storage you prefer to use. For example, Shared preferences.

Once you have new access token saved, you can use it to fetch data using get method shown in the same code below.

onError(DioError error) async {
    if (error.response?.statusCode == 401) {
      Response response;
      var authToken = base64
          .encode(utf8.encode("username_value" + ":" + "password_value"));
      FormData formData = new FormData.from(
          {"grant_type": "refresh_token", "refresh_token": refresh_token_value});
      response = await dio.post(
        url,
        data: formData,
        options: new Options(
            contentType: ContentType.parse("application/x-www-form-urlencoded"),
            headers: {HttpHeaders.authorizationHeader: 'Basic $authToken'}),
      );
      if (response.statusCode == 200) {
        response = await dio.get(
          url,
          options: new Options(headers: {
            HttpHeaders.authorizationHeader: 'Bearer access_token_value'
          }),
        );
        return response;
      } else {
        print(response.data);
        return null;
      }
    }
    return error;
  }
Illene answered 10/9, 2019 at 4:10 Comment(0)
D
0

Below is a snippet from my interceptor

 dio.interceptors
        .add(InterceptorsWrapper(onRequest: (RequestOptions options) async {

/* Write your request logic setting your Authorization header from prefs*/

      String token = await prefs.accessToken;
      if (token != null) {
        options.headers["Authorization"] = "Bearer " + token;
      return options; //continue
    }, onResponse: (Response response) async {
// Write your response logic

      return response; // continue
    }, onError: (DioError dioError) async {

      // Refresh Token
      if (dioError.response?.statusCode == 401) {
        Response response;
        var data = <String, dynamic>{
          "grant_type": "refresh_token",
          "refresh_token": await prefs.refreshToken,
          'email': await prefs.userEmail
        };
        response = await dio
            .post("api/url/for/refresh/token", data: data);
        if (response.statusCode == 200) {
          var newRefreshToken = response.data["data"]["refresh_token"]; // get new refresh token from response
          var newAccessToken = response.data["data"]["access_token"]; // get new access token from response
          prefs.refreshToken = newRefreshToken;
          prefs.accessToken = newAccessToken; // to be used in the request section of the interceptor
          return dio.request(dioError.request.baseUrl + dioError.request.path,
              options: dioError.request);
        }
      }
      return dioError;
    }));
    return dio;
  }
}
Daffi answered 27/8, 2020 at 16:50 Comment(0)
N
0

it is working 100%

 RestClient client;
        
          static BaseOptions options = new BaseOptions(
            connectTimeout: 5000,
            receiveTimeout: 3000,
          );
          RemoteService() {
            // or new Dio with a BaseOptions instance.
            final dio = Dio(options);
        
            dio.interceptors
                .add(InterceptorsWrapper(onRequest: (RequestOptions options) async {
              // Do something before request is sent
              return options; //continue
            }, onResponse: (Response response) async {
              // Do something with response data
              return response; // continue
            }, onError: (DioError error) async {
              // Do something with response error
              if (error.response.statusCode == 401) {
                Response response =
                    await dio.post("http://addrees-server/oauth/token",
                        options: Options(
                          headers: {
                            'Authorization': ApiUtils.BASIC_TOKEN,
                            'Content-Type': ApiUtils.CONTENT_TYPE,
                          },
                        ),
                        queryParameters: {
                      "grant_type": ApiUtils.GRANT_TYPE,
                      "username": AppConstants.LOGIN,
                      "password": AppConstants.PASSWORD
                    });
        
                Sessions.access_token = response.data['access_token'];
        
                error.response.request.queryParameters
                    .update('access_token', (value) => Sessions.access_token);
                RequestOptions options = error.response.request;
        
                return dio.request(options.path, options: options); //continue
              } else {
                return error;
              }
            }));
            client = RestClient(dio);
          }
Nigger answered 7/1, 2021 at 0:26 Comment(2)
How do you handle concurrent calls?Hying
Hi, what do you mean, please give an example.Nigger

© 2022 - 2024 — McMap. All rights reserved.