feat(oidc): a Serverpod module for signing in with OpenID Connect, SimpleLogin a preset (ADR 0023)
halkyon_oidc_server and its generated client halkyon_oidc_client: any provider following OpenID Connect's code flow, beside Serverpod's own providers, with a preset for SimpleLogin (no PKCE, no `typ`). Nothing of Halkyon's is in it; BSD-3-Clause, `publish_to: none` for now. - Attempts in `halkyon_oidc_attempt` (ADR 0020's D13): the ID PostgreSQL's gen_random_uuid(), a state and a nonce of 32 random bytes, PKCE's verifier where used, the caller if signed in; ten minutes, taken once. A host may begin the state with a marker. - The code exchanged by the module itself, so a provider out of reach (a timeout, a 5xx) is told apart from a refused code; the ID token checked with dart_jsonwebtoken's algorithms (ADR 0020's D12): the provider's algorithms alone, the key its `kid` names, of the right type; issuer, audience and `azp`, expiry, time issued, `nbf`, a minute's leeway; the attempt's nonce; a `sub`. - Keys kept an hour, fetched again for an unknown `kid` at most once a minute; discovery checked against the issuer; every request with a timeout. - Identities in `halkyon_oidc_identity`: the provider's key and the `sub`, one per person and provider, deleted with the person. - The stock endpoint (`OidcIdpBaseEndpoint`), and the parts for a host with rules of its own: begin, verify, keep and takeKept, link, accountOf, unlink; merging people; a limit per network. - The forwarding page, moved here from halkyon_base in the next commit. The package's main library is the code generator's, as halkyon_base's; a host imports `package:halkyon_oidc_server/oidc.dart`. ADR 0023's point 5 no longer names OAuth2PkceUtil, which the module doesn't use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
630226b51e
commit
f549fa59df
12 changed files with 15870 additions and 2 deletions
13
packages/halkyon_oidc_client/.gitignore
vendored
Normal file
13
packages/halkyon_oidc_client/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Files and directories created by pub
|
||||
.dart_tool/
|
||||
.packages
|
||||
|
||||
# Omit committing pubspec.lock for library packages:
|
||||
# https://dart.dev/guides/libraries/private-files#pubspeclock
|
||||
pubspec.lock
|
||||
|
||||
# Conventional directory for build outputs
|
||||
build/
|
||||
|
||||
# Directory created by dartdoc
|
||||
doc/api/
|
||||
3
packages/halkyon_oidc_client/CHANGELOG.md
Normal file
3
packages/halkyon_oidc_client/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
## 0.1.0
|
||||
|
||||
* The client of halkyon_oidc 0.1.0, generated by Serverpod.
|
||||
28
packages/halkyon_oidc_client/LICENSE
Normal file
28
packages/halkyon_oidc_client/LICENSE
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2026, the Project Halkyon authors
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
9
packages/halkyon_oidc_client/README.md
Normal file
9
packages/halkyon_oidc_client/README.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# halkyon_oidc_client
|
||||
|
||||
The client of [`halkyon_oidc`](../halkyon_oidc_server/README.md), generated by
|
||||
Serverpod: the models an app gets when it signs in with OpenID Connect (`OidcStart`,
|
||||
`OidcRefusedException` with its `OidcRefusal`), and the stock endpoint a host extends.
|
||||
Never edited by hand; run `serverpod --no-analytics generate` in `halkyon_oidc_server`
|
||||
instead.
|
||||
|
||||
Under the BSD 3-Clause licence, Serverpod's own.
|
||||
16
packages/halkyon_oidc_client/analysis_options.yaml
Normal file
16
packages/halkyon_oidc_client/analysis_options.yaml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# Defines a default set of lint rules enforced for
|
||||
# projects at Google. For details and rationale,
|
||||
# see https://github.com/dart-lang/pedantic#enabled-lints.
|
||||
|
||||
# For lint rules and documentation, see http://dart-lang.github.io/linter/lints.
|
||||
# Uncomment to specify additional rules.
|
||||
# linter:
|
||||
# rules:
|
||||
# - camel_case_types
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
- lib/src/protocol/**
|
||||
|
||||
formatter:
|
||||
trailing_commas: preserve
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export 'src/protocol/protocol.dart';
|
||||
|
||||
export 'package:serverpod_client/serverpod_client.dart';
|
||||
62
packages/halkyon_oidc_client/lib/src/protocol/client.dart
Normal file
62
packages/halkyon_oidc_client/lib/src/protocol/client.dart
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/* 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 'dart:async' as _ida;
|
||||
import 'package:halkyon_oidc_client/src/protocol/oidc_start.dart' as _innqwlkm;
|
||||
import 'package:serverpod_auth_core_client/serverpod_auth_core_client.dart'
|
||||
as _iacc;
|
||||
import 'package:serverpod_auth_idp_client/serverpod_auth_idp_client.dart'
|
||||
as _iaic;
|
||||
import 'package:serverpod_client/serverpod_client.dart' as _isc;
|
||||
|
||||
/// The stock endpoint of OpenID Connect sign-in (ADR 0023, 6), as
|
||||
/// Serverpod's own providers have theirs: a host extends it in its own
|
||||
/// server (`class OidcIdpEndpoint extends OidcIdpBaseEndpoint {}`), which
|
||||
/// puts it into the host's client.
|
||||
///
|
||||
/// Its per-network limit counts by the request's `remoteInfo`, the connection's
|
||||
/// address, or the `Forwarded` or `X-Forwarded-For` header's first address
|
||||
/// where one came, whoever wrote it: behind a proxy that passes either on as
|
||||
/// the visitor sent it, a host overrides [begin] and counts by the address
|
||||
/// its proxy saw.
|
||||
/// {@category Endpoint}
|
||||
abstract class EndpointOidcIdpBase extends _iaic.EndpointIdpBase {
|
||||
EndpointOidcIdpBase(_isc.EndpointCaller caller) : super(caller);
|
||||
|
||||
/// Begins a sign-in with [provider] (its key): signed in, to add the
|
||||
/// identity to the caller ([link]); signed out, to sign in ([login]).
|
||||
_ida.Future<_innqwlkm.OidcStart> begin({required String provider});
|
||||
|
||||
/// Signs in with the identity [code] proves for [attempt], begun signed
|
||||
/// out: a session for its person, made for one nobody has.
|
||||
_ida.Future<_iacc.AuthSuccess> login({
|
||||
required _isc.UuidValue attempt,
|
||||
required String code,
|
||||
});
|
||||
|
||||
/// Links the identity [code] proves for [attempt], begun by the caller
|
||||
/// signed in, to them.
|
||||
_ida.Future<void> link({
|
||||
required _isc.UuidValue attempt,
|
||||
required String code,
|
||||
});
|
||||
|
||||
@override
|
||||
_ida.Future<bool> hasAccount();
|
||||
}
|
||||
|
||||
class Caller extends _isc.ModuleEndpointCaller {
|
||||
Caller(_isc.ServerpodClientShared client) : super(client) {}
|
||||
|
||||
@override
|
||||
Map<String, _isc.EndpointRef> get endpointRefLookup => {};
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
/* 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_client/serverpod_client.dart' as _isc;
|
||||
|
||||
/// Why a sign-in with an OpenID Connect provider was refused.
|
||||
enum OidcRefusal implements _isc.SerializableModel {
|
||||
/// The host names no provider by this key.
|
||||
unknownProvider,
|
||||
|
||||
/// No attempt has this ID: none began, it was ended already, or its time
|
||||
/// ran out.
|
||||
unknownAttempt,
|
||||
|
||||
/// The attempt was begun by someone else, or signed in where this call is
|
||||
/// signed out, or the other way round.
|
||||
wrongCaller,
|
||||
|
||||
/// The provider refused the code, or its answer didn't prove anyone: a
|
||||
/// token not signed by its key, for another client or attempt, expired.
|
||||
rejected,
|
||||
|
||||
/// The provider couldn't be reached, or answered nothing readable; trying
|
||||
/// again later may work.
|
||||
providerUnavailable,
|
||||
|
||||
/// The identity is another person's.
|
||||
takenByAnotherPerson,
|
||||
|
||||
/// The person has another identity at this provider.
|
||||
otherIdentityLinked,
|
||||
|
||||
/// Too many sign-ins were begun from this network; later.
|
||||
tooManyAttempts;
|
||||
|
||||
static OidcRefusal fromJson(String name) {
|
||||
switch (name) {
|
||||
case 'unknownProvider':
|
||||
return OidcRefusal.unknownProvider;
|
||||
case 'unknownAttempt':
|
||||
return OidcRefusal.unknownAttempt;
|
||||
case 'wrongCaller':
|
||||
return OidcRefusal.wrongCaller;
|
||||
case 'rejected':
|
||||
return OidcRefusal.rejected;
|
||||
case 'providerUnavailable':
|
||||
return OidcRefusal.providerUnavailable;
|
||||
case 'takenByAnotherPerson':
|
||||
return OidcRefusal.takenByAnotherPerson;
|
||||
case 'otherIdentityLinked':
|
||||
return OidcRefusal.otherIdentityLinked;
|
||||
case 'tooManyAttempts':
|
||||
return OidcRefusal.tooManyAttempts;
|
||||
default:
|
||||
throw ArgumentError(
|
||||
'Value "$name" cannot be converted to "OidcRefusal"',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toJson() => name;
|
||||
|
||||
@override
|
||||
String toString() => name;
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/* 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_client/serverpod_client.dart' as _isc;
|
||||
import 'oidc_refusal.dart' as _i8gvcysd;
|
||||
|
||||
/// A sign-in with an OpenID Connect provider was refused; nothing changed.
|
||||
abstract class OidcRefusedException
|
||||
implements
|
||||
_isc.SerializableException,
|
||||
_isc.SerializableModel,
|
||||
_isc.ProtocolSerialization {
|
||||
OidcRefusedException._({required this.reason});
|
||||
|
||||
factory OidcRefusedException({required _i8gvcysd.OidcRefusal reason}) =
|
||||
_OidcRefusedExceptionImpl;
|
||||
|
||||
factory OidcRefusedException.fromJson(
|
||||
Map<String, dynamic> jsonSerialization,
|
||||
) {
|
||||
return OidcRefusedException(
|
||||
reason: _i8gvcysd.OidcRefusal.fromJson(
|
||||
(jsonSerialization['reason'] as String),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Why it was refused.
|
||||
_i8gvcysd.OidcRefusal reason;
|
||||
|
||||
/// Returns a shallow copy of this [OidcRefusedException]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
OidcRefusedException copyWith({_i8gvcysd.OidcRefusal? reason});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'halkyon_oidc.OidcRefusedException',
|
||||
'reason': reason.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'halkyon_oidc.OidcRefusedException',
|
||||
'reason': reason.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'OidcRefusedException(reason: $reason)';
|
||||
}
|
||||
}
|
||||
|
||||
class _OidcRefusedExceptionImpl extends OidcRefusedException {
|
||||
_OidcRefusedExceptionImpl({required _i8gvcysd.OidcRefusal reason})
|
||||
: super._(reason: reason);
|
||||
|
||||
/// Returns a shallow copy of this [OidcRefusedException]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
@override
|
||||
OidcRefusedException copyWith({_i8gvcysd.OidcRefusal? reason}) {
|
||||
return OidcRefusedException(reason: reason ?? this.reason);
|
||||
}
|
||||
}
|
||||
114
packages/halkyon_oidc_client/lib/src/protocol/oidc_start.dart
Normal file
114
packages/halkyon_oidc_client/lib/src/protocol/oidc_start.dart
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
/* 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_client/serverpod_client.dart' as _isc;
|
||||
|
||||
/// What a device needs to send a person to their provider: the attempt it
|
||||
/// ends the sign-in with, which only the device holds, and where to go.
|
||||
abstract class OidcStart
|
||||
implements _isc.SerializableModel, _isc.ProtocolSerialization {
|
||||
OidcStart._({
|
||||
required this.attempt,
|
||||
required this.authorizationUrl,
|
||||
required this.state,
|
||||
});
|
||||
|
||||
factory OidcStart({
|
||||
required _isc.UuidValue attempt,
|
||||
required Uri authorizationUrl,
|
||||
required String state,
|
||||
}) = _OidcStartImpl;
|
||||
|
||||
factory OidcStart.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return OidcStart(
|
||||
attempt: _isc.UuidValueJsonExtension.fromJson(
|
||||
jsonSerialization['attempt'],
|
||||
),
|
||||
authorizationUrl: _isc.UriJsonExtension.fromJson(
|
||||
jsonSerialization['authorizationUrl'],
|
||||
),
|
||||
state: jsonSerialization['state'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
/// The attempt's ID, which ends it (`verify`); never shown, never sent
|
||||
/// anywhere but to this server.
|
||||
_isc.UuidValue attempt;
|
||||
|
||||
/// The provider's authorization address, with everything it needs.
|
||||
Uri authorizationUrl;
|
||||
|
||||
/// The state the provider sends back with the code, which the device
|
||||
/// compares with what comes back before it goes on.
|
||||
String state;
|
||||
|
||||
/// Returns a shallow copy of this [OidcStart]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
OidcStart copyWith({
|
||||
_isc.UuidValue? attempt,
|
||||
Uri? authorizationUrl,
|
||||
String? state,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'halkyon_oidc.OidcStart',
|
||||
'attempt': attempt.toJson(),
|
||||
'authorizationUrl': authorizationUrl.toJson(),
|
||||
'state': state,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'halkyon_oidc.OidcStart',
|
||||
'attempt': attempt.toJson(),
|
||||
'authorizationUrl': authorizationUrl.toJson(),
|
||||
'state': state,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _isc.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _OidcStartImpl extends OidcStart {
|
||||
_OidcStartImpl({
|
||||
required _isc.UuidValue attempt,
|
||||
required Uri authorizationUrl,
|
||||
required String state,
|
||||
}) : super._(
|
||||
attempt: attempt,
|
||||
authorizationUrl: authorizationUrl,
|
||||
state: state,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [OidcStart]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
@override
|
||||
OidcStart copyWith({
|
||||
_isc.UuidValue? attempt,
|
||||
Uri? authorizationUrl,
|
||||
String? state,
|
||||
}) {
|
||||
return OidcStart(
|
||||
attempt: attempt ?? this.attempt,
|
||||
authorizationUrl: authorizationUrl ?? this.authorizationUrl,
|
||||
state: state ?? this.state,
|
||||
);
|
||||
}
|
||||
}
|
||||
231
packages/halkyon_oidc_client/lib/src/protocol/protocol.dart
Normal file
231
packages/halkyon_oidc_client/lib/src/protocol/protocol.dart
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
/* 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: dead_code, unnecessary_type_check
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod_auth_core_client/serverpod_auth_core_client.dart'
|
||||
as _iacc;
|
||||
import 'package:serverpod_auth_idp_client/serverpod_auth_idp_client.dart'
|
||||
as _iaic;
|
||||
import 'package:serverpod_client/serverpod_client.dart' as _isc;
|
||||
import 'oidc_refusal.dart' as _i8gvcysd;
|
||||
import 'oidc_refused_exception.dart' as _i70wwg4b;
|
||||
import 'oidc_start.dart' as _i4fk0f61;
|
||||
export 'oidc_refusal.dart';
|
||||
export 'oidc_refused_exception.dart';
|
||||
export 'oidc_start.dart';
|
||||
export 'client.dart';
|
||||
|
||||
class Protocol extends _isc.SerializationManager {
|
||||
Protocol._();
|
||||
|
||||
factory Protocol() => _instance;
|
||||
|
||||
static final Protocol _instance = Protocol._();
|
||||
|
||||
final Set<_isc.SerializationManager> _hostProtocols = {};
|
||||
|
||||
void registerHostProtocol(
|
||||
String projectName,
|
||||
_isc.SerializationManager protocol,
|
||||
) {
|
||||
_hostProtocols.add(protocol);
|
||||
}
|
||||
|
||||
static String? getClassNameFromObjectJson(dynamic data) {
|
||||
if (data is! Map) return null;
|
||||
final className = data['__className__'] as String?;
|
||||
if (className == null) return null;
|
||||
if (!className.startsWith('halkyon_oidc.')) return className;
|
||||
return className.substring(13);
|
||||
}
|
||||
|
||||
@override
|
||||
T deserialize<T>(
|
||||
dynamic data, [
|
||||
Type? t,
|
||||
]) {
|
||||
t ??= T;
|
||||
|
||||
final dataClassName = getClassNameFromObjectJson(data);
|
||||
if (dataClassName != null && dataClassName != getClassNameForType(t)) {
|
||||
try {
|
||||
return deserializeByClassName({
|
||||
'className': dataClassName,
|
||||
'data': data,
|
||||
});
|
||||
} on _isc.DeserializationClassNameNotFoundException catch (_) {
|
||||
// If the className is not recognized (e.g., older client receiving
|
||||
// data with a new subtype), fall back to deserializing without the
|
||||
// className, using the expected type T.
|
||||
}
|
||||
}
|
||||
|
||||
if (t == _i8gvcysd.OidcRefusal) {
|
||||
return _i8gvcysd.OidcRefusal.fromJson(data) as T;
|
||||
}
|
||||
if (t == _i70wwg4b.OidcRefusedException) {
|
||||
return _i70wwg4b.OidcRefusedException.fromJson(data) as T;
|
||||
}
|
||||
if (t == _i4fk0f61.OidcStart) {
|
||||
return _i4fk0f61.OidcStart.fromJson(data) as T;
|
||||
}
|
||||
if (t == _isc.getType<_i8gvcysd.OidcRefusal?>()) {
|
||||
return (data != null ? _i8gvcysd.OidcRefusal.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _isc.getType<_i70wwg4b.OidcRefusedException?>()) {
|
||||
return (data != null
|
||||
? _i70wwg4b.OidcRefusedException.fromJson(data)
|
||||
: null)
|
||||
as T;
|
||||
}
|
||||
if (t == _isc.getType<_i4fk0f61.OidcStart?>()) {
|
||||
return (data != null ? _i4fk0f61.OidcStart.fromJson(data) : null) as T;
|
||||
}
|
||||
try {
|
||||
return _iaic.Protocol().deserialize<T>(data, t);
|
||||
} on _isc.DeserializationTypeNotFoundException catch (_) {}
|
||||
try {
|
||||
return _iacc.Protocol().deserialize<T>(data, t);
|
||||
} on _isc.DeserializationTypeNotFoundException catch (_) {}
|
||||
return super.deserialize<T>(data, t);
|
||||
}
|
||||
|
||||
static String? getClassNameForType(Type type) {
|
||||
return switch (type) {
|
||||
_i8gvcysd.OidcRefusal => 'OidcRefusal',
|
||||
_i70wwg4b.OidcRefusedException => 'OidcRefusedException',
|
||||
_i4fk0f61.OidcStart => 'OidcStart',
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String? getClassNameForObject(Object? data) {
|
||||
String? className = super.getClassNameForObject(data);
|
||||
if (className != null) return className;
|
||||
|
||||
if (data is Map<String, dynamic> && data['__className__'] is String) {
|
||||
return (data['__className__'] as String).replaceFirst(
|
||||
'halkyon_oidc.',
|
||||
'',
|
||||
);
|
||||
}
|
||||
|
||||
switch (data) {
|
||||
case _i8gvcysd.OidcRefusal():
|
||||
return 'OidcRefusal';
|
||||
case _i70wwg4b.OidcRefusedException():
|
||||
return 'OidcRefusedException';
|
||||
case _i4fk0f61.OidcStart():
|
||||
return 'OidcStart';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
dynamic deserializeByClassName(Map<String, dynamic> data) {
|
||||
var dataClassName = data['className'];
|
||||
if (dataClassName is! String) {
|
||||
return super.deserializeByClassName(data);
|
||||
}
|
||||
if (dataClassName == 'OidcRefusal') {
|
||||
return deserialize<_i8gvcysd.OidcRefusal>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'OidcRefusedException') {
|
||||
return deserialize<_i70wwg4b.OidcRefusedException>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'OidcStart') {
|
||||
return deserialize<_i4fk0f61.OidcStart>(data['data']);
|
||||
}
|
||||
return super.deserializeByClassName(data);
|
||||
}
|
||||
|
||||
@override
|
||||
Object? dynamicFieldToJson(
|
||||
Object? object, {
|
||||
bool forProtocol = false,
|
||||
}) {
|
||||
if ((object is List || object is Set || object is Map) ||
|
||||
getClassNameForObject(object) != null) {
|
||||
return super.dynamicFieldToJson(object, forProtocol: forProtocol);
|
||||
}
|
||||
for (final protocol in _hostProtocols) {
|
||||
final className = protocol.getClassNameForObject(object);
|
||||
if (className == null) continue;
|
||||
final host = protocol.getModuleName();
|
||||
final wrapped = {
|
||||
'className': className.contains('.') ? className : '$host.$className',
|
||||
'data': object,
|
||||
};
|
||||
return forProtocol
|
||||
? _isc.SerializationManager.toEncodableForProtocol(wrapped)
|
||||
: _isc.SerializationManager.toEncodable(wrapped);
|
||||
}
|
||||
return super.dynamicFieldToJson(object, forProtocol: forProtocol);
|
||||
}
|
||||
|
||||
@override
|
||||
dynamic deserializeDynamicFieldValue(Object? value) {
|
||||
if (value == null) return null;
|
||||
if (value is! Map<String, dynamic> || value['className'] is! String) {
|
||||
throw FormatException(
|
||||
'Dynamic fields are encoded as a Map with className and data, but got '
|
||||
'${value.runtimeType} instead.',
|
||||
);
|
||||
}
|
||||
final className = value['className'] as String;
|
||||
for (final protocol in _hostProtocols) {
|
||||
final host = protocol.getModuleName();
|
||||
final hostPrefix = '$host.';
|
||||
if (className.startsWith(hostPrefix)) {
|
||||
final strippedClassName = className.substring(hostPrefix.length);
|
||||
if (strippedClassName.contains('.')) {
|
||||
throw FormatException(
|
||||
'Dynamic field className must not use multiple prefixes: $className',
|
||||
);
|
||||
}
|
||||
final hostData = Map<String, dynamic>.from(value);
|
||||
hostData['className'] = strippedClassName;
|
||||
return protocol.deserializeByClassName(hostData);
|
||||
}
|
||||
}
|
||||
if (className.contains('.')) {
|
||||
for (final protocol in _hostProtocols) {
|
||||
try {
|
||||
return protocol.deserializeByClassName(value);
|
||||
} on _isc.DeserializationClassNameNotFoundException catch (_) {}
|
||||
}
|
||||
}
|
||||
return deserializeByClassName(value);
|
||||
}
|
||||
|
||||
@override
|
||||
String getModuleName() => 'halkyon_oidc';
|
||||
|
||||
/// Maps any `Record`s known to this [Protocol] to their JSON representation
|
||||
///
|
||||
/// Throws in case the record type is not known.
|
||||
///
|
||||
/// This method will return `null` (only) for `null` inputs.
|
||||
Map<String, dynamic>? mapRecordToJson(Record? record) {
|
||||
if (record == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return _iaic.Protocol().mapRecordToJson(record);
|
||||
} catch (_) {}
|
||||
try {
|
||||
return _iacc.Protocol().mapRecordToJson(record);
|
||||
} catch (_) {}
|
||||
throw Exception('Unsupported record type ${record.runtimeType}');
|
||||
}
|
||||
}
|
||||
16
packages/halkyon_oidc_client/pubspec.yaml
Normal file
16
packages/halkyon_oidc_client/pubspec.yaml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
name: halkyon_oidc_client
|
||||
description: >-
|
||||
The client side of halkyon_oidc, generated by Serverpod: what an app calls
|
||||
to sign in with an OpenID Connect provider.
|
||||
version: 0.1.0
|
||||
# In the Halkyon repository until it runs on learn.halkyon.eu (ADR 0023, D3).
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: ^3.13.0
|
||||
|
||||
resolution: workspace
|
||||
|
||||
dependencies:
|
||||
serverpod_auth_idp_client: 4.0.4
|
||||
serverpod_client: 4.0.4
|
||||
Loading…
Reference in a new issue