Skip to content

Commit 9b9b05a

Browse files
authored
[ID-1099]getUser will get new token when existed token expired. (#913)
1 parent 3836508 commit 9b9b05a

File tree

8 files changed

+189
-21
lines changed

8 files changed

+189
-21
lines changed

packages/passport/sdk/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"@metamask/detect-provider": "^2.0.0",
2020
"axios": "^1.3.5",
2121
"ethers": "^5.7.2",
22+
"jwt-decode": "^3.1.2",
2223
"magic-sdk": "^13.3.1",
2324
"oidc-client-ts": "^2.2.1"
2425
},
@@ -28,6 +29,7 @@
2829
"@swc/jest": "^0.2.24",
2930
"@types/axios": "^0.14.0",
3031
"@types/jest": "^29.4.3",
32+
"@types/jwt-encode": "^1.0.1",
3133
"@types/node": "^18.14.2",
3234
"@types/react": "^18.0.28",
3335
"@types/react-dom": "^18.0.11",
@@ -37,6 +39,7 @@
3739
"eslint": "^8.40.0",
3840
"jest": "^29.4.3",
3941
"jest-environment-jsdom": "^29.4.3",
42+
"jwt-encode": "^1.0.1",
4043
"msw": "^1.2.2",
4144
"prettier": "^2.8.7",
4245
"rollup": "^3.17.2",

