@metamask-previews/profile-sync-controller 32.3.1-preview-3c2e73813 → 32.3.1-preview-33429d645
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 +18 -1
- package/README.md +19 -11
- package/dist/controllers/authentication/AuthenticationController-method-action-types.d.ts +25 -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 -21
- package/dist/controllers/authentication/AuthenticationController.d.ts.map +1 -1
- package/dist/controllers/authentication/AuthenticationController.js +115 -69
- 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/sdk/authentication-jwt-bearer/flow-srp.d.ts +13 -11
- package/dist/sdk/authentication-jwt-bearer/flow-srp.d.ts.map +1 -1
- package/dist/sdk/authentication-jwt-bearer/flow-srp.js +9 -7
- package/dist/sdk/authentication-jwt-bearer/flow-srp.js.map +1 -1
- package/dist/sdk/authentication-jwt-bearer/mfa/schemas.d.ts +13 -14
- package/dist/sdk/authentication-jwt-bearer/mfa/schemas.d.ts.map +1 -1
- package/dist/sdk/authentication-jwt-bearer/mfa/schemas.js +19 -17
- package/dist/sdk/authentication-jwt-bearer/mfa/schemas.js.map +1 -1
- package/dist/sdk/authentication-jwt-bearer/mfa/services.d.ts +6 -6
- package/dist/sdk/authentication-jwt-bearer/mfa/services.d.ts.map +1 -1
- package/dist/sdk/authentication-jwt-bearer/mfa/services.js +8 -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 +4 -4
- package/dist/sdk/authentication.d.ts.map +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 +2 -2
- package/dist/sdk/mocks/auth.d.ts.map +1 -1
- package/dist/sdk/mocks/auth.js +3 -3
- package/dist/sdk/mocks/auth.js.map +1 -1
- package/package.json +3 -3
|
@@ -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,7 +70,7 @@ const metadata = {
|
|
|
70
70
|
includeInDebugSnapshot: false,
|
|
71
71
|
usedInUi: true,
|
|
72
72
|
},
|
|
73
|
-
|
|
73
|
+
verificationSessionExpiresAt: {
|
|
74
74
|
includeInStateLogs: false,
|
|
75
75
|
persist: false,
|
|
76
76
|
includeInDebugSnapshot: false,
|
|
@@ -78,11 +78,25 @@ const metadata = {
|
|
|
78
78
|
},
|
|
79
79
|
};
|
|
80
80
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
81
|
+
* Upper bound on a verification session's lifetime. The session also ends at
|
|
82
|
+
* the verification token's own `exp` (15 minutes today), whichever comes
|
|
83
|
+
* first, so this only matters if the server ever issues longer-lived tokens.
|
|
84
|
+
* Callers needing a fresher proof pass `maxSessionAgeMs` to
|
|
85
|
+
* `getVerificationToken`.
|
|
84
86
|
*/
|
|
85
|
-
export const
|
|
87
|
+
export const VERIFICATION_SESSION_TTL_MS = 15 * 60_000;
|
|
88
|
+
/**
|
|
89
|
+
* Default maximum age of a verification session that may authorize enrolling a
|
|
90
|
+
* credential. A session opened for an unrelated operation must not be able to
|
|
91
|
+
* add a factor long after the fact, while a session proven moments ago (or
|
|
92
|
+
* during the same setup flow, via `maxSessionAgeMs`) may.
|
|
93
|
+
*/
|
|
94
|
+
export const ENROLLMENT_MAX_SESSION_AGE_MS = 2 * 60_000;
|
|
95
|
+
/**
|
|
96
|
+
* Key of the only verification session held today. Sessions are keyed by token
|
|
97
|
+
* audience so audience-scoped tokens can be added without reshaping storage.
|
|
98
|
+
*/
|
|
99
|
+
const DEFAULT_AUDIENCE = '';
|
|
86
100
|
const MESSENGER_EXPOSED_METHODS = [
|
|
87
101
|
'performSignIn',
|
|
88
102
|
'performSignOut',
|
|
@@ -98,10 +112,10 @@ const MESSENGER_EXPOSED_METHODS = [
|
|
|
98
112
|
'refreshEnrolledCredentials',
|
|
99
113
|
'beginCredentialEnrollment',
|
|
100
114
|
'completeCredentialEnrollment',
|
|
101
|
-
'
|
|
102
|
-
'
|
|
103
|
-
'
|
|
104
|
-
'
|
|
115
|
+
'beginCredentialVerification',
|
|
116
|
+
'completeCredentialVerification',
|
|
117
|
+
'getVerificationToken',
|
|
118
|
+
'clearVerificationSession',
|
|
105
119
|
];
|
|
106
120
|
/**
|
|
107
121
|
* Controller that enables authentication for restricted endpoints.
|
|
@@ -121,8 +135,12 @@ export class AuthenticationController extends BaseController {
|
|
|
121
135
|
* ceremony started under the previous session cannot apply its result.
|
|
122
136
|
*/
|
|
123
137
|
#authSessionEpoch = 0;
|
|
124
|
-
|
|
125
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Verification sessions keyed by token audience. At most one session per
|
|
140
|
+
* audience; only `DEFAULT_AUDIENCE` is used until audience-scoped tokens
|
|
141
|
+
* exist.
|
|
142
|
+
*/
|
|
143
|
+
#verificationSessions = new Map();
|
|
126
144
|
/**
|
|
127
145
|
* Sequence number of the most recently started credentials refresh. Only
|
|
128
146
|
* that refresh may write the cache, so a slower, earlier request cannot
|
|
@@ -145,7 +163,7 @@ export class AuthenticationController extends BaseController {
|
|
|
145
163
|
this.messenger.subscribe('KeyringController:lock', () => {
|
|
146
164
|
this.#authSessionEpoch += 1;
|
|
147
165
|
this.#isUnlocked = false;
|
|
148
|
-
this.
|
|
166
|
+
this.clearVerificationSession();
|
|
149
167
|
});
|
|
150
168
|
},
|
|
151
169
|
};
|
|
@@ -604,8 +622,9 @@ export class AuthenticationController extends BaseController {
|
|
|
604
622
|
this.#setTraceAttribute(context, 'mfaErrorCode', mfaCode);
|
|
605
623
|
if (mfaCode === 'authentication_required' && this.#isUnlocked) {
|
|
606
624
|
this.#invalidateSrpSession(this.#getPrimaryEntropySourceId());
|
|
607
|
-
//
|
|
608
|
-
|
|
625
|
+
// A verification session must not outlive a rejected base
|
|
626
|
+
// session.
|
|
627
|
+
this.clearVerificationSession();
|
|
609
628
|
}
|
|
610
629
|
}
|
|
611
630
|
throw error;
|
|
@@ -664,9 +683,13 @@ export class AuthenticationController extends BaseController {
|
|
|
664
683
|
const sessionEpoch = this.#authSessionEpoch;
|
|
665
684
|
assertValidMfaRequest(request, BeginEnrollmentRequestStruct);
|
|
666
685
|
const primaryEntropySourceId = this.#getPrimaryEntropySourceId();
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
|
|
686
|
+
// The server requires AAL2 to add a credential once one that proves AAL2
|
|
687
|
+
// exists: send the verification token only while a recent enough session
|
|
688
|
+
// is live and let the server decide. An older session stays open for
|
|
689
|
+
// other consumers.
|
|
690
|
+
const accessToken = this.getVerificationToken({
|
|
691
|
+
maxSessionAgeMs: request.maxSessionAgeMs ?? ENROLLMENT_MAX_SESSION_AGE_MS,
|
|
692
|
+
})?.accessToken;
|
|
670
693
|
const challenge = await this.#runMfaRequest('MFA Enroll Begin', request.reason.operation, request.type, async () => await this.#auth.beginMfaEnrollment(request.type, {
|
|
671
694
|
email: request.email,
|
|
672
695
|
entropySourceId: primaryEntropySourceId,
|
|
@@ -705,8 +728,9 @@ export class AuthenticationController extends BaseController {
|
|
|
705
728
|
}
|
|
706
729
|
throw error;
|
|
707
730
|
}
|
|
708
|
-
// The
|
|
709
|
-
|
|
731
|
+
// The verification session is deliberately kept: adding a factor does not
|
|
732
|
+
// weaken an earlier proof, and `beginCredentialEnrollment` already limits
|
|
733
|
+
// which sessions may add the next one.
|
|
710
734
|
try {
|
|
711
735
|
return await this.refreshEnrolledCredentials();
|
|
712
736
|
}
|
|
@@ -720,74 +744,77 @@ export class AuthenticationController extends BaseController {
|
|
|
720
744
|
}
|
|
721
745
|
}
|
|
722
746
|
/**
|
|
723
|
-
* Begins
|
|
747
|
+
* Begins verification with an enrolled credential.
|
|
724
748
|
*
|
|
725
749
|
* @param request - Credential type and trace reason.
|
|
726
750
|
* @returns A challenge for the client-owned ceremony.
|
|
727
751
|
*/
|
|
728
|
-
async
|
|
729
|
-
this.#assertIsUnlocked('
|
|
752
|
+
async beginCredentialVerification(request) {
|
|
753
|
+
this.#assertIsUnlocked('beginCredentialVerification');
|
|
730
754
|
const sessionEpoch = this.#authSessionEpoch;
|
|
731
|
-
assertValidMfaRequest(request,
|
|
755
|
+
assertValidMfaRequest(request, BeginVerificationRequestStruct);
|
|
732
756
|
const primaryEntropySourceId = this.#getPrimaryEntropySourceId();
|
|
733
|
-
const challenge = await this.#runMfaRequest('MFA
|
|
734
|
-
this.#assertAuthSessionEpoch(sessionEpoch, '
|
|
757
|
+
const challenge = await this.#runMfaRequest('MFA Verification Begin', request.reason.operation, request.type, async () => await this.#auth.beginMfaVerification(request.type, primaryEntropySourceId));
|
|
758
|
+
this.#assertAuthSessionEpoch(sessionEpoch, 'beginCredentialVerification');
|
|
735
759
|
return challenge;
|
|
736
760
|
}
|
|
737
761
|
/**
|
|
738
|
-
* Completes
|
|
762
|
+
* Completes verification and opens a short-lived verification session.
|
|
739
763
|
*
|
|
740
|
-
* The
|
|
741
|
-
*
|
|
742
|
-
*
|
|
764
|
+
* The assertion returned by the MFA service is exchanged at Hydra for an
|
|
765
|
+
* access token. Its assurance level is not checked: the services receiving
|
|
766
|
+
* the token enforce their own requirements. The token itself never enters
|
|
767
|
+
* controller state.
|
|
743
768
|
*
|
|
744
769
|
* @param request - Flow identifier, platform or email proof, and trace reason.
|
|
745
|
-
* @returns The
|
|
770
|
+
* @returns The verification token.
|
|
746
771
|
*/
|
|
747
|
-
async
|
|
748
|
-
this.#assertIsUnlocked('
|
|
772
|
+
async completeCredentialVerification(request) {
|
|
773
|
+
this.#assertIsUnlocked('completeCredentialVerification');
|
|
749
774
|
const sessionEpoch = this.#authSessionEpoch;
|
|
750
|
-
assertValidMfaRequest(request,
|
|
775
|
+
assertValidMfaRequest(request, CompleteVerificationRequestStruct);
|
|
751
776
|
const { type } = request.proof;
|
|
752
777
|
const { operation } = request.reason;
|
|
753
778
|
const primaryEntropySourceId = this.#getPrimaryEntropySourceId();
|
|
754
|
-
const assertion = await this.#runMfaRequest('MFA
|
|
755
|
-
this.#assertAuthSessionEpoch(sessionEpoch, '
|
|
779
|
+
const assertion = await this.#runMfaRequest('MFA Verification Complete', operation, type, async () => await this.#auth.completeMfaVerification(request.flowId, request.proof, primaryEntropySourceId));
|
|
780
|
+
this.#assertAuthSessionEpoch(sessionEpoch, 'completeCredentialVerification');
|
|
756
781
|
const accessToken = await this.#runMfaRequest('MFA Token Exchange', operation, type, async () => await this.#auth.exchangeMfaAssertion(assertion.token));
|
|
757
|
-
this.#assertAuthSessionEpoch(sessionEpoch, '
|
|
782
|
+
this.#assertAuthSessionEpoch(sessionEpoch, 'completeCredentialVerification');
|
|
758
783
|
let decodedClaims;
|
|
759
784
|
try {
|
|
760
785
|
decodedClaims = decodeJwtPayload(accessToken.accessToken);
|
|
761
786
|
}
|
|
762
787
|
catch (error) {
|
|
763
|
-
throw new
|
|
788
|
+
throw new VerificationTokenInvalidError(toErrorMessage(error));
|
|
764
789
|
}
|
|
765
|
-
const claims =
|
|
790
|
+
const claims = parseVerificationTokenClaims(decodedClaims);
|
|
766
791
|
if (claims.exp * 1000 <= Date.now()) {
|
|
767
|
-
throw new
|
|
792
|
+
throw new VerificationTokenInvalidError('Verification token is expired');
|
|
768
793
|
}
|
|
769
794
|
const token = { ...accessToken, claims };
|
|
770
|
-
this.#
|
|
795
|
+
this.#openVerificationSession(token);
|
|
771
796
|
return token;
|
|
772
797
|
}
|
|
773
798
|
/**
|
|
774
|
-
* Returns the active
|
|
799
|
+
* Returns the active verification token when it meets the requested
|
|
800
|
+
* freshness.
|
|
775
801
|
*
|
|
776
802
|
* @param request - Optional maximum session age in milliseconds, measured
|
|
777
803
|
* from when the token was obtained. Zero always requires a new ceremony.
|
|
778
|
-
* @returns A live
|
|
804
|
+
* @returns A live verification token, or null when no reusable session
|
|
805
|
+
* exists.
|
|
779
806
|
*/
|
|
780
|
-
|
|
781
|
-
this.#assertIsUnlocked('
|
|
782
|
-
assertValidMfaRequest(request,
|
|
783
|
-
const session = this.#
|
|
807
|
+
getVerificationToken(request = {}) {
|
|
808
|
+
this.#assertIsUnlocked('getVerificationToken');
|
|
809
|
+
assertValidMfaRequest(request, GetVerificationTokenRequestStruct);
|
|
810
|
+
const session = this.#verificationSessions.get(DEFAULT_AUDIENCE);
|
|
784
811
|
if (!session) {
|
|
785
812
|
return null;
|
|
786
813
|
}
|
|
787
814
|
const now = Date.now();
|
|
788
815
|
if (now >= session.expiresAt) {
|
|
789
816
|
// The hard-expiry timer has not fired yet (e.g. a suspended tab).
|
|
790
|
-
this
|
|
817
|
+
this.#dropVerificationSession(DEFAULT_AUDIENCE);
|
|
791
818
|
return null;
|
|
792
819
|
}
|
|
793
820
|
// `>=` so a zero max age always forces a fresh ceremony. Not cleared: the
|
|
@@ -799,34 +826,53 @@ export class AuthenticationController extends BaseController {
|
|
|
799
826
|
return session.token;
|
|
800
827
|
}
|
|
801
828
|
/**
|
|
802
|
-
* Opens the
|
|
829
|
+
* Opens the verification session. Its lifetime is the session TTL clamped to
|
|
803
830
|
* the token's own `exp`, so the session never outlives the token.
|
|
804
831
|
*
|
|
805
|
-
* @param token - The freshly exchanged
|
|
832
|
+
* @param token - The freshly exchanged verification token.
|
|
806
833
|
*/
|
|
807
|
-
#
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
834
|
+
#openVerificationSession(token) {
|
|
835
|
+
const audience = DEFAULT_AUDIENCE;
|
|
836
|
+
this.#dropVerificationSession(audience);
|
|
837
|
+
const expiresAt = Math.min(token.obtainedAt + VERIFICATION_SESSION_TTL_MS, token.claims.exp * 1000);
|
|
838
|
+
const timer = setTimeout(() => this.#dropVerificationSession(audience), Math.max(0, expiresAt - Date.now()));
|
|
839
|
+
// Never keep a Node process alive for the expiry timer (tests, tooling).
|
|
840
|
+
timer.unref?.();
|
|
841
|
+
this.#verificationSessions.set(audience, { token, expiresAt, timer });
|
|
811
842
|
this.update((state) => {
|
|
812
|
-
state.
|
|
843
|
+
state.verificationSessionExpiresAt = expiresAt;
|
|
813
844
|
});
|
|
814
|
-
this.#stepUpTimer = setTimeout(() => this.clearStepUpSession(), Math.max(0, expiresAt - Date.now()));
|
|
815
|
-
// Never keep a Node process alive for the expiry timer (tests, tooling).
|
|
816
|
-
this.#stepUpTimer.unref?.();
|
|
817
845
|
}
|
|
818
846
|
/**
|
|
819
|
-
*
|
|
847
|
+
* Drops one audience's verification session and its expiration timer.
|
|
848
|
+
* `verificationSessionExpiresAt` mirrors the default audience only.
|
|
849
|
+
*
|
|
850
|
+
* @param audience - Audience whose session to drop.
|
|
851
|
+
*/
|
|
852
|
+
#dropVerificationSession(audience) {
|
|
853
|
+
const session = this.#verificationSessions.get(audience);
|
|
854
|
+
if (session) {
|
|
855
|
+
clearTimeout(session.timer);
|
|
856
|
+
this.#verificationSessions.delete(audience);
|
|
857
|
+
}
|
|
858
|
+
if (audience === DEFAULT_AUDIENCE &&
|
|
859
|
+
this.state.verificationSessionExpiresAt !== undefined) {
|
|
860
|
+
this.update((state) => {
|
|
861
|
+
state.verificationSessionExpiresAt = undefined;
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Clears every in-memory verification session and its expiration timer.
|
|
820
867
|
*/
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
clearTimeout(
|
|
824
|
-
this.#stepUpTimer = undefined;
|
|
868
|
+
clearVerificationSession() {
|
|
869
|
+
for (const { timer } of this.#verificationSessions.values()) {
|
|
870
|
+
clearTimeout(timer);
|
|
825
871
|
}
|
|
826
|
-
this.#
|
|
827
|
-
if (this.state.
|
|
872
|
+
this.#verificationSessions.clear();
|
|
873
|
+
if (this.state.verificationSessionExpiresAt !== undefined) {
|
|
828
874
|
this.update((state) => {
|
|
829
|
-
state.
|
|
875
|
+
state.verificationSessionExpiresAt = undefined;
|
|
830
876
|
});
|
|
831
877
|
}
|
|
832
878
|
}
|
|
@@ -848,7 +894,7 @@ export class AuthenticationController extends BaseController {
|
|
|
848
894
|
}
|
|
849
895
|
performSignOut() {
|
|
850
896
|
this.#authSessionEpoch += 1;
|
|
851
|
-
this.
|
|
897
|
+
this.clearVerificationSession();
|
|
852
898
|
this.#clearEnrolledCredentials();
|
|
853
899
|
this.update((state) => {
|
|
854
900
|
state.isSignedIn = false;
|
|
@@ -862,7 +908,7 @@ export class AuthenticationController extends BaseController {
|
|
|
862
908
|
clearState() {
|
|
863
909
|
this.#profilePairingRequestEpoch += 1;
|
|
864
910
|
this.#authSessionEpoch += 1;
|
|
865
|
-
this.
|
|
911
|
+
this.clearVerificationSession();
|
|
866
912
|
this.#clearEnrolledCredentials();
|
|
867
913
|
this.update(() => ({ ...defaultState }));
|
|
868
914
|
}
|