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

[Android] expiration date mismatch #286

Closed
6 tasks done
o-artebiakin opened this issue Jul 9, 2023 · 30 comments
Closed
6 tasks done

[Android] expiration date mismatch #286

o-artebiakin opened this issue Jul 9, 2023 · 30 comments
Assignees
Labels
android needs investigation An issue that has more questions to answer or otherwise needs work to fully understand the issue

Comments

@o-artebiakin
Copy link

Checklist

Description

Credentials Manager for the Android app is returning an invalid expiration time. The credentialsManager.credentials() method is returning a Credentials object, but the expiresAt field does not match the expiration date provided in the idToken. This mismatch is causing the token to be invalidated on the server.

This issue does not occur on iOS, as it is working correctly.

Credentials.expiresAt         =>  2023-07-09 15:15:29.779Z
Credentials.idToken['exp'] => 2023-07-09 21:15:29.000Z

Reproduction

  1. Log in to the app.
  2. Call Auth0().credentialsManager.credentials().
  3. Compare the expiresAt field with the expiration date parsed from the idToken.

Additional context

No response

auth0_flutter version

1.2.1

Flutter version

3.10.4

Platform

Android

Platform version(s)

No response

@Widcket
Copy link
Contributor

Widcket commented Jul 10, 2023

Hi @o-artebiakin, thanks for raising this.

@poovamraj could you please take a look?

@Widcket Widcket added the needs investigation An issue that has more questions to answer or otherwise needs work to fully understand the issue label Jul 10, 2023
@poovamraj
Copy link
Contributor

@o-artebiakin The expiresAt field will not match the expiration of idToken. The expiresAt refers to the expiration of the access_token and you should only use that for your APIs. You can find more information on difference between a ID Token and an access token here - https://auth0.com/blog/id-token-access-token-what-is-the-difference/#What-Is-an-ID-Token.

Hope this answers your question. Please feel free to comment here and we can reopen this issue if you have more doubts.

@o-artebiakin
Copy link
Author

o-artebiakin commented Jul 12, 2023

@poovamraj
Thank you for the response! It's my mistake that I displayed the idToken. Indeed, we only use the accessToken, but the issue remains the same for it as well.

Credential Expiration:    2023-07-12 11:59:23.160Z
Decoded Token Expiration: 2023-07-12 05:59:23.000Z

Reproduction

  1. Log in to the app.
  2. Call Auth0().credentialsManager.credentials().
  3. Compare the expiresAt field with the expiration date parsed from the accessToken.

@o-artebiakin
Copy link
Author

PS: I noticed that the dates also differ slightly in iOS, but it's insignificant and doesn't cause any communication issues with the backend.

// iOS logs
Credential Expiration:    2023-07-12 05:55:26.632Z
Decoded Token Expiration: 2023-07-12 05:55:26.000Z

@Widcket Widcket reopened this Jul 12, 2023
@poovamraj
Copy link
Contributor

Decoded Token Expiration: 2023-07-12 05:59:23.000Z

The ID Tokens are supposed to be much shorter lived than the access token. Can you show me how you are decoding the ID token in Android vs how you decode it in iOS?

Also can you let us know the timezone in both the devices/emulators you are using?

@o-artebiakin
Copy link
Author

@poovamraj
For token parsing I use this plugin: https://pub.dev/packages/jwt_decoder
I got the same result if I used this resource: https://jwt.io

Example code where I compare results:

class Auth0Service {
  final Auth0 _auth0Client = Auth0(
    'auth0Domain',
    Platform.isIOS ? 'auth0ClientIdIOS' : 'auth0ClientIdAndroid',
  );

