The register's side of signing in with an address, a password that never leaves the device (SRP-6a) and a code from an authenticator (ADR 0017), with members forwarding every step through AccountRegister: - password.set: salt, Argon2id's costs, SRP's verifier, the public keys and the private keys wrapped twice, all made on the device; with the address a mailed code proves, an anonymous person becomes identified under their ID. It signs in once an authenticator is confirmed. - passwordSignIn.begin and complete: SRP's two steps with a TOTP or recovery code. An address nobody has is answered from a decoy key as an account would be; every wrong credential is refused alike. - authenticator: secrets sealed under totpKey (XChaCha20-Poly1305, bound to their row), each code accepted once, ten recovery codes with the first, the last one kept. - accountRecovery: a lost authenticator reset 72 hours after the password and a mailed code or the recovery phrase ask, a lost password set anew with the phrase's signature, or the account reset with new keys 72 hours after a mailed code asks; a mail at each start, a sign-in calling it off. - An account with a password no longer signs in by a mailed code or GitHub; no new GitHub account is linked where passwords are offered, and from GITHUB_SIGN_IN_ENDS GitHub signs nobody in and its IDs go. Five tables, added by the module's migration; deletePerson deletes their rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
88 lines
2.8 KiB
Dart
88 lines
2.8 KiB
Dart
import 'package:halkyon_base_server/base.dart';
|
|
import 'package:serverpod/serverpod.dart';
|
|
import 'package:serverpod_auth_idp_server/core.dart';
|
|
|
|
import '../test_tools/serverpod_test_tools.dart';
|
|
|
|
/// The app the tests' host is.
|
|
const testAppName = 'Halkyon Test';
|
|
|
|
/// The id of the app the tests' host serves.
|
|
const testApp = 'test';
|
|
|
|
/// Where the tests' host says its project publishes its source.
|
|
const testUpstreamSourceCodeUrl = 'https://code.example.org/halkyon_test';
|
|
|
|
/// The oldest app build the tests' host works with.
|
|
const testMinimumAppBuild = 7;
|
|
|
|
/// Sets the module up on the pod [sessionBuilder]'s sessions run on, as a
|
|
/// host does before its pod starts, with [deletePersonData] as its
|
|
/// deletions and [profilePageNote] at the foot of profile pages, and
|
|
/// authentication exactly as hosts run it
|
|
/// (`auth_config.dart`). Not on `Serverpod.instance`: that is the pod made
|
|
/// last, and a test file with several `withServerpod` groups has a pod each.
|
|
void useBase(
|
|
TestSessionBuilder sessionBuilder, {
|
|
List<PersonDataDeletion> deletePersonData = const [],
|
|
ProfilePageNote? profilePageNote,
|
|
AccountsMode accounts = const HoldsRegister(),
|
|
}) {
|
|
final pod = sessionBuilder.build().serverpod;
|
|
HalkyonBase.configure(
|
|
pod,
|
|
HalkyonBase(
|
|
app: testApp,
|
|
appName: testAppName,
|
|
upstreamSourceCodeUrl: testUpstreamSourceCodeUrl,
|
|
minimumAppBuild: testMinimumAppBuild,
|
|
deletePersonData: deletePersonData,
|
|
profilePageNote: profilePageNote,
|
|
accounts: accounts,
|
|
),
|
|
);
|
|
AuthServices.set(
|
|
tokenManagerBuilders: tokenManagerBuilders,
|
|
identityProviderBuilders: identityProviderBuilders(pod),
|
|
);
|
|
// No password sign-in unless a test gives it keys, whatever the test
|
|
// run's passwords hold.
|
|
usePasswordKeys(pod, null);
|
|
}
|
|
|
|
/// Signs [person] in on a new device and returns the new session's id.
|
|
Future<UuidValue> signInNewDevice(
|
|
Session session,
|
|
UuidValue person, {
|
|
Set<Scope> scopes = const {},
|
|
}) async {
|
|
Future<Set<UuidValue>> sessionIds() async => {
|
|
for (final serverSideSession in await ServerSideSession.db.find(
|
|
session,
|
|
where: (t) => t.authUserId.equals(person),
|
|
))
|
|
serverSideSession.id!,
|
|
};
|
|
|
|
final before = await sessionIds();
|
|
await AuthServices.instance.tokenManager.createToken(
|
|
session,
|
|
authUserId: person,
|
|
method: 'test',
|
|
scopes: scopes,
|
|
);
|
|
return (await sessionIds()).difference(before).single;
|
|
}
|
|
|
|
/// A person as an app's first launch creates them: anonymous.
|
|
Future<UuidValue> newAnonymousPerson(
|
|
Session session, {
|
|
DateTime? createdAt,
|
|
}) async {
|
|
final person = (await AuthServices.instance.authUsers.create(session)).id;
|
|
await AnonymousAccount.db.insertRow(
|
|
session,
|
|
AnonymousAccount(authUserId: person, createdAt: createdAt),
|
|
);
|
|
return person;
|
|
}
|