@metamask/profile-sync-controller 32.3.1 → 34.0.0
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 +46 -2
- package/README.md +26 -11
- package/dist/controllers/authentication/AuthenticationController-method-action-types.d.ts +36 -22
- package/dist/controllers/authentication/AuthenticationController-method-action-types.d.ts.map +1 -1
- package/dist/controllers/authentication/AuthenticationController-method-action-types.js.map +1 -1
- package/dist/controllers/authentication/AuthenticationController.d.ts +33 -23
- package/dist/controllers/authentication/AuthenticationController.d.ts.map +1 -1
- package/dist/controllers/authentication/AuthenticationController.js +156 -80
- package/dist/controllers/authentication/AuthenticationController.js.map +1 -1
- package/dist/controllers/authentication/index.d.ts +1 -1
- package/dist/controllers/authentication/index.d.ts.map +1 -1
- package/dist/controllers/authentication/index.js.map +1 -1
- package/dist/controllers/user-storage/contact-syncing/controller-integration.js +5 -5
- package/dist/controllers/user-storage/contact-syncing/controller-integration.js.map +1 -1
- package/dist/sdk/authentication-jwt-bearer/flow-srp.d.ts +15 -12
- package/dist/sdk/authentication-jwt-bearer/flow-srp.d.ts.map +1 -1
- package/dist/sdk/authentication-jwt-bearer/flow-srp.js +11 -8
- package/dist/sdk/authentication-jwt-bearer/flow-srp.js.map +1 -1
- package/dist/sdk/authentication-jwt-bearer/mfa/schemas.d.ts +21 -16
- package/dist/sdk/authentication-jwt-bearer/mfa/schemas.d.ts.map +1 -1
- package/dist/sdk/authentication-jwt-bearer/mfa/schemas.js +24 -20
- package/dist/sdk/authentication-jwt-bearer/mfa/schemas.js.map +1 -1
- package/dist/sdk/authentication-jwt-bearer/mfa/services.d.ts +9 -7
- package/dist/sdk/authentication-jwt-bearer/mfa/services.d.ts.map +1 -1
- package/dist/sdk/authentication-jwt-bearer/mfa/services.js +11 -6
- package/dist/sdk/authentication-jwt-bearer/mfa/services.js.map +1 -1
- package/dist/sdk/authentication-jwt-bearer/mfa/types.d.ts +25 -11
- package/dist/sdk/authentication-jwt-bearer/mfa/types.d.ts.map +1 -1
- package/dist/sdk/authentication-jwt-bearer/mfa/types.js.map +1 -1
- package/dist/sdk/authentication.d.ts +5 -5
- package/dist/sdk/authentication.d.ts.map +1 -1
- package/dist/sdk/authentication.js +1 -1
- package/dist/sdk/authentication.js.map +1 -1
- package/dist/sdk/errors.d.ts +4 -4
- package/dist/sdk/errors.d.ts.map +1 -1
- package/dist/sdk/errors.js +6 -6
- package/dist/sdk/errors.js.map +1 -1
- package/dist/sdk/index.d.ts +2 -2
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js.map +1 -1
- package/dist/sdk/mocks/auth.d.ts +12 -5
- package/dist/sdk/mocks/auth.d.ts.map +1 -1
- package/dist/sdk/mocks/auth.js +4 -6
- package/dist/sdk/mocks/auth.js.map +1 -1
- package/dist/shared/encryption/cache.d.ts +4 -4
- package/dist/shared/encryption/cache.d.ts.map +1 -1
- package/dist/shared/encryption/cache.js.map +1 -1
- package/dist/shared/encryption/constants.d.ts +0 -1
- package/dist/shared/encryption/constants.d.ts.map +1 -1
- package/dist/shared/encryption/constants.js +0 -2
- package/dist/shared/encryption/constants.js.map +1 -1
- package/dist/shared/encryption/encryption.d.ts.map +1 -1
- package/dist/shared/encryption/encryption.js +23 -25
- package/dist/shared/encryption/encryption.js.map +1 -1
- package/dist/shared/storage-schema.js.map +1 -1
- package/dist/shared/types/encryption.d.ts +1 -1
- package/dist/shared/types/encryption.d.ts.map +1 -1
- package/dist/shared/types/encryption.js.map +1 -1
- package/dist/shared/utils/message-signing.d.ts.map +1 -1
- package/dist/shared/utils/message-signing.js +2 -14
- package/dist/shared/utils/message-signing.js.map +1 -1
- package/package.json +14 -15
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BaseController } from '@metamask/base-controller';
|
|
2
2
|
import { selectHdKeyringEntropySourceIds } from '@metamask/keyring-controller';
|
|
3
|
-
import { BeginEnrollmentRequestStruct, CompleteEnrollmentRequestStruct, assertValidMfaRequest,
|
|
4
|
-
import { assertMessageStartsWithMetamask, AuthType, Env, JwtBearerAuth, PairConflictError, getMfaErrorCode,
|
|
3
|
+
import { BeginEnrollmentRequestStruct, CompleteEnrollmentRequestStruct, assertValidMfaRequest, BeginVerificationRequestStruct, CompleteVerificationRequestStruct, GetVerificationTokenRequestStruct, parseVerificationTokenClaims, } from '../../sdk/authentication-jwt-bearer/mfa/schemas.js';
|
|
4
|
+
import { assertMessageStartsWithMetamask, AuthType, Env, JwtBearerAuth, PairConflictError, getMfaErrorCode, VerificationTokenInvalidError, } from '../../sdk/index.js';
|
|
5
5
|
import { decodeJwtPayload } from '../../sdk/utils/jwt.js';
|
|
6
6
|
import { toErrorMessage } from '../../sdk/utils/to-error-message.js';
|
|
7
7
|
import { getPrimaryHdKeyringEntropySourceId } from '../../shared/utils/entropy-source.js';
|
|
@@ -70,19 +70,19 @@ const metadata = {
|
|
|
70
70
|
includeInDebugSnapshot: false,
|
|
71
71
|
usedInUi: true,
|
|
72
72
|
},
|
|
73
|
-
stepUpSessionExpiresAt: {
|
|
74
|
-
includeInStateLogs: false,
|
|
75
|
-
persist: false,
|
|
76
|
-
includeInDebugSnapshot: false,
|
|
77
|
-
usedInUi: true,
|
|
78
|
-
},
|
|
79
73
|
};
|
|
80
74
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
75
|
+
* Default maximum age of a verification session that may authorize enrolling a
|
|
76
|
+
* credential. A session opened for an unrelated operation must not be able to
|
|
77
|
+
* add a factor long after the fact, while a session proven moments ago (or
|
|
78
|
+
* during the same setup flow, via `maxSessionAgeMs`) may.
|
|
84
79
|
*/
|
|
85
|
-
export const
|
|
80
|
+
export const ENROLLMENT_MAX_SESSION_AGE_MS = 2 * 60_000;
|
|
81
|
+
/**
|
|
82
|
+
* Key of the only verification session held today. Sessions are keyed by token
|
|
83
|
+
* audience so audience-scoped tokens can be added without reshaping storage.
|
|
84
|
+
*/
|
|
85
|
+
const DEFAULT_AUDIENCE = '';
|
|
86
86
|
const MESSENGER_EXPOSED_METHODS = [
|
|
87
87
|
'performSignIn',
|
|
88
88
|
'performSignOut',
|
|
@@ -98,10 +98,10 @@ const MESSENGER_EXPOSED_METHODS = [
|
|
|
98
98
|
'refreshEnrolledCredentials',
|
|
99
99
|
'beginCredentialEnrollment',
|
|
100
100
|
'completeCredentialEnrollment',
|
|
101
|
-
'
|
|
102
|
-
'
|
|
103
|
-
'
|
|
104
|
-
'
|
|
101
|
+
'beginCredentialVerification',
|
|
102
|
+
'completeCredentialVerification',
|
|
103
|
+
'getVerificationToken',
|
|
104
|
+
'clearVerificationSession',
|
|
105
105
|
];
|
|
106
106
|
/**
|
|
107
107
|
* Controller that enables authentication for restricted endpoints.
|
|
@@ -121,8 +121,12 @@ export class AuthenticationController extends BaseController {
|
|
|
121
121
|
* ceremony started under the previous session cannot apply its result.
|
|
122
122
|
*/
|
|
123
123
|
#authSessionEpoch = 0;
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Verification sessions keyed by token audience. At most one session per
|
|
126
|
+
* audience; only `DEFAULT_AUDIENCE` is used until audience-scoped tokens
|
|
127
|
+
* exist.
|
|
128
|
+
*/
|
|
129
|
+
#verificationSessions = new Map();
|
|
126
130
|
/**
|
|
127
131
|
* Sequence number of the most recently started credentials refresh. Only
|
|
128
132
|
* that refresh may write the cache, so a slower, earlier request cannot
|
|
@@ -135,6 +139,10 @@ export class AuthenticationController extends BaseController {
|
|
|
135
139
|
* `performSignIn` can't clear `needsProfilePairing` afterwards.
|
|
136
140
|
*/
|
|
137
141
|
#profilePairingRequestEpoch = 0;
|
|
142
|
+
/**
|
|
143
|
+
* Sign-in currently running. Overlapping callers share it.
|
|
144
|
+
*/
|
|
145
|
+
#ongoingSignIn;
|
|
138
146
|
#keyringController = {
|
|
139
147
|
setupLockedStateSubscriptions: () => {
|
|
140
148
|
const { isUnlocked } = this.messenger.call('KeyringController:getState');
|
|
@@ -145,7 +153,7 @@ export class AuthenticationController extends BaseController {
|
|
|
145
153
|
this.messenger.subscribe('KeyringController:lock', () => {
|
|
146
154
|
this.#authSessionEpoch += 1;
|
|
147
155
|
this.#isUnlocked = false;
|
|
148
|
-
this.
|
|
156
|
+
this.clearVerificationSession();
|
|
149
157
|
});
|
|
150
158
|
},
|
|
151
159
|
};
|
|
@@ -337,6 +345,18 @@ export class AuthenticationController extends BaseController {
|
|
|
337
345
|
}
|
|
338
346
|
async performSignIn() {
|
|
339
347
|
this.#assertIsUnlocked('performSignIn');
|
|
348
|
+
if (this.#ongoingSignIn) {
|
|
349
|
+
return await this.#ongoingSignIn;
|
|
350
|
+
}
|
|
351
|
+
const signIn = this.#runPerformSignIn().finally(() => {
|
|
352
|
+
if (this.#ongoingSignIn === signIn) {
|
|
353
|
+
this.#ongoingSignIn = undefined;
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
this.#ongoingSignIn = signIn;
|
|
357
|
+
return await signIn;
|
|
358
|
+
}
|
|
359
|
+
async #runPerformSignIn() {
|
|
340
360
|
const epochAtStart = this.#profilePairingRequestEpoch;
|
|
341
361
|
const entropySourceIds = this.#getHdKeyringEntropySourceIds();
|
|
342
362
|
const accessTokens = [];
|
|
@@ -604,8 +624,9 @@ export class AuthenticationController extends BaseController {
|
|
|
604
624
|
this.#setTraceAttribute(context, 'mfaErrorCode', mfaCode);
|
|
605
625
|
if (mfaCode === 'authentication_required' && this.#isUnlocked) {
|
|
606
626
|
this.#invalidateSrpSession(this.#getPrimaryEntropySourceId());
|
|
607
|
-
//
|
|
608
|
-
|
|
627
|
+
// A verification session must not outlive a rejected base
|
|
628
|
+
// session.
|
|
629
|
+
this.clearVerificationSession();
|
|
609
630
|
}
|
|
610
631
|
}
|
|
611
632
|
throw error;
|
|
@@ -664,9 +685,13 @@ export class AuthenticationController extends BaseController {
|
|
|
664
685
|
const sessionEpoch = this.#authSessionEpoch;
|
|
665
686
|
assertValidMfaRequest(request, BeginEnrollmentRequestStruct);
|
|
666
687
|
const primaryEntropySourceId = this.#getPrimaryEntropySourceId();
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
|
|
688
|
+
// The server requires AAL2 to add a credential once one that proves AAL2
|
|
689
|
+
// exists: send the verification token only while a recent enough session
|
|
690
|
+
// is live and let the server decide. An older session stays open for
|
|
691
|
+
// other consumers.
|
|
692
|
+
const accessToken = this.getVerificationToken({
|
|
693
|
+
maxSessionAgeMs: request.maxSessionAgeMs ?? ENROLLMENT_MAX_SESSION_AGE_MS,
|
|
694
|
+
})?.accessToken;
|
|
670
695
|
const challenge = await this.#runMfaRequest('MFA Enroll Begin', request.reason.operation, request.type, async () => await this.#auth.beginMfaEnrollment(request.type, {
|
|
671
696
|
email: request.email,
|
|
672
697
|
entropySourceId: primaryEntropySourceId,
|
|
@@ -678,6 +703,11 @@ export class AuthenticationController extends BaseController {
|
|
|
678
703
|
/**
|
|
679
704
|
* Completes credential enrollment and refreshes the credential cache.
|
|
680
705
|
*
|
|
706
|
+
* The server returns an assertion for the new credential, which opens a
|
|
707
|
+
* verification session like `completeCredentialVerification`, replacing
|
|
708
|
+
* any earlier one. If that exchange fails, the earlier session is kept:
|
|
709
|
+
* the credential is enrolled either way.
|
|
710
|
+
*
|
|
681
711
|
* A cache-refresh failure does not undo successful enrollment. Email
|
|
682
712
|
* enrollment invalidates the primary SRP session *after* refresh so the
|
|
683
713
|
* credentials call can reuse the still-valid access token; the next token
|
|
@@ -694,8 +724,22 @@ export class AuthenticationController extends BaseController {
|
|
|
694
724
|
const sessionEpoch = this.#authSessionEpoch;
|
|
695
725
|
assertValidMfaRequest(request, CompleteEnrollmentRequestStruct);
|
|
696
726
|
const { type } = request.proof;
|
|
727
|
+
const { operation } = request.reason;
|
|
697
728
|
const primaryEntropySourceId = this.#getPrimaryEntropySourceId();
|
|
698
|
-
await this.#runMfaRequest('MFA Enroll Complete',
|
|
729
|
+
const assertion = await this.#runMfaRequest('MFA Enroll Complete', operation, type, async () => await this.#auth.completeMfaEnrollment(request.flowId, request.proof, primaryEntropySourceId));
|
|
730
|
+
try {
|
|
731
|
+
await this.#openVerificationSessionFromAssertion(assertion, {
|
|
732
|
+
operation,
|
|
733
|
+
type,
|
|
734
|
+
sessionEpoch,
|
|
735
|
+
methodName: 'completeCredentialEnrollment',
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
catch {
|
|
739
|
+
// Callers see no session and verify the credential instead.
|
|
740
|
+
}
|
|
741
|
+
// Also catches a session that ended during the enrollment request; the
|
|
742
|
+
// exchange above then ran for nothing, and its session was never opened.
|
|
699
743
|
try {
|
|
700
744
|
this.#assertAuthSessionEpoch(sessionEpoch, 'completeCredentialEnrollment');
|
|
701
745
|
}
|
|
@@ -705,8 +749,6 @@ export class AuthenticationController extends BaseController {
|
|
|
705
749
|
}
|
|
706
750
|
throw error;
|
|
707
751
|
}
|
|
708
|
-
// The factor set changed: require a fresh ceremony against the new set.
|
|
709
|
-
this.clearStepUpSession();
|
|
710
752
|
try {
|
|
711
753
|
return await this.refreshEnrolledCredentials();
|
|
712
754
|
}
|
|
@@ -720,74 +762,103 @@ export class AuthenticationController extends BaseController {
|
|
|
720
762
|
}
|
|
721
763
|
}
|
|
722
764
|
/**
|
|
723
|
-
* Begins
|
|
765
|
+
* Begins verification with an enrolled credential.
|
|
724
766
|
*
|
|
725
767
|
* @param request - Credential type and trace reason.
|
|
726
768
|
* @returns A challenge for the client-owned ceremony.
|
|
727
769
|
*/
|
|
728
|
-
async
|
|
729
|
-
this.#assertIsUnlocked('
|
|
770
|
+
async beginCredentialVerification(request) {
|
|
771
|
+
this.#assertIsUnlocked('beginCredentialVerification');
|
|
730
772
|
const sessionEpoch = this.#authSessionEpoch;
|
|
731
|
-
assertValidMfaRequest(request,
|
|
773
|
+
assertValidMfaRequest(request, BeginVerificationRequestStruct);
|
|
732
774
|
const primaryEntropySourceId = this.#getPrimaryEntropySourceId();
|
|
733
|
-
const challenge = await this.#runMfaRequest('MFA
|
|
734
|
-
this.#assertAuthSessionEpoch(sessionEpoch, '
|
|
775
|
+
const challenge = await this.#runMfaRequest('MFA Verification Begin', request.reason.operation, request.type, async () => await this.#auth.beginMfaVerification(request.type, primaryEntropySourceId));
|
|
776
|
+
this.#assertAuthSessionEpoch(sessionEpoch, 'beginCredentialVerification');
|
|
735
777
|
return challenge;
|
|
736
778
|
}
|
|
737
779
|
/**
|
|
738
|
-
* Completes
|
|
780
|
+
* Completes verification and opens a short-lived verification session.
|
|
739
781
|
*
|
|
740
|
-
* The
|
|
741
|
-
*
|
|
742
|
-
*
|
|
782
|
+
* The assertion returned by the MFA service is exchanged at Hydra for an
|
|
783
|
+
* access token. Its assurance level is not checked: the services receiving
|
|
784
|
+
* the token enforce their own requirements. The token itself never enters
|
|
785
|
+
* controller state.
|
|
743
786
|
*
|
|
744
787
|
* @param request - Flow identifier, platform or email proof, and trace reason.
|
|
745
|
-
* @returns The
|
|
788
|
+
* @returns The verification token.
|
|
746
789
|
*/
|
|
747
|
-
async
|
|
748
|
-
this.#assertIsUnlocked('
|
|
790
|
+
async completeCredentialVerification(request) {
|
|
791
|
+
this.#assertIsUnlocked('completeCredentialVerification');
|
|
749
792
|
const sessionEpoch = this.#authSessionEpoch;
|
|
750
|
-
assertValidMfaRequest(request,
|
|
793
|
+
assertValidMfaRequest(request, CompleteVerificationRequestStruct);
|
|
751
794
|
const { type } = request.proof;
|
|
752
795
|
const { operation } = request.reason;
|
|
753
796
|
const primaryEntropySourceId = this.#getPrimaryEntropySourceId();
|
|
754
|
-
const assertion = await this.#runMfaRequest('MFA
|
|
755
|
-
this.#assertAuthSessionEpoch(sessionEpoch, '
|
|
797
|
+
const assertion = await this.#runMfaRequest('MFA Verification Complete', operation, type, async () => await this.#auth.completeMfaVerification(request.flowId, request.proof, primaryEntropySourceId));
|
|
798
|
+
this.#assertAuthSessionEpoch(sessionEpoch, 'completeCredentialVerification');
|
|
799
|
+
return await this.#openVerificationSessionFromAssertion(assertion, {
|
|
800
|
+
operation,
|
|
801
|
+
type,
|
|
802
|
+
sessionEpoch,
|
|
803
|
+
methodName: 'completeCredentialVerification',
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Exchanges an MFA assertion at Hydra and opens the verification session
|
|
808
|
+
* with the resulting token.
|
|
809
|
+
*
|
|
810
|
+
* @param assertion - Assertion returned by a verify or enroll completion.
|
|
811
|
+
* @param context - Trace tags and the session the call started in.
|
|
812
|
+
* @param context.operation - Feature operation, for tracing.
|
|
813
|
+
* @param context.type - Credential type the assertion proves, for tracing.
|
|
814
|
+
* @param context.sessionEpoch - Authenticated session the call started in.
|
|
815
|
+
* @param context.methodName - Public method name, for error messages.
|
|
816
|
+
* @returns The verification token.
|
|
817
|
+
*/
|
|
818
|
+
async #openVerificationSessionFromAssertion(assertion, { operation, type, sessionEpoch, methodName, }) {
|
|
756
819
|
const accessToken = await this.#runMfaRequest('MFA Token Exchange', operation, type, async () => await this.#auth.exchangeMfaAssertion(assertion.token));
|
|
757
|
-
this.#assertAuthSessionEpoch(sessionEpoch,
|
|
820
|
+
this.#assertAuthSessionEpoch(sessionEpoch, methodName);
|
|
758
821
|
let decodedClaims;
|
|
759
822
|
try {
|
|
760
823
|
decodedClaims = decodeJwtPayload(accessToken.accessToken);
|
|
761
824
|
}
|
|
762
825
|
catch (error) {
|
|
763
|
-
throw new
|
|
826
|
+
throw new VerificationTokenInvalidError(toErrorMessage(error));
|
|
764
827
|
}
|
|
765
|
-
const claims =
|
|
766
|
-
if (
|
|
767
|
-
throw new
|
|
828
|
+
const claims = parseVerificationTokenClaims(decodedClaims);
|
|
829
|
+
if (!Number.isFinite(accessToken.expiresIn) || accessToken.expiresIn <= 0) {
|
|
830
|
+
throw new VerificationTokenInvalidError('Verification token has no remaining lifetime');
|
|
768
831
|
}
|
|
769
832
|
const token = { ...accessToken, claims };
|
|
770
|
-
this.#
|
|
833
|
+
this.#openVerificationSession(token);
|
|
771
834
|
return token;
|
|
772
835
|
}
|
|
773
836
|
/**
|
|
774
|
-
* Returns the active
|
|
837
|
+
* Returns the active verification token when it meets the requested
|
|
838
|
+
* freshness.
|
|
839
|
+
*
|
|
840
|
+
* Low-level: features should go through the client MFA kit
|
|
841
|
+
* (`verifyOrEnroll`), which reuses a matching session without showing any
|
|
842
|
+
* screen and checks which method proved it. Read the token directly only
|
|
843
|
+
* from code that cannot show UI, and treat `null` as "let the UI layer
|
|
844
|
+
* ask".
|
|
775
845
|
*
|
|
776
846
|
* @param request - Optional maximum session age in milliseconds, measured
|
|
777
847
|
* from when the token was obtained. Zero always requires a new ceremony.
|
|
778
|
-
* @returns A live
|
|
848
|
+
* @returns A live verification token, or null when no reusable session
|
|
849
|
+
* exists.
|
|
779
850
|
*/
|
|
780
|
-
|
|
781
|
-
this.#assertIsUnlocked('
|
|
782
|
-
assertValidMfaRequest(request,
|
|
783
|
-
const session = this.#
|
|
851
|
+
getVerificationToken(request = {}) {
|
|
852
|
+
this.#assertIsUnlocked('getVerificationToken');
|
|
853
|
+
assertValidMfaRequest(request, GetVerificationTokenRequestStruct);
|
|
854
|
+
const session = this.#verificationSessions.get(DEFAULT_AUDIENCE);
|
|
784
855
|
if (!session) {
|
|
785
856
|
return null;
|
|
786
857
|
}
|
|
787
858
|
const now = Date.now();
|
|
788
859
|
if (now >= session.expiresAt) {
|
|
789
860
|
// The hard-expiry timer has not fired yet (e.g. a suspended tab).
|
|
790
|
-
this
|
|
861
|
+
this.#dropVerificationSession(DEFAULT_AUDIENCE);
|
|
791
862
|
return null;
|
|
792
863
|
}
|
|
793
864
|
// `>=` so a zero max age always forces a fresh ceremony. Not cleared: the
|
|
@@ -799,36 +870,41 @@ export class AuthenticationController extends BaseController {
|
|
|
799
870
|
return session.token;
|
|
800
871
|
}
|
|
801
872
|
/**
|
|
802
|
-
* Opens the
|
|
803
|
-
*
|
|
873
|
+
* Opens the verification session for the lifetime the server gave the token.
|
|
874
|
+
* It is measured from `expires_in` on the local clock rather than read from
|
|
875
|
+
* `exp`, so a skewed device clock cannot shorten or extend it.
|
|
804
876
|
*
|
|
805
|
-
* @param token - The freshly exchanged
|
|
877
|
+
* @param token - The freshly exchanged verification token.
|
|
806
878
|
*/
|
|
807
|
-
#
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
state.stepUpSessionExpiresAt = expiresAt;
|
|
813
|
-
});
|
|
814
|
-
this.#stepUpTimer = setTimeout(() => this.clearStepUpSession(), Math.max(0, expiresAt - Date.now()));
|
|
879
|
+
#openVerificationSession(token) {
|
|
880
|
+
const audience = DEFAULT_AUDIENCE;
|
|
881
|
+
this.#dropVerificationSession(audience);
|
|
882
|
+
const expiresAt = token.obtainedAt + token.expiresIn * 1000;
|
|
883
|
+
const timer = setTimeout(() => this.#dropVerificationSession(audience), Math.max(0, expiresAt - Date.now()));
|
|
815
884
|
// Never keep a Node process alive for the expiry timer (tests, tooling).
|
|
816
|
-
|
|
885
|
+
timer.unref?.();
|
|
886
|
+
this.#verificationSessions.set(audience, { token, expiresAt, timer });
|
|
817
887
|
}
|
|
818
888
|
/**
|
|
819
|
-
*
|
|
889
|
+
* Drops one audience's verification session and its expiration timer.
|
|
890
|
+
*
|
|
891
|
+
* @param audience - Audience whose session to drop.
|
|
820
892
|
*/
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
893
|
+
#dropVerificationSession(audience) {
|
|
894
|
+
const session = this.#verificationSessions.get(audience);
|
|
895
|
+
if (session) {
|
|
896
|
+
clearTimeout(session.timer);
|
|
897
|
+
this.#verificationSessions.delete(audience);
|
|
825
898
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Clears every in-memory verification session and its expiration timer.
|
|
902
|
+
*/
|
|
903
|
+
clearVerificationSession() {
|
|
904
|
+
for (const { timer } of this.#verificationSessions.values()) {
|
|
905
|
+
clearTimeout(timer);
|
|
831
906
|
}
|
|
907
|
+
this.#verificationSessions.clear();
|
|
832
908
|
}
|
|
833
909
|
/**
|
|
834
910
|
* Drops the cached credential list. Callers that wipe profile state must go
|
|
@@ -848,7 +924,7 @@ export class AuthenticationController extends BaseController {
|
|
|
848
924
|
}
|
|
849
925
|
performSignOut() {
|
|
850
926
|
this.#authSessionEpoch += 1;
|
|
851
|
-
this.
|
|
927
|
+
this.clearVerificationSession();
|
|
852
928
|
this.#clearEnrolledCredentials();
|
|
853
929
|
this.update((state) => {
|
|
854
930
|
state.isSignedIn = false;
|
|
@@ -862,7 +938,7 @@ export class AuthenticationController extends BaseController {
|
|
|
862
938
|
clearState() {
|
|
863
939
|
this.#profilePairingRequestEpoch += 1;
|
|
864
940
|
this.#authSessionEpoch += 1;
|
|
865
|
-
this.
|
|
941
|
+
this.clearVerificationSession();
|
|
866
942
|
this.#clearEnrolledCredentials();
|
|
867
943
|
this.update(() => ({ ...defaultState }));
|
|
868
944
|
}
|