Separate second-factor verification and delivery from auth (#40)
This commit is contained in:
1 parent
12a38cd654
commit
ecb63b61c6
33 files changed
+1772
-123
No files matched your search
@@ -56,6 +56,8 @@ void main() {
|
||||
return response({
|
||||
'userId': 'user',
|
||||
'pendingToken': {'token': 'signed-pending'},
|
||||
'challengeId': 'server-challenge',
|
||||
'channel': 'email',
|
||||
'target': 'u***@example.com',
|
||||
'expiresAt': now.add(const Duration(minutes: 10)).toIso8601String(),
|
||||
'resendAt': now.add(const Duration(minutes: 1)).toIso8601String(),
|
||||
@@ -63,6 +65,7 @@ void main() {
|
||||
}
|
||||
expect(request.url.path, '/api/v1/accounts/login/verify');
|
||||
expect(body['pendingToken'], 'signed-pending');
|
||||
expect(body['challengeId'], 'server-challenge');
|
||||
if (body['code'] != '123456')
|
||||
return response({'error': 'invalid'}, 400);
|
||||
verified = true;
|
||||
@@ -145,4 +148,75 @@ void main() {
|
||||
),
|
||||
);
|
||||
});
|
||||
test(
|
||||
'resend preserves server challenge and retries the same delivery key',
|
||||
() async {
|
||||
final now = DateTime.now();
|
||||
final keys = <String>[];
|
||||
Map<String, Object?> challenge() => {
|
||||
'userId': 'user',
|
||||
'pendingToken': {'token': 'pending'},
|
||||
'challengeId': 'server-challenge',
|
||||
'channel': 'email',
|
||||
'target': 's***@example.com',
|
||||
'expiresAt': now.add(const Duration(minutes: 10)).toIso8601String(),
|
||||
'resendAt': now.add(const Duration(minutes: 1)).toIso8601String(),
|
||||
};
|
||||
final service = BackendAuthServices(
|
||||
baseUri: Uri.parse('https://api.example/api/v1'),
|
||||
clientId: 'web',
|
||||
client: MockClient((request) async {
|
||||
if (request.url.path.endsWith('/login')) return response(challenge());
|
||||
final body = jsonDecode(request.body);
|
||||
expect(body['challengeId'], 'server-challenge');
|
||||
expect(body['pendingToken'], 'pending');
|
||||
keys.add(body['idempotencyKey'] as String);
|
||||
if (keys.length == 1) return response({'error': 'unavailable'}, 503);
|
||||
return response(challenge());
|
||||
}),
|
||||
);
|
||||
addTearDown(service.close);
|
||||
final result = await service.login(
|
||||
const LoginRequest(
|
||||
email: 'user@example.com',
|
||||
password: 'password',
|
||||
locale: 'en',
|
||||
),
|
||||
);
|
||||
final id = result.challenge!.id;
|
||||
await expectLater(service.resend(id), throwsA(isA<FactorException>()));
|
||||
final resent = await service.resend(id);
|
||||
expect(resent.id, id);
|
||||
expect(resent.target, 's***@example.com');
|
||||
expect(keys[0], keys[1]);
|
||||
expect(service.session, isNull);
|
||||
},
|
||||
);
|
||||
for (final channel in ['sms', '']) {
|
||||
test(
|
||||
'unsupported or missing channel $channel cannot create a login challenge',
|
||||
() async {
|
||||
final service = BackendAuthServices(
|
||||
baseUri: Uri.parse('https://api.example/api/v1'),
|
||||
clientId: 'web',
|
||||
client: MockClient(
|
||||
(_) async =>
|
||||
response({'challengeId': 'server', 'channel': channel}),
|
||||
),
|
||||
);
|
||||
addTearDown(service.close);
|
||||
await expectLater(
|
||||
service.login(
|
||||
const LoginRequest(
|
||||
email: 'user@example.com',
|
||||
password: 'password',
|
||||
locale: 'en',
|
||||
),
|
||||
),
|
||||
throwsA(isA<FactorException>()),
|
||||
);
|
||||
expect(service.session, isNull);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user