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
17 changed files with 201 additions and 182 deletions

View file

@ -87,11 +87,12 @@ Endpoints, under the nickname `base` in a host's client (`client.modules.base.em
`/simplelogin-callback`, the one address the instance's SimpleLogin
app registers, as SimpleLogin takes only https addresses, each registered exactly. It
answers a 303 to the app that began the sign-in, with SimpleLogin's `code` (or its
`error`) and the `state`, and nothing else: the state is `<app>-<platform>.<rest>`, the
marker one of `simpleLoginReturns` (the desktop's loopback, `127.0.0.1` on a port of each
app's own, or the phone's scheme, the app's ID), the rest 1 to 512 characters of
base64url; anything else gets a page saying the sign-in can't go on, repeating nothing
it was given, in English or German. It keeps and logs nothing.
`error`) and the `state`, and nothing else: the state is `<app>-<way back>.<rest>`, the
marker one of `simpleLoginReturns` (the loopback on Linux and Windows, `127.0.0.1` on a
port of each app's own, or the app's scheme, its ID, on Android, iOS and macOS;
`simpleLogin.begin` takes the way back as a `SimpleLoginReturn`), the rest 1 to 512
characters of base64url; anything else gets a page saying the sign-in can't go on,
repeating nothing it was given, in English or German. It keeps and logs nothing.
- **Per-network limits** (new anonymous accounts, codes by mail, password sign-ins,
proofs of the recovery phrase) count by the visitor's IP address (`networkOf`): the
connection's, or, for a connection from a proxy the operator trusts

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

View file

