Closed o-artebiakin closed 10 months ago
Hi @o-artebiakin, thanks for raising this.
@poovamraj could you please take a look?
@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.
@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
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
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?
@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.
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
@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.
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
@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.
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.
@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?
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 theidToken
anywhere. And yes, we cannot reproduce it on iOS.
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.
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.
@o-artebiakin The credential manager checks the expiresAt
value returned in the credentials. You can use the hasValidCredentials
method (Docs link) to check this.
@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.
@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.
We will close this issue now. Let us know if you have more doubts and we can reopen it
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?
@poovamraj any updates on that one ? On Android those methods are working incorrect
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
@poovamraj could you please take a look?
@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:
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
Yes, this looks good 👌
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.
@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
@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
The library compares:
@joymyr I take that the issue is solved for you now from your edited comment here?
@poovamraj Yes. Just waiting for the fix to be published, but I have tested it directly from Git
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.
Reproduction
Auth0().credentialsManager.credentials()
.expiresAt
field with the expiration date parsed from theidToken
.Additional context
No response
auth0_flutter version
1.2.1
Flutter version
3.10.4
Platform
Android
Platform version(s)
No response