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

View file

@ -101,7 +101,7 @@ void main() {
}) async {
final start = await endpoints.simpleLogin.begin(
as,
platform: SimpleLoginPlatform.desktop,
back: SimpleLoginReturn.loopback,
);
return (
attempt: start.attempt,
@ -138,7 +138,7 @@ void main() {
await expectLater(
endpoints.simpleLogin.begin(
notSignedIn(),
platform: SimpleLoginPlatform.desktop,
back: SimpleLoginReturn.loopback,
),
refused(OidcRefusal.unknownProvider),
);
@ -159,7 +159,7 @@ void main() {
await expectLater(
endpoints.simpleLogin.begin(
notSignedIn(),
platform: SimpleLoginPlatform.desktop,
back: SimpleLoginReturn.loopback,
),
refused(OidcRefusal.unknownProvider),
);
@ -196,17 +196,17 @@ void main() {
group('beginning', () {
test('sends the person to SimpleLogin, the state marked with the app '
'and its platform for the forwarding page', () async {
final desktop = await endpoints.simpleLogin.begin(
'and its way back for the forwarding page', () async {
final loopback = await endpoints.simpleLogin.begin(
notSignedIn(),
platform: SimpleLoginPlatform.desktop,
back: SimpleLoginReturn.loopback,
);
final phone = await endpoints.simpleLogin.begin(
final scheme = await endpoints.simpleLogin.begin(
notSignedIn(),
platform: SimpleLoginPlatform.phone,
back: SimpleLoginReturn.scheme,
);
final url = desktop.authorizationUrl;
final url = loopback.authorizationUrl;
expect(
'${url.origin}${url.path}',
'https://app.simplelogin.io/oauth2/authorize',
@ -216,10 +216,13 @@ void main() {
url.queryParameters['redirect_uri'],
'https://learn.test/simplelogin-callback',
);
expect(desktop.state, startsWith('learn-desktop.'));
expect(phone.state, startsWith('learn-phone.'));
expect(loopback.state, startsWith('learn-loopback.'));
expect(scheme.state, startsWith('learn-scheme.'));
expect(
(await OidcAttempt.db.findById(session, desktop.attempt))!.callerId,
(await OidcAttempt.db.findById(
session,
loopback.attempt,
))!.callerId,
isNull,
);
});
@ -227,7 +230,7 @@ void main() {
test('signed in, notes who began it', () async {
final start = await endpoints.simpleLogin.begin(
asAnna(),
platform: SimpleLoginPlatform.phone,
back: SimpleLoginReturn.scheme,
);
expect(
@ -245,7 +248,7 @@ void main() {
await expectLater(
endpoints.simpleLogin.begin(
notSignedIn(),
platform: SimpleLoginPlatform.desktop,
back: SimpleLoginReturn.loopback,
),
throwsA(isA<StateError>()),
);
@ -255,14 +258,14 @@ void main() {
for (var i = 0; i < 30; i++) {
await endpoints.simpleLogin.begin(
notSignedIn(),
platform: SimpleLoginPlatform.desktop,
back: SimpleLoginReturn.loopback,
);
}
await expectLater(
endpoints.simpleLogin.begin(
notSignedIn(),
platform: SimpleLoginPlatform.desktop,
back: SimpleLoginReturn.loopback,
),
refused(OidcRefusal.tooManyAttempts),
);