  Future<void> getCredentials() async {
    try {
      final credential = await _auth0Client.credentialsManager.credentials();

      final decodedIdToken = JwtDecoder.decode(credential.idToken);
      final decodedAccessToken = JwtDecoder.decode(credential.accessToken);

      final credentialExpiration = credential.expiresAt.toUtc();
      final idTokenExpiration = DateTime.fromMillisecondsSinceEpoch(
        (decodedIdToken['exp'] as int) * 1000,
      ).toUtc();
      final accessTokenExpiration = DateTime.fromMillisecondsSinceEpoch(
        (decodedAccessToken['exp'] as int) * 1000,
      ).toUtc();

      debugPrint('''
Credential Expiration:                  $credentialExpiration
Decoded Token Expiration(idToken):      $idTokenExpiration
Decoded Token Expiration(accessToken):  $accessTokenExpiration
''');
    } catch (e) {
      debugPrint(e.toString());
    }
  }
}

Response(Android):

I/flutter ( 4496): Credential Expiration:                  2023-07-12 18:27:00.010Z
I/flutter ( 4496): Decoded Token Expiration(idToken):      2023-07-13 00:26:59.000Z
I/flutter ( 4496): Decoded Token Expiration(accessToken):  2023-07-12 15:26:59.000Z

Response(iOS):

flutter: Credential Expiration:                  2023-07-12 14:39:42.738Z
flutter: Decoded Token Expiration(idToken):      2023-07-12 23:39:42.000Z
flutter: Decoded Token Expiration(accessToken):  2023-07-12 14:39:42.000Z

On the backend side, we use the standard auth0 library to validate the token.

Timezone:

GMT+03.00 (EEST)

If I change the timezone on an android emulator, for example, to: GMT-04.00(EDT)
Response(Android):

I/flutter ( 5461): Credential Expiration:                  2023-07-12 18:27:00.010Z
I/flutter ( 5461): Decoded Token Expiration(idToken):      2023-07-13 00:26:59.000Z
I/flutter ( 5461): Decoded Token Expiration(accessToken):  2023-07-12 15:26:59.000Z

Response(iOS):

flutter: Credential Expiration:                  2023-07-12 15:40:37.589Z
flutter: Decoded Token Expiration(idToken):      2023-07-13 00:40:37.000Z
flutter: Decoded Token Expiration(accessToken):  2023-07-12 15:40:37.000Z

@poovamraj
Copy link
Contributor

@o-artebiakin From the code you shared looks like you are just getting a string value back from our SDK and parsing it using a seperate plugin.

As you can see here

      final decodedIdToken = JwtDecoder.decode(credential.idToken);
      final decodedAccessToken = JwtDecoder.decode(credential.accessToken);

The string value is returned to you and how you parse it is left to you. The error you are mentioning looks like it happens in how the jwt_decoder plugin handles it.

What I would suggest is to use the expiresAt value returned in our Credentials which should provide you with the correct expiration time of the access token rather than parsing it from the JWT.

If parsing is still mandatory, the error is in how it is parsed, as the SDK just returns a string value and doesn't handle the parsing logic. If you print the exact value without parsing, it would work for you.

@poovamraj
Copy link
Contributor

Also, looks like you have different client id for Android and iOS. Which means your backend configuration could be different and this could lead expiration time being different. i would suggest you to check that out as well

@o-artebiakin
Copy link
Author

@poovamraj
I use this method only for debugging and error demonstration. In a production environment, I rely on CredentialsManager. But it returns a token that is identified as expired on the backend.
I've also narrowed everything down to one application, and my demo code looks as follows.

class Auth0Service {
  final Auth0 _auth0Client = Auth0(
    'auth0Domain',
// Remove iOS
    'auth0ClientIdAndroid',
  );

