auth0 / auth0-flutter

Auth0 SDK for Flutter
https://pub.dev/documentation/auth0_flutter/latest/
Apache License 2.0
59 stars 39 forks source link

[Android] expiration date mismatch #286

Closed o-artebiakin closed 10 months ago

o-artebiakin commented 1 year ago

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 commented 1 year ago

Hi @o-artebiakin, thanks for raising this.

@poovamraj could you please take a look?

poovamraj commented 1 year ago

@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 commented 1 year ago

@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 commented 1 year ago

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
poovamraj commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

@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 commented 1 year ago

@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 commented 1 year ago

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

marcellplentific commented 1 year ago

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

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 commented 11 months ago

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

aprzedecki commented 11 months ago

It looks like this PR introduced a bug: https://github.com/auth0/auth0-flutter/pull/162 On iOS this is not occuring. Any comments pls @Widcket

Widcket commented 11 months ago

@poovamraj could you please take a look?

poovamraj commented 11 months ago

@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 - https://github.com/auth0/auth0-flutter/pull/315

Please have a look and let us know

aprzedecki commented 11 months ago

Yes, this looks good 👌

joymyr commented 10 months ago

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 commented 10 months ago

@joymyr Can you check whether the issue you mention is in reference to the one mentioned here - https://github.com/auth0/Auth0.Android/issues/695

joymyr commented 10 months ago

@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 commented 10 months ago

The library compares:

system time API provided UTC time

poovamraj commented 10 months ago

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

joymyr commented 10 months ago

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

Widcket commented 10 months ago

The fix is now out in v1.3.0.