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
23 changed files with 429 additions and 3835 deletions
|
|
@ -3,23 +3,21 @@ import 'dart:convert';
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:halkyon_base_server/src/auth/code_mail.dart';
|
||||
import 'package:halkyon_base_server/src/auth/identified.dart';
|
||||
import 'package:halkyon_base_server/src/common/network.dart';
|
||||
import 'package:halkyon_base_server/src/common/short_code.dart';
|
||||
import 'package:halkyon_base_server/src/generated/protocol.dart';
|
||||
import 'package:halkyon_base_server/src/halkyon_base.dart';
|
||||
import 'package:halkyon_base_server/src/password/passwords.dart';
|
||||
import 'package:halkyon_base_server/src/register/people.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
|
||||
// Signing in with an email address, by a code mailed to it each time
|
||||
// (Halkyon Learn's ADR 0010). The instance keeps only a keyed hash of the
|
||||
// address: it can find the person and tell whether an address is free from
|
||||
// what someone types, and it mails only an address someone has just typed.
|
||||
// So the login is our own, on Serverpod's helpers (its code challenges and
|
||||
// rate limiter): Serverpod's email sign-in keeps the address itself, in three
|
||||
// tables, and passwords.
|
||||
// An email address, and the codes mailed to it (Halkyon Learn's ADR 0010).
|
||||
// The instance keeps only a keyed hash of the address: it can find the
|
||||
// person and tell whether an address is free from what someone types, and
|
||||
// it mails only an address someone has just typed. So this is our own, on
|
||||
// Serverpod's helpers (its code challenges and rate limiter): Serverpod's
|
||||
// email sign-in keeps the address itself, in three tables. Since ADR 0017 a
|
||||
// code signs nobody in by itself: it proves the address for a password's
|
||||
// setup, a sign-up, a setup never finished and the resets.
|
||||
|
||||
/// Serverpod's name for email sign-in on session rows.
|
||||
const emailMethod = 'email';
|
||||
|
|
@ -193,105 +191,12 @@ Future<UuidValue> sendSignInCode(
|
|||
});
|
||||
}
|
||||
|
||||
/// Accepts [code] for [request] from the person [authUserId]: an address
|
||||
/// nobody has becomes theirs, in one transaction (they become `identified`,
|
||||
/// on their sessions too, and stop being anonymous, keeping everything they
|
||||
/// have; a member's person becomes an account of the register under their
|
||||
/// ID, ADR 0016); their own changes nothing; another person's changes nothing
|
||||
/// here and gives a token that signs in as that person
|
||||
/// ([completeEmailSwitch]), unless that person signs in with a password
|
||||
/// ([signsInWithPassword], ADR 0017).
|
||||
///
|
||||
/// Refused for a wrong code, too many tries, a code no longer valid, a
|
||||
/// second address for someone who has one, or another person's address who
|
||||
/// signs in with a password ([EmailSignInRefusal.usePassword])
|
||||
/// ([EmailSignInRefusedException]); nothing changes then.
|
||||
Future<EmailCodeAccepted> acceptSignInCode(
|
||||
Session session, {
|
||||
required UuidValue authUserId,
|
||||
required UuidValue request,
|
||||
required String code,
|
||||
}) async {
|
||||
final pod = session.serverpod;
|
||||
if (!emailSignInAvailable(pod)) {
|
||||
throw EmailSignInRefusedException(reason: EmailSignInRefusal.unavailable);
|
||||
}
|
||||
return await session.db.transaction((transaction) async {
|
||||
final switchToken = await _refusingAsEmail(
|
||||
() => _challengesOf(pod).verifyChallenge(
|
||||
session,
|
||||
requestId: request,
|
||||
verificationCode: normalizeShortCode(code),
|
||||
transaction: transaction,
|
||||
),
|
||||
);
|
||||
final accepted = (await _requestById(
|
||||
session,
|
||||
request,
|
||||
transaction: transaction,
|
||||
))!;
|
||||
// SELECT * FROM "email_identity"
|
||||
// WHERE "addressHash" = $1 OR "authUserId" = $2
|
||||
final identities = await EmailIdentity.db.find(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.addressHash.equals(accepted.addressHash) |
|
||||
t.authUserId.equals(authUserId),
|
||||
transaction: transaction,
|
||||
);
|
||||
final owner = identities
|
||||
.where((identity) => identity.addressHash == accepted.addressHash)
|
||||
.firstOrNull;
|
||||
if (owner != null && owner.authUserId != authUserId) {
|
||||
if (await signsInWithPassword(
|
||||
session,
|
||||
owner.authUserId,
|
||||
transaction: transaction,
|
||||
)) {
|
||||
throw EmailSignInRefusedException(
|
||||
reason: EmailSignInRefusal.usePassword,
|
||||
);
|
||||
}
|
||||
// Kept until the token is used, or its time is up.
|
||||
return EmailCodeAccepted(switchToken: switchToken);
|
||||
}
|
||||
if (owner == null) {
|
||||
if (identities.isNotEmpty) {
|
||||
throw EmailSignInRefusedException(
|
||||
reason: EmailSignInRefusal.otherAddressLinked,
|
||||
);
|
||||
}
|
||||
await ensurePerson(session, authUserId, transaction: transaction);
|
||||
// INSERT INTO "email_identity" ("authUserId", "addressHash",
|
||||
// "createdAt") VALUES ($1, $2, $3)
|
||||
await EmailIdentity.db.insertRow(
|
||||
session,
|
||||
EmailIdentity(
|
||||
authUserId: authUserId,
|
||||
addressHash: accepted.addressHash,
|
||||
createdAt: clock.now(),
|
||||
),
|
||||
transaction: transaction,
|
||||
);
|
||||
await grantIdentified(session, authUserId, transaction: transaction);
|
||||
// DELETE FROM "serverpod_auth_idp_anonymous_account"
|
||||
// WHERE "authUserId" = $1
|
||||
await AnonymousAccount.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(authUserId),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
await _forget(session, [accepted], transaction: transaction);
|
||||
return EmailCodeAccepted();
|
||||
});
|
||||
}
|
||||
|
||||
/// The keyed hash of the address [code] proves for [request], once: the
|
||||
/// request goes as it is accepted. For what an address proves without
|
||||
/// becoming anyone's way in: resets of an account (ADR 0017), whose register
|
||||
/// compares the hash with the account's. Refusals as [acceptSignInCode]'s
|
||||
/// ([EmailSignInRefusedException]).
|
||||
/// request goes as it is accepted. A code by mail signs nobody in (ADR
|
||||
/// 0017): it proves an address for a password's setup, a sign-up, a setup
|
||||
/// never finished and the resets, whose register compares the hash with
|
||||
/// the account's. Refused for a wrong code, too many tries, or a code no
|
||||
/// longer valid ([EmailSignInRefusedException]).
|
||||
Future<String> proveAddressCode(
|
||||
Session session, {
|
||||
required UuidValue request,
|
||||
|
|
@ -320,52 +225,6 @@ Future<String> proveAddressCode(
|
|||
});
|
||||
}
|
||||
|
||||
/// The person whose address a code proved, from the token
|
||||
/// [acceptSignInCode] gave; once only. A session for them is the caller's to
|
||||
/// issue (`issueSession`): this server's own, or a member's (ADR 0016).
|
||||
/// Refused once the token was used or its time is up, or for a token it
|
||||
/// never gave ([EmailSignInRefusal.codeExpired]), and for a person who signs
|
||||
/// in with a password since ([EmailSignInRefusal.usePassword]).
|
||||
Future<UuidValue> completeEmailSwitch(
|
||||
Session session, {
|
||||
required String switchToken,
|
||||
}) async {
|
||||
final pod = session.serverpod;
|
||||
if (!emailSignInAvailable(pod)) {
|
||||
throw EmailSignInRefusedException(reason: EmailSignInRefusal.unavailable);
|
||||
}
|
||||
return await session.db.transaction((transaction) async {
|
||||
final request = await _refusingAsEmail(
|
||||
() => _challengesOf(pod).completeChallenge(
|
||||
session,
|
||||
completionToken: switchToken,
|
||||
transaction: transaction,
|
||||
),
|
||||
);
|
||||
// SELECT * FROM "email_identity" WHERE "addressHash" = $1
|
||||
final identity = await EmailIdentity.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.addressHash.equals(request.addressHash),
|
||||
transaction: transaction,
|
||||
);
|
||||
await _forget(session, [request], transaction: transaction);
|
||||
if (identity == null) {
|
||||
// The person deleted their account since.
|
||||
throw EmailSignInRefusedException(
|
||||
reason: EmailSignInRefusal.codeExpired,
|
||||
);
|
||||
}
|
||||
if (await signsInWithPassword(
|
||||
session,
|
||||
identity.authUserId,
|
||||
transaction: transaction,
|
||||
)) {
|
||||
throw EmailSignInRefusedException(reason: EmailSignInRefusal.usePassword);
|
||||
}
|
||||
return identity.authUserId;
|
||||
});
|
||||
}
|
||||
|
||||
/// Deletes the code requests past their time at [now], and returns how many:
|
||||
/// hourly, with the rate limit's notes (`PruningFutureCall`).
|
||||
Future<int> forgetExpiredEmailCodes(
|
||||
|
|
@ -462,7 +321,10 @@ SecretChallengeUtil<EmailCodeRequest> _newChallenges(String pepper) {
|
|||
return SecretChallengeUtil(
|
||||
// Codes are short: Argon2id at its usual cost.
|
||||
hashUtil: Argon2HashUtil(hashPepper: pepper, hashSaltLength: 16),
|
||||
// Switch tokens are long and random.
|
||||
// Serverpod's challenges hand out a completion token for every code
|
||||
// accepted; nothing takes it since codes sign nobody in (ADR 0017), and
|
||||
// it goes with its request (`switchChallengeId`, kept as the column the
|
||||
// util links, and as older servers read it during a deploy).
|
||||
completionTokenHash: Argon2HashUtil.forRandomSecrets(
|
||||
hashPepper: pepper,
|
||||
hashSaltLength: 16,
|
||||
|
|
@ -509,8 +371,7 @@ SecretChallengeUtil<EmailCodeRequest> _newChallenges(String pepper) {
|
|||
},
|
||||
),
|
||||
completionConfig: SecretChallengeCompletionConfig(
|
||||
// Switch tokens are long and random, but tries are counted all the
|
||||
// same.
|
||||
// Never completed any more (above); counted all the same.
|
||||
rateLimiter: DatabaseRateLimiter(
|
||||
RateLimiterConfig(
|
||||
domain: 'email_sign_in',
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
### A code accepted: the address is now the signed-in person's (or already
|
||||
### was), or it is another person's and [switchToken] signs in as them.
|
||||
class: EmailCodeAccepted
|
||||
fields:
|
||||
### Null when the address is the signed-in person's; else what signs in as
|
||||
### the person who has it, once, within the code's 15 minutes
|
||||
### (`EmailSignInEndpoint.signIn`).
|
||||
switchToken: String?
|
||||
|
|
@ -1,22 +1,15 @@
|
|||
import 'package:halkyon_base_server/src/auth/email.dart';
|
||||
import 'package:halkyon_base_server/src/auth/membership.dart';
|
||||
import 'package:halkyon_base_server/src/common/network.dart';
|
||||
import 'package:halkyon_base_server/src/generated/protocol.dart';
|
||||
import 'package:halkyon_base_server/src/register/account_register.dart';
|
||||
import 'package:halkyon_base_server/src/register/people.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
|
||||
/// Signing in with an email address, by a code mailed to it:
|
||||
/// the way to keep an anonymous account, as linking GitHub is, and a second
|
||||
/// way in for someone who signs in with GitHub. Signed in only, as every
|
||||
/// device is from its first start. The instance's register mails the codes
|
||||
/// and keeps the addresses' hashes (ADR 0016), this server's own database
|
||||
/// when it holds the register.
|
||||
/// Codes by mail (ADR 0017): what proves an address for a password's setup,
|
||||
/// a sign-up, a setup never finished and the resets. A code signs nobody in
|
||||
/// by itself. Without a session, as signing up and getting back in have
|
||||
/// none. The instance's register mails the codes and keeps the addresses'
|
||||
/// hashes (ADR 0016), this server's own database when it holds the
|
||||
/// register.
|
||||
class EmailCodeEndpoint extends Endpoint {
|
||||
@override
|
||||
bool get requireLogin => true;
|
||||
|
||||
/// Mails a code to [address], worded in [language] (`en`, `de`), and
|
||||
/// returns the request it answers; refusals as in [sendSignInCode].
|
||||
Future<UuidValue> sendCode(
|
||||
|
|
@ -29,22 +22,4 @@ class EmailCodeEndpoint extends Endpoint {
|
|||
language: language,
|
||||
network: networkOf(session),
|
||||
);
|
||||
|
||||
/// Accepts [code] for [request]: the address becomes the caller's unless
|
||||
/// it is another person's, whose token then signs in as them
|
||||
/// (`EmailSignInEndpoint.signIn`); refusals as in [acceptSignInCode].
|
||||
Future<EmailCodeAccepted> verifyCode(
|
||||
Session session, {
|
||||
required UuidValue request,
|
||||
required String code,
|
||||
}) async {
|
||||
final person = session.authenticated!.authUserId;
|
||||
final accepted = await AccountRegister.of(
|
||||
session.serverpod,
|
||||
).verifySignInCode(session, person: person, request: request, code: code);
|
||||
if (accepted.switchToken == null && isMember(session.serverpod)) {
|
||||
await becomeIdentified(session, person);
|
||||
}
|
||||
return accepted;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
import 'package:halkyon_base_server/src/auth/email.dart';
|
||||
import 'package:halkyon_base_server/src/register/account_register.dart';
|
||||
import 'package:halkyon_base_server/src/register/people.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
|
||||
/// Signs in the person whose address a mailed code proved, when the device
|
||||
/// switches to them. Called without a session: Serverpod issues no session
|
||||
/// for another person inside a signed-in call.
|
||||
class EmailSignInEndpoint extends Endpoint {
|
||||
/// A session for the person [switchToken] names; refusals as in
|
||||
/// [completeEmailSwitch].
|
||||
Future<AuthSuccess> signIn(
|
||||
Session session, {
|
||||
required String switchToken,
|
||||
}) async {
|
||||
final account = await AccountRegister.of(
|
||||
session.serverpod,
|
||||
).completeEmailSwitch(session, switchToken: switchToken);
|
||||
return await issueSession(session, account, method: emailMethod);
|
||||
}
|
||||
}
|
||||
|
|
@ -23,5 +23,6 @@ values:
|
|||
### The mail couldn't be sent; nothing was kept of the request.
|
||||
- mailFailed
|
||||
### The account signs in with its password and an authenticator (ADR
|
||||
### 0017): a code by mail no longer signs into it.
|
||||
### 0017): a code by mail no longer signs into it. No longer refused for
|
||||
### since codes sign nobody in; kept, as older apps know it by name.
|
||||
- usePassword
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
/// A code accepted: the address is now the signed-in person's (or already
|
||||
/// was), or it is another person's and [switchToken] signs in as them.
|
||||
abstract class EmailCodeAccepted
|
||||
implements _is.SerializableModel, _is.ProtocolSerialization {
|
||||
EmailCodeAccepted._({this.switchToken});
|
||||
|
||||
factory EmailCodeAccepted({String? switchToken}) = _EmailCodeAcceptedImpl;
|
||||
|
||||
factory EmailCodeAccepted.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return EmailCodeAccepted(
|
||||
switchToken: jsonSerialization['switchToken'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
/// Null when the address is the signed-in person's; else what signs in as
|
||||
/// the person who has it, once, within the code's 15 minutes
|
||||
/// (`EmailSignInEndpoint.signIn`).
|
||||
String? switchToken;
|
||||
|
||||
/// Returns a shallow copy of this [EmailCodeAccepted]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
EmailCodeAccepted copyWith({String? switchToken});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'halkyon_base.EmailCodeAccepted',
|
||||
if (switchToken != null) 'switchToken': switchToken,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'halkyon_base.EmailCodeAccepted',
|
||||
if (switchToken != null) 'switchToken': switchToken,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _is.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _Undefined {}
|
||||
|
||||
class _EmailCodeAcceptedImpl extends EmailCodeAccepted {
|
||||
_EmailCodeAcceptedImpl({String? switchToken})
|
||||
: super._(switchToken: switchToken);
|
||||
|
||||
/// Returns a shallow copy of this [EmailCodeAccepted]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
EmailCodeAccepted copyWith({Object? switchToken = _Undefined}) {
|
||||
return EmailCodeAccepted(
|
||||
switchToken: switchToken is String? ? switchToken : this.switchToken,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -43,7 +43,8 @@ enum EmailSignInRefusal implements _is.SerializableModel {
|
|||
mailFailed,
|
||||
|
||||
/// The account signs in with its password and an authenticator (ADR
|
||||
/// 0017): a code by mail no longer signs into it.
|
||||
/// 0017): a code by mail no longer signs into it. No longer refused for
|
||||
/// since codes sign nobody in; kept, as older apps know it by name.
|
||||
usePassword;
|
||||
|
||||
static EmailSignInRefusal fromJson(String name) {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ import 'package:serverpod_auth_idp_server/serverpod_auth_idp_server.dart'
|
|||
import '../auth/account_endpoint.dart' as _ii6ycf6v;
|
||||
import '../auth/anonymous_idp_endpoint.dart' as _in0zita6;
|
||||
import '../auth/email_code_endpoint.dart' as _in3cimlf;
|
||||
import '../auth/email_sign_in_endpoint.dart' as _irqdog5c;
|
||||
import '../auth/github_sign_in_endpoint.dart' as _iasw79m5;
|
||||
import '../auth/github_upgrade_endpoint.dart' as _ivnugbf2;
|
||||
import '../instance/instance_endpoint.dart' as _i3g8bfc2;
|
||||
|
|
@ -57,12 +56,6 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
'emailCode',
|
||||
'halkyon_base',
|
||||
),
|
||||
'emailSignIn': _irqdog5c.EmailSignInEndpoint()
|
||||
..initialize(
|
||||
server,
|
||||
'emailSignIn',
|
||||
'halkyon_base',
|
||||
),
|
||||
'gitHubSignIn': _iasw79m5.GitHubSignInEndpoint()
|
||||
..initialize(
|
||||
server,
|
||||
|
|
@ -199,57 +192,6 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
language: params['language'],
|
||||
),
|
||||
),
|
||||
'verifyCode': _is.MethodConnector(
|
||||
name: 'verifyCode',
|
||||
params: {
|
||||
'request': _is.ParameterDescription(
|
||||
name: 'request',
|
||||
type: _is.getType<_is.UuidValue>(),
|
||||
nullable: false,
|
||||
),
|
||||
'code': _is.ParameterDescription(
|
||||
name: 'code',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async => (endpoints['emailCode'] as _in3cimlf.EmailCodeEndpoint)
|
||||
.verifyCode(
|
||||
session,
|
||||
request: params['request'],
|
||||
code: params['code'],
|
||||
),
|
||||
),
|
||||
},
|
||||
);
|
||||
connectors['emailSignIn'] = _is.EndpointConnector(
|
||||
name: 'emailSignIn',
|
||||
endpoint: endpoints['emailSignIn']!,
|
||||
methodConnectors: {
|
||||
'signIn': _is.MethodConnector(
|
||||
name: 'signIn',
|
||||
params: {
|
||||
'switchToken': _is.ParameterDescription(
|
||||
name: 'switchToken',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['emailSignIn'] as _irqdog5c.EmailSignInEndpoint)
|
||||
.signIn(
|
||||
session,
|
||||
switchToken: params['switchToken'],
|
||||
),
|
||||
),
|
||||
},
|
||||
);
|
||||
connectors['gitHubSignIn'] = _is.EndpointConnector(
|
||||
|
|
@ -349,33 +291,6 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
name: 'accountRecovery',
|
||||
endpoint: endpoints['accountRecovery']!,
|
||||
methodConnectors: {
|
||||
'sendCode': _is.MethodConnector(
|
||||
name: 'sendCode',
|
||||
params: {
|
||||
'address': _is.ParameterDescription(
|
||||
name: 'address',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'language': _is.ParameterDescription(
|
||||
name: 'language',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['accountRecovery']
|
||||
as _ivnbjhab.AccountRecoveryEndpoint)
|
||||
.sendCode(
|
||||
session,
|
||||
address: params['address'],
|
||||
language: params['language'],
|
||||
),
|
||||
),
|
||||
'resetAuthenticator': _is.MethodConnector(
|
||||
name: 'resetAuthenticator',
|
||||
params: {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import 'package:serverpod_auth_core_server/serverpod_auth_core_server.dart'
|
|||
as _iacs;
|
||||
import 'package:serverpod_auth_idp_server/serverpod_auth_idp_server.dart'
|
||||
as _iais;
|
||||
import 'auth/email_code_accepted.dart' as _iys46ui1;
|
||||
import 'auth/email_code_request.dart' as _iqxum1j8;
|
||||
import 'auth/email_identity.dart' as _id1dpd4g;
|
||||
import 'auth/email_sign_in_refusal.dart' as _ieyvcag1;
|
||||
|
|
@ -55,7 +54,6 @@ import 'profile/profile_refused_exception.dart' as _idvc5e7p;
|
|||
import 'profile/public_profile.dart' as _ioppe2ep;
|
||||
import 'register/account_deletion_notice.dart' as _i2lyzpm9;
|
||||
import 'register/sessions_ended_notice.dart' as _io5c3tod;
|
||||
export 'auth/email_code_accepted.dart';
|
||||
export 'auth/email_code_request.dart';
|
||||
export 'auth/email_identity.dart';
|
||||
export 'auth/email_sign_in_refusal.dart';
|
||||
|
|
@ -881,9 +879,6 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
}
|
||||
}
|
||||
|
||||
if (t == _iys46ui1.EmailCodeAccepted) {
|
||||
return _iys46ui1.EmailCodeAccepted.fromJson(data) as T;
|
||||
}
|
||||
if (t == _iqxum1j8.EmailCodeRequest) {
|
||||
return _iqxum1j8.EmailCodeRequest.fromJson(data) as T;
|
||||
}
|
||||
|
|
@ -989,10 +984,6 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (t == _io5c3tod.SessionsEndedNotice) {
|
||||
return _io5c3tod.SessionsEndedNotice.fromJson(data) as T;
|
||||
}
|
||||
if (t == _is.getType<_iys46ui1.EmailCodeAccepted?>()) {
|
||||
return (data != null ? _iys46ui1.EmailCodeAccepted.fromJson(data) : null)
|
||||
as T;
|
||||
}
|
||||
if (t == _is.getType<_iqxum1j8.EmailCodeRequest?>()) {
|
||||
return (data != null ? _iqxum1j8.EmailCodeRequest.fromJson(data) : null)
|
||||
as T;
|
||||
|
|
@ -1181,7 +1172,6 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
|
||||
static String? getClassNameForType(Type type) {
|
||||
return switch (type) {
|
||||
_iys46ui1.EmailCodeAccepted => 'EmailCodeAccepted',
|
||||
_iqxum1j8.EmailCodeRequest => 'EmailCodeRequest',
|
||||
_id1dpd4g.EmailIdentity => 'EmailIdentity',
|
||||
_ieyvcag1.EmailSignInRefusal => 'EmailSignInRefusal',
|
||||
|
|
@ -1234,8 +1224,6 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
}
|
||||
|
||||
switch (data) {
|
||||
case _iys46ui1.EmailCodeAccepted():
|
||||
return 'EmailCodeAccepted';
|
||||
case _iqxum1j8.EmailCodeRequest():
|
||||
return 'EmailCodeRequest';
|
||||
case _id1dpd4g.EmailIdentity():
|
||||
|
|
@ -1320,9 +1308,6 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (dataClassName is! String) {
|
||||
return super.deserializeByClassName(data);
|
||||
}
|
||||
if (dataClassName == 'EmailCodeAccepted') {
|
||||
return deserialize<_iys46ui1.EmailCodeAccepted>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'EmailCodeRequest') {
|
||||
return deserialize<_iqxum1j8.EmailCodeRequest>(data['data']);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,6 @@ anonymousIdp:
|
|||
- login:
|
||||
emailCode:
|
||||
- sendCode:
|
||||
- verifyCode:
|
||||
emailSignIn:
|
||||
- signIn:
|
||||
gitHubSignIn:
|
||||
- signIn:
|
||||
gitHubUpgrade:
|
||||
|
|
@ -15,7 +12,6 @@ gitHubUpgrade:
|
|||
instance:
|
||||
- basics:
|
||||
accountRecovery:
|
||||
- sendCode:
|
||||
- resetAuthenticator:
|
||||
- beginKeyProof:
|
||||
- resetPassword:
|
||||
|
|
|
|||
|
|
@ -13,24 +13,10 @@ import 'package:serverpod_auth_idp_server/core.dart';
|
|||
/// without a session, as the person can't sign in. The instance's register
|
||||
/// checks every proof (ADR 0016); a session is this server's.
|
||||
class AccountRecoveryEndpoint extends Endpoint {
|
||||
/// Mails a code to [address], worded in [language] (`en`, `de`), as email
|
||||
/// sign-in mails it, and returns the request it answers: the proof of the
|
||||
/// address that resets take. Refusals as `sendSignInCode`'s.
|
||||
Future<UuidValue> sendCode(
|
||||
Session session, {
|
||||
required String address,
|
||||
required String language,
|
||||
}) => AccountRegister.of(session.serverpod).sendSignInCode(
|
||||
session,
|
||||
address: address,
|
||||
language: language,
|
||||
network: networkOf(session),
|
||||
);
|
||||
|
||||
/// Asks for the reset of the authenticators of the account at [address]:
|
||||
/// the password's proof for [challenge] (`passwordSignIn.begin`), and a
|
||||
/// [code] mailed for [request] or the recovery phrase's [signature] for
|
||||
/// [keyChallenge] ([beginKeyProof]). It takes effect 72 hours later
|
||||
/// [keyChallenge] ([beginKeyProof]); codes by mail come from `emailCode.sendCode`. It takes effect 72 hours later
|
||||
/// unless the owner signs in with a code meanwhile; a mail in [language]
|
||||
/// says so. Refusals as `resetAuthenticator`'s.
|
||||
Future<ResetPending> resetAuthenticator(
|
||||
|
|
|
|||
|
|
@ -119,24 +119,6 @@ abstract interface class AccountRegister {
|
|||
required String network,
|
||||
});
|
||||
|
||||
/// Accepts [code] for [request] from [person]: the address becomes theirs
|
||||
/// (an account of the register, under their ID) unless it is another
|
||||
/// person's, whose switch token it then gives. Refusals as
|
||||
/// `acceptSignInCode`'s.
|
||||
Future<EmailCodeAccepted> verifySignInCode(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required UuidValue request,
|
||||
required String code,
|
||||
});
|
||||
|
||||
/// The account a switch token names, once only. Refusals as
|
||||
/// `completeEmailSwitch`'s.
|
||||
Future<UuidValue> completeEmailSwitch(
|
||||
Session session, {
|
||||
required String switchToken,
|
||||
});
|
||||
|
||||
/// Links the GitHub account [gitHubUserId] to [person] (an account of the
|
||||
/// register, under their ID). Refusals as `linkGitHubAccount`'s.
|
||||
Future<void> linkGitHub(
|
||||
|
|
|
|||
|
|
@ -178,20 +178,6 @@ Future<Map<String, Object?>?> _answer(
|
|||
network: arguments.string('network'),
|
||||
);
|
||||
return {'request': request.uuid};
|
||||
case RegisterOperation.verifySignInCode:
|
||||
final accepted = await register.verifySignInCode(
|
||||
session,
|
||||
person: arguments.id('person'),
|
||||
request: arguments.id('request'),
|
||||
code: arguments.string('code'),
|
||||
);
|
||||
return {'accepted': accepted.toJson()};
|
||||
case RegisterOperation.completeEmailSwitch:
|
||||
final account = await register.completeEmailSwitch(
|
||||
session,
|
||||
switchToken: arguments.string('switchToken'),
|
||||
);
|
||||
return {'account': account.uuid};
|
||||
case RegisterOperation.linkGitHub:
|
||||
await register.linkGitHub(
|
||||
session,
|
||||
|
|
|
|||
|
|
@ -343,25 +343,6 @@ final class LocalRegister implements AccountRegister {
|
|||
network: network,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<EmailCodeAccepted> verifySignInCode(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required UuidValue request,
|
||||
required String code,
|
||||
}) => email.acceptSignInCode(
|
||||
session,
|
||||
authUserId: person,
|
||||
request: request,
|
||||
code: code,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<UuidValue> completeEmailSwitch(
|
||||
Session session, {
|
||||
required String switchToken,
|
||||
}) => email.completeEmailSwitch(session, switchToken: switchToken);
|
||||
|
||||
@override
|
||||
Future<void> linkGitHub(
|
||||
Session session, {
|
||||
|
|
|
|||
|
|
@ -26,8 +26,6 @@ abstract final class RegisterOperation {
|
|||
static const fixHandle = 'fix-handle';
|
||||
static const emailSignInOffered = 'email-sign-in-offered';
|
||||
static const sendSignInCode = 'send-sign-in-code';
|
||||
static const verifySignInCode = 'verify-sign-in-code';
|
||||
static const completeEmailSwitch = 'complete-email-switch';
|
||||
static const linkGitHub = 'link-github';
|
||||
static const accountWithGitHub = 'account-with-github';
|
||||
static const signInWaysOf = 'sign-in-ways-of';
|
||||
|
|
|
|||
|
|
@ -243,33 +243,6 @@ final class RemoteRegister implements AccountRegister {
|
|||
}))['request'],
|
||||
)!;
|
||||
|
||||
@override
|
||||
Future<EmailCodeAccepted> verifySignInCode(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required UuidValue request,
|
||||
required String code,
|
||||
}) async => switch ((await _ask(RegisterOperation.verifySignInCode, {
|
||||
'person': person.uuid,
|
||||
'request': request.uuid,
|
||||
'code': code,
|
||||
}))['accepted']) {
|
||||
final Map<String, Object?> accepted => EmailCodeAccepted.fromJson(accepted),
|
||||
_ => throw RegisterUnavailableException(
|
||||
'${RegisterOperation.verifySignInCode}: no answer',
|
||||
),
|
||||
};
|
||||
|
||||
@override
|
||||
Future<UuidValue> completeEmailSwitch(
|
||||
Session session, {
|
||||
required String switchToken,
|
||||
}) async => _id(
|
||||
(await _ask(RegisterOperation.completeEmailSwitch, {
|
||||
'switchToken': switchToken,
|
||||
}))['account'],
|
||||
)!;
|
||||
|
||||
@override
|
||||
Future<void> linkGitHub(
|
||||
Session session, {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue