feat(auth): sign in with an email address, keeping only a keyed hash of it
People without GitHub can now keep an anonymous account, and sign in on another device, with an email address and a code mailed to it each time; no password. The instance keeps only HMAC-SHA256 of the address, keyed with its emailSignInPepper (ADR 0010): from what someone types it finds the person and tells whether an address is free, it mails only an address someone has just typed, and it can't write to anybody otherwise or show anybody their address. The device remembers the address it signed in with, in the person's folder, to show it. It works as GitHub does. A code for a new address makes it the signed-in person's, who becomes identified and keeps everything. A code for another person's address gives a switch token, which signs in as them without a session; a device holding studysets asks first. Asking for a code answers alike for every address. Codes are eight Crockford characters, valid 15 minutes and three tries (Serverpod's code challenges); three codes an hour per address, ten per network. In development the code goes to the server's log; elsewhere there is no email sign-in until a mail service is set up (instance.info().emailSignIn). Someone identified may add the other way in: the Profile tab lists how they sign in and adds GitHub or an address. They are never switched to another account: an address or GitHub account that is another's is said to be so. Switching now deletes the account left behind only when it was anonymous; it deleted any, which only anonymous people could reach so far. The migration adds email_identity and email_code_request with their indexes and foreign keys to Serverpod's challenges; nothing else changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
58e13521b1
commit
1cfd391dbc
1 changed files with 21720 additions and 103 deletions
|
|
@ -1762,6 +1762,112 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get profileSwitchConfirm => 'Switch';
|
||||
|
||||
@override
|
||||
String get profileContinueWithEmail => 'Continue with email';
|
||||
|
||||
@override
|
||||
String get profileSignInWays => 'Ways to sign in';
|
||||
|
||||
@override
|
||||
String get profileWayGitHub => 'GitHub';
|
||||
|
||||
@override
|
||||
String get profileWayEmail => 'Email address';
|
||||
|
||||
@override
|
||||
String get profileWayLinked => 'Signs you in';
|
||||
|
||||
@override
|
||||
String get profileAddGitHub => 'Add GitHub';
|
||||
|
||||
@override
|
||||
String get profileAddEmail => 'Add an email address';
|
||||
|
||||
@override
|
||||
String get profileGitHubTaken =>
|
||||
'This GitHub account already signs in to another account.';
|
||||
|
||||
@override
|
||||
String get emailSignInTitle => 'Continue with email';
|
||||
|
||||
@override
|
||||
String get emailSignInIntro =>
|
||||
'We mail you a code to sign in. This instance keeps only a fingerprint of your address, never the address itself, and writes to it only when you ask for a code.';
|
||||
|
||||
@override
|
||||
String get emailSignInAddress => 'Email address';
|
||||
|
||||
@override
|
||||
String get emailSignInSend => 'Send code';
|
||||
|
||||
@override
|
||||
String emailSignInCodeSent(String address) {
|
||||
return 'We mailed a code to $address. It\'s valid for 15 minutes.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get emailSignInResent => 'A new code is on its way.';
|
||||
|
||||
@override
|
||||
String get emailSignInCode => 'Code';
|
||||
|
||||
@override
|
||||
String get emailSignInContinue => 'Continue';
|
||||
|
||||
@override
|
||||
String get emailSignInResend => 'Send a new code';
|
||||
|
||||
@override
|
||||
String get emailSignInOtherAddress => 'Use another address';
|
||||
|
||||
@override
|
||||
String emailSignInSwitchTitle(String address) {
|
||||
return 'Switch to the account of $address?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get emailSignInSwitchMessage =>
|
||||
'This address already has progress from another device. The studysets on this device can\'t be combined with it yet: if you switch, they are deleted.';
|
||||
|
||||
@override
|
||||
String emailSignInTaken(String address) {
|
||||
return '$address already signs in to another account. To switch to it, sign out first.';
|
||||
}
|
||||
|
||||
@override
|
||||
String emailSignInDone(String address) {
|
||||
return 'Signed in with $address.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get emailSignInUnavailable =>
|
||||
'This instance doesn\'t offer signing in with email.';
|
||||
|
||||
@override
|
||||
String get emailSignInNotAnAddress => 'This isn\'t an email address.';
|
||||
|
||||
@override
|
||||
String get emailSignInTooManyCodes =>
|
||||
'Too many codes in the last hour. Try again later.';
|
||||
|
||||
@override
|
||||
String get emailSignInWrongCode => 'That code isn\'t right.';
|
||||
|
||||
@override
|
||||
String get emailSignInTooManyTries =>
|
||||
'Too many wrong codes. Ask for a new one.';
|
||||
|
||||
@override
|
||||
String get emailSignInCodeExpired =>
|
||||
'This code is no longer valid. Ask for a new one.';
|
||||
|
||||
@override
|
||||
String get emailSignInOtherAddressLinked =>
|
||||
'You already sign in with another address.';
|
||||
|
||||
@override
|
||||
String get emailSignInFailed => 'Couldn\'t reach the instance. Try again.';
|
||||
|
||||
@override
|
||||
String get publicProfileTitle => 'Public profile';
|
||||
|
||||
|
|
@ -1781,7 +1887,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get publicProfileAnonymousHint =>
|
||||
'Sign in with GitHub to have a public profile and publish.';
|
||||
'Sign in to have a public profile and publish.';
|
||||
|
||||
@override
|
||||
String get publicProfileDisplayName => 'Display name';
|
||||
|
|
@ -1996,7 +2102,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String accountRemoveMessage(String name) {
|
||||
return 'Removes the account on $name from this device, with all the device holds of it. What it synced stays on $name: sign in there with GitHub to have it again. An anonymous account can\'t be signed into again.';
|
||||
return 'Removes the account on $name from this device, with all the device holds of it. What it synced stays on $name: sign in there again to have it. An anonymous account can\'t be signed into again.';
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -5846,7 +5952,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get publishAnonymous =>
|
||||
'Sign in with GitHub to have a public profile and publish.';
|
||||
'Sign in to have a public profile and publish.';
|
||||
|
||||
@override
|
||||
String get publishName => 'Name';
|
||||
|
|
@ -6350,7 +6456,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get feedbackNeedsIdentity =>
|
||||
'Sign in with GitHub to report to the author and suggest.';
|
||||
'Sign in to report to the author and suggest.';
|
||||
|
||||
@override
|
||||
String get kanjiExampleWords => 'Words with it';
|
||||
|
|
|
|||
Loading…
Reference in a new issue