  Future<void> getCredentials() async {
    try {
      final credential = await _auth0Client.credentialsManager.credentials();

      final decodedIdToken = JwtDecoder.decode(credential.idToken);
      final decodedAccessToken = JwtDecoder.decode(credential.accessToken);

      final credentialExpiration = credential.expiresAt.toUtc();
      final idTokenExpiration = DateTime.fromMillisecondsSinceEpoch(
        (decodedIdToken['exp'] as int) * 1000,
      ).toUtc();
      final accessTokenExpiration = DateTime.fromMillisecondsSinceEpoch(
        (decodedAccessToken['exp'] as int) * 1000,
      ).toUtc();

      debugPrint('''
Credential Expiration:                  $credentialExpiration
Decoded Token Expiration(idToken):      $idTokenExpiration
Decoded Token Expiration(accessToken):  $accessTokenExpiration
''');
    } catch (e) {
      debugPrint(e.toString());
    }
  }
}

Response:

// iOS
flutter: Credential Expiration:                  2023-07-13 14:45:55.365Z
flutter: Decoded Token Expiration(idToken):      2023-07-13 23:45:55.000Z
flutter: Decoded Token Expiration(accessToken):  2023-07-13 14:45:55.000Z

// Android
I/flutter ( 5880): Credential Expiration:                  2023-07-13 17:42:04.220Z
I/flutter ( 5880): Decoded Token Expiration(idToken):      2023-07-13 23:42:04.000Z
I/flutter ( 5880): Decoded Token Expiration(accessToken):  2023-07-13 14:42:04.000Z

The application settings are standard.
image

I believe that this method of verification is correct, because after 3 hours, when the CredentialsManager finally updates the token, the backend recognizes this key as valid.

@poovamraj
Copy link
Contributor

@o-artebiakin thanks for these details. Can you also share the snippet on how you are doing the verification on the backend. With this I can fully reproduce the end to end working.

Also you response is that the accessToken is valid in iOS and not in Android right? ID token doesn't have any role here right?

@o-artebiakin
Copy link
Author

Code snippet:

/**
 * Auth0 token validation middleware
 */
import { NextFunction, Request, Response } from 'express';
import { auth, JWTPayload } from 'express-oauth2-jwt-bearer';
import { logger } from 'firebase-functions';

import { UnauthorizedError } from '../../error-classes';
import { decodeTokenBody, getAuth0Config, getTokenFromHeader, isValidIssuer } from '../jwt-utils';

export function verifyAuth0Jwt(req: Request, res: Response, next: NextFunction): void {
  const token = getTokenFromHeader(req.headers);
  if (!token) {
    return next(new UnauthorizedError('No token'));
  }

  let tokenBody: JWTPayload;

  try {
    tokenBody = decodeTokenBody(token);
  } catch (e) {
    logger.warn((e as Error).message);
    return next(new UnauthorizedError('Bad token format'));
  }

  const auth0Config = getAuth0Config(tokenBody);
  const issuer = tokenBody.iss!;

  if (!isValidIssuer(auth0Config, issuer)) {
    return next(new UnauthorizedError('Unknown issuer'));
  }

  const authHandler = auth({
    audience: auth0Config.audience,
    issuer,
    jwksUri: `${issuer}.well-known/jwks.json`
  });

  return authHandler(req, res, next);
}

Also you response is that the accessToken is valid in iOS and not in Android right? ID token doesn't have any role here right?
Absolutely. We only use the accessToken in the authorization header. We don't use the idToken anywhere.
And yes, we cannot reproduce it on iOS.

@poovamraj
Copy link
Contributor

Hi @o-artebiakin as you can see the access token is not manipulated anywhere. We just send the value returned from the backend. It cannot be done in SDK as well as it is a jWT and you are just sending that value. Your backend code seems fine but I would suggest you check whether you are using the same client id for both iOS and Android and getting same result. You can post this code to our express-auth2-bearer where you can get more help on this.

@o-artebiakin
Copy link
Author

Hi @poovamraj and thanks for your support.
Last question. How does the CredentialsManager decide that the token needs to be updated? Or is this also done using the API request? Initially, my assumption was that the CredentialsManager parses the date incorrectly or in the wrong time zone, and this is causing the error. As it believes the token is still valid, but this is not the case.

@poovamraj
Copy link
Contributor

