Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Returning empty token when the refresh method is called #128

Open
Mubaola23 opened this issue Apr 20, 2022 · 10 comments
Open

Returning empty token when the refresh method is called #128

Mubaola23 opened this issue Apr 20, 2022 · 10 comments

Comments

@Mubaola23
Copy link

When the token expires, it tries to refresh the token in the secure storage but replaces it with an empty variable causing it to redirect to the login page. I’ve been on this issue for a while and seek possible solutions including checking the GitHub issues but still unable to get a better solution.

Here is where i defined my helper class

class MyOAuth2Client extends OAuth2Client {
MyOAuth2Client({required String redirectUri, required String customUriScheme})
: super(
authorizeUrl:
'https://$baseurl/connect/authorize', //Your service's authorization url
tokenUrl:
'https://$baseurl/connect/token', //Your service access token url
redirectUri: redirectUri,
revokeUrl: "https://$baseurl/connect/revocation",
customUriScheme: customUriScheme);
}

class MyHelper {

OAuth2Client getClient() {
var client;
client = MyOAuth2Client(
redirectUri: REDIRECT_URL,
customUriScheme: CUSTOM_SCHEME,
);
return client;
}

FlutterSecureStorage _storage = FlutterSecureStorage();

Future authorize() async {
try {
var client = getClient();
await _storage.deleteAll();

  if (client != null) {
    var hlp = toPerformRequest(getClient());

    await hlp.disconnect();


    var result = await hlp.getToken().catchError((onError) async {
      await hlp.disconnect();

      print("error $onError");
    });
   

    Response user = await hlp.get(USER_INFO);

    UserInfo userinfo = UserInfo.fromJson(jsonDecode(user.body));

    await _storage.write(key: "email", value: userinfo.email);

    await _storage.write(key: "userID", value: userinfo.userID);

    await _storage.write(key: "id", value: userinfo.sub);

    await _storage.write(key: "name", value: userinfo.givenName);

    await _storage.write(key: "tenantId", value: userinfo.tenantId);
  }

} catch (ex) {
  throw (ex.toString());
}

}

OAuth2Helper toPerformRequest(OAuth2Client client) {
String clientId = CLIENT_ID;
String clientSecret = CLIENT_SECRET;

var hlp = OAuth2Helper(client,
    clientId: clientId,
    clientSecret: clientSecret,
    scopes: [ 'openid', 'profile', 'offline_access']);
return hlp;

}

logout() async {
String clientId = CLIENT_ID;
String clientSecret = CLIENT_SECRET;
var hlp = OAuth2Helper(getClient(),
clientId: clientId,
clientSecret: clientSecret,
scopes: [ 'openid', 'profile', 'offline_access']);

await hlp.disconnect();

await hlp.removeAllTokens();
await _storage.deleteAll();

}

get(
String url, {
Map<String, String>? data,
}) async {
try {
var hlp = toPerformRequest(getClient());

  await hlp.getToken();
  var token = await hlp.getTokenFromStorage();

//the token gets printed here until it expires an no longer able to get another token form storage
print(token?.accessToken.toString());
var response =
await hlp.get("$baseurl$url", headers: data);
return jsonDecode(response.body);
} on SocketException {
throw Failure('No Internet connection 😑');
} on HttpException {
throw Failure("Couldn't find the post 😱");
} on FormatException {
throw Failure("Bad response format 👎");
} on TimeoutException {
throw Failure('Connection Timeout: Check your internet connection');
} on Failure catch (e) {
throw Failure(e.message);
}
}

post(
String url,
dynamic data,
) async {
try {
var hlp = toPerformRequest(getClient());
await hlp.getToken();

  var decodeBody = jsonEncode(data);

  var response = await hlp.post(
    "$baseurl$url",
    headers: {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
    },
    body: decodeBody,
  );
  return jsonDecode(response.body);
} on SocketException {
  throw Failure('No Internet connection 😑');
} on HttpException {
  throw Failure("Couldn't find the post 😱");
} on FormatException {
  throw Failure("Bad response format 👎");
} on TimeoutException {
  throw Failure('Connection Timeout: Check your internet connection');
} on Failure catch (e) {
  throw Failure(e.message);
}

}
}

