Skip to content

Commit 2f92c99

Browse files
committed
fix: fixed invalid revoke token in refreshAuthToken method
1 parent 9851013 commit 2f92c99

File tree

4 files changed

+214
-75
lines changed

4 files changed

+214
-75
lines changed

packages/seedless-onboarding-controller/src/SeedlessOnboardingController.test.ts

Lines changed: 158 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,7 @@ async function createMockVault(
500500
revokeToken: mockRevokeToken,
501501
accessToken: mockAccessToken,
502502
});
503+
console.log('serializedKeyData', serializedKeyData);
503504

504505
const { vault: encryptedMockVault, exportedKeyString } =
505506
await encryptor.encryptWithDetail(MOCK_PASSWORD, serializedKeyData);
@@ -1288,6 +1289,7 @@ describe('SeedlessOnboardingController', () => {
12881289
pwEncKey,
12891290
authKeyPair,
12901291
MOCK_PASSWORD,
1292+
controller.state.revokeToken,
12911293
);
12921294

12931295
const expectedVaultValue = await encryptor.decrypt(
@@ -1299,7 +1301,7 @@ describe('SeedlessOnboardingController', () => {
12991301
controller.state.vault as string,
13001302
);
13011303

1302-
expect(expectedVaultValue).toStrictEqual(resultedVaultValue);
1304+
expect(resultedVaultValue).toStrictEqual(expectedVaultValue);
13031305

13041306
// should be able to get the hash of the seed phrase backup from the state
13051307
expect(
@@ -1377,6 +1379,36 @@ describe('SeedlessOnboardingController', () => {
13771379
);
13781380
});
13791381

1382+
it('should throw error if revokeToken is missing when creating new vault', async () => {
1383+
await withController(
1384+
{
1385+
state: getMockInitialControllerState({
1386+
withMockAuthenticatedUser: true,
1387+
withMockAuthPubKey: true,
1388+
withoutMockRevokeToken: true,
1389+
}),
1390+
},
1391+
async ({ controller, toprfClient }) => {
1392+
mockcreateLocalKey(toprfClient, MOCK_PASSWORD);
1393+
1394+
// persist the local enc key
1395+
jest.spyOn(toprfClient, 'persistLocalKey').mockResolvedValueOnce();
1396+
1397+
// encrypt and store the secret data
1398+
handleMockSecretDataAdd();
1399+
await expect(
1400+
controller.createToprfKeyAndBackupSeedPhrase(
1401+
MOCK_PASSWORD,
1402+
MOCK_SEED_PHRASE,
1403+
MOCK_KEYRING_ID,
1404+
),
1405+
).rejects.toThrow(
1406+
SeedlessOnboardingControllerErrorMessage.InvalidRevokeToken,
1407+
);
1408+
},
1409+
);
1410+
});
1411+
13801412
it('should throw an error if create encryption key fails', async () => {
13811413
await withController(
13821414
{
@@ -2084,27 +2116,6 @@ describe('SeedlessOnboardingController', () => {
20842116
);
20852117
});
20862118

2087-
it('should throw an error if the user is not authenticated', async () => {
2088-
await withController(
2089-
{
2090-
state: {
2091-
userId,
2092-
authConnectionId,
2093-
groupedAuthConnectionId,
2094-
metadataAccessToken,
2095-
refreshToken,
2096-
revokeToken,
2097-
nodeAuthTokens: MOCK_NODE_AUTH_TOKENS,
2098-
},
2099-
},
2100-
async ({ controller }) => {
2101-
await expect(controller.fetchAllSecretData()).rejects.toThrow(
2102-
SeedlessOnboardingControllerErrorMessage.InvalidAccessToken,
2103-
);
2104-
},
2105-
);
2106-
});
2107-
21082119
it('should be able to fetch seed phrases with cached encryption key without providing password', async () => {
21092120
const mockToprfEncryptor = createMockToprfEncryptor();
21102121

@@ -5331,6 +5342,24 @@ describe('SeedlessOnboardingController', () => {
53315342
});
53325343
});
53335344

5345+
it('should return true if access token is missing', async () => {
5346+
await withController(
5347+
{
5348+
state: getMockInitialControllerState({
5349+
withMockAuthenticatedUser: true,
5350+
withoutMockAccessToken: true,
5351+
}),
5352+
},
5353+
async ({ controller }) => {
5354+
// Restore the original implementation to test the real logic
5355+
jest.spyOn(controller, 'checkAccessTokenExpired').mockRestore();
5356+
5357+
const result = controller.checkAccessTokenExpired();
5358+
expect(result).toBe(true);
5359+
},
5360+
);
5361+
});
5362+
53345363
it('should return true if token has invalid format', async () => {
53355364
await withController(
53365365
{
@@ -5350,6 +5379,113 @@ describe('SeedlessOnboardingController', () => {
53505379
});
53515380
});
53525381

5382+
describe('#getAccessToken', () => {
5383+
const MOCK_PASSWORD = 'mock-password';
5384+
5385+
it('should retrieve the access token from the vault if it is not available in the state', async () => {
5386+
const mockToprfEncryptor = createMockToprfEncryptor();
5387+
const MOCK_ENCRYPTION_KEY =
5388+
mockToprfEncryptor.deriveEncKey(MOCK_PASSWORD);
5389+
const MOCK_PASSWORD_ENCRYPTION_KEY =
5390+
mockToprfEncryptor.derivePwEncKey(MOCK_PASSWORD);
5391+
const MOCK_AUTH_KEY_PAIR =
5392+
mockToprfEncryptor.deriveAuthKeyPair(MOCK_PASSWORD);
5393+
5394+
const mockResult = await createMockVault(
5395+
MOCK_ENCRYPTION_KEY,
5396+
MOCK_PASSWORD_ENCRYPTION_KEY,
5397+
MOCK_AUTH_KEY_PAIR,
5398+
MOCK_PASSWORD,
5399+
);
5400+
5401+
const MOCK_VAULT = mockResult.encryptedMockVault;
5402+
const MOCK_VAULT_ENCRYPTION_KEY = mockResult.vaultEncryptionKey;
5403+
const MOCK_VAULT_ENCRYPTION_SALT = mockResult.vaultEncryptionSalt;
5404+
5405+
await withController(
5406+
{
5407+
state: getMockInitialControllerState({
5408+
withMockAuthenticatedUser: true,
5409+
withoutMockAccessToken: true,
5410+
vault: MOCK_VAULT,
5411+
vaultEncryptionKey: MOCK_VAULT_ENCRYPTION_KEY,
5412+
vaultEncryptionSalt: MOCK_VAULT_ENCRYPTION_SALT,
5413+
}),
5414+
},
5415+
async ({ controller, toprfClient }) => {
5416+
// fetch and decrypt the secret data
5417+
mockRecoverEncKey(toprfClient, MOCK_PASSWORD);
5418+
5419+
// mock the secret data get
5420+
jest
5421+
.spyOn(toprfClient, 'fetchAllSecretDataItems')
5422+
.mockResolvedValueOnce([
5423+
stringToBytes(
5424+
JSON.stringify({
5425+
data: bytesToBase64(MOCK_SEED_PHRASE),
5426+
timestamp: 1234567890,
5427+
type: SecretType.Mnemonic,
5428+
version: 'v1',
5429+
}),
5430+
),
5431+
stringToBytes(
5432+
JSON.stringify({
5433+
data: bytesToBase64(MOCK_PRIVATE_KEY),
5434+
timestamp: 1234567890,
5435+
type: SecretType.PrivateKey,
5436+
version: 'v1',
5437+
}),
5438+
),
5439+
]);
5440+
5441+
const secretData = await controller.fetchAllSecretData(MOCK_PASSWORD);
5442+
expect(secretData).toBeDefined();
5443+
expect(secretData).toHaveLength(2);
5444+
expect(secretData[0].type).toStrictEqual(SecretType.Mnemonic);
5445+
expect(secretData[0].data).toStrictEqual(MOCK_SEED_PHRASE);
5446+
expect(secretData[1].type).toStrictEqual(SecretType.PrivateKey);
5447+
expect(secretData[1].data).toStrictEqual(MOCK_PRIVATE_KEY);
5448+
5449+
// expect(mockSecretDataGet.isDone()).toBe(true);
5450+
},
5451+
);
5452+
});
5453+
5454+
it('should throw error if access token is not available either in the state or the vault', async () => {
5455+
await withController(
5456+
{
5457+
state: getMockInitialControllerState({
5458+
withMockAuthenticatedUser: true,
5459+
withoutMockAccessToken: true,
5460+
}),
5461+
},
5462+
async ({ controller, toprfClient }) => {
5463+
// fetch and decrypt the secret data
5464+
mockRecoverEncKey(toprfClient, MOCK_PASSWORD);
5465+
// mock the incorrect data shape
5466+
jest
5467+
.spyOn(toprfClient, 'fetchAllSecretDataItems')
5468+
.mockResolvedValueOnce([
5469+
stringToBytes(
5470+
JSON.stringify({
5471+
data: 'value',
5472+
timestamp: 1234567890,
5473+
type: 'mnemonic',
5474+
version: 'v1',
5475+
}),
5476+
),
5477+
]);
5478+
5479+
await expect(
5480+
controller.fetchAllSecretData(MOCK_PASSWORD),
5481+
).rejects.toThrow(
5482+
SeedlessOnboardingControllerErrorMessage.InvalidAccessToken,
5483+
);
5484+
},
5485+
);
5486+
});
5487+
});
5488+
53535489
describe('renewRefreshToken', () => {
53545490
const MOCK_PASSWORD = 'mock-password';
53555491
const MOCK_REVOKE_TOKEN = 'newRevokeToken';

packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import { secp256k1 } from '@noble/curves/secp256k1';
2222
import { Mutex } from 'async-mutex';
2323

2424
import {
25-
assertIsAuthUserInfoValid,
2625
assertIsPasswordOutdatedCacheValid,
2726
assertIsSeedlessOnboardingUserAuthenticated,
2827
assertIsValidVaultData,
@@ -365,7 +364,7 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
365364
groupedAuthConnectionId?: string;
366365
socialLoginEmail?: string;
367366
refreshToken: string;
368-
revokeToken: string;
367+
revokeToken?: string;
369368
skipLock?: boolean;
370369
}) {
371370
const doAuthenticateWithNodes = async () => {
@@ -399,8 +398,10 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
399398
state.socialLoginEmail = socialLoginEmail;
400399
state.metadataAccessToken = metadataAccessToken;
401400
state.refreshToken = refreshToken;
402-
// Temporarily store revoke token & access token in state for later vault creation
403-
state.revokeToken = revokeToken;
401+
if (revokeToken) {
402+
// Temporarily store revoke token & access token in state for later vault creation
403+
state.revokeToken = revokeToken;
404+
}
404405
state.accessToken = accessToken;
405406

406407
// we will check if the controller state is properly set with the authenticated user info
@@ -892,7 +893,7 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
892893
}
893894
}
894895

895-
assertIsAuthUserInfoValid(this.state);
896+
this.#assertIsAuthenticatedUser(this.state);
896897
const {
897898
nodeAuthTokens,
898899
authConnectionId,
@@ -948,8 +949,9 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
948949
async checkIsSeedlessOnboardingUserAuthenticated(): Promise<boolean> {
949950
let isAuthenticated = false;
950951
try {
951-
assertIsSeedlessOnboardingUserAuthenticated(this.state);
952-
isAuthenticated = true;
952+
this.#assertIsAuthenticatedUser(this.state);
953+
isAuthenticated =
954+
Boolean(this.state.accessToken) && Boolean(this.state.refreshToken);
953955
} catch {
954956
isAuthenticated = false;
955957
}
@@ -959,6 +961,35 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
959961
return isAuthenticated;
960962
}
961963

964+
/**
965+
* Get the access token from the state or the vault.
966+
* If the access token is not in the state, it will be retrieved from the vault by decrypting it with the password.
967+
*
968+
* If both the access token and the vault are not available, an error will be thrown.
969+
*
970+
* @param password - The optional password to unlock the vault. If not provided, the access token will be retrieved from the vault.
971+
* @returns The access token.
972+
*/
973+
async #getAccessToken(password: string): Promise<string> {
974+
const { accessToken, vault } = this.state;
975+
if (accessToken) {
976+
// if the access token is in the state, return it
977+
return accessToken;
978+
}
979+
980+
// otherwise, check the vault availability and decrypt the access token from the vault
981+
if (!vault) {
982+
throw new Error(
983+
SeedlessOnboardingControllerErrorMessage.InvalidAccessToken,
984+
);
985+
}
986+
987+
const { vaultData } = await this.#decryptAndParseVaultData({
988+
password,
989+
});
990+
return vaultData.accessToken;
991+
}
992+
962993
#setUnlocked(): void {
963994
this.#isUnlocked = true;
964995
}
@@ -1116,7 +1147,7 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
11161147
async #recoverEncKey(
11171148
password: string,
11181149
): Promise<Omit<RecoverEncryptionKeyResult, 'rateLimitResetResult'>> {
1119-
assertIsAuthUserInfoValid(this.state);
1150+
this.#assertIsAuthenticatedUser(this.state);
11201151
const {
11211152
nodeAuthTokens,
11221153
authConnectionId,
@@ -1529,7 +1560,13 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
15291560
}): Promise<void> {
15301561
this.#assertIsAuthenticatedUser(this.state);
15311562

1532-
const { revokeToken, accessToken } = this.state;
1563+
const { revokeToken } = this.state;
1564+
if (!revokeToken) {
1565+
throw new Error(
1566+
SeedlessOnboardingControllerErrorMessage.InvalidRevokeToken,
1567+
);
1568+
}
1569+
const accessToken = await this.#getAccessToken(password);
15331570

15341571
const vaultData: DeserializedVaultData = {
15351572
toprfAuthKeyPair: rawToprfAuthKeyPair,
@@ -1768,7 +1805,7 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
17681805
*/
17691806
async refreshAuthTokens(): Promise<void> {
17701807
this.#assertIsAuthenticatedUser(this.state);
1771-
const { refreshToken, revokeToken } = this.state;
1808+
const { refreshToken } = this.state;
17721809

17731810
const res = await this.#refreshJWTToken({
17741811
connection: this.state.authConnection,
@@ -1792,7 +1829,6 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
17921829
groupedAuthConnectionId: this.state.groupedAuthConnectionId,
17931830
userId: this.state.userId,
17941831
refreshToken,
1795-
revokeToken,
17961832
skipLock: true,
17971833
});
17981834
} catch (error) {
@@ -1988,9 +2024,10 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
19882024
try {
19892025
// proactively check for expired tokens and refresh them if needed
19902026
const isNodeAuthTokenExpired = this.checkNodeAuthTokenExpired();
2027+
console.log('isNodeAuthTokenExpired', isNodeAuthTokenExpired);
19912028
const isMetadataAccessTokenExpired =
19922029
this.checkMetadataAccessTokenExpired();
1993-
2030+
console.log('isMetadataAccessTokenExpired', isMetadataAccessTokenExpired);
19942031
// access token is only accessible when the vault is unlocked
19952032
// so skip the check if the vault is locked
19962033
let isAccessTokenExpired = false;
@@ -2078,6 +2115,9 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
20782115
try {
20792116
this.#assertIsAuthenticatedUser(this.state);
20802117
const { accessToken } = this.state;
2118+
if (!accessToken) {
2119+
return true; // Consider missing token as expired
2120+
}
20812121
const decodedToken = decodeJWTToken(accessToken);
20822122
return decodedToken.exp < Math.floor(Date.now() / 1000);
20832123
} catch {

0 commit comments

Comments
 (0)