@o-artebiakin The credential manager checks the expiresAt value returned in the credentials. You can use the hasValidCredentials method (Docs link) to check this.

@o-artebiakin
Copy link
Author

@poovamraj If my method of checking the date through parsing the accessToken is incorrect, how can I make sure the time is parsed correctly? hasValidCredentials doesn't solve the problem, because it also returns true when the token expiration time has passed.

@poovamraj
Copy link
Contributor

poovamraj commented Jul 24, 2023

@o-artebiakin when you mention

because it also returns true when the token expiration time has passed.

You mean the access token? Refresh token is the only other information that is checked and looks like you have disabled it from the configuration you shared earlier.

@poovamraj
Copy link
Contributor

We will close this issue now. Let us know if you have more doubts and we can reopen it

@marcellplentific
Copy link

Hi. We are experiencing time differences in access token expiration dates given by Credentials.expiresAt, between Android and iOS.

  • Access token expiration: 2 hours
  • Current time: 14:00 GMT+2
  • iOS expiresAt: 14:00 UTC
  • Android expiresAt: 16:00 UTC

We are comparing DateTime.now() which is in the users time zone, so comparing 14:00 GMT+2 with 14:00 UTC gives the right value on iOS (2 hours). But on Android it gives the right value if we ignore the users timezone.
Could you check if you are also experiencing this?

@aprzedecki
Copy link

@poovamraj any updates on that one ? On Android those methods are working incorrect

@aprzedecki
Copy link

It looks like this PR introduced a bug: #162
On iOS this is not occuring. Any comments pls @Widcket

@poovamraj poovamraj reopened this Oct 2, 2023
@Widcket
Copy link
Contributor

Widcket commented Oct 2, 2023

@poovamraj could you please take a look?

@poovamraj
Copy link
Contributor

@aprzedecki @marcellplentific @o-artebiakin We were able to figure out this issue, thanks to @aprzedecki.

As mentioned it is the use of UTC that caused this issue

Before:
before

After
after

We have create a PR fixing this and explaining the issue - #315

Please have a look and let us know

@aprzedecki
Copy link

Yes, this looks good 👌

@joymyr
Copy link

joymyr commented Oct 19, 2023

Edit: Didn't realize that the expiration was calculated when storing the token. The PR looks good then.

The PR doesn't solve the problem for me, when referencing the branch in my project.
I'm in Norway (UTC+2), and calling credentialsManager.credentials() less than two hours after expiration doesn't refresh the token. But as soon as my local time passes the UTC expiration time, the token refreshes upon calling credentialsManager.credentials().
The problem is that I have two hours with an invalid token, that's rejected by our APIs.

@poovamraj
Copy link
Contributor

@joymyr Can you check whether the issue you mention is in reference to the one mentioned here - auth0/Auth0.Android#695

@joymyr
Copy link

joymyr commented Oct 19, 2023

@poovamraj based on my tests, setting the timezone to New York, or any timezone behind UTC should only result in refreshing the token too early. So this seems odd.
Example (UTC+2):
Time returned from credentialsManager.credentials().expiresAt: 13:31:00 UTC
Time extracted from exp in the token: 11:30:58 UTC
The result is that the token is invalid for two hours before it's refreshed

@nt
Copy link

nt commented Oct 19, 2023

The library compares:

system time
API provided UTC time

@poovamraj
Copy link
Contributor

@joymyr I take that the issue is solved for you now from your edited comment here?

@joymyr
Copy link

joymyr commented Oct 23, 2023

@poovamraj Yes. Just waiting for the fix to be published, but I have tested it directly from Git

@Widcket
Copy link
Contributor

Widcket commented Oct 27, 2023

The fix is now out in v1.3.0.

@Widcket Widcket closed this as completed Oct 27, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
android needs investigation An issue that has more questions to answer or otherwise needs work to fully understand the issue
Projects
None yet
Development

No branches or pull requests

7 participants