This is from the helper class, At this point when the token expires , its suppose to refresh, but instead its calling the fetchToken() method which means it has previously try to refresh the token and it replaced it with an empty token

/// Returns a previously required token, if any, or requires a new one.
///
/// If a token already exists but is expired, a new token is generated through the refresh_token grant.
Future<AccessTokenResponse?> getToken() async {
_validateAuthorizationParams();

var tknResp = await getTokenFromStorage();

// [At this point when the token expires , its suppose to refresh, but instead its calling the fetchToken() method which means it has previously try to refresh the token and it replaced it with an empty token ]
if (tknResp != null) {
if (tknResp.refreshNeeded()) {
//The access token is expired
if (tknResp.refreshToken != null) {
tknResp = await refreshToken(tknResp.refreshToken!);
} else {
//No refresh token, fetch a new token
tknResp = await fetchToken();
}
}
} else {
tknResp = await fetchToken();
}

if (!tknResp.isValid()) {
  throw Exception(
      'Provider error ${tknResp.httpStatusCode}: ${tknResp.error}: ${tknResp.errorDescription}');
}

if (!tknResp.isBearer()) {
  throw Exception('Only Bearer tokens are currently supported');
}

return tknResp;

}

@okrad
Copy link
Collaborator

okrad commented Apr 20, 2022

Hi @Mubaola23,
it's not clear to me how you are using your helper class, but there's something weird with your code.

For example, you don't need to call getToken() each time you instantiate the OAuthHelper: the helper retrieves it from the storage or from the server as soon as it is needed (i.e. before a get or post request).

Furthermore I don't understand why you are calling disconnect() in the authorize method. Calling it will delete both the access and the refresh token from the storage, after that it will invoke the token revocation endpoint (that should in turn clear the access and refresh tokens on the server).

I'll try to review and comment your code below, let me know if I misunderstood anything...

class MyOAuth2Client extends OAuth2Client {
  MyOAuth2Client({required String redirectUri, required String customUriScheme})
      : super(
            authorizeUrl:
                'https://$baseurl/connect/authorize', //Your service's authorization url
            tokenUrl:
                'https://$baseurl/connect/token', //Your service access token url
            redirectUri: redirectUri,
            revokeUrl: "https://$baseurl/connect/revocation",
            customUriScheme: customUriScheme);
}

class MyHelper {
  OAuth2Client getClient() {
    var client;
    client = MyOAuth2Client(
      redirectUri: REDIRECT_URL,
      customUriScheme: CUSTOM_SCHEME,
    );
    return client;
  }

  FlutterSecureStorage _storage = FlutterSecureStorage();

  Future authorize() async {
    try {
      var client = getClient();
      await _storage.deleteAll();

      if (client != null) {

        // ###No harm here, but you already have the client instantiated, you should pass it to the helper...###
        //var hlp = toPerformRequest(getClient());
        var hlp = toPerformRequest(client);

        // ### No need to disconnect and retrieving the token.... ###
/*
        await hlp.disconnect();

        var result = await hlp.getToken().catchError((onError) async {
          await hlp.disconnect();

          print("error $onError");
        });
*/
        Response user = await hlp.get(USER_INFO);

        UserInfo userinfo = UserInfo.fromJson(jsonDecode(user.body));

        await _storage.write(key: "email", value: userinfo.email);

        await _storage.write(key: "userID", value: userinfo.userID);

        await _storage.write(key: "id", value: userinfo.sub);

        await _storage.write(key: "name", value: userinfo.givenName);

        await _storage.write(key: "tenantId", value: userinfo.tenantId);
      }
    } catch (ex) {
      throw (ex.toString());
    }
  }

  OAuth2Helper toPerformRequest(OAuth2Client client) {
    String clientId = CLIENT_ID;
    String clientSecret = CLIENT_SECRET;

    var hlp = OAuth2Helper(client,
        clientId: clientId,
        clientSecret: clientSecret,
        scopes: ['openid', 'profile', 'offline_access']);
    return hlp;
  }

