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:
Tizian 「ティツィアーン」 2026-10-20 14:53:58 +02:00
commit d00da1f24a
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
5 changed files with 43506 additions and 126 deletions

View file

@ -0,0 +1,100 @@
{
"__className__": "serverpod.DatabaseMigration",
"actions": [
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "createTable",
"createTable": {
"__className__": "serverpod.TableDefinition",
"name": "sessions_ended_notice",
"dartName": "SessionsEndedNotice",
"module": "halkyon_base",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 6,
"isNullable": false,
"columnDefault": "serial",
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "account",
"fieldName": "account",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "member",
"fieldName": "member",
"columnType": 0,
"isNullable": false,
"dartType": "String"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "endedBefore",
"fieldName": "endedBefore",
"columnType": 4,
"isNullable": false,
"dartType": "DateTime"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "keepSession",
"fieldName": "keepSession",
"columnType": 7,
"isNullable": true,
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "attempts",
"fieldName": "attempts",
"columnType": 6,
"isNullable": false,
"dartType": "int"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "lastAttemptAt",
"fieldName": "lastAttemptAt",
"columnType": 4,
"isNullable": true,
"dartType": "DateTime?"
}
],
"foreignKeys": [],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "sessions_ended_notice_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "account"
},
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "member"
}
],
"type": "btree",
"isUnique": true,
"isPrimary": false
}
],
"managed": true
}
}
],
"warnings": [],
"migrationApiVersion": 1
}

View file

@ -19,6 +19,7 @@ part '20261019121406578-halkyon-base/migration.dart';
part '20261019201023295-profiles-in-halkyon-base/migration.dart';
part '20261019204112282-deletion-notices/migration.dart';
part '20261020111943300-passwords/migration.dart';
part '20261020125117584-sessions-ended/migration.dart';
/// Migration registry for the client-side database.
class MigrationRegistry {
@ -40,6 +41,7 @@ class MigrationRegistry {
_Migration20261019201023295_profiles_in_halkyon_base(),
_Migration20261019204112282_deletion_notices(),
_Migration20261020111943300_passwords(),
_Migration20261020125117584_sessions_ended(),
];
/// List of all client-side database migration versions.