@metamask-previews/seedless-onboarding-controller 8.1.0-preview-152e02d → 8.1.0-preview-e0f8f1907
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/SeedlessOnboardingController.cjs +163 -74
- package/dist/SeedlessOnboardingController.cjs.map +1 -1
- package/dist/SeedlessOnboardingController.d.cts +14 -10
- package/dist/SeedlessOnboardingController.d.cts.map +1 -1
- package/dist/SeedlessOnboardingController.d.mts +14 -10
- package/dist/SeedlessOnboardingController.d.mts.map +1 -1
- package/dist/SeedlessOnboardingController.mjs +163 -74
- package/dist/SeedlessOnboardingController.mjs.map +1 -1
- package/dist/constants.cjs +1 -0
- package/dist/constants.cjs.map +1 -1
- package/dist/constants.d.cts +1 -0
- package/dist/constants.d.cts.map +1 -1
- package/dist/constants.d.mts +1 -0
- package/dist/constants.d.mts.map +1 -1
- package/dist/constants.mjs +1 -0
- package/dist/constants.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Token refresh now uses a dedicated private `#reAuthenticate` method that only accepts the tokens returned by the JWT-refresh service (`idTokens`, `accessToken`, `metadataAccessToken`), making the re-authentication path stricter than the initial `authenticate` call ([#8148](https://github.com/MetaMask/core/pull/8148))
|
|
13
|
+
- After a successful JWT refresh the controller proactively rotates the refresh/revoke token pair (when the vault is unlocked) via the public `rotateRefreshToken` method ([#8148](https://github.com/MetaMask/core/pull/8148))
|
|
14
|
+
- Token expiry checks now use a 90% lifetime threshold for proactive refresh: access tokens and metadata access tokens are refreshed when less than 10% of their lifetime remains (requires `iat`), while node auth tokens fall back to exact-expiry checking ([#8148](https://github.com/MetaMask/core/pull/8148))
|
|
15
|
+
|
|
16
|
+
### Removed
|
|
17
|
+
|
|
18
|
+
- **BREAKING:** Removed public `renewRefreshToken` method — refresh token rotation is now handled by `rotateRefreshToken`, which can also be called directly ([#8148](https://github.com/MetaMask/core/pull/8148))
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Fixed `FailedToFetchAuthPubKey` errors caused by expired tokens in `addNewSecretData` and `changePassword` — `assertPasswordInSync` is now called inside the token-refresh wrapper so expired `nodeAuthTokens` are refreshed before reaching the backend
|
|
23
|
+
|
|
10
24
|
## [8.1.0]
|
|
11
25
|
|
|
12
26
|
### Changed
|
|
@@ -10,7 +10,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
10
10
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
11
11
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
12
12
|
};
|
|
13
|
-
var _SeedlessOnboardingController_instances, _SeedlessOnboardingController_vaultEncryptor, _SeedlessOnboardingController_controllerOperationMutex, _SeedlessOnboardingController_vaultOperationMutex, _SeedlessOnboardingController_pendingRefreshPromise, _SeedlessOnboardingController_refreshJWTToken, _SeedlessOnboardingController_revokeRefreshToken, _SeedlessOnboardingController_renewRefreshToken, _SeedlessOnboardingController_passwordOutdatedCacheTTL, _SeedlessOnboardingController_isUnlocked, _SeedlessOnboardingController_cachedDecryptedVaultData, _SeedlessOnboardingController_submitGlobalPassword, _SeedlessOnboardingController_setUnlocked, _SeedlessOnboardingController_pickLatestAccessToken, _SeedlessOnboardingController_persistOprfKey, _SeedlessOnboardingController_persistAuthPubKey, _SeedlessOnboardingController_storeKeyringEncryptionKey, _SeedlessOnboardingController_loadKeyringEncryptionKey, _SeedlessOnboardingController_loadSeedlessEncryptionKey, _SeedlessOnboardingController_recoverAuthPubKey, _SeedlessOnboardingController_recoverEncKey, _SeedlessOnboardingController_fetchAllSecretDataFromMetadataStore, _SeedlessOnboardingController_changeEncryptionKey, _SeedlessOnboardingController_encryptAndStoreSecretData, _SeedlessOnboardingController_unlockVaultAndGetVaultData, _SeedlessOnboardingController_decryptAndParseVaultData, _SeedlessOnboardingController_withPersistedSecretMetadataBackupsState, _SeedlessOnboardingController_filterDupesAndUpdateSocialBackupsMetadata, _SeedlessOnboardingController_createNewVaultWithAuthData, _SeedlessOnboardingController_updateVault, _SeedlessOnboardingController_encryptSeedlessEncryptionKey, _SeedlessOnboardingController_getAccessTokenAndRevokeToken, _SeedlessOnboardingController_withControllerLock, _SeedlessOnboardingController_withVaultLock, _SeedlessOnboardingController_parseVaultData, _SeedlessOnboardingController_assertIsUnlocked, _SeedlessOnboardingController_assertIsAuthenticatedUser, _SeedlessOnboardingController_assertPasswordInSync, _SeedlessOnboardingController_resetPasswordOutdatedCache, _SeedlessOnboardingController_doRefreshAuthTokens, _SeedlessOnboardingController_addRefreshTokenToRevokeList, _SeedlessOnboardingController_isAuthTokenError, _SeedlessOnboardingController_isMaxKeyChainLengthError, _SeedlessOnboardingController_executeWithTokenRefresh, _SeedlessOnboardingController_checkTokensExpired;
|
|
13
|
+
var _SeedlessOnboardingController_instances, _SeedlessOnboardingController_vaultEncryptor, _SeedlessOnboardingController_controllerOperationMutex, _SeedlessOnboardingController_vaultOperationMutex, _SeedlessOnboardingController_pendingRefreshPromise, _SeedlessOnboardingController_refreshJWTToken, _SeedlessOnboardingController_revokeRefreshToken, _SeedlessOnboardingController_renewRefreshToken, _SeedlessOnboardingController_passwordOutdatedCacheTTL, _SeedlessOnboardingController_isUnlocked, _SeedlessOnboardingController_cachedDecryptedVaultData, _SeedlessOnboardingController_submitGlobalPassword, _SeedlessOnboardingController_setUnlocked, _SeedlessOnboardingController_pickLatestAccessToken, _SeedlessOnboardingController_persistOprfKey, _SeedlessOnboardingController_persistAuthPubKey, _SeedlessOnboardingController_storeKeyringEncryptionKey, _SeedlessOnboardingController_loadKeyringEncryptionKey, _SeedlessOnboardingController_loadSeedlessEncryptionKey, _SeedlessOnboardingController_recoverAuthPubKey, _SeedlessOnboardingController_recoverEncKey, _SeedlessOnboardingController_fetchAllSecretDataFromMetadataStore, _SeedlessOnboardingController_changeEncryptionKey, _SeedlessOnboardingController_encryptAndStoreSecretData, _SeedlessOnboardingController_unlockVaultAndGetVaultData, _SeedlessOnboardingController_decryptAndParseVaultData, _SeedlessOnboardingController_withPersistedSecretMetadataBackupsState, _SeedlessOnboardingController_filterDupesAndUpdateSocialBackupsMetadata, _SeedlessOnboardingController_createNewVaultWithAuthData, _SeedlessOnboardingController_updateVault, _SeedlessOnboardingController_encryptSeedlessEncryptionKey, _SeedlessOnboardingController_getAccessTokenAndRevokeToken, _SeedlessOnboardingController_withControllerLock, _SeedlessOnboardingController_withVaultLock, _SeedlessOnboardingController_parseVaultData, _SeedlessOnboardingController_assertIsUnlocked, _SeedlessOnboardingController_assertIsAuthenticatedUser, _SeedlessOnboardingController_assertPasswordInSync, _SeedlessOnboardingController_resetPasswordOutdatedCache, _SeedlessOnboardingController_doRefreshAuthTokens, _SeedlessOnboardingController_reAuthenticate, _SeedlessOnboardingController_addRefreshTokenToRevokeList, _SeedlessOnboardingController_isAuthTokenError, _SeedlessOnboardingController_isMaxKeyChainLengthError, _SeedlessOnboardingController_executeWithTokenRefresh, _SeedlessOnboardingController_checkTokensExpired;
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.SeedlessOnboardingController = exports.getInitialSeedlessOnboardingControllerStateWithDefaults = void 0;
|
|
16
16
|
const auth_network_utils_1 = require("@metamask/auth-network-utils");
|
|
@@ -255,10 +255,11 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
|
|
|
255
255
|
if (!metadataAccessToken) {
|
|
256
256
|
throw new Error(constants_1.SeedlessOnboardingControllerErrorMessage.InvalidMetadataAccessToken);
|
|
257
257
|
}
|
|
258
|
-
//
|
|
258
|
+
// Use the same 90%-lifetime threshold as checkMetadataAccessTokenExpired
|
|
259
|
+
// so both code paths agree on when a refresh is needed.
|
|
259
260
|
const decodedToken = (0, utils_3.decodeJWTToken)(metadataAccessToken);
|
|
260
|
-
if (decodedToken.exp
|
|
261
|
-
// Token is expired, refresh it
|
|
261
|
+
if (isTokenNearExpiry(decodedToken.exp, decodedToken.iat)) {
|
|
262
|
+
// Token is near expiry or already expired, refresh it
|
|
262
263
|
await this.refreshAuthTokens();
|
|
263
264
|
// Get the new token after refresh
|
|
264
265
|
const { metadataAccessToken: newMetadataAccessToken } = this.state;
|
|
@@ -291,7 +292,7 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
|
|
|
291
292
|
* @param params.userId - user email or id from Social login
|
|
292
293
|
* @param params.groupedAuthConnectionId - Optional grouped authConnectionId to be used for the authenticate request.
|
|
293
294
|
* @param params.socialLoginEmail - The user email from Social login.
|
|
294
|
-
* @param params.refreshToken -
|
|
295
|
+
* @param params.refreshToken - Refresh token issued during OAuth login. Written to state when provided.
|
|
295
296
|
* @param params.revokeToken - revoke token for revoking refresh token and get new refresh token and new revoke token.
|
|
296
297
|
* @param params.accessToken - Access token for pairing with profile sync auth service and to access other services.
|
|
297
298
|
* @param params.metadataAccessToken - Metadata access token for accessing the metadata service before the vault is created or unlocked.
|
|
@@ -397,11 +398,11 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
|
|
|
397
398
|
async addNewSecretData(data, type, options) {
|
|
398
399
|
return await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_withControllerLock).call(this, async () => {
|
|
399
400
|
__classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertIsUnlocked).call(this);
|
|
400
|
-
await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertPasswordInSync).call(this, {
|
|
401
|
-
skipCache: true,
|
|
402
|
-
skipLock: true, // skip lock since we already have the lock
|
|
403
|
-
});
|
|
404
401
|
const performBackup = async () => {
|
|
402
|
+
await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertPasswordInSync).call(this, {
|
|
403
|
+
skipCache: true,
|
|
404
|
+
skipLock: true, // skip lock since we already have the lock
|
|
405
|
+
});
|
|
405
406
|
// verify the password and unlock the vault
|
|
406
407
|
const { toprfEncryptionKey, toprfAuthKeyPair } = await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_unlockVaultAndGetVaultData).call(this);
|
|
407
408
|
// encrypt and store the secret data
|
|
@@ -476,11 +477,11 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
|
|
|
476
477
|
await this.verifyVaultPassword(oldPassword, {
|
|
477
478
|
skipLock: true, // skip lock since we already have the lock
|
|
478
479
|
});
|
|
479
|
-
const { latestKeyIndex } = await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertPasswordInSync).call(this, {
|
|
480
|
-
skipCache: true,
|
|
481
|
-
skipLock: true, // skip lock since we already have the lock
|
|
482
|
-
});
|
|
483
480
|
const attemptChangePassword = async () => {
|
|
481
|
+
const { latestKeyIndex } = await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertPasswordInSync).call(this, {
|
|
482
|
+
skipCache: true,
|
|
483
|
+
skipLock: true, // skip lock since we already have the lock
|
|
484
|
+
});
|
|
484
485
|
// load keyring encryption key if it exists
|
|
485
486
|
let keyringEncryptionKey;
|
|
486
487
|
if (this.state.encryptedKeyringEncryptionKey) {
|
|
@@ -779,54 +780,64 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
|
|
|
779
780
|
* @returns A promise that resolves when the tokens have been refreshed.
|
|
780
781
|
*/
|
|
781
782
|
async refreshAuthTokens() {
|
|
783
|
+
// Coalesce concurrent calls to avoid issuing parallel HTTP requests
|
|
784
|
+
// with the same refresh token.
|
|
782
785
|
if (__classPrivateFieldGet(this, _SeedlessOnboardingController_pendingRefreshPromise, "f")) {
|
|
783
786
|
return __classPrivateFieldGet(this, _SeedlessOnboardingController_pendingRefreshPromise, "f");
|
|
784
787
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
788
|
+
const promise = __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_doRefreshAuthTokens).call(this).finally(() => {
|
|
789
|
+
if (__classPrivateFieldGet(this, _SeedlessOnboardingController_pendingRefreshPromise, "f") === promise) {
|
|
790
|
+
__classPrivateFieldSet(this, _SeedlessOnboardingController_pendingRefreshPromise, undefined, "f");
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
__classPrivateFieldSet(this, _SeedlessOnboardingController_pendingRefreshPromise, promise, "f");
|
|
794
|
+
return promise;
|
|
789
795
|
}
|
|
790
796
|
/**
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
*
|
|
797
|
+
* Rotate the refresh token — fetch a new refresh/revoke token pair from the
|
|
798
|
+
* auth service and persist the new revoke token in the vault.
|
|
799
|
+
*
|
|
800
|
+
* This method should be called after a successful JWT refresh.
|
|
794
801
|
*
|
|
795
|
-
* @param password - The password to encrypt the vault.
|
|
796
802
|
* @returns A Promise that resolves to void.
|
|
797
803
|
*/
|
|
798
|
-
async
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
804
|
+
async rotateRefreshToken() {
|
|
805
|
+
__classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertIsAuthenticatedUser).call(this, this.state);
|
|
806
|
+
const vaultData = __classPrivateFieldGet(this, _SeedlessOnboardingController_cachedDecryptedVaultData, "f");
|
|
807
|
+
// Safety net: the caller (#doRefreshAuthTokens) already guards with
|
|
808
|
+
// `this.#isUnlocked && this.#cachedDecryptedVaultData`, so this branch
|
|
809
|
+
// is unreachable in normal flow. This check is added to satisfy the linter and
|
|
810
|
+
// to make sure any future caller of this method does not bypass the check.
|
|
811
|
+
/* istanbul ignore if */
|
|
812
|
+
if (!vaultData) {
|
|
813
|
+
throw new errors_1.SeedlessOnboardingError(constants_1.SeedlessOnboardingControllerErrorMessage.VaultLocked);
|
|
814
|
+
}
|
|
815
|
+
const { refreshToken } = this.state;
|
|
816
|
+
const { toprfPwEncryptionKey: pwEncKey, revokeToken } = vaultData;
|
|
817
|
+
const { newRevokeToken, newRefreshToken } = await __classPrivateFieldGet(this, _SeedlessOnboardingController_renewRefreshToken, "f").call(this, {
|
|
818
|
+
connection: this.state.authConnection,
|
|
819
|
+
revokeToken,
|
|
820
|
+
});
|
|
821
|
+
if (newRevokeToken && newRefreshToken) {
|
|
822
|
+
// Persist the new revoke token to the vault first. Only update state after
|
|
823
|
+
// the vault write succeeds, so state and vault stay in sync if the write fails.
|
|
824
|
+
await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_updateVault).call(this, {
|
|
825
|
+
vaultData: {
|
|
826
|
+
...vaultData,
|
|
827
|
+
revokeToken: newRevokeToken,
|
|
828
|
+
},
|
|
829
|
+
pwEncKey,
|
|
830
|
+
});
|
|
831
|
+
this.update((state) => {
|
|
832
|
+
state.revokeToken = newRevokeToken;
|
|
833
|
+
state.refreshToken = newRefreshToken;
|
|
805
834
|
});
|
|
806
|
-
|
|
807
|
-
|
|
835
|
+
// add the old refresh token to the list to be revoked later when possible
|
|
836
|
+
__classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_addRefreshTokenToRevokeList).call(this, {
|
|
837
|
+
refreshToken,
|
|
808
838
|
revokeToken,
|
|
809
839
|
});
|
|
810
|
-
|
|
811
|
-
this.update((state) => {
|
|
812
|
-
// set new revoke token in state temporarily for persisting in vault
|
|
813
|
-
state.revokeToken = newRevokeToken;
|
|
814
|
-
// set new refresh token to persist in state
|
|
815
|
-
state.refreshToken = newRefreshToken;
|
|
816
|
-
});
|
|
817
|
-
await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_createNewVaultWithAuthData).call(this, {
|
|
818
|
-
password,
|
|
819
|
-
rawToprfEncryptionKey,
|
|
820
|
-
rawToprfPwEncryptionKey,
|
|
821
|
-
rawToprfAuthKeyPair,
|
|
822
|
-
});
|
|
823
|
-
// add the old refresh token to the list to be revoked later when possible
|
|
824
|
-
__classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_addRefreshTokenToRevokeList).call(this, {
|
|
825
|
-
refreshToken,
|
|
826
|
-
revokeToken,
|
|
827
|
-
});
|
|
828
|
-
}
|
|
829
|
-
});
|
|
840
|
+
}
|
|
830
841
|
}
|
|
831
842
|
/**
|
|
832
843
|
* Revoke all pending refresh tokens.
|
|
@@ -898,13 +909,15 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
|
|
|
898
909
|
const firstAuthToken = nodeAuthTokens[0]?.authToken;
|
|
899
910
|
// node auth token is base64 encoded json object
|
|
900
911
|
const decodedToken = (0, utils_3.decodeNodeAuthToken)(firstAuthToken);
|
|
901
|
-
//
|
|
902
|
-
return decodedToken.exp
|
|
912
|
+
// Node auth tokens do not carry a reliable iat field — use exact expiry.
|
|
913
|
+
return isTokenNearExpiry(decodedToken.exp);
|
|
903
914
|
}
|
|
904
915
|
/**
|
|
905
|
-
* Check if the current metadata access token
|
|
916
|
+
* Check if the current metadata access token should be refreshed.
|
|
917
|
+
* Returns true when the token is expired or when less than 10% of its
|
|
918
|
+
* lifetime remains (proactive refresh).
|
|
906
919
|
*
|
|
907
|
-
* @returns True if the metadata access token
|
|
920
|
+
* @returns True if the metadata access token should be refreshed, false otherwise.
|
|
908
921
|
*/
|
|
909
922
|
checkMetadataAccessTokenExpired() {
|
|
910
923
|
try {
|
|
@@ -912,17 +925,19 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
|
|
|
912
925
|
const { metadataAccessToken } = this.state;
|
|
913
926
|
// assertIsAuthenticatedUser will throw if metadataAccessToken is missing
|
|
914
927
|
const decodedToken = (0, utils_3.decodeJWTToken)(metadataAccessToken);
|
|
915
|
-
return decodedToken.exp
|
|
928
|
+
return isTokenNearExpiry(decodedToken.exp, decodedToken.iat);
|
|
916
929
|
}
|
|
917
930
|
catch {
|
|
918
931
|
return true; // Consider unauthenticated user as having expired tokens
|
|
919
932
|
}
|
|
920
933
|
}
|
|
921
934
|
/**
|
|
922
|
-
* Check if the current access token
|
|
935
|
+
* Check if the current access token should be refreshed.
|
|
936
|
+
* Returns true when the token is expired or when less than 10% of its
|
|
937
|
+
* lifetime remains (proactive refresh).
|
|
923
938
|
* When the vault is locked, the access token is not accessible, so we return false.
|
|
924
939
|
*
|
|
925
|
-
* @returns True if the access token
|
|
940
|
+
* @returns True if the access token should be refreshed, false otherwise.
|
|
926
941
|
*/
|
|
927
942
|
checkAccessTokenExpired() {
|
|
928
943
|
try {
|
|
@@ -932,7 +947,7 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
|
|
|
932
947
|
return true; // Consider missing token as expired
|
|
933
948
|
}
|
|
934
949
|
const decodedToken = (0, utils_3.decodeJWTToken)(accessToken);
|
|
935
|
-
return decodedToken.exp
|
|
950
|
+
return isTokenNearExpiry(decodedToken.exp, decodedToken.iat);
|
|
936
951
|
}
|
|
937
952
|
catch {
|
|
938
953
|
return true; // Consider unauthenticated user as having expired tokens
|
|
@@ -1619,25 +1634,12 @@ async function _SeedlessOnboardingController_doRefreshAuthTokens() {
|
|
|
1619
1634
|
});
|
|
1620
1635
|
try {
|
|
1621
1636
|
const { idTokens, accessToken, metadataAccessToken } = res;
|
|
1622
|
-
//
|
|
1623
|
-
//
|
|
1624
|
-
|
|
1625
|
-
// `refreshToken` captured at the start of this method. If renewRefreshToken()
|
|
1626
|
-
// ran concurrently while our HTTP call was in-flight, it will have already
|
|
1627
|
-
// updated state.refreshToken to the new token. Using the captured (now-stale)
|
|
1628
|
-
// value would overwrite that newer token and revert the state, causing the
|
|
1629
|
-
// old token to be used for subsequent refreshes — and once that old token is
|
|
1630
|
-
// revoked (15 s after the renewal), every subsequent refresh will return 401.
|
|
1631
|
-
await this.authenticate({
|
|
1637
|
+
// Re-authenticate with the refreshed id tokens to update node auth
|
|
1638
|
+
// tokens, accessToken, and metadataAccessToken in state.
|
|
1639
|
+
await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_reAuthenticate).call(this, {
|
|
1632
1640
|
idTokens,
|
|
1633
1641
|
accessToken,
|
|
1634
1642
|
metadataAccessToken,
|
|
1635
|
-
authConnection: this.state.authConnection,
|
|
1636
|
-
authConnectionId: this.state.authConnectionId,
|
|
1637
|
-
groupedAuthConnectionId: this.state.groupedAuthConnectionId,
|
|
1638
|
-
userId: this.state.userId,
|
|
1639
|
-
refreshToken: this.state.refreshToken,
|
|
1640
|
-
skipLock: true,
|
|
1641
1643
|
});
|
|
1642
1644
|
// update the vault with new access token if wallet is unlocked
|
|
1643
1645
|
if (__classPrivateFieldGet(this, _SeedlessOnboardingController_isUnlocked, "f") && __classPrivateFieldGet(this, _SeedlessOnboardingController_cachedDecryptedVaultData, "f")) {
|
|
@@ -1650,6 +1652,15 @@ async function _SeedlessOnboardingController_doRefreshAuthTokens() {
|
|
|
1650
1652
|
vaultData: updatedVaultData,
|
|
1651
1653
|
pwEncKey,
|
|
1652
1654
|
});
|
|
1655
|
+
// Proactively rotate the refresh token now that we have vault access.
|
|
1656
|
+
await this.rotateRefreshToken().catch((error) => {
|
|
1657
|
+
// Rotation failure is intentionally non-fatal: the JWT refresh
|
|
1658
|
+
// itself succeeded and the caller should not be blocked.
|
|
1659
|
+
// However the user is now operating with a stale refresh token
|
|
1660
|
+
// that may be revoked server-side, so log prominently.
|
|
1661
|
+
log('Failed to rotate refresh token after successful JWT refresh. ' +
|
|
1662
|
+
'The user may be logged out when the old token is revoked.', error);
|
|
1663
|
+
});
|
|
1653
1664
|
}
|
|
1654
1665
|
}
|
|
1655
1666
|
catch (error) {
|
|
@@ -1658,6 +1669,58 @@ async function _SeedlessOnboardingController_doRefreshAuthTokens() {
|
|
|
1658
1669
|
cause: error,
|
|
1659
1670
|
});
|
|
1660
1671
|
}
|
|
1672
|
+
}, _SeedlessOnboardingController_reAuthenticate =
|
|
1673
|
+
/**
|
|
1674
|
+
* Re-authenticate the user using freshly issued tokens from a JWT refresh.
|
|
1675
|
+
*
|
|
1676
|
+
* Unlike the public `authenticate` method, this variant is called exclusively
|
|
1677
|
+
* from token-refresh paths where the user's identity has already been
|
|
1678
|
+
* established. It only accepts the tokens returned by the JWT-refresh
|
|
1679
|
+
* service (`idTokens`, `accessToken`, `metadataAccessToken`) and reads all
|
|
1680
|
+
* other auth-connection details from the existing controller state.
|
|
1681
|
+
*
|
|
1682
|
+
* `refreshToken` and `revokeToken` are intentionally absent — token-refresh
|
|
1683
|
+
* paths must not touch `state.refreshToken` directly; that field is managed
|
|
1684
|
+
* exclusively by `rotateRefreshToken`.
|
|
1685
|
+
*
|
|
1686
|
+
* @param params - Tokens issued by the JWT-refresh service.
|
|
1687
|
+
* @param params.idTokens - New node id tokens.
|
|
1688
|
+
* @param params.accessToken - New access token.
|
|
1689
|
+
* @param params.metadataAccessToken - New metadata access token.
|
|
1690
|
+
* @returns A promise that resolves to the authentication result.
|
|
1691
|
+
*/
|
|
1692
|
+
async function _SeedlessOnboardingController_reAuthenticate(params) {
|
|
1693
|
+
const { idTokens, accessToken, metadataAccessToken } = params;
|
|
1694
|
+
const { authConnection, authConnectionId, groupedAuthConnectionId, userId, socialLoginEmail, } = this.state;
|
|
1695
|
+
try {
|
|
1696
|
+
const authenticationResult = await this.toprfClient.authenticate({
|
|
1697
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1698
|
+
authConnectionId: authConnectionId,
|
|
1699
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1700
|
+
userId: userId,
|
|
1701
|
+
idTokens,
|
|
1702
|
+
groupedAuthConnectionId,
|
|
1703
|
+
});
|
|
1704
|
+
this.update((state) => {
|
|
1705
|
+
state.nodeAuthTokens = authenticationResult.nodeAuthTokens;
|
|
1706
|
+
state.authConnection = authConnection;
|
|
1707
|
+
state.authConnectionId = authConnectionId;
|
|
1708
|
+
state.groupedAuthConnectionId = groupedAuthConnectionId;
|
|
1709
|
+
state.userId = userId;
|
|
1710
|
+
state.socialLoginEmail = socialLoginEmail;
|
|
1711
|
+
state.metadataAccessToken = metadataAccessToken;
|
|
1712
|
+
state.accessToken = accessToken;
|
|
1713
|
+
(0, assertions_1.assertIsSeedlessOnboardingUserAuthenticated)(state);
|
|
1714
|
+
state.isSeedlessOnboardingUserAuthenticated = true;
|
|
1715
|
+
});
|
|
1716
|
+
return authenticationResult;
|
|
1717
|
+
}
|
|
1718
|
+
catch (error) {
|
|
1719
|
+
log('Error re-authenticating user', error);
|
|
1720
|
+
throw new errors_1.SeedlessOnboardingError(constants_1.SeedlessOnboardingControllerErrorMessage.AuthenticationError, {
|
|
1721
|
+
cause: error,
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1661
1724
|
}, _SeedlessOnboardingController_addRefreshTokenToRevokeList = function _SeedlessOnboardingController_addRefreshTokenToRevokeList({ refreshToken, revokeToken, }) {
|
|
1662
1725
|
this.update((state) => {
|
|
1663
1726
|
state.pendingToBeRevokedTokens = [
|
|
@@ -1732,6 +1795,32 @@ async function _SeedlessOnboardingController_executeWithTokenRefresh(operation,
|
|
|
1732
1795
|
isMetadataAccessTokenExpired ||
|
|
1733
1796
|
isAccessTokenExpired);
|
|
1734
1797
|
};
|
|
1798
|
+
/**
|
|
1799
|
+
* Determine whether a token should be proactively refreshed.
|
|
1800
|
+
*
|
|
1801
|
+
* When `iat` is provided: returns `true` when less than 10% of the token's
|
|
1802
|
+
* lifetime remains (i.e. we are in the last 10% before expiry).
|
|
1803
|
+
* When `iat` is omitted (e.g. node auth tokens): returns `true` when the token
|
|
1804
|
+
* is already expired.
|
|
1805
|
+
*
|
|
1806
|
+
* @param exp - Token expiration time in seconds (Unix epoch).
|
|
1807
|
+
* @param iat - Optional issued-at time in seconds (Unix epoch). Required for 10% threshold.
|
|
1808
|
+
* @returns True if the token should be refreshed.
|
|
1809
|
+
*/
|
|
1810
|
+
function isTokenNearExpiry(exp, iat) {
|
|
1811
|
+
const now = Date.now() / 1000;
|
|
1812
|
+
if (iat === undefined) {
|
|
1813
|
+
return now >= exp;
|
|
1814
|
+
}
|
|
1815
|
+
const lifetime = exp - iat;
|
|
1816
|
+
// Guard against malformed tokens where iat >= exp (zero or negative lifetime).
|
|
1817
|
+
// Fall back to exact-expiry check so bad tokens are always considered stale.
|
|
1818
|
+
if (lifetime <= 0) {
|
|
1819
|
+
return now >= exp;
|
|
1820
|
+
}
|
|
1821
|
+
const remaining = exp - now;
|
|
1822
|
+
return remaining <= 0.1 * lifetime;
|
|
1823
|
+
}
|
|
1735
1824
|
/**
|
|
1736
1825
|
* Lock the given mutex before executing the given function,
|
|
1737
1826
|
* and release it after the function is resolved or after an
|