  logout() async {
/*
    String clientId = CLIENT_ID;
    String clientSecret = CLIENT_SECRET;
    var hlp = OAuth2Helper(getClient(),
        clientId: clientId,
        clientSecret: clientSecret,
        scopes: ['openid', 'profile', 'offline_access']);
*/
    var hlp = toPerformRequest(getClient());

    await hlp.disconnect();

    // ### Why are you removing all tokens in the storage? ###
    //await hlp.removeAllTokens();

    await _storage.deleteAll();
  }

  get(
    String url, {
    Map<String, String>? data,
  }) async {
    try {
      var hlp = toPerformRequest(getClient());

      // ### Again, no need to retrieve the token... ###
      //await hlp.getToken();

      // ### Does the token expire on its own or due to the calls to the disconnect method? ### 
      var token = await hlp.getTokenFromStorage();
//the token gets printed here until it expires an no longer able to get another token form storage
      print(token?.accessToken.toString());

      var response = await hlp.get("$baseurl$url", headers: data);
      return jsonDecode(response.body);
    } on SocketException {
      throw Failure('No Internet connection expressionless');
    } on HttpException {
      throw Failure("Couldn't find the post scream");
    } on FormatException {
      throw Failure("Bad response format -1");
    } on TimeoutException {
      throw Failure('Connection Timeout: Check your internet connection');
    } on Failure catch (e) {
      throw Failure(e.message);
    }
  }

  post(
    String url,
    dynamic data,
  ) async {
    try {
      var hlp = toPerformRequest(getClient());
      // ### Again, no need to retrieve the token... ###
     //await hlp.getToken();

      var decodeBody = jsonEncode(data);

      var response = await hlp.post(
        "$baseurl$url",
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json',
        },
        body: decodeBody,
      );
      return jsonDecode(response.body);
    } on SocketException {
      throw Failure('No Internet connection 😑');
    } on HttpException {
      throw Failure("Couldn't find the post 😱");
    } on FormatException {
      throw Failure("Bad response format 👎");
    } on TimeoutException {
      throw Failure('Connection Timeout: Check your internet connection');
    } on Failure catch (e) {
      throw Failure(e.message);
    }
  }
}

@Mubaola23
Copy link
Author

I'm using the disconnect in the authorize method in order to empty the storage if at all there's token stored already before it authorizes the user.

I will effect these corrections and see if it solves the issue.
Thank you for taking time to review the code.

@Mubaola23
Copy link
Author

Mubaola23 commented Apr 21, 2022

@okrad

