diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 493e66cf..f8c9669d 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -13,7 +13,8 @@ - The properties inside the `user` object will now be camelCase instead of snake_case - Removed the `type` property returned in the `Credentials` object in Android. Use `tokenType` instead. -- `Credentials` object in Android will return `expiresIn` instead of `expiresAt` +- `Credentials` object in iOS will return `expiresAt` instead of `expiresIn` +- `expiresIn` value will now return `expiresAt` value which is a UNIX timestamp of the expiration time. - `max_age` parameter is changed to `maxAge` in `WebAuth.authorize()` - `skipLegacyListener` has been removed in `authorize` and `clearSession` - `customScheme` is now part of `ClearSessionOptions` instead of `ClearSessionParameters` in `clearSession` diff --git a/android/src/main/java/com/auth0/react/CredentialsParser.java b/android/src/main/java/com/auth0/react/CredentialsParser.java index 2907675b..fdcfb497 100644 --- a/android/src/main/java/com/auth0/react/CredentialsParser.java +++ b/android/src/main/java/com/auth0/react/CredentialsParser.java @@ -18,13 +18,12 @@ public class CredentialsParser { private static final String SCOPE = "scope"; private static final String REFRESH_TOKEN_KEY = "refreshToken"; private static final String TOKEN_TYPE_KEY = "tokenType"; - private static final String EXPIRES_IN_KEY = "expiresIn"; private static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; public static ReadableMap toMap(Credentials credentials) { WritableNativeMap map = new WritableNativeMap(); map.putString(ACCESS_TOKEN_KEY, credentials.getAccessToken()); - map.putDouble(EXPIRES_IN_KEY, credentials.getExpiresAt().getTime()); + map.putDouble(EXPIRES_AT_KEY, credentials.getExpiresAt().getTime() / 1000); map.putString(ID_TOKEN_KEY, credentials.getIdToken()); map.putString(SCOPE, credentials.getScope()); map.putString(REFRESH_TOKEN_KEY, credentials.getRefreshToken()); @@ -38,23 +37,8 @@ public static Credentials fromMap(ReadableMap map) { String tokenType = map.getString(TOKEN_TYPE_KEY); String refreshToken = map.getString(REFRESH_TOKEN_KEY); String scope = map.getString(SCOPE); - SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT, Locale.US); - Date expiresAt = null; - String expiresAtText = map.getString(EXPIRES_AT_KEY); - if(expiresAtText != null) { - try { - expiresAt = sdf.parse(expiresAtText); - } catch (ParseException e) { - throw new CredentialsManagerException("Invalid date format - "+expiresAtText, e); - } - } - double expiresIn = 0; - if(map.hasKey(EXPIRES_IN_KEY)) { - expiresIn = map.getDouble(EXPIRES_IN_KEY); - } - if (expiresAt == null && expiresIn != 0) { - expiresAt = new Date((long) (System.currentTimeMillis() + expiresIn * 1000)); - } + Double expiresAtUnix = map.getDouble(EXPIRES_AT_KEY); + Date expiresAt = new Date(expiresAtUnix.longValue() * 1000); return new Credentials( idToken, accessToken, diff --git a/ios/NativeBridge.swift b/ios/NativeBridge.swift index da1cc7c2..7da3c7b0 100644 --- a/ios/NativeBridge.swift +++ b/ios/NativeBridge.swift @@ -21,7 +21,6 @@ public class NativeBridge: NSObject { static let refreshTokenKey = "refreshToken"; static let typeKey = "type"; static let tokenTypeKey = "tokenType"; - static let expiresInKey = "expiresIn"; static let dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; static let credentialsManagerErrorCode = "a0.invalid_state.credential_manager_exception" @@ -108,11 +107,11 @@ public class NativeBridge: NSObject { let refreshToken = credentialsDict[NativeBridge.refreshTokenKey] as? String let scope = credentialsDict[NativeBridge.scopeKey] as? String var expiresIn: Date? - if let string = credentialsDict[NativeBridge.expiresInKey] as? String, let double = Double(string) { + if let string = credentialsDict[NativeBridge.expiresAtKey] as? String, let double = Double(string) { expiresIn = Date(timeIntervalSince1970: double) - } else if let double = credentialsDict[NativeBridge.expiresInKey] as? Double { + } else if let double = credentialsDict[NativeBridge.expiresAtKey] as? Double { expiresIn = Date(timeIntervalSince1970: double) - } else if let dateStr = credentialsDict[NativeBridge.expiresInKey] as? String { + } else if let dateStr = credentialsDict[NativeBridge.expiresAtKey] as? String { let dateFormatter = DateFormatter() dateFormatter.dateFormat = NativeBridge.dateFormat expiresIn = dateFormatter.date(from: dateStr) @@ -185,7 +184,7 @@ extension Credentials { NativeBridge.tokenTypeKey: self.tokenType, NativeBridge.idTokenKey: self.idToken, NativeBridge.refreshTokenKey: self.refreshToken as Any, - NativeBridge.expiresInKey: floor(self.expiresIn.timeIntervalSince1970), + NativeBridge.expiresAtKey: floor(self.expiresIn.timeIntervalSince1970), NativeBridge.scopeKey: self.scope as Any ] } diff --git a/src/auth/__tests__/__snapshots__/index.spec.js.snap b/src/auth/__tests__/__snapshots__/index.spec.js.snap index 29b8bb40..82e7d575 100644 --- a/src/auth/__tests__/__snapshots__/index.spec.js.snap +++ b/src/auth/__tests__/__snapshots__/index.spec.js.snap @@ -26,7 +26,15 @@ exports[`auth Multifactor Challenge Flow should require MFA Token 1`] = ` ]" `; -exports[`auth OOB flow binding code should be optional 1`] = `{}`; +exports[`auth OOB flow binding code should be optional 1`] = ` +{ + "accessToken": "1234", + "expiresAt": 1672617600, + "idToken": "id-123", + "scope": "openid profile email address phone", + "tokenType": "Bearer", +} +`; exports[`auth OOB flow should handle custom parameter 1`] = ` [ @@ -57,7 +65,7 @@ exports[`auth OOB flow should handle malformed OOB code 1`] = `[invalid_grant: M exports[`auth OOB flow should handle success with binding code 1`] = ` { "accessToken": "1234", - "expiresIn": 86400, + "expiresAt": 1672617600, "idToken": "id-123", "scope": "openid profile email address phone", "tokenType": "Bearer", @@ -67,7 +75,7 @@ exports[`auth OOB flow should handle success with binding code 1`] = ` exports[`auth OOB flow should handle success without binding code 1`] = ` { "accessToken": "1234", - "expiresIn": 86400, + "expiresAt": 1672617600, "idToken": "id-123", "scope": "openid profile email address phone", "tokenType": "Bearer", @@ -121,7 +129,7 @@ exports[`auth OTP flow when MFA is not associated 1`] = `[unsupported_challenge_ exports[`auth OTP flow when MFA succeeds 1`] = ` { "accessToken": "1234", - "expiresIn": 86400, + "expiresAt": 1672617600, "idToken": "id-123", "scope": "openid profile email address phone", "tokenType": "Bearer", @@ -166,7 +174,7 @@ exports[`auth Recovery Code flow should require MFA Token and Recovery Code 1`] exports[`auth Recovery Code flow when Recovery code succeeds 1`] = ` { "accessToken": "1234", - "expiresIn": 86400, + "expiresAt": 1672617600, "idToken": "id-123", "scope": "openid profile email address phone", "tokenType": "Bearer", @@ -216,7 +224,7 @@ exports[`auth code exchange for native social should handle unexpected error 1`] exports[`auth code exchange for native social should return successful response 1`] = ` { "accessToken": "an access token", - "expiresIn": 1234567890, + "expiresAt": 2907099090, "idToken": "an id token", "scope": "openid", "state": "a random state for auth", @@ -226,7 +234,7 @@ exports[`auth code exchange for native social should return successful response exports[`auth code exchange for native social should return successful response with optional parameters 1`] = ` { "accessToken": "an access token", - "expiresIn": 1234567890, + "expiresAt": 2907099090, "idToken": "an id token", "scope": "openid", "state": "a random state for auth", @@ -288,7 +296,7 @@ exports[`auth code exchange should handle unexpected error 1`] = `[a0.response.i exports[`auth code exchange should return successful response 1`] = ` { "accessToken": "an access token", - "expiresIn": 1234567890, + "expiresAt": 2907099090, "idToken": "an id token", "scope": "openid", "state": "a random state for auth", @@ -419,7 +427,7 @@ exports[`auth password realm should handle unexpected error 1`] = `[a0.response. exports[`auth password realm should return successful response 1`] = ` { "accessToken": "an access token", - "expiresIn": 1234567890, + "expiresAt": 2907099090, "idToken": "an id token", "scope": "openid", "state": "a random state for auth", @@ -821,7 +829,7 @@ exports[`auth refresh token should handle unknown error 1`] = `[a0.response.inva exports[`auth refresh token should return successful response 1`] = ` { "accessToken": "an access token", - "expiresIn": 1234567890, + "expiresAt": 2907099090, "idToken": "an id token", "scope": "openid", "state": "a random state for auth", diff --git a/src/auth/__tests__/index.spec.js b/src/auth/__tests__/index.spec.js index 39ba64e2..8a4c82bf 100644 --- a/src/auth/__tests__/index.spec.js +++ b/src/auth/__tests__/index.spec.js @@ -38,6 +38,12 @@ describe('auth', () => { }; const auth = new Auth({baseUrl, clientId, telemetry}); + beforeAll(() => { + jest + .useFakeTimers() + .setSystemTime(new Date('2023-01-01')); + }); + beforeEach(fetchMock.restore); describe('constructor', () => { @@ -875,7 +881,7 @@ describe('auth', () => { }); it('binding code should be optional', async () => { - fetchMock.postOnce('https://samples.auth0.com/oauth/token', {}); + fetchMock.postOnce('https://samples.auth0.com/oauth/token', success); expect.assertions(1); await expect(auth.loginWithOOB(parameters)).resolves.toMatchSnapshot(); }); diff --git a/src/auth/index.ts b/src/auth/index.ts index 5792974d..f156b346 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -4,6 +4,7 @@ import { toCamelCase } from '../utils/camel'; import AuthError from './authError'; import Auth0Error from './auth0Error'; import { Telemetry } from '../networking/telemetry'; +import { convertExpiresInToExpiresAt } from '../utils/timestampConversion' import { AuthorizeUrlOptions, CreateUserOptions, @@ -28,6 +29,7 @@ import { UserInfoOptions, } from '../types'; import { + CredentialsResponse, RawCredentials, RawMultifactorChallengeResponse, RawUser, @@ -43,6 +45,15 @@ function responseHandler( throw new AuthError(response); } +function convertTimestampInCredentials(rawCredentials: RawCredentials): Credentials { + let expiresAt = convertExpiresInToExpiresAt(rawCredentials.expiresIn) + if(!expiresAt) { + throw Error('invalid expiry value found') + } + const { expiresIn, ...credentials } = rawCredentials + return {...credentials, expiresAt} +} + /** * Class for interfacing with the Auth0 Authentication API endpoints. * @@ -144,13 +155,13 @@ class Auth { parameters ); return this.client - .post('/oauth/token', { + .post('/oauth/token', { ...payload, client_id: this.clientId, grant_type: 'authorization_code', }) .then((response) => - responseHandler(response) + convertTimestampInCredentials(responseHandler(response)) ); } @@ -176,14 +187,14 @@ class Auth { parameters ); return this.client - .post('/oauth/token', { + .post('/oauth/token', { ...payload, client_id: this.clientId, grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', }) - .then((response) => - responseHandler(response) - ); + .then((response) => { + return convertTimestampInCredentials(responseHandler(response)) + }); } /** @@ -194,13 +205,13 @@ class Auth { */ passwordRealm(parameters: PasswordRealmOptions): Promise { return this.client - .post('/oauth/token', { + .post('/oauth/token', { ...parameters, client_id: this.clientId, grant_type: 'http://auth0.com/oauth/grant-type/password-realm', }) .then((response) => - responseHandler(response) + convertTimestampInCredentials(responseHandler(response)) ); } @@ -222,13 +233,13 @@ class Auth { parameters ); return this.client - .post('/oauth/token', { + .post('/oauth/token', { ...payload, client_id: this.clientId, grant_type: 'refresh_token', }) .then((response) => - responseHandler(response) + convertTimestampInCredentials(responseHandler(response)) ); } @@ -294,14 +305,14 @@ class Auth { parameters ); return this.client - .post('/oauth/token', { + .post('/oauth/token', { ...payload, client_id: this.clientId, realm: 'email', grant_type: 'http://auth0.com/oauth/grant-type/passwordless/otp', }) .then((response) => - responseHandler(response) + convertTimestampInCredentials(responseHandler(response)) ); } @@ -324,14 +335,14 @@ class Auth { parameters ); return this.client - .post('/oauth/token', { + .post('/oauth/token', { ...payload, client_id: this.clientId, realm: 'sms', grant_type: 'http://auth0.com/oauth/grant-type/passwordless/otp', }) .then((response) => - responseHandler(response) + convertTimestampInCredentials(responseHandler(response)) ); } @@ -356,13 +367,13 @@ class Auth { parameters ); return this.client - .post('/oauth/token', { + .post('/oauth/token', { ...payload, client_id: this.clientId, grant_type: 'http://auth0.com/oauth/grant-type/mfa-otp', }) .then((response) => - responseHandler(response) + convertTimestampInCredentials(responseHandler(response)) ); } @@ -389,13 +400,13 @@ class Auth { ); return this.client - .post('/oauth/token', { + .post('/oauth/token', { ...payload, client_id: this.clientId, grant_type: 'http://auth0.com/oauth/grant-type/mfa-oob', }) .then((response) => - responseHandler(response) + convertTimestampInCredentials(responseHandler(response)) ); } @@ -422,13 +433,13 @@ class Auth { ); return this.client - .post('/oauth/token', { + .post('/oauth/token', { ...payload, client_id: this.clientId, grant_type: 'http://auth0.com/oauth/grant-type/mfa-recovery-code', }) .then((response) => - responseHandler(response) + convertTimestampInCredentials(responseHandler(response)) ); } diff --git a/src/credentials-manager/__tests__/credentials-manager.spec.js b/src/credentials-manager/__tests__/credentials-manager.spec.js index c44626fb..1ccd0377 100644 --- a/src/credentials-manager/__tests__/credentials-manager.spec.js +++ b/src/credentials-manager/__tests__/credentials-manager.spec.js @@ -19,7 +19,7 @@ describe('credentials manager tests', () => { idToken: '1234', accessToken: '1234', tokenType: 'Bearer', - expiresIn: 86000, + expiresAt: 1691603391, }; describe('test saving credentials', () => { @@ -49,7 +49,7 @@ describe('credentials manager tests', () => { it('throws when expiresIn type is empty', async () => { const testToken = Object.assign({}, validToken); - testToken.expiresIn = undefined; + testToken.expiresAt = undefined; await expect( credentialsManager.saveCredentials(testToken) ).rejects.toThrow(); @@ -57,7 +57,7 @@ describe('credentials manager tests', () => { it('throws when expiresIn type is zero', async () => { const testToken = Object.assign({}, validToken); - testToken.expiresIn = 0; + testToken.expiresAt = 0; await expect( credentialsManager.saveCredentials(testToken) ).rejects.toThrow(); diff --git a/src/credentials-manager/index.ts b/src/credentials-manager/index.ts index f9ea0f95..6593c194 100644 --- a/src/credentials-manager/index.ts +++ b/src/credentials-manager/index.ts @@ -23,7 +23,7 @@ class CredentialsManager { * Saves the provided credentials */ async saveCredentials(credentials: Credentials): Promise { - const validateKeys = ['idToken', 'accessToken', 'tokenType', 'expiresIn']; + const validateKeys = ['idToken', 'accessToken', 'tokenType', 'expiresAt']; validateKeys.forEach((key) => { if (!credentials[key]) { const json = { diff --git a/src/internal-types.ts b/src/internal-types.ts index a42c5e5e..e8c02a99 100644 --- a/src/internal-types.ts +++ b/src/internal-types.ts @@ -2,7 +2,7 @@ import { JwtPayload } from 'jwt-decode'; import LocalAuthenticationStrategy from './credentials-manager/localAuthenticationStrategy'; import { Credentials } from './types'; -export type RawCredentials = { +export type CredentialsResponse = { id_token: string; access_token: string; token_type: string; @@ -12,6 +12,16 @@ export type RawCredentials = { [key: string]: any; }; +export type RawCredentials = { + idToken: string; + accessToken: string; + tokenType: string; + expiresIn: number; + refreshToken?: string; + scope?: string; + [key: string]: any; +}; + export type RawUser = { name?: string; given_name?: string; diff --git a/src/types.ts b/src/types.ts index 447f731f..2bbdb503 100644 --- a/src/types.ts +++ b/src/types.ts @@ -12,9 +12,9 @@ export type Credentials = { */ tokenType: string; /** - * Used to denote when the token will expire from the issued time + * Used to denote when the token will expire, as a UNIX timestamp */ - expiresIn: number; + expiresAt: number; /** * The token used to refresh the access token */ diff --git a/src/utils/__tests__/__snapshots__/timestampConversion.spec.js.snap b/src/utils/__tests__/__snapshots__/timestampConversion.spec.js.snap new file mode 100644 index 00000000..4a14d0b8 --- /dev/null +++ b/src/utils/__tests__/__snapshots__/timestampConversion.spec.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`timestamp conversion test convertExpiresInToExpiresAt should handle null 1`] = `null`; + +exports[`timestamp conversion test convertExpiresInToExpiresAt should handle undefined 1`] = `null`; + +exports[`timestamp conversion test convertExpiresInToExpiresAt should handle zero 1`] = `1672531200`; + +exports[`timestamp conversion test convertExpiresInToExpiresAt should successfully convert 1`] = `1672617600`; diff --git a/src/utils/__tests__/timestampConversion.spec.js b/src/utils/__tests__/timestampConversion.spec.js new file mode 100644 index 00000000..ce3b5148 --- /dev/null +++ b/src/utils/__tests__/timestampConversion.spec.js @@ -0,0 +1,35 @@ +import { convertExpiresInToExpiresAt, convertUnixTimestampToDate } from '../timestampConversion' + +describe('timestamp conversion', () => { + beforeAll(() => { + jest + .useFakeTimers() + .setSystemTime(new Date('2023-01-01')); + }); + + describe('test convertExpiresInToExpiresAt', () => { + it('should successfully convert', () => { + let expiresIn = 86400 + const result = convertExpiresInToExpiresAt(expiresIn) + expect(result).toMatchSnapshot(); + }); + + it('should handle zero', () => { + let expiresIn = 0 + const result = convertExpiresInToExpiresAt(expiresIn) + expect(result).toMatchSnapshot(); + }); + + it('should handle null', () => { + let expiresIn = null + const result = convertExpiresInToExpiresAt(expiresIn) + expect(result).toMatchSnapshot(); + }); + + it('should handle undefined', () => { + let expiresIn = undefined + const result = convertExpiresInToExpiresAt(expiresIn) + expect(result).toMatchSnapshot(); + }); + }) +}); diff --git a/src/utils/timestampConversion.ts b/src/utils/timestampConversion.ts new file mode 100644 index 00000000..678c5391 --- /dev/null +++ b/src/utils/timestampConversion.ts @@ -0,0 +1,6 @@ +export function convertExpiresInToExpiresAt(expiresIn: number): number | null { + if(expiresIn === null || expiresIn === undefined) { + return null + } + return Math.floor(Date.now() / 1000 + expiresIn) +} \ No newline at end of file