@ -131,7 +131,7 @@ void main() {
) async {
final start = await endpoints.simpleLogin.begin(
as,
platform: SimpleLoginPlatform.desktop,
back: SimpleLoginReturn.loopback,
);
await endpoints.simpleLogin.complete(
as,

View file

@ -1183,30 +1183,30 @@ void main() {
});
group('SimpleLogin, by asking the register (ADR 0020)', () {
test('begins for this app and platform, signed out, the device '
test('begins for this app and way back, signed out, the device '
'network passed on', () async {
final start = await endpoints.simpleLogin.begin(
notSignedIn(),
platform: SimpleLoginPlatform.phone,
back: SimpleLoginReturn.scheme,
);
expect(register.asked, ['beginSimpleLogin']);
expect(register.simpleLoginsBegun.single, (
marker: '$testApp-phone',
marker: '$testApp-scheme',
caller: null,
));
expect(register.networks, hasLength(1));
expect(start.state, startsWith('$testApp-phone.'));
expect(start.state, startsWith('$testApp-scheme.'));
});
test('begins signed in for the caller', () async {
await endpoints.simpleLogin.begin(
asAnna(),
platform: SimpleLoginPlatform.desktop,
back: SimpleLoginReturn.loopback,
);
expect(register.simpleLoginsBegun.single, (
marker: '$testApp-desktop',
marker: '$testApp-loopback',
caller: anna,
));
});

View file

@ -565,7 +565,7 @@ void main() {
final start = await remote.beginSimpleLogin(
session,
marker: 'todo-desktop',
marker: 'todo-loopback',
network: '203.0.113.0/24',
);
final proof = await remote.completeSimpleLogin(
@ -577,7 +577,7 @@ void main() {
),
);
expect(start.state, startsWith('todo-desktop.'));
expect(start.state, startsWith('todo-loopback.'));
expect(proof.outcome, SimpleLoginOutcome.signedIn);
expect(proof.account, ben);
});
@ -588,7 +588,7 @@ void main() {
final start = await remote.beginSimpleLogin(
session,
marker: 'todo-phone',
marker: 'todo-scheme',
caller: annaOnTodo,
network: '203.0.113.0/24',
);
@ -617,7 +617,7 @@ void main() {
test('passes on the handle suggested to someone new', () async {
final start = await remote.beginSimpleLogin(
session,
marker: 'todo-desktop',
marker: 'todo-loopback',
network: '203.0.113.0/24',
);
final proof = await remote.completeSimpleLogin(
@ -639,7 +639,7 @@ void main() {
'crossing whole', () async {
final start = await remote.beginSimpleLogin(
session,
marker: 'todo-desktop',
marker: 'todo-loopback',
network: '203.0.113.0/24',
);
final code = simpleLogin.authorize(
@ -677,7 +677,7 @@ void main() {
final device = await KeysDevice.make();
final start = await remote.beginSimpleLogin(
session,
marker: 'todo-desktop',
marker: 'todo-loopback',
network: '203.0.113.0/24',
);
await remote.completeSimpleLogin(
@ -737,7 +737,7 @@ void main() {
final device = await KeysDevice.make();
final start = await remote.beginSimpleLogin(
session,
marker: 'todo-desktop',
marker: 'todo-loopback',
network: '203.0.113.0/24',
);
await remote.completeSimpleLogin(
@ -810,7 +810,7 @@ void main() {
) async {
final start = await remote.beginSimpleLogin(
session,
marker: 'todo-desktop',
marker: 'todo-loopback',
caller: person,
network: '203.0.113.0/24',
);
@ -885,7 +885,7 @@ void main() {
final next = await KeysDevice.make(seed: 7);
final start = await remote.beginSimpleLogin(
session,
marker: 'todo-desktop',
marker: 'todo-loopback',
network: '203.0.113.0/24',
);
await remote.completeSimpleLogin(
@ -988,7 +988,7 @@ void main() {
final device = await KeysDevice.make();
final first = await remote.beginSimpleLogin(
session,
marker: 'todo-desktop',
marker: 'todo-loopback',
network: '203.0.113.0/24',
);
await remote.completeSimpleLogin(
@ -1008,7 +1008,7 @@ void main() {
);
final start = await remote.beginSimpleLogin(
session,
marker: 'todo-desktop',
marker: 'todo-loopback',
network: '203.0.113.0/24',
);
await remote.completeSimpleLogin(
@ -1073,7 +1073,7 @@ void main() {
await expectLater(
remote.beginSimpleLogin(
session,
marker: 'todo-desktop',
marker: 'todo-loopback',
network: '203.0.113.0/24',
),
refused(OidcRefusal.unknownProvider),

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),
);

View file

@ -60,8 +60,8 @@ import 'package:halkyon_base_server/src/generated/profile/profile_refusal.dart'
as _idszjcpz;
import 'package:halkyon_base_server/src/generated/simplelogin/simplelogin_completed.dart'
as _i8g1l2iv;
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'
@ -1866,7 +1866,7 @@ class _SimpleLoginEndpoint {
_ida.Future<_ipoqfiqr.OidcStart> begin(
_ist.TestSessionBuilder sessionBuilder, {
required _ixrtiuh0.SimpleLoginPlatform platform,
required _igmh0yku.SimpleLoginReturn back,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
@ -1879,7 +1879,7 @@ class _SimpleLoginEndpoint {
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'simpleLogin',
methodName: 'begin',
parameters: _ist.testObjectToJson({'platform': platform}),
parameters: _ist.testObjectToJson({'back': back}),
serializationManager: _serializationManager,
);
var _localReturnValue =

View file

@ -18,13 +18,13 @@ OidcCallbackRequest _get(
);
void main() {
test("names Halkyon's apps on the desktop's loopback and the phones' "
'schemes alone', () {
test("names Halkyon's apps on the loopback (Linux, Windows) and their "
'schemes (Android, iOS, macOS) alone', () {
expect(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',
});
});
@ -48,8 +48,8 @@ void main() {
test('refuses a marker of no Halkyon app', () {
for (final state in [
'evil-desktop.abc',
'Learn-desktop.abc',
'evil-loopback.abc',
'Learn-loopback.abc',
'learn.abc',
]) {
final answer = simpleLoginCallback(_get({'code': 'abc', 'state': state}));

View file

@ -102,18 +102,21 @@ void main() {
expect(simpleLoginAppFrom((name) => null, webServer: null), isNull);
});
test('marks a sign-in with the app and the platform, which the forwarding '
test('marks a sign-in with the app and its way back, which the forwarding '
'page knows', () {
expect(
simpleLoginMarker('learn', SimpleLoginPlatform.desktop),
'learn-desktop',
simpleLoginMarker('learn', SimpleLoginReturn.loopback),
'learn-loopback',
);
expect(
simpleLoginMarker('todo', SimpleLoginReturn.scheme),
'todo-scheme',
);
expect(simpleLoginMarker('todo', SimpleLoginPlatform.phone), 'todo-phone');
for (final app in ['learn', 'todo']) {
for (final platform in SimpleLoginPlatform.values) {
for (final back in SimpleLoginReturn.values) {
expect(
simpleLoginReturns,
contains(simpleLoginMarker(app, platform)),
contains(simpleLoginMarker(app, back)),
);
}
}