packages/passport/sdk/src/Passport.int.test.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Magic } from 'magic-sdk';
22
import { UserManager } from 'oidc-client-ts';
33
import { TransactionRequest } from '@ethersproject/providers';
44
import { Environment, ImmutableConfiguration } from '@imtbl/config';
5+
import { mockValidIdToken } from './token.test';
56
import { Passport } from './Passport';
67
import { RequestArguments } from './zkEvm/types';
78
import {
@@ -27,7 +28,7 @@ const mockOidcUser = {
2728
nickname: 'test',
2829
},
2930
expired: false,
30-
id_token: 'idToken123',
31+
id_token: mockValidIdToken,
3132
access_token: 'accessToken123',
3233
refresh_token: 'refreshToken123',
3334
};
@@ -134,8 +135,7 @@ describe('Passport', () => {
134135
it('registers the user and returns the ether key', async () => {
135136
mockSigninPopup.mockResolvedValue(mockOidcUser);
136137
mockGetUser.mockResolvedValueOnce(null);
137-
mockGetUser.mockResolvedValueOnce(mockOidcUser);
138-
mockSigninSilent.mockResolvedValue(mockOidcUserZkevm);
138+
mockGetUser.mockResolvedValueOnce(mockOidcUserZkevm);
139139
useMswHandlers([
140140
mswHandlers.counterfactualAddress.success,
141141
]);
@@ -148,7 +148,6 @@ describe('Passport', () => {
148148

149149
expect(accounts).toEqual([mockOidcUserZkevm.profile.passport.zkevm_eth_address]);
150150
expect(mockGetUser).toHaveBeenCalledTimes(2);
151-
expect(mockSigninSilent).toHaveBeenCalledTimes(1);
152151
expect(mockMagicRequest).toHaveBeenCalledTimes(3);
153152
});
154153

packages/passport/sdk/src/authManager.test.ts

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import AuthManager from './authManager';
44
import { PassportError, PassportErrorType } from './errors/passportError';
55
import { PassportConfiguration } from './config';
66
import { mockUser, mockUserImx, mockUserZkEvm } from './test/mocks';
7+
import { isTokenExpired } from './token';
78

89
jest.mock('oidc-client-ts');
10+
jest.mock('./token');
911

1012
const baseConfig = new ImmutableConfiguration({
1113
environment: Environment.SANDBOX,
@@ -18,10 +20,9 @@ const config = new PassportConfiguration({
1820
scope: 'email profile',
1921
});
2022

21-
const mockOidcUser: OidcUser = {
23+
const commonOidcUser: OidcUser = {
2224
id_token: mockUser.idToken,
2325
access_token: mockUser.accessToken,
24-
refresh_token: mockUser.refreshToken,
2526
token_type: 'Bearer',
2627
scope: 'openid',
2728
expires_in: 167222,
@@ -30,9 +31,25 @@ const mockOidcUser: OidcUser = {
3031
email: mockUser.profile.email,
3132
nickname: mockUser.profile.nickname,
3233
},
34+
} as OidcUser;
35+
36+
const mockOidcUser: OidcUser = {
37+
...commonOidcUser,
38+
refresh_token: mockUser.refreshToken,
3339
expired: false,
3440
} as OidcUser;
3541

42+
const mockOidcExpiredUser: OidcUser = {
43+
...commonOidcUser,
44+
refresh_token: mockUser.refreshToken,
45+
expired: true,
46+
} as OidcUser;
47+
48+
const mockOidcExpiredNoRefreshTokenUser: OidcUser = {
49+
...commonOidcUser,
50+
expired: true,
51+
} as OidcUser;
52+
3653
const imxProfileData = {
3754
imx_eth_address: mockUserImx.imx.ethAddress,
3855
imx_stark_address: mockUserImx.imx.starkAddress,
@@ -251,7 +268,8 @@ describe('AuthManager', () => {
251268
});
252269

253270
it('should return null if user is returned', async () => {
254-
getUserMock.mockReturnValue(mockOidcUser);
271+
getUserMock.mockReturnValue(mockOidcExpiredUser);
272+
(isTokenExpired as jest.Mock).mockReturnValue(true);
255273
signinSilentMock.mockResolvedValue(null);
256274

257275
const result = await authManager.loginSilent();
@@ -318,12 +336,44 @@ describe('AuthManager', () => {
318336
describe('getUser', () => {
319337
it('should retrieve the user from the userManager and return the domain model', async () => {
320338
getUserMock.mockReturnValue(mockOidcUser);
339+
(isTokenExpired as jest.Mock).mockReturnValue(false);
321340

322341
const result = await authManager.getUser();
323342

324343
expect(result).toEqual(mockUser);
325344
});
326345

346+
it('should call signinSilent and returns user when user token is expired with the refresh token', async () => {
347+
getUserMock.mockReturnValue(mockOidcExpiredUser);
348+
(isTokenExpired as jest.Mock).mockReturnValue(true);
349+
signinSilentMock.mockResolvedValue(mockOidcUser);
350+
351+
const result = await authManager.getUser();
352+
353+
expect(signinSilentMock).toBeCalledTimes(1);
354+
expect(result).toEqual(mockUser);
355+
});
356+
357+
it('should return null when the user token is expired without refresh token', async () => {
358+
getUserMock.mockReturnValue(mockOidcExpiredNoRefreshTokenUser);
359+
(isTokenExpired as jest.Mock).mockReturnValue(true);
360+
361+
const result = await authManager.getUser();
362+
363+
expect(signinSilentMock).toBeCalledTimes(0);
364+
expect(result).toEqual(null);
365+
});
366+
367+
it('should return null when the user token is expired with the refresh token, but signinSilent returns null', async () => {
368+
getUserMock.mockReturnValue(mockOidcExpiredUser);
369+
(isTokenExpired as jest.Mock).mockReturnValue(true);
370+
signinSilentMock.mockResolvedValue(null);
371+
const result = await authManager.getUser();
372+
373+
expect(signinSilentMock).toBeCalledTimes(1);
374+
expect(result).toEqual(null);
375+
});
376+
327377
it('should return null if no user is returned', async () => {
328378
getUserMock.mockReturnValue(null);
329379

packages/passport/sdk/src/authManager.ts

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import {
66
WebStorageStateStore,
77
} from 'oidc-client-ts';
88
import axios from 'axios';
9-
import jwt_decode from 'jwt-decode';
109
import DeviceCredentialsManager from 'storage/device_credentials_manager';
1110
import * as crypto from 'crypto';
11+
import jwt_decode from 'jwt-decode';
12+
import { isTokenExpired } from './token';
1213
import { PassportErrorType, withPassportError } from './errors/passportError';
1314
import {
1415
PassportMetadata,
@@ -369,24 +370,32 @@ export default class AuthManager {
369370
}
370371

371372
public async loginSilent(): Promise<User | null> {
372-
return withPassportError<User | null>(async () => {
373-
const existedUser = await this.getUser();
374-
if (!existedUser) {
375-
return null;
376-
}
377-
const oidcUser = await this.userManager.signinSilent();
378-
if (!oidcUser) {
379-
return null;
380-
}
373+
return withPassportError<User | null>(async () => this.getUser(), PassportErrorType.SILENT_LOGIN_ERROR);
374+
}
375+
376+
private async getWebUser() : Promise<User | null> {
377+
const oidcUser = await this.userManager.getUser();
378+
if (!oidcUser) {
379+
return null;
380+
}
381+
const tokenExpired = isTokenExpired(oidcUser);
382+
if (!tokenExpired) {
381383
return AuthManager.mapOidcUserToDomainModel(oidcUser);
382-
}, PassportErrorType.SILENT_LOGIN_ERROR);
384+
}
385+
if (oidcUser.refresh_token) {
386+
const newOidcUser = await this.userManager.signinSilent();
387+
if (newOidcUser) {
388+
return AuthManager.mapOidcUserToDomainModel(newOidcUser);
389+
}
390+
}
391+
return null;
383392
}
384393

385394
public async getUser(): Promise<User | null> {
386395
return withPassportError<User | null>(async () => {
387-
const oidcUser = await this.userManager.getUser();
388-
if (oidcUser) {
389-
return AuthManager.mapOidcUserToDomainModel(oidcUser);
396+
const user = await this.getWebUser();
397+
if (user) {
398+
return user;
390399
}
391400

392401
const deviceToken = this.deviceCredentialsManager.getCredentials();
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import encode from 'jwt-encode';
2+
import {
3+
User as OidcUser,
4+
} from 'oidc-client-ts';
5+
import { isIdTokenExpired, isTokenExpired } from './token';
6+
7+
const now = Math.floor(Date.now() / 1000);
8+
const oneHourLater = now + 3600;
9+
const oneHourBefore = now - 3600;
10+
11+
const mockExpiredIdToken = encode({
12+
iat: oneHourBefore,
13+
exp: oneHourBefore,
14+
}, 'secret');
15+
export const mockValidIdToken = encode({
16+
iat: now,
17+
exp: oneHourLater,
18+
}, 'secret');
19+
20+
describe('isIdTokenExpired', () => {
21+
it('should return false if idToken is undefined', () => {
22+
expect(isIdTokenExpired(undefined)).toBe(false);
23+
});
24+
25+
it('should return true if idToken is expired', () => {
26+
expect(isIdTokenExpired(mockExpiredIdToken)).toBe(true);
27+
});
28+
29+
it('should return false if idToken is not expired', () => {
30+
expect(isIdTokenExpired(mockValidIdToken)).toBe(false);
31+
});
32+
});
33+
34+
describe('isTokenExpired', () => {
35+
it('should return true if expired is true', () => {
36+
const user = {
37+
id_token: mockValidIdToken,
38+
expired: true,
39+
} as unknown as OidcUser;
40+
expect(isTokenExpired(user)).toBe(true);
41+
});
42+
43+
it('should return false if idToken is valid', () => {
44+
const user = {
45+
id_token: mockValidIdToken,
46+
expired: false,
47+
} as unknown as OidcUser;
48+
expect(isTokenExpired(user)).toBe(false);
49+
});
50+
51+
it('should return true idToken is expired', () => {
52+
const user = {
53+
id_token: mockExpiredIdToken,
54+
expired: false,
55+
} as unknown as OidcUser;
56+
expect(isTokenExpired(user)).toBe(true);
57+
});
58+
});

packages/passport/sdk/src/token.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { IdTokenPayload } from 'types';
2+
import jwt_decode from 'jwt-decode';
3+
import {
4+
User as OidcUser,
5+
} from 'oidc-client-ts';
6+
7+
export function isIdTokenExpired(idToken: string | undefined): boolean {
8+
if (!idToken) {
9+
return false;
10+
}
11+
const decodedToken: IdTokenPayload = jwt_decode(idToken);
12+
const now = Math.floor(Date.now() / 1000);
13+
return decodedToken.exp < now;
14+
}
15+
16+
export function isTokenExpired(oidcUser: OidcUser): boolean {
17+
const { id_token: idToken, expired } = oidcUser;
18+
if (expired) {
19+
return true;
20+
}
21+
return isIdTokenExpired(idToken);
22+
}

packages/passport/sdk/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export type IdTokenPayload = {
119119
nickname: string;
120120
aud: string;
121121
sub: string;
122+
exp: number;
122123
};
123124

124125
export type DeviceErrorResponse = {

yarn.lock

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3644,6 +3644,7 @@ __metadata:
36443644
"@swc/jest": ^0.2.24
36453645
"@types/axios": ^0.14.0
36463646
"@types/jest": ^29.4.3
3647+
"@types/jwt-encode": ^1.0.1
36473648
"@types/node": ^18.14.2
36483649
"@types/react": ^18.0.28
36493650
"@types/react-dom": ^18.0.11
@@ -3655,6 +3656,8 @@ __metadata:
36553656
ethers: ^5.7.2
36563657
jest: ^29.4.3
36573658
jest-environment-jsdom: ^29.4.3
3659+
jwt-decode: ^3.1.2
3660+
jwt-encode: ^1.0.1
36583661
magic-sdk: ^13.3.1
36593662
msw: ^1.2.2
36603663
oidc-client-ts: ^2.2.1
@@ -9780,6 +9783,13 @@ __metadata:
97809783
languageName: node
97819784
linkType: hard
97829785

9786+
"@types/jwt-encode@npm:^1.0.1":
9787+
version: 1.0.1
9788+
resolution: "@types/jwt-encode@npm:1.0.1"
9789+
checksum: 63bdfe9ebe0fab5a3635a4219d9e7667b15fbd48534fa07a1d3c9036c30f06ce8c7bcae1d4b17e293186b1c281d2ff4ef943877f5a92fcc0a5cd0d9b6823c5a1
9790+
languageName: node
9791+
linkType: hard
9792+
97839793
"@types/keyv@npm:^3.1.4":
97849794
version: 3.1.4
97859795
resolution: "@types/keyv@npm:3.1.4"
@@ -21026,6 +21036,15 @@ __metadata:
2102621036
languageName: node
2102721037
linkType: hard
2102821038

21039+
"jwt-encode@npm:^1.0.1":
21040+
version: 1.0.1
21041+
resolution: "jwt-encode@npm:1.0.1"
21042+
dependencies:
21043+
ts.cryptojs256: ^1.0.1
21044+
checksum: 440e0e80a45fcad5e2e2542e632bbf7692d50a2574ca895e0961e8a6c08dc91b537b2e7bcd1bbd1c9a01ca937287d6078dd1e04dcc63832facbe8abb9f6291e6
21045+
languageName: node
21046+
linkType: hard
21047+
2102921048
"keccak@npm:^3.0.0, keccak@npm:^3.0.2":
2103021049
version: 3.0.3
2103121050
resolution: "keccak@npm:3.0.3"
@@ -28949,6 +28968,13 @@ __metadata:
2894928968
languageName: node
2895028969
linkType: hard
2895128970

28971+
"ts.cryptojs256@npm:^1.0.1":
28972+
version: 1.0.1
28973+
resolution: "ts.cryptojs256@npm:1.0.1"
28974+
checksum: 94f5a3d7e779e7cb533226731bb0d3cf6ed0a3f8e4032e44e80f9a2de68c57c86db1a39e69c412a4012c769186232b586d554fd863bbebd6d431d606d8980c72
28975+
languageName: node
28976+
linkType: hard
28977+
2895228978
"tsc-watch@npm:^6.0.0":
2895328979
version: 6.0.4
2895428980
resolution: "tsc-watch@npm:6.0.4"

0 commit comments

Comments
 (0)