Skip to content

Commit 1188243

Browse files
committed
use sealed class and deprecate old method
1 parent 9b79c68 commit 1188243

File tree

3 files changed

+148
-57
lines changed

3 files changed

+148
-57
lines changed

packages/gotrue/lib/src/gotrue_mfa_api.dart

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,37 +41,56 @@ class GoTrueMFAApi {
4141
/// [phone] : Phone number of the MFA factor in E.164 format. Used to send messages.
4242
///
4343
/// [friendlyName] : Human readable name assigned to the factor.
44+
@Deprecated('Use enrollWithParams instead for better type safety')
4445
Future<AuthMFAEnrollResponse> enroll({
4546
FactorType factorType = FactorType.totp,
4647
String? issuer,
4748
String? phone,
4849
String? friendlyName,
4950
}) async {
50-
if (factorType == FactorType.phone && phone == null) {
51-
throw ArgumentError('Phone number is required for phone factor type');
51+
if (factorType == FactorType.phone && phone != null) {
52+
return enrollWithParams(
53+
PhoneEnrollParams(phone: phone, friendlyName: friendlyName));
5254
}
5355

54-
if (factorType == FactorType.totp && issuer == null) {
55-
throw ArgumentError('Issuer is required for totp factor type');
56+
if (factorType == FactorType.totp && issuer != null) {
57+
return enrollWithParams(
58+
TOTPEnrollParams(issuer: issuer, friendlyName: friendlyName));
5659
}
5760

61+
throw ArgumentError('Invalid arguments, expected either phone or issuer.');
62+
}
63+
64+
/// Starts the enrollment process for a new Multi-Factor Authentication (MFA) factor.
65+
/// This method creates a new `unverified` factor.
66+
/// To verify a factor, present the QR code or secret to the user and ask them to add it to their authenticator app.
67+
///
68+
/// The user has to enter the code from their authenticator app to verify it.
69+
///
70+
/// Upon verifying a factor, all other sessions are logged out and the current session's authenticator level is promoted to `aal2`.
71+
///
72+
/// [params] : Type-safe parameters for enrolling a new MFA factor.
73+
Future<AuthMFAEnrollResponse> enrollWithParams(MFAEnrollParams params) async {
5874
final session = _client.currentSession;
5975
final data = await _fetch.request(
6076
'${_client._url}/factors',
6177
RequestMethodType.post,
6278
options: GotrueRequestOptions(
6379
headers: _client._headers,
6480
body: {
65-
'friendly_name': friendlyName,
66-
'factor_type': factorType.name,
67-
if (factorType == FactorType.totp) 'issuer': issuer,
68-
if (factorType == FactorType.phone) 'phone': phone,
81+
'friendly_name': params.friendlyName,
82+
'factor_type': switch (params) {
83+
TOTPEnrollParams() => FactorType.totp.name,
84+
PhoneEnrollParams() => FactorType.phone.name,
85+
},
86+
if (params is TOTPEnrollParams) 'issuer': params.issuer,
87+
if (params is PhoneEnrollParams) 'phone': params.phone,
6988
},
7089
jwt: session?.accessToken,
7190
),
7291
);
7392

74-
if (factorType == FactorType.totp &&
93+
if (params is TOTPEnrollParams &&
7594
data['totp'] != null &&
7695
data['totp']['qr_code'] != null) {
7796
data['totp']['qr_code'] =

packages/gotrue/lib/src/types/mfa.dart

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ class AuthMFAEnrollResponse {
2525
id: json['id'],
2626
type: FactorType.values.firstWhere((e) => e.name == json['type']),
2727
totp: json['totp'] != null ? TOTPEnrollment.fromJson(json['totp']) : null,
28-
phone: json['phone'] != null ? PhoneEnrollment.fromJson(json['phone']) : null,
28+
phone: json['phone'] != null
29+
? PhoneEnrollment.fromJson(json['phone'])
30+
: null,
2931
);
3032
}
3133
}
@@ -337,3 +339,38 @@ class AMREntry {
337339
);
338340
}
339341
}
342+
343+
/// Parameters for enrolling a new MFA factor
344+
sealed class MFAEnrollParams {
345+
/// Friendly name of the factor, useful to disambiguate between multiple factors.
346+
final String? friendlyName;
347+
348+
/// Type of factor being enrolled.
349+
final FactorType factorType;
350+
351+
const MFAEnrollParams({this.friendlyName, required this.factorType});
352+
}
353+
354+
/// Parameters for enrolling a TOTP factor
355+
class TOTPEnrollParams extends MFAEnrollParams {
356+
/// Domain which the user is enrolled with.
357+
final String issuer;
358+
359+
const TOTPEnrollParams({
360+
required this.issuer,
361+
super.friendlyName,
362+
super.factorType = FactorType.totp,
363+
});
364+
}
365+
366+
/// Parameters for enrolling a phone factor
367+
class PhoneEnrollParams extends MFAEnrollParams {
368+
/// Phone number of the MFA factor in E.164 format. Used to send messages.
369+
final String phone;
370+
371+
const PhoneEnrollParams({
372+
required this.phone,
373+
super.friendlyName,
374+
super.factorType = FactorType.phone,
375+
});
376+
}

packages/gotrue/test/src/gotrue_mfa_api_test.dart

Lines changed: 82 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -34,55 +34,90 @@ void main() {
3434
});
3535

3636
group('enroll', () {
37-
test('totp', () async {
38-
await client.signInWithPassword(password: password, email: email1);
39-
40-
final res = await client.mfa
41-
.enroll(issuer: 'MyFriend', friendlyName: 'MyFriendName');
42-
final uri = Uri.parse(res.totp!.uri);
43-
44-
expect(res.type, FactorType.totp);
45-
expect(uri.queryParameters['issuer'], 'MyFriend');
46-
expect(uri.scheme, 'otpauth');
47-
});
48-
49-
test('phone', () async {
50-
await client.signInWithPassword(password: password, email: email1);
51-
52-
expect(
53-
() => client.mfa
54-
.enroll(factorType: FactorType.phone, phone: '+1234567890'),
55-
throwsA(isA<AuthApiException>().having(
56-
(e) => e.message,
57-
'message',
58-
'MFA enroll is disabled for Phone',
59-
)),
60-
);
61-
});
62-
63-
test(
64-
'throws ArgumentError when phone factor type is used without phone number',
65-
() async {
66-
expect(
67-
() => client.mfa.enroll(factorType: FactorType.phone),
68-
throwsA(isA<ArgumentError>().having(
69-
(e) => e.message,
70-
'message',
71-
'Phone number is required for phone factor type',
72-
)),
73-
);
37+
group('deprecated method', () {
38+
test('totp', () async {
39+
await client.signInWithPassword(password: password, email: email1);
40+
41+
final res = await client.mfa.enroll(
42+
issuer: 'MyFriend',
43+
friendlyName: 'MyFriendName',
44+
);
45+
final uri = Uri.parse(res.totp!.uri);
46+
47+
expect(res.type, FactorType.totp);
48+
expect(uri.queryParameters['issuer'], 'MyFriend');
49+
expect(uri.scheme, 'otpauth');
50+
});
51+
52+
test('phone', () async {
53+
await client.signInWithPassword(password: password, email: email1);
54+
55+
expect(
56+
() => client.mfa.enroll(
57+
factorType: FactorType.phone,
58+
phone: '+1234567890',
59+
),
60+
throwsA(isA<AuthApiException>().having(
61+
(e) => e.message,
62+
'message',
63+
'MFA enroll is disabled for Phone',
64+
)),
65+
);
66+
});
67+
68+
test('throws ArgumentError when invalid arguments are provided',
69+
() async {
70+
expect(
71+
() => client.mfa.enroll(factorType: FactorType.phone),
72+
throwsA(isA<ArgumentError>().having(
73+
(e) => e.message,
74+
'message',
75+
'Invalid arguments, expected either phone or issuer.',
76+
)),
77+
);
78+
79+
expect(
80+
() => client.mfa.enroll(factorType: FactorType.totp),
81+
throwsA(isA<ArgumentError>().having(
82+
(e) => e.message,
83+
'message',
84+
'Invalid arguments, expected either phone or issuer.',
85+
)),
86+
);
87+
});
7488
});
7589

76-
test('throws ArgumentError when totp factor type is used without issuer',
77-
() async {
78-
expect(
79-
() => client.mfa.enroll(factorType: FactorType.totp),
80-
throwsA(isA<ArgumentError>().having(
81-
(e) => e.message,
82-
'message',
83-
'Issuer is required for totp factor type',
84-
)),
85-
);
90+
group('enrollWithParams', () {
91+
test('totp', () async {
92+
await client.signInWithPassword(password: password, email: email1);
93+
94+
final res = await client.mfa.enrollWithParams(
95+
TOTPEnrollParams(
96+
issuer: 'MyFriend',
97+
friendlyName: 'MyFriendName',
98+
),
99+
);
100+
final uri = Uri.parse(res.totp!.uri);
101+
102+
expect(res.type, FactorType.totp);
103+
expect(uri.queryParameters['issuer'], 'MyFriend');
104+
expect(uri.scheme, 'otpauth');
105+
});
106+
107+
test('phone', () async {
108+
await client.signInWithPassword(password: password, email: email1);
109+
110+
expect(
111+
() => client.mfa.enrollWithParams(
112+
PhoneEnrollParams(phone: '+1234567890'),
113+
),
114+
throwsA(isA<AuthApiException>().having(
115+
(e) => e.message,
116+
'message',
117+
'MFA enroll is disabled for Phone',
118+
)),
119+
);
120+
});
86121
});
87122
});
88123

0 commit comments

Comments
 (0)