Skip to content

Commit 96b1644

Browse files
committed
fix: fixed invalid-revoke-token issue
1 parent e343f73 commit 96b1644

File tree

4 files changed

+211
-75
lines changed

4 files changed

+211
-75
lines changed

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

Lines changed: 157 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1288,6 +1288,7 @@ describe('SeedlessOnboardingController', () => {
12881288
pwEncKey,
12891289
authKeyPair,
12901290
MOCK_PASSWORD,
1291+
controller.state.revokeToken,
12911292
);
12921293

12931294
const expectedVaultValue = await encryptor.decrypt(
@@ -1299,7 +1300,7 @@ describe('SeedlessOnboardingController', () => {
12991300
controller.state.vault as string,
13001301
);
13011302

1302-
expect(expectedVaultValue).toStrictEqual(resultedVaultValue);
1303+
expect(resultedVaultValue).toStrictEqual(expectedVaultValue);
13031304

13041305
// should be able to get the hash of the seed phrase backup from the state
13051306
expect(
@@ -1377,6 +1378,36 @@ describe('SeedlessOnboardingController', () => {
13771378
);
13781379
});
13791380

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

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-
21082118
it('should be able to fetch seed phrases with cached encryption key without providing password', async () => {
21092119
const mockToprfEncryptor = createMockToprfEncryptor();
21102120

@@ -5331,6 +5341,24 @@ describe('SeedlessOnboardingController', () => {
53315341
});
53325342
});
53335343

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

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

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

Lines changed: 50 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) {
@@ -1990,7 +2026,6 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
19902026
const isNodeAuthTokenExpired = this.checkNodeAuthTokenExpired();
19912027
const isMetadataAccessTokenExpired =
19922028
this.checkMetadataAccessTokenExpired();
1993-
19942029
// access token is only accessible when the vault is unlocked
19952030
// so skip the check if the vault is locked
19962031
let isAccessTokenExpired = false;
@@ -2078,6 +2113,9 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
20782113
try {
20792114
this.#assertIsAuthenticatedUser(this.state);
20802115
const { accessToken } = this.state;
2116+
if (!accessToken) {
2117+
return true; // Consider missing token as expired
2118+
}
20812119
const decodedToken = decodeJWTToken(accessToken);
20822120
return decodedToken.exp < Math.floor(Date.now() / 1000);
20832121
} catch {

0 commit comments

Comments
 (0)