I made the corrections you suggested and here is what i have. When the token expires, it prints the previous token unlike before but redirects back to the login page( whereby it's suppose to refresh the token )

I/flutter ( 9737): eyJhbGciOiJSUzI1NiIsImtpZCI6IjgyODUxRDU4Mzc2ODE3RUEzOTMyM0VGRTBEQzlENDZGQUJERENENDVSUzI1NiIsInR5cCI6ImF0K2p3dCIsIng1dCI6ImdvVWRXRGRvRi1vNU1qNy1EY25VYjZ2ZHpVVSJ9.eyJuYmYiOjE2NTA1MzU0MDgsImV4cCI6MTY1MDUzNzIwOCwiaXNzIjoiaHR0cHM6Ly9pZGVudGl0eS5vbmRnby5uZyIsImNsaWVudF9pZCI6Im9uRGdvQXBwQ2xpZW50Iiwic3ViIjoiNjI0MmUyZGZlNzUyNmNhbCIsImdpdmVuX25hbWUiOiJkYXlvIiwiZmFtaWx5X25hbWUiOiJvbmRnbyIsInJvbGUiOiJvbkRnb1VzZXIiLCJlbWFpbCI6ImRheW8zQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjoidHJ1ZSIsImFkZHJlc3MiOiIgIiwib25EZ29JZCI6ImRlbW8tb25EZ28tMTAwMDA0MCIsInRlbmIiwidGVuYW50TmFtZSI6Ik9OREdPIExpbWl0ZWQiLCJzaWQiOiIwQzA4RjBBRTEwN0ZGNTcxQ0M0QjFENEVDMURDRkY0RCIsImlhdCI6MTY1MDUzNTQwOCwic2NvcGUiOlsib3BlbmlkIiwicHJvZmlsZSIsIm9mZmxpbmVfYWNjZXNzIl0sImFtciI6WyJwd2QiXX0.DHrGOp9ZOFCqCW97tpNNuB5ze3IAXAP6tbuDW-yZewxdm_11PctqkhTVYtA0iay2mg8nkWP2yPCYZBcnf7ZrYrgbKCRhl4A54G5OjXDZQMtrcdLtmDu6Uct3RQWkQcENJuwQY3MEsivbZhzpa8Lfp5GDXkWgexwgTgAvHVraZNdkenQkLAsCHj
D/ViewRootImpl@213c661MainActivity: MSG_WINDOW_FOCUS_CHANGED 0 1
D/InputMethodManager( 9737): prepareNavigationBarInfo() DecorView@5eaa44f[MainActivity]
D/InputMethodManager( 9737): getNavigationBarColor() -855310
D/SurfaceView( 9737): onWindowVisibilityChanged(8) false io.flutter.embedding.android.FlutterSurfaceView{f70762f V.E...... ........ 0,0-720,1494} of ViewRootImpl@213c661[MainActivity]
D/SurfaceView( 9737): surfaceDestroyed callback.size 1 #2 io.flutter.embedding.android.FlutterSurfaceView{f70762f V.E...... ........ 0,0-720,1494}
W/libEGL ( 9737): EGLNativeWindowType 0xc5b44008 disconnect failed
D/SurfaceView( 9737): remove() io.flutter.embedding.android.FlutterSurfaceView{f70762f V.E...... ........ 0,0-720,1494} Surface(name=SurfaceView - ng.ondgo.ondgoapp/ng.ondgo.ondgoapp.MainActivity@f70762f@8)/@0x12104fe
W/libEGL ( 9737): EGLNativeWindowType 0xc5c8ff48 disconnect failed
D/ViewRootImpl@213c661MainActivity: Relayout returned: old=(0,0,720,1520) new=(0,0,720,1520) req=(720,1520)8 dur=36 res=0x5 s={false 0} ch=true
D/InputTransport( 9737): Input channel destroyed: 'ClientS', fd=95
D/ViewRootImpl@213c661MainActivity: stopped(true) old=false
D/SurfaceView( 9737): windowStopped(true) false io.flutter.embedding.android.FlutterSurfaceView{f70762f V.E...... ........ 0,0-720,1494} of ViewRootImpl@213c661[MainActivity]

when i tried it the second time here is the response

I/flutter ( 9737): null
D/ViewRootImpl@213c661MainActivity: MSG_WINDOW_FOCUS_CHANGED 0 1
D/InputMethodManager( 9737): prepareNavigationBarInfo() DecorView@5eaa44f[MainActivity]
D/InputMethodManager( 9737): getNavigationBarColor() -855310
D/InputTransport( 9737): Input channel destroyed: 'ClientS', fd=95
D/ViewRootImpl@213c661MainActivity: stopped(true) old=false
D/SurfaceView( 9737): windowStopped(true) false io.flutter.embedding.android.FlutterSurfaceView{f70762f V.E...... ........ 0,0-720,1494} of ViewRootImpl@213c661[MainActivity]
D/SurfaceView( 9737): surfaceDestroyed callback.size 1 #1 io.flutter.embedding.android.FlutterSurfaceView{f70762f V.E...... ........ 0,0-720,1494}
W/libEGL ( 9737): EGLNativeWindowType 0xc5c89808 disconnect failed
D/SurfaceView( 9737): remove() io.flutter.embedding.android.FlutterSurfaceView{f70762f V.E...... ........ 0,0-720,1494} Surface(name=SurfaceView - ng.ondgo.ondgoapp/ng.ondgo.ondgoapp.MainActivity@f70762f@9)/@0x816a9ae
D/SurfaceView( 9737): onWindowVisibilityChanged(8) false io.flutter.embedding.android.FlutterSurfaceView{f70762f V.E...... ........ 0,0-720,1494} of ViewRootImpl@213c661[MainActivity]
W/libEGL ( 9737): EGLNativeWindowType 0xc5d0a188 disconnect failed
D/ViewRootImpl@213c661MainActivity: Relayout returned: old=(0,0,720,1520) new=(0,0,720,1520) req=(720,1520)8 dur=14 res=0x5 s={false 0} ch=false

@okrad
Copy link
Collaborator

okrad commented Apr 21, 2022

@Mubaola23, are you sure the server provides a refresh endpoint? If so, can it be that the refresh endpoint url is different than the access token url? Usually they are the same, but they can differ.
In this case, you can set the refresh token endopoint url by passing the refreshUrl property to the client constructor.

For example:

class MyOAuth2Client extends OAuth2Client {
  MyOAuth2Client({required String redirectUri, required String customUriScheme})
      : super(
            authorizeUrl:
                'https://$baseurl/connect/authorize', //Your service's authorization url
            tokenUrl:
                'https://$baseurl/connect/token', //Your service access token url
            refreshUrl: 'https://$baseurl/connect/refresh', //***Just a made up url, you should check the server docs...
            redirectUri: redirectUri,
            revokeUrl: "https://$baseurl/connect/revocation",
            customUriScheme: customUriScheme);
}

But first of all, can you verify if actually a refresh token exists before the access token expires?
For example:

 get(
    String url, {
    Map<String, String>? data,
  }) async {
    try {
      var hlp = toPerformRequest(getClient());

      var token = await hlp.getTokenFromStorage();
      print(token?.accessToken.toString());
      print(token?.refreshToken.toString());

      ...
  }

@Mubaola23
Copy link
Author

Yes , I was able to print the refresh token along with the access token

Access Token: eyJhbGciOiJSUzI1NiIsImtpZCI6IjgyODUxRDU4Mzc2ODE3RUEzOTMyM0VGRTBEQzlENDZGQUJERENENDVSUzI1NiIsInR5cCI6ImF0K2p3dCIsIng1dCI6ImY1MDYxOTkxMiwic2NvcGUiOlsib3BlbmlkIiwicHJvZmlsZSIsIm9mZmxpbmVfYWNjZXNzIl0sImFtciI6WyJwd2QiXX0.Vqz21ZUuyvHhoE9sBRuZAr8FJxH8VJX0s__h9wRfPS8xl3B9Zlew73B2_eGgHigCtBd4fzZ_0M6X7ymmT5OW-oosfZ866RYhC1CXti1Hk0vZ2l0W78tginYdrxd5Z66_VuYVSVf_Nader7RMRrseKudKUGFtcIvV6_PmXYjG

I/flutter ( 3440): Refresh Token :2A245D099FD4C628571264BBDDE5C673F90A689483541B

I

@okrad
Copy link
Collaborator

okrad commented Apr 26, 2022

Hi @Mubaola23, I managed to replicate your issue, now I need to understand what's going on and how to solve it...
I'll let you know as soon as possible, thank you for taking the time to report your problem!

@Mubaola23
Copy link
Author

Hi @okrad , thank you for taking time to look into this issue, i'll look forward to a reply from you.

okrad added a commit that referenced this issue May 3, 2022
@okrad
Copy link
Collaborator

okrad commented May 3, 2022

Hi @Mubaola23, can you try out the new version?

@Mubaola23
Copy link
Author

Mubaola23 commented May 4, 2022

Hi @okrad , I Added the updated version already and here is what changed:
When the token expires , in order to refresh it loads the login page. if i enter the login details, it shows " State parameter error", When i canceled the login "Unhandled Exception: PlatformException(CANCELED, User canceled login, null, null)" it shows this and fetches another token then continues with the request.

@okrad
Copy link
Collaborator

okrad commented May 5, 2022

That's quite strange...

If you are presented the login page this could mean one of two things:

  1. The refresh token is itself expired
  2. There's no refresh token

In all of these cases, the client is forced to fetch a new token by means of the appropriate oauth flow.
The state parameter error is even stranger... Can you understand if it Is a server generated error or a client side one?

One thing you could try is to clear the token storage by calling the OAuth2Helper.removeAllTokens method.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants