feat(auth)!: codes by mail no longer sign in
ADR 0017, step 3 (D4: nobody signs in with email codes yet, so the password replaces them outright). `emailCode.verifyCode`, `emailSignIn.signIn` and the switch token go, with the register's `verifySignInCode` and `completeEmailSwitch` and their operations between services. `emailCode.sendCode` stays, now without a session (signing up and getting back in have none), as what proves an address for a password's setup, a sign-up, a setup never finished and the resets; `accountRecovery.sendCode`, which did that for the resets, goes. The `switchChallengeId` column stays: Serverpod's challenges still link a completion token, and older servers read it during a deploy. The apps: halkyon_app's email sign-in (its bloc, sheet, gateway calls and the start screen's Continue with email) goes; Learn's profile and Todo's account sheet show `AccountSecuritySection` (a password set up by someone anonymous or signing in with GitHub, whose end it names; the authenticators; changing the password; new recovery codes and phrase), which reads anew after every change the repository makes. BREAKING CHANGE: Learn's `minimumAppBuild` is 22: builds up to 21 sign in with the endpoints that went. Todo was never released and stays at build 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
735dfa892a
commit
ae57cbe864
7 changed files with 429 additions and 3835 deletions
|
|
@ -1,4 +1,3 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
|
|
@ -60,7 +59,7 @@ final class _Refusing implements CodeMailer {
|
|||
|
||||
void main() {
|
||||
withServerpod(
|
||||
'Email sign-in',
|
||||
'Codes by mail',
|
||||
// Mailing and accepting codes count attempts in transactions of their
|
||||
// own (Serverpod's rate limiter), which the test's database rollback
|
||||
// can't wrap. So these tests delete what they make instead.
|
||||
|
|
@ -75,11 +74,9 @@ void main() {
|
|||
|
||||
/// Identified, signing in with ben@example.org.
|
||||
late UuidValue ben;
|
||||
late UuidValue bensLaptop;
|
||||
|
||||
/// Identified, signing in with GitHub.
|
||||
late UuidValue carla;
|
||||
late UuidValue carlasPhone;
|
||||
|
||||
setUp(() async {
|
||||
useBase(sessionBuilder);
|
||||
|
|
@ -106,7 +103,7 @@ void main() {
|
|||
createdAt: clock.now(),
|
||||
),
|
||||
);
|
||||
bensLaptop = await signInNewDevice(
|
||||
await signInNewDevice(
|
||||
session,
|
||||
ben,
|
||||
scopes: {identified},
|
||||
|
|
@ -119,7 +116,7 @@ void main() {
|
|||
session,
|
||||
GitHubAccount(userIdentifier: '1001', authUserId: carla),
|
||||
);
|
||||
carlasPhone = await signInNewDevice(
|
||||
await signInNewDevice(
|
||||
session,
|
||||
carla,
|
||||
scopes: {identified},
|
||||
|
|
@ -142,50 +139,31 @@ void main() {
|
|||
),
|
||||
);
|
||||
TestSessionBuilder asAnna() => signedIn(anna, annasPhone);
|
||||
TestSessionBuilder asBen() =>
|
||||
signedIn(ben, bensLaptop, isIdentified: true);
|
||||
TestSessionBuilder asCarla() =>
|
||||
signedIn(carla, carlasPhone, isIdentified: true);
|
||||
TestSessionBuilder notSignedIn() => sessionBuilder.copyWith(
|
||||
authentication: AuthenticationOverride.unauthenticated(),
|
||||
);
|
||||
|
||||
/// Mails a code to [address] for [as] and types it.
|
||||
Future<EmailCodeAccepted> signInWith(
|
||||
TestSessionBuilder as,
|
||||
String address,
|
||||
) async {
|
||||
final request = await endpoints.emailCode.sendCode(
|
||||
as,
|
||||
address: address,
|
||||
language: 'en',
|
||||
);
|
||||
return await endpoints.emailCode.verifyCode(
|
||||
as,
|
||||
request: request,
|
||||
code: mailbox.codeFor(address.trim().toLowerCase()),
|
||||
);
|
||||
}
|
||||
|
||||
group('refuses', () {
|
||||
test('mailing and accepting codes when not signed in', () async {
|
||||
await expectLater(
|
||||
endpoints.emailCode.sendCode(
|
||||
notSignedIn(),
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
throwsA(isA<ServerpodUnauthenticatedException>()),
|
||||
test('nothing that turns a code into a session: a code signs nobody '
|
||||
"in, and makes no address anyone's", () async {
|
||||
final request = await endpoints.emailCode.sendCode(
|
||||
notSignedIn(),
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.emailCode.verifyCode(
|
||||
notSignedIn(),
|
||||
request: const Uuid().v7obj(),
|
||||
code: 'K7QX-M2PD',
|
||||
|
||||
expect(
|
||||
await proveAddressCode(
|
||||
session,
|
||||
request: request,
|
||||
code: mailbox.codeFor('anna@example.org'),
|
||||
),
|
||||
throwsA(isA<ServerpodUnauthenticatedException>()),
|
||||
addressHash(session.serverpod, 'anna@example.org'),
|
||||
);
|
||||
expect(mailbox.sent, isEmpty);
|
||||
expect(await _addressesOf(session, anna), isEmpty);
|
||||
expect(await ServerSideSession.db.count(session), 3);
|
||||
expect(await EmailCodeRequest.db.count(session), 0);
|
||||
});
|
||||
|
||||
test(
|
||||
|
|
@ -247,8 +225,8 @@ void main() {
|
|||
);
|
||||
for (var i = 0; i < 3; i++) {
|
||||
await expectLater(
|
||||
endpoints.emailCode.verifyCode(
|
||||
asAnna(),
|
||||
proveAddressCode(
|
||||
session,
|
||||
request: request,
|
||||
code: 'AAAA-AAAA',
|
||||
),
|
||||
|
|
@ -256,8 +234,8 @@ void main() {
|
|||
);
|
||||
}
|
||||
await expectLater(
|
||||
endpoints.emailCode.verifyCode(
|
||||
asAnna(),
|
||||
proveAddressCode(
|
||||
session,
|
||||
request: request,
|
||||
code: mailbox.codeFor('anna@example.org'),
|
||||
),
|
||||
|
|
@ -277,8 +255,8 @@ void main() {
|
|||
await withClock(
|
||||
Clock.fixed(clock.now().add(const Duration(minutes: 16))),
|
||||
() => expectLater(
|
||||
endpoints.emailCode.verifyCode(
|
||||
asAnna(),
|
||||
proveAddressCode(
|
||||
session,
|
||||
request: late,
|
||||
code: lateCode,
|
||||
),
|
||||
|
|
@ -298,8 +276,8 @@ void main() {
|
|||
language: 'en',
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.emailCode.verifyCode(
|
||||
asAnna(),
|
||||
proveAddressCode(
|
||||
session,
|
||||
request: replaced,
|
||||
code: replacedCode,
|
||||
),
|
||||
|
|
@ -307,14 +285,14 @@ void main() {
|
|||
);
|
||||
|
||||
final newerCode = mailbox.codeFor('anna@example.org');
|
||||
await endpoints.emailCode.verifyCode(
|
||||
asAnna(),
|
||||
await proveAddressCode(
|
||||
session,
|
||||
request: newer,
|
||||
code: newerCode,
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.emailCode.verifyCode(
|
||||
asAnna(),
|
||||
proveAddressCode(
|
||||
session,
|
||||
request: newer,
|
||||
code: newerCode,
|
||||
),
|
||||
|
|
@ -322,19 +300,6 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'a second address for a person who has one; nothing changes',
|
||||
() async {
|
||||
await expectLater(
|
||||
signInWith(asBen(), 'ben@elsewhere.org'),
|
||||
throwsA(_refusal(EmailSignInRefusal.otherAddressLinked)),
|
||||
);
|
||||
expect(await _addressesOf(session, ben), [
|
||||
addressHash(session.serverpod, 'ben@example.org'),
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
test('a fourth code for one address within an hour, and an '
|
||||
'eleventh from one network', () async {
|
||||
for (var i = 0; i < 3; i++) {
|
||||
|
|
@ -370,21 +335,6 @@ void main() {
|
|||
expect(mailbox.sent, hasLength(10));
|
||||
});
|
||||
|
||||
test('a switch token used twice, or made up', () async {
|
||||
final token = (await signInWith(
|
||||
asAnna(),
|
||||
'ben@example.org',
|
||||
)).switchToken!;
|
||||
await endpoints.emailSignIn.signIn(notSignedIn(), switchToken: token);
|
||||
|
||||
for (final used in [token, 'made-up']) {
|
||||
await expectLater(
|
||||
endpoints.emailSignIn.signIn(notSignedIn(), switchToken: used),
|
||||
throwsA(_refusal(EmailSignInRefusal.codeExpired)),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('saying how someone signs in when not signed in', () async {
|
||||
await expectLater(
|
||||
endpoints.account.signInWays(notSignedIn()),
|
||||
|
|
@ -417,106 +367,6 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test("a code for an address nobody has makes it the person's: they "
|
||||
'become identified, on every session, and stop being anonymous; '
|
||||
'the instance keeps only a keyed hash of it', () async {
|
||||
final request = await endpoints.emailCode.sendCode(
|
||||
asAnna(),
|
||||
address: ' Anna@Example.org',
|
||||
language: 'en',
|
||||
);
|
||||
final code = mailbox.codeFor('anna@example.org');
|
||||
final accepted = await endpoints.emailCode.verifyCode(
|
||||
asAnna(),
|
||||
request: request,
|
||||
// As typed off the mail: lower case, a space for the dash.
|
||||
code: code.toLowerCase().replaceAll('-', ' '),
|
||||
);
|
||||
|
||||
expect(accepted.switchToken, isNull);
|
||||
final hash = addressHash(session.serverpod, 'anna@example.org');
|
||||
expect(hash, matches(RegExp(r'^[0-9a-f]{64}$')));
|
||||
expect(await _addressesOf(session, anna), [hash]);
|
||||
final person = await AuthServices.instance.authUsers.get(
|
||||
session,
|
||||
authUserId: anna,
|
||||
);
|
||||
expect(person.scopeNames, contains(identified.name));
|
||||
expect(
|
||||
(await ServerSideSession.db.findById(
|
||||
session,
|
||||
annasPhone,
|
||||
))?.scopeNames,
|
||||
contains(identified.name),
|
||||
);
|
||||
expect(
|
||||
await AnonymousAccount.db.count(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(anna),
|
||||
),
|
||||
0,
|
||||
);
|
||||
expect(await EmailCodeRequest.db.count(session), 0);
|
||||
final kept = jsonEncode([
|
||||
...await EmailIdentity.db.find(session),
|
||||
...await EmailCodeRequest.db.find(session),
|
||||
...await RateLimitedRequestAttempt.db.find(session),
|
||||
]);
|
||||
expect(kept.toLowerCase(), isNot(contains('anna')));
|
||||
expect(kept, isNot(contains(code)));
|
||||
});
|
||||
|
||||
test('a code for the address the person signs in with changes '
|
||||
'nothing', () async {
|
||||
final accepted = await signInWith(asBen(), 'ben@example.org');
|
||||
|
||||
expect(accepted.switchToken, isNull);
|
||||
expect(await EmailIdentity.db.count(session), 1);
|
||||
});
|
||||
|
||||
test("a code for another person's address changes nothing here, and "
|
||||
'gives a token that signs in as them, without a session', () async {
|
||||
final accepted = await signInWith(asAnna(), 'ben@example.org');
|
||||
|
||||
expect(accepted.switchToken, isNotNull);
|
||||
expect(await _addressesOf(session, anna), isEmpty);
|
||||
expect(
|
||||
await AnonymousAccount.db.count(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(anna),
|
||||
),
|
||||
1,
|
||||
);
|
||||
|
||||
final signedIn = await endpoints.emailSignIn.signIn(
|
||||
notSignedIn(),
|
||||
switchToken: accepted.switchToken!,
|
||||
);
|
||||
expect(signedIn.authUserId, ben);
|
||||
expect(signedIn.scopeNames, contains(identified.name));
|
||||
final sessions = await ServerSideSession.db.find(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(ben) & t.method.equals('email'),
|
||||
);
|
||||
expect(sessions, hasLength(1));
|
||||
expect(await EmailCodeRequest.db.count(session), 0);
|
||||
});
|
||||
|
||||
test('a person who signs in with GitHub adds an address', () async {
|
||||
Future<(bool, bool)> waysOf(TestSessionBuilder person) async {
|
||||
final ways = await endpoints.account.signInWays(person);
|
||||
return (ways.gitHub, ways.email);
|
||||
}
|
||||
|
||||
expect(await waysOf(asCarla()), (true, false));
|
||||
|
||||
final accepted = await signInWith(asCarla(), 'carla@example.org');
|
||||
|
||||
expect(accepted.switchToken, isNull);
|
||||
expect(await waysOf(asCarla()), (true, true));
|
||||
expect(await waysOf(asAnna()), (false, false));
|
||||
});
|
||||
|
||||
test('tells the app that this instance offers it', () async {
|
||||
expect(
|
||||
(await endpoints.instance.basics(notSignedIn())).emailSignIn,
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ void main() {
|
|||
Future<({UuidValue request, String code})> mailedCode(
|
||||
String address,
|
||||
) async {
|
||||
final request = await endpoints.accountRecovery.sendCode(
|
||||
final request = await endpoints.emailCode.sendCode(
|
||||
notSignedIn(),
|
||||
address: address,
|
||||
language: 'en',
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ void main() {
|
|||
Future<({UuidValue request, String code})> mailedCode(
|
||||
String address,
|
||||
) async {
|
||||
final request = await endpoints.accountRecovery.sendCode(
|
||||
final request = await endpoints.emailCode.sendCode(
|
||||
notSignedIn(),
|
||||
address: address,
|
||||
language: 'en',
|
||||
|
|
@ -674,37 +674,13 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('refuses email codes and GitHub for an account that signs in '
|
||||
'with a password and an authenticator, not before', () async {
|
||||
final ben = await newAnonymousPerson(session);
|
||||
final bensLaptop = await signInNewDevice(session, ben);
|
||||
final asBen = sessionBuilder.copyWith(
|
||||
authentication: AuthenticationOverride.authenticationInfo(
|
||||
ben.uuid,
|
||||
{},
|
||||
authId: bensLaptop.uuid,
|
||||
),
|
||||
);
|
||||
test('refuses GitHub for an account that signs in with a password '
|
||||
'and an authenticator, not before', () async {
|
||||
await annaSetsPassword();
|
||||
await GitHubAccount.db.insertRow(
|
||||
session,
|
||||
GitHubAccount(userIdentifier: '1001', authUserId: anna),
|
||||
);
|
||||
Future<EmailCodeAccepted> switchByCode() async {
|
||||
final request = await endpoints.emailCode.sendCode(
|
||||
asBen,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
return await endpoints.emailCode.verifyCode(
|
||||
asBen,
|
||||
request: request,
|
||||
code: mailbox.codeFor('anna@example.org'),
|
||||
);
|
||||
}
|
||||
|
||||
final early = await switchByCode();
|
||||
expect(early.switchToken, isNotNull);
|
||||
expect(await signsInWithPassword(session, anna), isFalse);
|
||||
expect(await gitHubAccountToSignIn(session, '1001'), anna);
|
||||
|
||||
|
|
@ -722,17 +698,6 @@ void main() {
|
|||
});
|
||||
|
||||
expect(await signsInWithPassword(session, anna), isTrue);
|
||||
await expectLater(
|
||||
endpoints.emailSignIn.signIn(
|
||||
notSignedIn(),
|
||||
switchToken: early.switchToken!,
|
||||
),
|
||||
refusedAsEmail(EmailSignInRefusal.usePassword),
|
||||
);
|
||||
await expectLater(
|
||||
switchByCode(),
|
||||
refusedAsEmail(EmailSignInRefusal.usePassword),
|
||||
);
|
||||
await expectLater(
|
||||
gitHubAccountToSignIn(session, '1001'),
|
||||
throwsA(
|
||||
|
|
@ -1262,26 +1227,6 @@ void main() {
|
|||
);
|
||||
// Nor does a code by mail or GitHub sign in in its place.
|
||||
expect(await signsInWithPassword(session, anna), isTrue);
|
||||
final ben = await newAnonymousPerson(session);
|
||||
final asBen = sessionBuilder.copyWith(
|
||||
authentication: AuthenticationOverride.authenticationInfo(
|
||||
ben.uuid,
|
||||
{},
|
||||
),
|
||||
);
|
||||
final request = await endpoints.emailCode.sendCode(
|
||||
asBen,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.emailCode.verifyCode(
|
||||
asBen,
|
||||
request: request,
|
||||
code: mailbox.codeFor('anna@example.org'),
|
||||
),
|
||||
refusedAsEmail(EmailSignInRefusal.usePassword),
|
||||
);
|
||||
await expectLater(
|
||||
gitHubAccountToSignIn(session, '1001'),
|
||||
throwsA(
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ void main() {
|
|||
Future<({UuidValue request, String code})> mailedCode(
|
||||
String address,
|
||||
) async {
|
||||
final request = await endpoints.accountRecovery.sendCode(
|
||||
final request = await endpoints.emailCode.sendCode(
|
||||
notSignedIn(),
|
||||
address: address,
|
||||
language: 'en',
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ final class _Register implements AccountRegister {
|
|||
final gitHub = <String, UuidValue>{};
|
||||
final profiles = <UuidValue, Profile>{};
|
||||
final codes = <UuidValue, ({String address, String code})>{};
|
||||
final switches = <String, UuidValue>{};
|
||||
|
||||
/// The calls asked, by name, in their order.
|
||||
final asked = <String>[];
|
||||
|
|
@ -163,40 +162,6 @@ final class _Register implements AccountRegister {
|
|||
return request;
|
||||
});
|
||||
|
||||
@override
|
||||
Future<EmailCodeAccepted> verifySignInCode(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required UuidValue request,
|
||||
required String code,
|
||||
}) => _answer('verifySignInCode', () {
|
||||
final sent = codes[request];
|
||||
if (sent == null || sent.code != code) {
|
||||
throw EmailSignInRefusedException(reason: EmailSignInRefusal.wrongCode);
|
||||
}
|
||||
final owner = addresses[sent.address];
|
||||
if (owner != null && owner != person) {
|
||||
final token = 'switch to ${owner.uuid}';
|
||||
switches[token] = owner;
|
||||
return EmailCodeAccepted(switchToken: token);
|
||||
}
|
||||
add(person, address: sent.address);
|
||||
return EmailCodeAccepted();
|
||||
});
|
||||
|
||||
@override
|
||||
Future<UuidValue> completeEmailSwitch(
|
||||
Session session, {
|
||||
required String switchToken,
|
||||
}) => _answer(
|
||||
'completeEmailSwitch',
|
||||
() =>
|
||||
switches.remove(switchToken) ??
|
||||
(throw EmailSignInRefusedException(
|
||||
reason: EmailSignInRefusal.codeExpired,
|
||||
)),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> linkGitHub(
|
||||
Session session, {
|
||||
|
|
@ -574,97 +539,6 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('an address nobody has makes the person here identified, under '
|
||||
'the same ID, the register making them its account', () async {
|
||||
final request = await endpoints.emailCode.sendCode(
|
||||
asAnna(),
|
||||
address: 'anna@example.org',
|
||||
language: 'de',
|
||||
);
|
||||
|
||||
final accepted = await endpoints.emailCode.verifyCode(
|
||||
asAnna(),
|
||||
request: request,
|
||||
code: 'K7QX-M2PD',
|
||||
);
|
||||
|
||||
expect(accepted.switchToken, isNull);
|
||||
expect(register.accounts, contains(anna));
|
||||
expect(await scopesOf(anna), contains(identified.name));
|
||||
expect(
|
||||
(await ServerSideSession.db.findById(
|
||||
session,
|
||||
annasPhone,
|
||||
))!.scopeNames,
|
||||
contains(identified.name),
|
||||
);
|
||||
expect(
|
||||
await AnonymousAccount.db.count(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(anna),
|
||||
),
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
test('a wrong code changes nothing here', () async {
|
||||
final request = await endpoints.emailCode.sendCode(
|
||||
asAnna(),
|
||||
address: 'anna@example.org',
|
||||
language: 'de',
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
endpoints.emailCode.verifyCode(
|
||||
asAnna(),
|
||||
request: request,
|
||||
code: 'AAAA-AAAA',
|
||||
),
|
||||
throwsA(isA<EmailSignInRefusedException>()),
|
||||
);
|
||||
expect(await scopesOf(anna), isNot(contains(identified.name)));
|
||||
});
|
||||
|
||||
test("another account's address gives a switch, which signs in as "
|
||||
'that account here, made a person here under its ID the first '
|
||||
'time', () async {
|
||||
final carla = const Uuid().v7obj();
|
||||
register.add(carla, address: 'carla@example.org');
|
||||
final request = await endpoints.emailCode.sendCode(
|
||||
asAnna(),
|
||||
address: 'carla@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
final accepted = await endpoints.emailCode.verifyCode(
|
||||
asAnna(),
|
||||
request: request,
|
||||
code: 'K7QX-M2PD',
|
||||
);
|
||||
expect(accepted.switchToken, isNotNull);
|
||||
expect(await scopesOf(anna), isNot(contains(identified.name)));
|
||||
expect(await AuthUser.db.findById(session, carla), isNull);
|
||||
|
||||
final signedIn = await endpoints.emailSignIn.signIn(
|
||||
notSignedIn(),
|
||||
switchToken: accepted.switchToken!,
|
||||
);
|
||||
|
||||
expect(signedIn.authUserId, carla);
|
||||
expect(await scopesOf(carla), {identified.name});
|
||||
final sessions = await ServerSideSession.db.find(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(carla),
|
||||
);
|
||||
expect(sessions.single.scopeNames, {identified.name});
|
||||
await expectLater(
|
||||
endpoints.emailSignIn.signIn(
|
||||
notSignedIn(),
|
||||
switchToken: accepted.switchToken!,
|
||||
),
|
||||
throwsA(isA<EmailSignInRefusedException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('an account signing in again is the same person here', () async {
|
||||
await issueSession(session, ben, method: 'test');
|
||||
await issueSession(session, ben, method: 'test');
|
||||
|
|
|
|||
|
|
@ -354,78 +354,45 @@ void main() {
|
|||
expect(mailbox.sent, hasLength(11));
|
||||
});
|
||||
|
||||
test("makes a member's anonymous person an account under the same "
|
||||
'ID, identified, with the address', () async {
|
||||
test("a code proves the address for a member's password, and a "
|
||||
'wrong one refuses it, keeping nothing', () async {
|
||||
usePasswordKeys(pod, testPasswordKeys);
|
||||
addTearDown(() => usePasswordKeys(pod, null));
|
||||
final device = await PasswordDevice.make();
|
||||
final annaOnTodo = const Uuid().v7obj();
|
||||
|
||||
final request = await remote.sendSignInCode(
|
||||
session,
|
||||
address: 'Anna@example.org',
|
||||
language: 'de',
|
||||
network: '203.0.113.7',
|
||||
);
|
||||
final accepted = await remote.verifySignInCode(
|
||||
session,
|
||||
person: annaOnTodo,
|
||||
request: request,
|
||||
code: mailbox.codeFor('anna@example.org'),
|
||||
);
|
||||
|
||||
expect(accepted.switchToken, isNull);
|
||||
final account = await AuthUser.db.findById(session, annaOnTodo);
|
||||
expect(account!.scopeNames, contains(identified.name));
|
||||
expect(
|
||||
(await remote.signInWaysOf(session, annaOnTodo)).toJson(),
|
||||
SignInWays(gitHub: false, email: true).toJson(),
|
||||
);
|
||||
expect(await remote.knownAccounts(session, {annaOnTodo, ben}), {
|
||||
annaOnTodo,
|
||||
ben,
|
||||
});
|
||||
});
|
||||
|
||||
test('switches to the account that has the address, once, and '
|
||||
'refuses a wrong code', () async {
|
||||
final annaOnTodo = const Uuid().v7obj();
|
||||
final request = await remote.sendSignInCode(
|
||||
session,
|
||||
address: 'ben@example.org',
|
||||
language: 'en',
|
||||
network: '203.0.113.7',
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
remote.verifySignInCode(
|
||||
remote.setPassword(
|
||||
session,
|
||||
person: annaOnTodo,
|
||||
setup: device.setup(),
|
||||
request: request,
|
||||
code: 'AAAA-AAAA',
|
||||
),
|
||||
refused(EmailSignInRefusal.wrongCode),
|
||||
);
|
||||
final accepted = await remote.verifySignInCode(
|
||||
expect(await AuthUser.db.findById(session, annaOnTodo), isNull);
|
||||
await remote.setPassword(
|
||||
session,
|
||||
person: annaOnTodo,
|
||||
setup: device.setup(),
|
||||
request: request,
|
||||
code: mailbox.codeFor('ben@example.org'),
|
||||
code: mailbox.codeFor('anna@example.org'),
|
||||
);
|
||||
|
||||
expect(accepted.switchToken, isNotNull);
|
||||
expect(await AuthUser.db.findById(session, annaOnTodo), isNull);
|
||||
expect(
|
||||
await remote.completeEmailSwitch(
|
||||
session,
|
||||
switchToken: accepted.switchToken!,
|
||||
),
|
||||
(await AuthUser.db.findById(session, annaOnTodo))!.scopeNames,
|
||||
contains(identified.name),
|
||||
);
|
||||
expect(await remote.knownAccounts(session, {annaOnTodo, ben}), {
|
||||
annaOnTodo,
|
||||
ben,
|
||||
);
|
||||
await expectLater(
|
||||
remote.completeEmailSwitch(
|
||||
session,
|
||||
switchToken: accepted.switchToken!,
|
||||
),
|
||||
refused(EmailSignInRefusal.codeExpired),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
import 'dart:async' as _ida;
|
||||
import 'dart:io' as _idi;
|
||||
import 'dart:typed_data' as _idt;
|
||||
import 'package:halkyon_base_server/src/generated/auth/email_code_accepted.dart'
|
||||
as _i611xxit;
|
||||
import 'package:halkyon_base_server/src/generated/auth/sign_in_ways.dart'
|
||||
as _ik4ah9vo;
|
||||
import 'package:halkyon_base_server/src/generated/instance/instance_basics.dart'
|
||||
|
|
@ -185,8 +183,6 @@ class TestEndpoints {
|
|||
|
||||
late final _EmailCodeEndpoint emailCode;
|
||||
|
||||
late final _EmailSignInEndpoint emailSignIn;
|
||||
|
||||
late final _GitHubSignInEndpoint gitHubSignIn;
|
||||
|
||||
late final _GitHubUpgradeEndpoint gitHubUpgrade;
|
||||
|
|
@ -225,10 +221,6 @@ class _InternalTestEndpoints extends TestEndpoints
|
|||
endpoints,
|
||||
serializationManager,
|
||||
);
|
||||
emailSignIn = _EmailSignInEndpoint(
|
||||
endpoints,
|
||||
serializationManager,
|
||||
);
|
||||
gitHubSignIn = _GitHubSignInEndpoint(
|
||||
endpoints,
|
||||
serializationManager,
|
||||
|
|
@ -423,83 +415,6 @@ class _EmailCodeEndpoint {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<_i611xxit.EmailCodeAccepted> verifyCode(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required _is.UuidValue request,
|
||||
required String code,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'emailCode',
|
||||
method: 'verifyCode',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'emailCode',
|
||||
methodName: 'verifyCode',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'request': request,
|
||||
'code': code,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_i611xxit.EmailCodeAccepted>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _EmailSignInEndpoint {
|
||||
_EmailSignInEndpoint(
|
||||
this._endpointDispatch,
|
||||
this._serializationManager,
|
||||
);
|
||||
|
||||
final _is.EndpointDispatch _endpointDispatch;
|
||||
|
||||
final _is.SerializationManager _serializationManager;
|
||||
|
||||
_ida.Future<_iacs.AuthSuccess> signIn(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required String switchToken,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'emailSignIn',
|
||||
method: 'signIn',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'emailSignIn',
|
||||
methodName: 'signIn',
|
||||
parameters: _ist.testObjectToJson({'switchToken': switchToken}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_iacs.AuthSuccess>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _GitHubSignInEndpoint {
|
||||
|
|
@ -649,41 +564,6 @@ class _AccountRecoveryEndpoint {
|
|||
|
||||
final _is.SerializationManager _serializationManager;
|
||||
|
||||
_ida.Future<_is.UuidValue> sendCode(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required String address,
|
||||
required String language,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'accountRecovery',
|
||||
method: 'sendCode',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'accountRecovery',
|
||||
methodName: 'sendCode',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'address': address,
|
||||
'language': language,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_is.UuidValue>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<_isakxto6.ResetPending> resetAuthenticator(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required _is.UuidValue challenge,
|
||||
|
|
|
|||
Loading…
Reference in a new issue