feat(auth): change the password and the recovery phrase while signed in
ADR 0017, step 3. `password.beginChange` and `password.change` take the old password's SRP proof and a TOTP or recovery code, and the same keys wrapped under the new password; a session alone changes nothing, and an account without a confirmed authenticator is told to set one up first. `password.setRecoveryKeys` replaces the keys' wrapping under the recovery phrase for a TOTP code, so a device holding the keys can make a new phrase. A change of password ends every other session of the account in every service of the instance; resets of the password and of the account end them all. The register ends its own and keeps a notice per member (`sessions_ended_notice`, a migration that only creates it), told at once and hourly until it confirms, as deletions are told: it names a moment, so sessions made since stay, and the session that asked for a change. A member answering one ends its sessions made before that moment; a member signing someone in after a reset ends its own first and makes the new session only once its clock is past the moment. A reset not due says when it takes effect (`takesEffectAt` on `PasswordRefusedException`), across members too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
f8076dcacf
commit
d00da1f24a
1 changed files with 43506 additions and 126 deletions
|
|
@ -36,6 +36,8 @@ import 'package:halkyon_base_server/src/generated/password/password_signed_in.da
|
|||
as _ikpyb5fh;
|
||||
import 'package:halkyon_base_server/src/generated/password/reset_pending.dart'
|
||||
as _isakxto6;
|
||||
import 'package:halkyon_base_server/src/generated/password/wrapped_keys.dart'
|
||||
as _iji0wmu8;
|
||||
import 'package:halkyon_base_server/src/generated/profile/profile.dart'
|
||||
as _it4tom37;
|
||||
import 'package:halkyon_base_server/src/generated/profile/profile_refusal.dart'
|
||||
|
|
@ -1107,6 +1109,114 @@ class _PasswordEndpoint {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<_isz0atlx.PasswordChallenge> beginChange(
|
||||
_ist.TestSessionBuilder sessionBuilder,
|
||||
) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'password',
|
||||
method: 'beginChange',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'password',
|
||||
methodName: 'beginChange',
|
||||
parameters: _ist.testObjectToJson({}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_isz0atlx.PasswordChallenge>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<void> change(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required _is.UuidValue challenge,
|
||||
required _idt.ByteData clientValue,
|
||||
required _idt.ByteData clientProof,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
required _ijye8mcv.NewPassword password,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'password',
|
||||
method: 'change',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'password',
|
||||
methodName: 'change',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'challenge': challenge,
|
||||
'clientValue': clientValue,
|
||||
'clientProof': clientProof,
|
||||
'totpCode': totpCode,
|
||||
'recoveryCode': recoveryCode,
|
||||
'password': password,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<void>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<void> setRecoveryKeys(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required String code,
|
||||
required _iji0wmu8.WrappedKeys recoveryKeys,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'password',
|
||||
method: 'setRecoveryKeys',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'password',
|
||||
methodName: 'setRecoveryKeys',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'code': code,
|
||||
'recoveryKeys': recoveryKeys,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<void>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _PasswordSignInEndpoint {
|
||||
|
|
|
|||
Loading…
Reference in a new issue