feat(base): SimpleLogin's sign-in comes back by the app's scheme or the loopback (ADR 0020, D9 corrected)

simpleLogin.begin takes the way back (SimpleLoginReturn: loopback,
scheme) rather than the platform, so macOS can come back through its
scheme as iOS and Android do; the markers become learn-loopback,
learn-scheme, todo-loopback and todo-scheme. Not breaking: the
endpoint has never been released.

D9 corrected by the developer after research: Android, iOS and macOS
come back through the app's scheme, Linux and Windows through the
loopback. Verified links protected nothing here: a code caught on its
way is worthless without the attempt's ID, and a hostile app beginning
an attempt of its own can ask for a way back that other platforms need
open. The ADR notes the residual risk and the check of prompt=consent
still to do. assetlinks.json stays served.

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-23 23:35:46 +02:00
commit 7ea9f16904
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
9 changed files with 201 additions and 182 deletions

View file

@ -31,8 +31,8 @@ import 'package:halkyon_base_server/src/generated/password/password_setup.dart'
as _ikhyee47;
import 'package:halkyon_base_server/src/generated/password/wrapped_keys.dart'
as _iji0wmu8;
import 'package:halkyon_base_server/src/generated/simplelogin/simplelogin_platform.dart'
as _ixrtiuh0;
import 'package:halkyon_base_server/src/generated/simplelogin/simplelogin_return.dart'
as _igmh0yku;
import 'package:halkyon_oidc_server/halkyon_oidc_server.dart' as _ipoqfiqr;
import 'package:serverpod/serverpod.dart' as _is;
import 'package:serverpod_auth_core_server/serverpod_auth_core_server.dart'
@ -1283,9 +1283,9 @@ class Endpoints extends _is.EndpointDispatch {
'begin': _is.MethodConnector(
name: 'begin',
params: {
'platform': _is.ParameterDescription(
name: 'platform',
type: _is.getType<_ixrtiuh0.SimpleLoginPlatform>(),
'back': _is.ParameterDescription(
name: 'back',
type: _is.getType<_igmh0yku.SimpleLoginReturn>(),
nullable: false,
),
},
@ -1297,7 +1297,7 @@ class Endpoints extends _is.EndpointDispatch {
(endpoints['simpleLogin'] as _ia8c9alv.SimpleLoginEndpoint)
.begin(
session,
platform: params['platform'],
back: params['back'],
),
),
'complete': _is.MethodConnector(

View file

@ -69,8 +69,8 @@ import 'register/account_deletion_notice.dart' as _i2lyzpm9;
import 'register/sessions_ended_notice.dart' as _io5c3tod;
import 'simplelogin/simplelogin_completed.dart' as _i51ry5s2;
import 'simplelogin/simplelogin_outcome.dart' as _i5ar1hzl;
import 'simplelogin/simplelogin_platform.dart' as _ivny5pkt;
import 'simplelogin/simplelogin_proof.dart' as _iauqtzog;
import 'simplelogin/simplelogin_return.dart' as _i8dcjd27;
export 'auth/email_code_purpose.dart';
export 'auth/email_code_request.dart';
export 'auth/email_identity.dart';
@ -120,8 +120,8 @@ export 'register/account_deletion_notice.dart';
export 'register/sessions_ended_notice.dart';
export 'simplelogin/simplelogin_completed.dart';
export 'simplelogin/simplelogin_outcome.dart';
export 'simplelogin/simplelogin_platform.dart';
export 'simplelogin/simplelogin_proof.dart';
export 'simplelogin/simplelogin_return.dart';
class Protocol extends _is.DatabaseSerializationManager {
Protocol._();
@ -1219,12 +1219,12 @@ class Protocol extends _is.DatabaseSerializationManager {
if (t == _i5ar1hzl.SimpleLoginOutcome) {
return _i5ar1hzl.SimpleLoginOutcome.fromJson(data) as T;
}
if (t == _ivny5pkt.SimpleLoginPlatform) {
return _ivny5pkt.SimpleLoginPlatform.fromJson(data) as T;
}
if (t == _iauqtzog.SimpleLoginProof) {
return _iauqtzog.SimpleLoginProof.fromJson(data) as T;
}
if (t == _i8dcjd27.SimpleLoginReturn) {
return _i8dcjd27.SimpleLoginReturn.fromJson(data) as T;
}
if (t == _is.getType<_ifpka2on.EmailCodePurpose?>()) {
return (data != null ? _ifpka2on.EmailCodePurpose.fromJson(data) : null)
as T;
@ -1435,16 +1435,14 @@ class Protocol extends _is.DatabaseSerializationManager {
return (data != null ? _i5ar1hzl.SimpleLoginOutcome.fromJson(data) : null)
as T;
}
if (t == _is.getType<_ivny5pkt.SimpleLoginPlatform?>()) {
return (data != null
? _ivny5pkt.SimpleLoginPlatform.fromJson(data)
: null)
as T;
}
if (t == _is.getType<_iauqtzog.SimpleLoginProof?>()) {
return (data != null ? _iauqtzog.SimpleLoginProof.fromJson(data) : null)
as T;
}
if (t == _is.getType<_i8dcjd27.SimpleLoginReturn?>()) {
return (data != null ? _i8dcjd27.SimpleLoginReturn.fromJson(data) : null)
as T;
}
if (t == Set<String>) {
return (data as List).map((e) => deserialize<String>(e)).toSet() as T;
}
@ -1532,8 +1530,8 @@ class Protocol extends _is.DatabaseSerializationManager {
_io5c3tod.SessionsEndedNotice => 'SessionsEndedNotice',
_i51ry5s2.SimpleLoginCompleted => 'SimpleLoginCompleted',
_i5ar1hzl.SimpleLoginOutcome => 'SimpleLoginOutcome',
_ivny5pkt.SimpleLoginPlatform => 'SimpleLoginPlatform',
_iauqtzog.SimpleLoginProof => 'SimpleLoginProof',
_i8dcjd27.SimpleLoginReturn => 'SimpleLoginReturn',
_ => null,
};
}
@ -1649,10 +1647,10 @@ class Protocol extends _is.DatabaseSerializationManager {
return 'SimpleLoginCompleted';
case _i5ar1hzl.SimpleLoginOutcome():
return 'SimpleLoginOutcome';
case _ivny5pkt.SimpleLoginPlatform():
return 'SimpleLoginPlatform';
case _iauqtzog.SimpleLoginProof():
return 'SimpleLoginProof';
case _i8dcjd27.SimpleLoginReturn():
return 'SimpleLoginReturn';
}
className = _isp.Protocol().getClassNameForObject(data);
if (className != null) {
@ -1814,12 +1812,12 @@ class Protocol extends _is.DatabaseSerializationManager {
if (dataClassName == 'SimpleLoginOutcome') {
return deserialize<_i5ar1hzl.SimpleLoginOutcome>(data['data']);
}
if (dataClassName == 'SimpleLoginPlatform') {
return deserialize<_ivny5pkt.SimpleLoginPlatform>(data['data']);
}
if (dataClassName == 'SimpleLoginProof') {
return deserialize<_iauqtzog.SimpleLoginProof>(data['data']);
}
if (dataClassName == 'SimpleLoginReturn') {
return deserialize<_i8dcjd27.SimpleLoginReturn>(data['data']);
}
if (dataClassName.startsWith('serverpod.')) {
data['className'] = dataClassName.substring(10);
return _isp.Protocol().deserializeByClassName(data);

View file

@ -0,0 +1,40 @@
/* 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;
/// How an app comes back from a SimpleLogin sign-in (ADR 0020, 3 and D9),
/// which says where the forwarding page sends it: the loopback (Linux,
/// Windows) or the app's scheme (Android, iOS, macOS).
enum SimpleLoginReturn implements _is.SerializableModel {
loopback,
scheme;
static SimpleLoginReturn fromJson(String name) {
switch (name) {
case 'loopback':
return SimpleLoginReturn.loopback;
case 'scheme':
return SimpleLoginReturn.scheme;
default:
throw ArgumentError(
'Value "$name" cannot be converted to "SimpleLoginReturn"',
);
}
}
@override
String toJson() => name;
@override
String toString() => name;
}

View file

@ -32,8 +32,8 @@ OidcIdp _simpleLoginOn(Session session) {
return AuthServices.instance.oidcIdp;
}
/// Begins a SimpleLogin sign-in for the app and platform [marker] names
/// (`learn-desktop`, one of [simpleLoginReturns]), the state beginning with
/// Begins a SimpleLogin sign-in for the app and way back [marker] names
/// (`learn-loopback`, one of [simpleLoginReturns]), the state beginning with
/// it for the forwarding page; [caller] began it signed in, to add
/// SimpleLogin to their account, a member's person becoming a person here
/// under their ID, as linking would make them (ADR 0016). At most 30 an hour

View file

@ -8,8 +8,8 @@ import 'package:serverpod/serverpod.dart';
// (`oidcCallbackForwarding`, ADR 0023, 7) with Halkyon's apps and look: the
// browser comes back here with SimpleLogin's code (or its refusal) and the
// state the sign-in began with, and the page sends both on to the app that
// began it, the desktop's loopback or the phone's scheme, by the marker its
// state begins with. It keeps and logs nothing: a code caught on its way is
// began it, the loopback or the app's scheme, by the marker its state begins
// with. It keeps and logs nothing: a code caught on its way is
// worthless without the attempt the app holds (ADR 0020, 4).
/// Where SimpleLogin sends the browser back to, on the register's web
@ -17,16 +17,14 @@ import 'package:serverpod/serverpod.dart';
const simpleLoginCallbackPath = '/simplelogin-callback';
/// Where each Halkyon app takes a sign-in back, by the marker its state
/// begins with (`<app>-<platform>`): the desktop's loopback, each app on a
/// port of its own (RFC 8252, 7.3), and the phone's scheme, the app's ID. A
/// phone with Halkyon Learn and learn.halkyon.eu never reaches the page:
/// Android hands the page's address to the app itself (App Links,
/// `assetlinks.json`).
/// begins with (`<app>-<way back>`): the loopback on Linux and Windows, each
/// app on a port of its own (RFC 8252, 7.3), and the app's scheme, its ID, on
/// Android, iOS and macOS, where the system's browser sheet catches it.
const simpleLoginReturns = {
'learn-desktop': 'http://127.0.0.1:43823/simplelogin-callback',
'learn-phone': 'app.halkyon.learn://simplelogin-callback',
'todo-desktop': 'http://127.0.0.1:43824/simplelogin-callback',
'todo-phone': 'app.halkyon.todo://simplelogin-callback',
'learn-loopback': 'http://127.0.0.1:43823/simplelogin-callback',
'learn-scheme': 'app.halkyon.learn://simplelogin-callback',
'todo-loopback': 'http://127.0.0.1:43824/simplelogin-callback',
'todo-scheme': 'app.halkyon.todo://simplelogin-callback',
};
final _returns = {

View file

@ -161,11 +161,11 @@ OidcIdpConfig? simpleLoginIdpConfig(Serverpod pod) {
);
}
/// The marker a sign-in's state begins with for [app] on [platform]
/// (`learn-desktop`), by which the forwarding page sends it back
/// The marker a sign-in's state begins with for [app] coming [back]
/// (`learn-loopback`), by which the forwarding page sends it back
/// ([simpleLoginReturns]).
String simpleLoginMarker(String app, SimpleLoginPlatform platform) =>
'$app-${platform.name}';
String simpleLoginMarker(String app, SimpleLoginReturn back) =>
'$app-${back.name}';
/// Puts [app] in place of [pod]'s SimpleLogin app, null meaning none
/// whatever the passwords hold, asking SimpleLogin through [client]: tests

View file

@ -15,18 +15,19 @@ import 'package:serverpod_auth_idp_server/core.dart';
/// and checks SimpleLogin's codes (ADR 0016); a session is this server's.
/// Refusals are `OidcRefusedException`s.
class SimpleLoginEndpoint extends Endpoint {
/// Begins a sign-in from this server's app on [platform]: the attempt,
/// whose ID the device keeps to itself, and SimpleLogin's address to open
/// in the browser, which comes back through the forwarding page with a
/// code for [complete]. Refusals as `beginSimpleLogin`'s.
/// Begins a sign-in from this server's app, coming [back] by the loopback
/// or the app's scheme: the attempt, whose ID the device keeps to itself,
/// and SimpleLogin's address to open in the browser, which comes back
/// through the forwarding page with a code for [complete]. Refusals as
/// `beginSimpleLogin`'s.
Future<OidcStart> begin(
Session session, {
required SimpleLoginPlatform platform,
required SimpleLoginReturn back,
}) {
final pod = session.serverpod;
return AccountRegister.of(pod).beginSimpleLogin(
session,
marker: simpleLoginMarker(HalkyonBase.of(pod).app, platform),
marker: simpleLoginMarker(HalkyonBase.of(pod).app, back),
caller: session.authenticated?.authUserId,
network: networkOf(session),
);

View file

@ -1,8 +0,0 @@
### Where an app begins a SimpleLogin sign-in (ADR 0020, 3), which says where
### the forwarding page sends it back: the desktop's loopback, or the phone's
### scheme (or, with learn.halkyon.eu's App Links, the phone's app itself).
enum: SimpleLoginPlatform
serialized: byName
values:
- desktop
- phone

View file

@ -0,0 +1,8 @@
### How an app comes back from a SimpleLogin sign-in (ADR 0020, 3 and D9),
### which says where the forwarding page sends it: the loopback (Linux,
### Windows) or the app's scheme (Android, iOS, macOS).
enum: SimpleLoginReturn
serialized: byName
values:
- loopback
- scheme