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:
Tizian 「ティツィアーン」 2026-10-23 20:28:37 +02:00
commit f549fa59df
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
44 changed files with 15870 additions and 2 deletions

19
packages/halkyon_oidc_server/.gitignore vendored Normal file
View file

@ -0,0 +1,19 @@
# 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/
# Passwords file: the tests' throwaway values, never committed.
config/passwords.yaml
# Embedded PostgreSQL runtime artifacts (data dir, sockets, pidfiles)
.serverpod/

View file

@ -0,0 +1,5 @@
## 0.1.0
* Signing in with OpenID Connect's code flow, with a SimpleLogin preset: attempts,
the ID token's checks, identities, the stock endpoint and the parts, the forwarding
page.

View 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.

View file

@ -0,0 +1,163 @@
# halkyon_oidc
Signing in with OpenID Connect for Serverpod v4, as a Serverpod module
(`halkyon_oidc_server`, with its generated client `halkyon_oidc_client`): any provider
following OpenID Connect's code flow (Keycloak, Authentik, Zitadel, Authelia, Pocket ID,
…) beside Serverpod's own providers, with a preset for SimpleLogin, the one tried live.
Halkyon signs in with it (ADR 0020, ADR 0023); nothing of Halkyon's is in it.
## What it does
- **Begins a sign-in** (`OidcIdp.begin`): an attempt in `halkyon_oidc_attempt`, its ID
PostgreSQL's `gen_random_uuid()` (122 random bits), the secret only the device holds,
with a state and a nonce of 32 random bytes each, PKCE's verifier where the provider
takes PKCE, and who began it, if signed in. It answers with the attempt, the address to
send the person to, and the state. Attempts last ten minutes and are ended once.
- **Ends it** (`OidcIdp.verify`): the attempt taken out first, so it ends once whatever
follows; the code exchanged with the client's secret (HTTP Basic, or in the form); the
ID token checked: three parts, the header's algorithm one of the provider's
(asymmetric only, no `typ` asked for, as SimpleLogin sends none), the key the `kid`
names among the provider's keys, of the algorithm's type; the signature; the issuer,
the audience (and `azp` with more than one, or wherever it is given), the expiry, the
time issued and `nbf`, each with a minute of clocks' difference; the attempt's nonce;
a `sub`. What it proves is the provider's key and the `sub`, with the token's other
claims for the host's code, which nothing keeps.
- **Keeps a person's identity** at a provider (`halkyon_oidc_identity`): the provider's
key and the `sub`, nothing else. Each `sub` is one person's at a provider, and a
person has one per provider. It goes with the person (a foreign key to Serverpod's
`AuthUser`, deleted with it), as do their unfinished attempts.
- **Asks providers sparingly**: the discovery document once (checked to name the
provider's issuer and its three endpoints, asked again after a failure), the keys
for an hour, fetched again for an unknown `kid` at most once a minute; every request
with a timeout (10 s unless said), a provider out of reach refused as
`providerUnavailable`, never as a refused sign-in.
- **Limits the sign-ins one network begins** (30 an hour unless said, Serverpod's
`DatabaseRateLimiter`, domain `halkyon_oidc`), counting by the address the host gives.
Refusals are `OidcRefusedException`s with an `OidcRefusal`: `unknownProvider`,
`unknownAttempt` (nobody began it, it ended, or its time ran out), `wrongCaller`,
`rejected` (the provider refused the code, or the token proves nothing; the server
logs why), `providerUnavailable`, `takenByAnotherPerson`, `otherIdentityLinked`,
`tooManyAttempts`.
## Setting it up
1. **Depend on it** (`halkyon_oidc_server` in the server, `halkyon_oidc_client` in the
client), and name it in the server's `config/generator.yaml`:
```yaml
modules:
halkyon_oidc:
nickname: oidc
```
2. **Name the providers** where the server sets up its sign-in
(`package:halkyon_oidc_server/oidc.dart`), each under a key of its own that never
changes, as it is kept with every identity:
```dart
pod.initializeAuthServices(
tokenManagerBuilders: [ServerSideSessionsConfigFromPasswords()],
identityProviderBuilders: [
OidcIdpConfig(
providers: [
// Issuer, endpoints, no PKCE, RS256: what SimpleLogin does.
OidcProvider.simpleLogin(
clientId: pod.getPassword('simpleLoginClientId')!,
clientSecret: pod.getPassword('simpleLoginClientSecret')!,
callbackUrl: Uri.parse('https://example.org/simplelogin-callback'),
),
// Endpoints from the issuer's discovery document.
OidcProvider(
key: 'keycloak',
issuer: 'https://id.example.org/realms/main',
clientId: 'my-app',
clientSecret: pod.getPassword('keycloakClientSecret')!,
callbackUrl: Uri.parse('my.app://oidc-callback'),
scopes: const ['openid', 'email'],
),
],
),
],
);
```
`OidcProvider` takes the scopes (`openid` alone unless said), PKCE (on unless said),
the algorithms its tokens may be signed with (`RS256` unless said), the endpoints
where it has no discovery document, and how the client gives its secret.
`OidcIdpConfig` takes how long attempts last, the limit per network, the timeout and
the HTTP client.
3. **Add the endpoint**, either way below, then `serverpod generate` and
`serverpod create-migration`, which adds the module's two tables and its version
row to the server's migrations.
### The stock way
As Serverpod's own providers: one endpoint in the host's server,
```dart
class OidcIdpEndpoint extends OidcIdpBaseEndpoint {}
```
with `begin(provider)`, `login(attempt, code)` (a session for the person with that
identity, a new person for one nobody has), `link(attempt, code)` (the identity added to
the signed-in caller, who began the attempt signed in) and `hasAccount`. Sessions it
issues name the method `oidc`.
`begin` counts its limit by the request's `remoteInfo`: the connection's address, or the
first address of a `Forwarded` or `X-Forwarded-For` header, whoever wrote it. Behind a
proxy that passes either on as the visitor sent it, override `begin` and pass the
address the proxy saw as `network`.
### The parts
For a host with rules of its own (who may have an account, a name chosen first), its own
endpoints call `AuthServices.instance.oidcIdp` (`package:halkyon_oidc_server/oidc.dart`,
the host's library; the package's main library is the code generator's):
- `begin(session, provider:, caller:, statePrefix:, network:)`: an attempt.
- `verify(session, attempt:, code:, caller:)`: the identity proven
(`VerifiedOidcIdentity`: the provider's key, the `sub`, the claims, the caller),
touching no account.
- `keep(session, attempt:, identity:, keepFor:)`: the identity kept that long under the
attempt's ID, which `takeKept` takes once, as when a person chooses a name before their
account is made.
- `accountOf`, `hasIdentity`, `link`, `unlink`: the identities.
- `forgetExpiredAttempts`: `begin` forgets them on the way; call it daily as well.
`mergeAuthUsers` moves one person's identities to another, refusing where both have one
at the same provider.
## The forwarding page
Some providers take only https callbacks, each registered exactly (SimpleLogin), so a
desktop app's loopback address or a phone's scheme can't be one. The host then registers
one page of its web server, and the page sends each sign-in on to the app that began it:
```dart
addOidcCallbackForwarding(
pod,
path: '/simplelogin-callback',
returns: {
'desktop': Uri.parse('http://127.0.0.1:43823/oidc-callback'),
'phone': Uri.parse('my.app://oidc-callback'),
},
);
```
The app begins with `statePrefix: 'desktop.'` (or `'phone.'`); the page answers a 303
to the address the marker names, with the provider's `code` (or its `error`) and the
`state`, each exactly once and well formed, and refuses anything else with a page that
repeats nothing it was given (`refusalPage` for the host's own, in its look and
languages). It keeps and logs nothing: a code caught on its way is worthless without the
attempt the app holds. `oidcCallbackForwarding` is the page itself, for tests.
## Tests
`dart test` here starts its own PostgreSQL (`config/test.yaml`), with throwaway
`database`, `serviceSecret` and `serverSideSessionKeyHashPepper` in a git-ignored
`config/passwords.yaml`. The providers in the tests are fakes that keep to the
specification, keys generated per run; the ID token's checks are tested one by one.
Under the BSD 3-Clause licence, Serverpod's own.

View file

@ -0,0 +1,22 @@
# The workspace's lint rules (see ../../analysis_options.yaml), minus generated code.
include: ../../analysis_options.yaml
analyzer:
exclude:
- lib/src/generated/**
- test/integration/test_tools/serverpod_test_tools.dart
# Serverpod's code generator reads this package with an analyzer that doesn't know
# Dart v3.13's primary constructors yet, and fails on them in endpoint files ("requires
# the 'primary-constructors' language feature"; checked with v4.0.4). So the module
# keeps the classic constructor and class-body syntax, as the Halkyon servers do;
# switch these back on once Serverpod parses it.
linter:
rules:
empty_container_bodies: false
unnecessary_type_name_in_constructor: false
use_declaring_parameters: false
use_primary_constructors: false
formatter:
trailing_commas: preserve

View file

@ -0,0 +1,7 @@
type: module
# The name its endpoints have in a host's client (`client.oidc.…`); a host may
# choose another in its own generator.yaml.
nickname: oidc
client_package_path: ../halkyon_oidc_client
server_test_tools_path: test/integration/test_tools

View file

@ -0,0 +1,40 @@
# The module's test server: every port 0, so the tests find free ones and can run
# side by side. The database is the embedded PostgreSQL Serverpod starts and
# manages itself (`dataPath`): no Docker needed, `dart test` is enough. Its password
# and the tests' other secrets are in config/passwords.yaml (git-ignored; see the
# README for the keys).
apiServer:
port: 0
publicHost: localhost
publicPort: 0
publicScheme: http
insightsServer:
port: 0
publicHost: localhost
publicPort: 0
publicScheme: http
webServer:
port: 0
publicHost: localhost
publicPort: 0
publicScheme: http
database:
host: localhost
port: 9090
name: halkyon_oidc_test
user: postgres
dataPath: .serverpod/test/pgdata
redis:
enabled: false
host: localhost
port: 9091
# Session logs are never kept, for privacy, as on every Halkyon server.
sessionLogs:
persistentEnabled: false
consoleEnabled: true

View file

@ -0,0 +1,2 @@
tags:
integration: {}

View file

@ -0,0 +1,6 @@
/// What Serverpod's code generator needs to put the module into a host's
/// server; not for the host's own code, which imports `oidc.dart`.
library;
export 'src/generated/endpoints.dart';
export 'src/generated/protocol.dart';

View file

@ -0,0 +1,13 @@
/// Signing in with OpenID Connect for Serverpod (ADR 0023): any provider
/// that follows the code flow, SimpleLogin a preset; Serverpod's contract for
/// providers of one's own, its stock endpoint, and the parts for a host with
/// rules of its own.
library;
export 'src/callback_forwarding.dart';
export 'src/endpoints/oidc_idp_base_endpoint.dart';
export 'src/generated/protocol.dart' hide Protocol;
export 'src/id_token.dart' show IdTokenRejected, checkIdToken, idTokenLeeway;
export 'src/oidc_idp.dart';
export 'src/oidc_provider.dart';
export 'src/provider_keys.dart' show ProviderKeys;

View file

@ -0,0 +1,198 @@
import 'dart:io';
import 'package:serverpod/serverpod.dart';
// A forwarding page for a provider's callbacks (ADR 0023, 7): for a provider
// that takes https callbacks alone, each registered exactly (SimpleLogin), so
// a desktop app's loopback or a phone's scheme can't be one. The host
// registers this page's address; the browser comes back here with the
// provider's code (or its error) and the state the sign-in began with, and
// the page sends both on to the app that began it. The state starts with a
// marker the host chose (`OidcIdp.begin`'s `statePrefix`), which names an
// address of the host's own table, so the page sends nobody anywhere else.
// It keeps and logs nothing (the web server's session names the path alone):
// a code caught on its way is worthless without the attempt the app holds.
/// A state as the page takes it: a marker (letters, digits, `-` and `_`, up
/// to 64), a dot, and the rest the sign-in began with, 1 to 512 characters
/// of base64url (`learn-desktop.q3J…`). Nothing else, so nothing in it can
/// reach the address it goes to but as the value of `state`.
final _state = RegExp(r'^([A-Za-z0-9_-]{1,64})\.([A-Za-z0-9_-]{1,512})$');
/// A code of RFC 3986's unreserved characters, up to 512 (SimpleLogin's are
/// ten lower-case letters).
final _code = RegExp(r'^[A-Za-z0-9._~-]{1,512}$');
/// An error as OAuth names them (RFC 6749, 4.1.2.1; SimpleLogin's own is
/// `deny`).
final _error = RegExp(r'^[A-Za-z0-9._-]{1,64}$');
/// The longest query the page reads: a sign-in's is far shorter.
const _maxQuery = 4096;
/// A request for the page as it reads one: plain values, so tests can make
/// one. The route turns Relic's requests into these.
typedef OidcCallbackRequest = ({
/// The method, upper case.
String method,
/// The query as the browser sent it, still encoded.
String query,
/// The `Accept-Language` header, if any.
String? acceptLanguage,
});
/// What the page answers: its status, where it sends the browser (`location`,
/// with an empty `html`) or the page saying why not, and its headers, by
/// lower-case name.
typedef OidcCallbackAnswer = ({
int status,
Uri? location,
String html,
Map<String, String> headers,
});
/// The page a refused callback shows, for a visitor whose browser asks for
/// [acceptLanguage]; it can say nothing of what came, as it is given none
/// of it.
typedef OidcRefusalPage = String Function(String? acceptLanguage);
/// The page's answer to [request]: a 303 to the address [returns] has for
/// the state's marker, with the code or the error, and the state, where
/// exactly one of them came, each once and well formed; [refusalPage]
/// otherwise, a 400 (a 405 for any method but GET).
OidcCallbackAnswer oidcCallbackForwarding(
OidcCallbackRequest request, {
required Map<String, Uri> returns,
OidcRefusalPage refusalPage = defaultOidcRefusalPage,
}) {
OidcCallbackAnswer refused(int status, {String? allow}) => (
status: status,
location: null,
html: refusalPage(request.acceptLanguage),
headers: {
..._kept,
// Nothing runs, nothing is fetched, and no other page frames it.
'content-security-policy':
"default-src 'none'; style-src 'unsafe-inline'; "
"frame-ancestors 'none'",
'allow': ?allow,
},
);
if (request.method != 'GET') {
return refused(405, allow: 'GET');
}
if (request.query.length > _maxQuery) {
return refused(400);
}
final Map<String, List<String>> query;
try {
query = Uri(query: request.query).queryParametersAll;
} on Object {
// Encoding Uri can't read: FormatException or ArgumentError.
return refused(400);
}
String? only(String name) => switch (query[name]) {
[final value] => value,
_ => null,
};
bool twice(String name) => (query[name]?.length ?? 0) > 1;
if (twice('state') || twice('code') || twice('error')) {
return refused(400);
}
final state = only('state');
final target = switch (_state.firstMatch(state ?? '')) {
final match? => returns[match[1]],
null => null,
};
final code = only('code');
final error = only('error');
if (target == null ||
state == null ||
(code == null) == (error == null) ||
(code != null && !_code.hasMatch(code)) ||
(error != null && !_error.hasMatch(error))) {
return refused(400);
}
return (
status: 303,
location: target.replace(
queryParameters: {'code': ?code, 'error': ?error, 'state': state},
),
html: '',
headers: _kept,
);
}
/// What every answer says to whoever passes it on: keep nothing, send no
/// referrer with the code in it, guess no type.
const _kept = {
'cache-control': 'no-store',
'referrer-policy': 'no-referrer',
'x-content-type-options': 'nosniff',
};
/// The module's own refusal page, in English, unstyled; a host gives its own
/// for its look and languages.
String defaultOidcRefusalPage(String? acceptLanguage) =>
'<!doctype html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n'
'<meta name="viewport" content="width=device-width, initial-scale=1">\n'
'<meta name="robots" content="noindex">\n'
'<title>Sign-in not completed</title>\n</head>\n<body>\n'
"<h1>This sign-in can't go on.</h1>\n"
'<p>Go back to the app and sign in again.</p>\n</body>\n</html>\n';
/// Serves the forwarding page on [pod]'s web server at [path], sending each
/// sign-in to the address [returns] has for its state's marker (see
/// [oidcCallbackForwarding]); nothing where the pod has no web server.
void addOidcCallbackForwarding(
Serverpod pod, {
required String path,
required Map<String, Uri> returns,
OidcRefusalPage refusalPage = defaultOidcRefusalPage,
}) {
if (pod.config.webServer == null) {
return;
}
pod.webServer.addRoute(
_CallbackForwardingRoute(returns, refusalPage),
path,
);
}
/// Every method, so the page answers them itself rather than another route.
class _CallbackForwardingRoute extends Route {
_CallbackForwardingRoute(this._returns, this._refusalPage)
: super(methods: Method.values.toSet());
final Map<String, Uri> _returns;
final OidcRefusalPage _refusalPage;
@override
Future<Result> handleCall(Session session, Request request) async {
final answer = oidcCallbackForwarding(
(
method: request.method.value,
query: request.url.query,
acceptLanguage: request.headers[HttpHeaders.acceptLanguageHeader]?.join(
',',
),
),
returns: _returns,
refusalPage: _refusalPage,
);
return Response(
answer.status,
body: answer.location == null
? Body.fromString(answer.html, mimeType: MimeType.html)
: Body.empty(),
headers: Headers.fromMap({
for (final MapEntry(:key, :value) in answer.headers.entries)
key: [value],
if (answer.location case final location?) 'location': ['$location'],
}),
);
}
}

View file

@ -0,0 +1,62 @@
import 'package:halkyon_oidc_server/src/generated/protocol.dart';
import 'package:halkyon_oidc_server/src/oidc_idp.dart';
import 'package:serverpod/serverpod.dart';
import 'package:serverpod_auth_idp_server/core.dart';
/// 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.
abstract class OidcIdpBaseEndpoint extends IdpBaseEndpoint {
/// The OpenID Connect sign-in the host set up.
OidcIdp get oidcIdp => AuthServices.instance.oidcIdp;
/// Begins a sign-in with [provider] (its key): signed in, to add the
/// identity to the caller ([link]); signed out, to sign in ([login]).
Future<OidcStart> begin(Session session, {required String provider}) =>
oidcIdp.begin(
session,
provider: provider,
caller: session.authenticated?.authUserId,
network: session.request?.remoteInfo,
);
/// Signs in with the identity [code] proves for [attempt], begun signed
/// out: a session for its person, made for one nobody has.
Future<AuthSuccess> login(
Session session, {
required UuidValue attempt,
required String code,
}) => oidcIdp.login(session, attempt: attempt, code: code);
/// Links the identity [code] proves for [attempt], begun by the caller
/// signed in, to them.
Future<void> link(
Session session, {
required UuidValue attempt,
required String code,
}) async {
final caller =
session.authenticated?.authUserId ??
(throw OidcRefusedException(reason: OidcRefusal.wrongCaller));
await oidcIdp.linkToCaller(
session,
attempt: attempt,
code: code,
caller: caller,
);
}
@override
Future<bool> hasAccount(Session session) async {
final caller = session.authenticated?.authUserId;
return caller != null &&
await oidcIdp.hasIdentity(session, authUserId: caller);
}
}

View file

@ -0,0 +1,27 @@
/* 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;
import 'package:serverpod_auth_core_server/serverpod_auth_core_server.dart'
as _iacs;
import 'package:serverpod_auth_idp_server/serverpod_auth_idp_server.dart'
as _iais;
class Endpoints extends _is.EndpointDispatch {
@override
void initializeEndpoints(_is.Server server) {
modules['serverpod_auth_idp'] = _iais.Endpoints()
..initializeEndpoints(server);
modules['serverpod_auth_core'] = _iacs.Endpoints()
..initializeEndpoints(server);
}
}

View file

@ -0,0 +1,852 @@
/* 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_null_comparison
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:halkyon_oidc_server/src/generated/protocol.dart' as _i7me1mu7;
import 'package:serverpod/serverpod.dart' as _is;
import 'package:serverpod_auth_core_server/serverpod_auth_core_server.dart'
as _iacs;
/// A sign-in a device began with a provider (`OidcIdp.begin`), until it is
/// ended (`OidcIdp.verify`), once, within ten minutes (ADR 0023, 4); or the
/// identity it proved, kept a while for the host to finish with
/// (`OidcIdp.takeKept`).
abstract class OidcAttempt
implements _is.TableRow<_is.UuidValue?>, _is.ProtocolSerialization {
OidcAttempt._({
this.id,
required this.provider,
required this.state,
required this.nonce,
this.codeVerifier,
this.callerId,
this.caller,
this.subject,
required this.expiresAt,
});
factory OidcAttempt({
_is.UuidValue? id,
required String provider,
required String state,
required String nonce,
String? codeVerifier,
_is.UuidValue? callerId,
_iacs.AuthUser? caller,
String? subject,
required DateTime expiresAt,
}) = _OidcAttemptImpl;
factory OidcAttempt.fromJson(Map<String, dynamic> jsonSerialization) {
return OidcAttempt(
id: jsonSerialization['id'] == null
? null
: _is.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
provider: jsonSerialization['provider'] as String,
state: jsonSerialization['state'] as String,
nonce: jsonSerialization['nonce'] as String,
codeVerifier: jsonSerialization['codeVerifier'] as String?,
callerId: jsonSerialization['callerId'] == null
? null
: _is.UuidValueJsonExtension.fromJson(jsonSerialization['callerId']),
caller: jsonSerialization['caller'] == null
? null
: _i7me1mu7.Protocol().deserialize<_iacs.AuthUser>(
jsonSerialization['caller'],
),
subject: jsonSerialization['subject'] as String?,
expiresAt: _is.DateTimeJsonExtension.fromJson(
jsonSerialization['expiresAt'],
),
);
}
static final t = OidcAttemptTable();
static const db = OidcAttemptRepository._();
@override
_is.UuidValue? id;
/// The provider, by its key (`OidcProvider.key`).
String provider;
/// The state sent with the person, which comes back with the code.
String state;
/// The nonce the provider's ID token must carry.
String nonce;
/// PKCE's code verifier; null for a provider without PKCE.
String? codeVerifier;
_is.UuidValue? callerId;
/// Who began it signed in, to have the identity linked to; null for a
/// sign-in.
_iacs.AuthUser? caller;
/// The ID the provider proved, for an attempt kept after it was verified;
/// null before.
String? subject;
/// Until when it can be ended, or taken once verified.
DateTime expiresAt;
@override
_is.Table<_is.UuidValue?> get table => t;
/// Returns a shallow copy of this [OidcAttempt]
/// with some or all fields replaced by the given arguments.
@_is.useResult
OidcAttempt copyWith({
_is.UuidValue? id,
String? provider,
String? state,
String? nonce,
String? codeVerifier,
_is.UuidValue? callerId,
_iacs.AuthUser? caller,
String? subject,
DateTime? expiresAt,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'halkyon_oidc.OidcAttempt',
if (id != null) 'id': id?.toJson(),
'provider': provider,
'state': state,
'nonce': nonce,
if (codeVerifier != null) 'codeVerifier': codeVerifier,
if (callerId != null) 'callerId': callerId?.toJson(),
if (caller != null) 'caller': caller?.toJson(),
if (subject != null) 'subject': subject,
'expiresAt': expiresAt.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {};
}
static OidcAttemptInclude include({_iacs.AuthUserInclude? caller}) {
return OidcAttemptInclude._(caller: caller);
}
static OidcAttemptIncludeList includeList({
_is.WhereExpressionBuilder<OidcAttemptTable>? where,
int? limit,
int? offset,
_is.OrderByBuilder<OidcAttemptTable>? orderBy,
_is.OrderByListBuilder<OidcAttemptTable>? orderByList,
OidcAttemptInclude? include,
}) {
return OidcAttemptIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(OidcAttempt.t),
orderByList: orderByList?.call(OidcAttempt.t),
include: include,
);
}
@override
String toString() {
return _is.SerializationManager.encode(this);
}
}
class _Undefined {}
class _OidcAttemptImpl extends OidcAttempt {
_OidcAttemptImpl({
_is.UuidValue? id,
required String provider,
required String state,
required String nonce,
String? codeVerifier,
_is.UuidValue? callerId,
_iacs.AuthUser? caller,
String? subject,
required DateTime expiresAt,
}) : super._(
id: id,
provider: provider,
state: state,
nonce: nonce,
codeVerifier: codeVerifier,
callerId: callerId,
caller: caller,
subject: subject,
expiresAt: expiresAt,
);
/// Returns a shallow copy of this [OidcAttempt]
/// with some or all fields replaced by the given arguments.
@_is.useResult
@override
OidcAttempt copyWith({
Object? id = _Undefined,
String? provider,
String? state,
String? nonce,
Object? codeVerifier = _Undefined,
Object? callerId = _Undefined,
Object? caller = _Undefined,
Object? subject = _Undefined,
DateTime? expiresAt,
}) {
return OidcAttempt(
id: id is _is.UuidValue? ? id : this.id,
provider: provider ?? this.provider,
state: state ?? this.state,
nonce: nonce ?? this.nonce,
codeVerifier: codeVerifier is String? ? codeVerifier : this.codeVerifier,
callerId: callerId is _is.UuidValue? ? callerId : this.callerId,
caller: caller is _iacs.AuthUser? ? caller : this.caller?.copyWith(),
subject: subject is String? ? subject : this.subject,
expiresAt: expiresAt ?? this.expiresAt,
);
}
}
class OidcAttemptUpdateTable extends _is.UpdateTable<OidcAttemptTable> {
OidcAttemptUpdateTable(super.table);
_is.ColumnValue<String, String> provider(String value) => _is.ColumnValue(
table.provider,
value,
);
_is.ColumnValue<String, String> state(String value) => _is.ColumnValue(
table.state,
value,
);
_is.ColumnValue<String, String> nonce(String value) => _is.ColumnValue(
table.nonce,
value,
);
_is.ColumnValue<String, String> codeVerifier(String? value) =>
_is.ColumnValue(
table.codeVerifier,
value,
);
_is.ColumnValue<_is.UuidValue, _is.UuidValue> callerId(
_is.UuidValue? value,
) => _is.ColumnValue(
table.callerId,
value,
);
_is.ColumnValue<String, String> subject(String? value) => _is.ColumnValue(
table.subject,
value,
);
_is.ColumnValue<DateTime, DateTime> expiresAt(DateTime value) =>
_is.ColumnValue(
table.expiresAt,
value,
);
}
class OidcAttemptTable extends _is.Table<_is.UuidValue?> {
OidcAttemptTable({super.tableRelation})
: super(tableName: 'halkyon_oidc_attempt') {
updateTable = OidcAttemptUpdateTable(this);
provider = _is.ColumnString(
'provider',
this,
);
state = _is.ColumnString(
'state',
this,
);
nonce = _is.ColumnString(
'nonce',
this,
);
codeVerifier = _is.ColumnString(
'codeVerifier',
this,
);
callerId = _is.ColumnUuid(
'callerId',
this,
);
subject = _is.ColumnString(
'subject',
this,
);
expiresAt = _is.ColumnDateTime(
'expiresAt',
this,
);
}
late final OidcAttemptUpdateTable updateTable;
/// The provider, by its key (`OidcProvider.key`).
late final _is.ColumnString provider;
/// The state sent with the person, which comes back with the code.
late final _is.ColumnString state;
/// The nonce the provider's ID token must carry.
late final _is.ColumnString nonce;
/// PKCE's code verifier; null for a provider without PKCE.
late final _is.ColumnString codeVerifier;
late final _is.ColumnUuid callerId;
/// Who began it signed in, to have the identity linked to; null for a
/// sign-in.
_iacs.AuthUserTable? _caller;
/// The ID the provider proved, for an attempt kept after it was verified;
/// null before.
late final _is.ColumnString subject;
/// Until when it can be ended, or taken once verified.
late final _is.ColumnDateTime expiresAt;
_iacs.AuthUserTable get caller {
if (_caller != null) return _caller!;
_caller = _is.createRelationTable(
relationFieldName: 'caller',
field: OidcAttempt.t.callerId,
foreignField: _iacs.AuthUser.t.id,
tableRelation: tableRelation,
createTable: (foreignTableRelation) =>
_iacs.AuthUserTable(tableRelation: foreignTableRelation),
);
return _caller!;
}
@override
List<_is.Column> get columns => [
id,
provider,
state,
nonce,
codeVerifier,
callerId,
subject,
expiresAt,
];
@override
_is.Table? getRelationTable(String relationField) {
if (relationField == 'caller') {
return caller;
}
return null;
}
}
class OidcAttemptInclude extends _is.IncludeObject {
OidcAttemptInclude._({_iacs.AuthUserInclude? caller}) {
_caller = caller;
}
_iacs.AuthUserInclude? _caller;
@override
Map<String, _is.Include?> get includes => {'caller': _caller};
@override
_is.Table<_is.UuidValue?> get table => OidcAttempt.t;
}
class OidcAttemptIncludeList extends _is.IncludeList {
OidcAttemptIncludeList._({
_is.WhereExpressionBuilder<OidcAttemptTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(OidcAttempt.t);
}
@override
Map<String, _is.Include?> get includes => include?.includes ?? {};
@override
_is.Table<_is.UuidValue?> get table => OidcAttempt.t;
}
class OidcAttemptRepository {
const OidcAttemptRepository._();
final attachRow = const OidcAttemptAttachRowRepository._();
final detachRow = const OidcAttemptDetachRowRepository._();
/// Returns a list of [OidcAttempt]s matching the given query parameters.
///
/// Use [where] to specify which items to include in the return value.
/// If none is specified, all items will be returned.
///
/// To specify the order of the items use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// The maximum number of items can be set by [limit]. If no limit is set,
/// all items matching the query will be returned.
///
/// [offset] defines how many items to skip, after which [limit] (or all)
/// items are read from the database.
///
/// ```dart
/// var persons = await Persons.db.find(
/// session,
/// where: (t) => t.lastName.equals('Jones'),
/// orderBy: (t) => t.firstName,
/// limit: 100,
/// );
/// ```
Future<List<OidcAttempt>> find(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<OidcAttemptTable>? where,
int? limit,
int? offset,
_is.OrderByBuilder<OidcAttemptTable>? orderBy,
_is.OrderByListBuilder<OidcAttemptTable>? orderByList,
_is.Transaction? transaction,
OidcAttemptInclude? include,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.find<OidcAttempt>(
where: where?.call(OidcAttempt.t),
orderBy: orderBy?.call(OidcAttempt.t),
orderByList: orderByList?.call(OidcAttempt.t),
limit: limit,
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [OidcAttempt] matching the given query parameters.
///
/// Use [where] to specify which items to include in the return value.
/// If none is specified, all items will be returned.
///
/// To specify the order use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// [offset] defines how many items to skip, after which the next one will be picked.
///
/// ```dart
/// var youngestPerson = await Persons.db.findFirstRow(
/// session,
/// where: (t) => t.lastName.equals('Jones'),
/// orderBy: (t) => t.age,
/// );
/// ```
Future<OidcAttempt?> findFirstRow(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<OidcAttemptTable>? where,
int? offset,
_is.OrderByBuilder<OidcAttemptTable>? orderBy,
_is.OrderByListBuilder<OidcAttemptTable>? orderByList,
_is.Transaction? transaction,
OidcAttemptInclude? include,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<OidcAttempt>(
where: where?.call(OidcAttempt.t),
orderBy: orderBy?.call(OidcAttempt.t),
orderByList: orderByList?.call(OidcAttempt.t),
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [OidcAttempt] by its [id] or null if no such row exists.
Future<OidcAttempt?> findById(
_is.DatabaseSession session,
_is.UuidValue id, {
_is.Transaction? transaction,
OidcAttemptInclude? include,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.findById<OidcAttempt>(
id,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [OidcAttempt]s in the list and returns the inserted rows.
///
/// The returned [OidcAttempt]s will have their `id` fields set.
///
/// This is an atomic operation, meaning that if one of the rows fails to
/// insert, none of the rows will be inserted.
///
/// If [ignoreConflicts] is set to `true`, rows that conflict with existing
/// rows are silently skipped, and only the successfully inserted rows are
/// returned.
///
/// If [noReturn] is set to `true`, the inserted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<OidcAttempt>> insert(
_is.DatabaseSession session,
List<OidcAttempt> rows, {
_is.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<OidcAttempt>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [OidcAttempt] and returns the inserted row.
///
/// The returned [OidcAttempt] will have its `id` field set.
Future<OidcAttempt> insertRow(
_is.DatabaseSession session,
OidcAttempt row, {
_is.Transaction? transaction,
}) async {
return session.db.insertRow<OidcAttempt>(
row,
transaction: transaction,
);
}
/// Upserts all [OidcAttempt]s in the list and returns the resulting rows.
///
/// If a row conflicts on the given [conflictColumns], the existing row is
/// updated with the new values. Otherwise, a new row is inserted.
///
/// If [updateColumns] is provided, only those columns will be updated on
/// conflict. If null, all non-conflict, non-id columns are updated.
///
/// If [updateWhere] is provided, the update only applies to rows matching the
/// given expression. Conflicting rows that don't match are skipped and not
/// returned, so the resulting list may be shorter than [rows].
///
/// The returned [OidcAttempt]s will have their `id` fields set.
///
/// This is an atomic operation, meaning that if one of the rows fails,
/// none of the rows will be affected.
///
/// If [noReturn] is set to `true`, the resulting rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<OidcAttempt>> upsert(
_is.DatabaseSession session,
List<OidcAttempt> rows, {
required _is.ColumnSelections<OidcAttemptTable> conflictColumns,
_is.ColumnSelections<OidcAttemptTable>? updateColumns,
_is.WhereExpressionBuilder<OidcAttemptTable>? updateWhere,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<OidcAttempt>(
rows,
conflictColumns: conflictColumns(OidcAttempt.t),
updateColumns: updateColumns?.call(OidcAttempt.t),
updateWhere: updateWhere?.call(OidcAttempt.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [OidcAttempt] and returns the resulting row.
///
/// If the row conflicts on the given [conflictColumns], the existing row is
/// updated. Otherwise, a new row is inserted.
///
/// If [updateColumns] is provided, only those columns will be updated on
/// conflict. If null, all non-conflict, non-id columns are updated.
///
/// If [updateWhere] is provided, the update only applies when the existing
/// row matches the expression. Returns `null` if no row was affected — for
/// example when [updateWhere] does not match the conflicting row.
///
/// The returned [OidcAttempt] will have its `id` field set.
Future<OidcAttempt?> upsertRow(
_is.DatabaseSession session,
OidcAttempt row, {
required _is.ColumnSelections<OidcAttemptTable> conflictColumns,
_is.ColumnSelections<OidcAttemptTable>? updateColumns,
_is.WhereExpressionBuilder<OidcAttemptTable>? updateWhere,
_is.Transaction? transaction,
}) async {
return session.db.upsertRow<OidcAttempt>(
row,
conflictColumns: conflictColumns(OidcAttempt.t),
updateColumns: updateColumns?.call(OidcAttempt.t),
updateWhere: updateWhere?.call(OidcAttempt.t),
transaction: transaction,
);
}
/// Updates all [OidcAttempt]s in the list and returns the updated rows. If
/// [columns] is provided, only those columns will be updated. Defaults to
/// all columns.
/// This is an atomic operation, meaning that if one of the rows fails to
/// update, none of the rows will be updated.
///
/// If [noReturn] is set to `true`, the updated rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<OidcAttempt>> update(
_is.DatabaseSession session,
List<OidcAttempt> rows, {
_is.ColumnSelections<OidcAttemptTable>? columns,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<OidcAttempt>(
rows,
columns: columns?.call(OidcAttempt.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [OidcAttempt]. The row needs to have its id set.
/// Optionally, a list of [columns] can be provided to only update those
/// columns. Defaults to all columns.
Future<OidcAttempt> updateRow(
_is.DatabaseSession session,
OidcAttempt row, {
_is.ColumnSelections<OidcAttemptTable>? columns,
_is.Transaction? transaction,
}) async {
return session.db.updateRow<OidcAttempt>(
row,
columns: columns?.call(OidcAttempt.t),
transaction: transaction,
);
}
/// Updates a single [OidcAttempt] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<OidcAttempt?> updateById(
_is.DatabaseSession session,
_is.UuidValue id, {
required _is.ColumnValueListBuilder<OidcAttemptUpdateTable> columnValues,
_is.Transaction? transaction,
}) async {
return session.db.updateById<OidcAttempt>(
id,
columnValues: columnValues(OidcAttempt.t.updateTable),
transaction: transaction,
);
}
/// Updates all [OidcAttempt]s matching the [where] expression with the specified [columnValues].
/// Returns the list of updated rows.
///
/// If [noReturn] is set to `true`, the updated rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<OidcAttempt>> updateWhere(
_is.DatabaseSession session, {
required _is.ColumnValueListBuilder<OidcAttemptUpdateTable> columnValues,
required _is.WhereExpressionBuilder<OidcAttemptTable> where,
int? limit,
int? offset,
_is.OrderByBuilder<OidcAttemptTable>? orderBy,
_is.OrderByListBuilder<OidcAttemptTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<OidcAttempt>(
columnValues: columnValues(OidcAttempt.t.updateTable),
where: where(OidcAttempt.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(OidcAttempt.t),
orderByList: orderByList?.call(OidcAttempt.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [OidcAttempt]s in the list and returns the deleted rows.
///
/// To specify the order of the returned rows use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// This is an atomic operation, meaning that if one of the rows fail to
/// be deleted, none of the rows will be deleted.
///
/// If [noReturn] is set to `true`, the deleted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<OidcAttempt>> delete(
_is.DatabaseSession session,
List<OidcAttempt> rows, {
_is.OrderByBuilder<OidcAttemptTable>? orderBy,
_is.OrderByListBuilder<OidcAttemptTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<OidcAttempt>(
rows,
orderBy: orderBy?.call(OidcAttempt.t),
orderByList: orderByList?.call(OidcAttempt.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [OidcAttempt].
Future<OidcAttempt> deleteRow(
_is.DatabaseSession session,
OidcAttempt row, {
_is.Transaction? transaction,
}) async {
return session.db.deleteRow<OidcAttempt>(
row,
transaction: transaction,
);
}
/// Deletes all rows matching the [where] expression.
///
/// To specify the order of the returned rows use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// If [noReturn] is set to `true`, the deleted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<OidcAttempt>> deleteWhere(
_is.DatabaseSession session, {
required _is.WhereExpressionBuilder<OidcAttemptTable> where,
_is.OrderByBuilder<OidcAttemptTable>? orderBy,
_is.OrderByListBuilder<OidcAttemptTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<OidcAttempt>(
where: where(OidcAttempt.t),
orderBy: orderBy?.call(OidcAttempt.t),
orderByList: orderByList?.call(OidcAttempt.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Counts the number of rows matching the [where] expression. If omitted,
/// will return the count of all rows in the table.
Future<int> count(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<OidcAttemptTable>? where,
int? limit,
_is.Transaction? transaction,
}) async {
return session.db.count<OidcAttempt>(
where: where?.call(OidcAttempt.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [OidcAttempt] rows matching the [where] expression.
Future<void> lockRows(
_is.DatabaseSession session, {
required _is.WhereExpressionBuilder<OidcAttemptTable> where,
required _is.LockMode lockMode,
required _is.Transaction transaction,
_is.LockBehavior lockBehavior = _is.LockBehavior.wait,
}) async {
return session.db.lockRows<OidcAttempt>(
where: where(OidcAttempt.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}
class OidcAttemptAttachRowRepository {
const OidcAttemptAttachRowRepository._();
/// Creates a relation between the given [OidcAttempt] and [AuthUser]
/// by setting the [OidcAttempt]'s foreign key `callerId` to refer to the [AuthUser].
Future<void> caller(
_is.DatabaseSession session,
OidcAttempt oidcAttempt,
_iacs.AuthUser caller, {
_is.Transaction? transaction,
}) async {
if (oidcAttempt.id == null) {
throw ArgumentError.notNull('oidcAttempt.id');
}
if (caller.id == null) {
throw ArgumentError.notNull('caller.id');
}
var $oidcAttempt = oidcAttempt.copyWith(callerId: caller.id);
await session.db.updateRow<OidcAttempt>(
$oidcAttempt,
columns: [OidcAttempt.t.callerId],
transaction: transaction,
);
}
}
class OidcAttemptDetachRowRepository {
const OidcAttemptDetachRowRepository._();
/// Detaches the relation between this [OidcAttempt] and the [AuthUser] set in `caller`
/// by setting the [OidcAttempt]'s foreign key `callerId` to `null`.
///
/// This removes the association between the two models without deleting
/// the related record.
Future<void> caller(
_is.DatabaseSession session,
OidcAttempt oidcAttempt, {
_is.Transaction? transaction,
}) async {
if (oidcAttempt.id == null) {
throw ArgumentError.notNull('oidcAttempt.id');
}
var $oidcAttempt = oidcAttempt.copyWith(callerId: null);
await session.db.updateRow<OidcAttempt>(
$oidcAttempt,
columns: [OidcAttempt.t.callerId],
transaction: transaction,
);
}
}

View file

@ -0,0 +1,742 @@
/* 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_null_comparison
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:halkyon_oidc_server/src/generated/protocol.dart' as _i7me1mu7;
import 'package:serverpod/serverpod.dart' as _is;
import 'package:serverpod_auth_core_server/serverpod_auth_core_server.dart'
as _iacs;
/// A person's identity at an OpenID Connect provider: the provider's ID of
/// them (`sub`) and nothing else of what it says (ADR 0023, 3).
abstract class OidcIdentity
implements _is.TableRow<_is.UuidValue?>, _is.ProtocolSerialization {
OidcIdentity._({
this.id,
required this.authUserId,
this.authUser,
required this.provider,
required this.subject,
DateTime? createdAt,
}) : createdAt = createdAt ?? DateTime.now();
factory OidcIdentity({
_is.UuidValue? id,
required _is.UuidValue authUserId,
_iacs.AuthUser? authUser,
required String provider,
required String subject,
DateTime? createdAt,
}) = _OidcIdentityImpl;
factory OidcIdentity.fromJson(Map<String, dynamic> jsonSerialization) {
return OidcIdentity(
id: jsonSerialization['id'] == null
? null
: _is.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
authUserId: _is.UuidValueJsonExtension.fromJson(
jsonSerialization['authUserId'],
),
authUser: jsonSerialization['authUser'] == null
? null
: _i7me1mu7.Protocol().deserialize<_iacs.AuthUser>(
jsonSerialization['authUser'],
),
provider: jsonSerialization['provider'] as String,
subject: jsonSerialization['subject'] as String,
createdAt: jsonSerialization['createdAt'] == null
? null
: _is.DateTimeJsonExtension.fromJson(jsonSerialization['createdAt']),
);
}
static final t = OidcIdentityTable();
static const db = OidcIdentityRepository._();
@override
_is.UuidValue? id;
_is.UuidValue authUserId;
/// The person, who goes with their identities.
_iacs.AuthUser? authUser;
/// The provider, by the key the host named it with (`OidcProvider.key`).
String provider;
/// The provider's ID of the person, its ID token's `sub`, as it came.
String subject;
DateTime createdAt;
@override
_is.Table<_is.UuidValue?> get table => t;
/// Returns a shallow copy of this [OidcIdentity]
/// with some or all fields replaced by the given arguments.
@_is.useResult
OidcIdentity copyWith({
_is.UuidValue? id,
_is.UuidValue? authUserId,
_iacs.AuthUser? authUser,
String? provider,
String? subject,
DateTime? createdAt,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'halkyon_oidc.OidcIdentity',
if (id != null) 'id': id?.toJson(),
'authUserId': authUserId.toJson(),
if (authUser != null) 'authUser': authUser?.toJson(),
'provider': provider,
'subject': subject,
'createdAt': createdAt.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {};
}
static OidcIdentityInclude include({_iacs.AuthUserInclude? authUser}) {
return OidcIdentityInclude._(authUser: authUser);
}
static OidcIdentityIncludeList includeList({
_is.WhereExpressionBuilder<OidcIdentityTable>? where,
int? limit,
int? offset,
_is.OrderByBuilder<OidcIdentityTable>? orderBy,
_is.OrderByListBuilder<OidcIdentityTable>? orderByList,
OidcIdentityInclude? include,
}) {
return OidcIdentityIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(OidcIdentity.t),
orderByList: orderByList?.call(OidcIdentity.t),
include: include,
);
}
@override
String toString() {
return _is.SerializationManager.encode(this);
}
}
class _Undefined {}
class _OidcIdentityImpl extends OidcIdentity {
_OidcIdentityImpl({
_is.UuidValue? id,
required _is.UuidValue authUserId,
_iacs.AuthUser? authUser,
required String provider,
required String subject,
DateTime? createdAt,
}) : super._(
id: id,
authUserId: authUserId,
authUser: authUser,
provider: provider,
subject: subject,
createdAt: createdAt,
);
/// Returns a shallow copy of this [OidcIdentity]
/// with some or all fields replaced by the given arguments.
@_is.useResult
@override
OidcIdentity copyWith({
Object? id = _Undefined,
_is.UuidValue? authUserId,
Object? authUser = _Undefined,
String? provider,
String? subject,
DateTime? createdAt,
}) {
return OidcIdentity(
id: id is _is.UuidValue? ? id : this.id,
authUserId: authUserId ?? this.authUserId,
authUser: authUser is _iacs.AuthUser?
? authUser
: this.authUser?.copyWith(),
provider: provider ?? this.provider,
subject: subject ?? this.subject,
createdAt: createdAt ?? this.createdAt,
);
}
}
class OidcIdentityUpdateTable extends _is.UpdateTable<OidcIdentityTable> {
OidcIdentityUpdateTable(super.table);
_is.ColumnValue<_is.UuidValue, _is.UuidValue> authUserId(
_is.UuidValue value,
) => _is.ColumnValue(
table.authUserId,
value,
);
_is.ColumnValue<String, String> provider(String value) => _is.ColumnValue(
table.provider,
value,
);
_is.ColumnValue<String, String> subject(String value) => _is.ColumnValue(
table.subject,
value,
);
_is.ColumnValue<DateTime, DateTime> createdAt(DateTime value) =>
_is.ColumnValue(
table.createdAt,
value,
);
}
class OidcIdentityTable extends _is.Table<_is.UuidValue?> {
OidcIdentityTable({super.tableRelation})
: super(tableName: 'halkyon_oidc_identity') {
updateTable = OidcIdentityUpdateTable(this);
authUserId = _is.ColumnUuid(
'authUserId',
this,
);
provider = _is.ColumnString(
'provider',
this,
);
subject = _is.ColumnString(
'subject',
this,
);
createdAt = _is.ColumnDateTime(
'createdAt',
this,
);
}
late final OidcIdentityUpdateTable updateTable;
late final _is.ColumnUuid authUserId;
/// The person, who goes with their identities.
_iacs.AuthUserTable? _authUser;
/// The provider, by the key the host named it with (`OidcProvider.key`).
late final _is.ColumnString provider;
/// The provider's ID of the person, its ID token's `sub`, as it came.
late final _is.ColumnString subject;
late final _is.ColumnDateTime createdAt;
_iacs.AuthUserTable get authUser {
if (_authUser != null) return _authUser!;
_authUser = _is.createRelationTable(
relationFieldName: 'authUser',
field: OidcIdentity.t.authUserId,
foreignField: _iacs.AuthUser.t.id,
tableRelation: tableRelation,
createTable: (foreignTableRelation) =>
_iacs.AuthUserTable(tableRelation: foreignTableRelation),
);
return _authUser!;
}
@override
List<_is.Column> get columns => [
id,
authUserId,
provider,
subject,
createdAt,
];
@override
_is.Table? getRelationTable(String relationField) {
if (relationField == 'authUser') {
return authUser;
}
return null;
}
}
class OidcIdentityInclude extends _is.IncludeObject {
OidcIdentityInclude._({_iacs.AuthUserInclude? authUser}) {
_authUser = authUser;
}
_iacs.AuthUserInclude? _authUser;
@override
Map<String, _is.Include?> get includes => {'authUser': _authUser};
@override
_is.Table<_is.UuidValue?> get table => OidcIdentity.t;
}
class OidcIdentityIncludeList extends _is.IncludeList {
OidcIdentityIncludeList._({
_is.WhereExpressionBuilder<OidcIdentityTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(OidcIdentity.t);
}
@override
Map<String, _is.Include?> get includes => include?.includes ?? {};
@override
_is.Table<_is.UuidValue?> get table => OidcIdentity.t;
}
class OidcIdentityRepository {
const OidcIdentityRepository._();
final attachRow = const OidcIdentityAttachRowRepository._();
/// Returns a list of [OidcIdentity]s matching the given query parameters.
///
/// Use [where] to specify which items to include in the return value.
/// If none is specified, all items will be returned.
///
/// To specify the order of the items use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// The maximum number of items can be set by [limit]. If no limit is set,
/// all items matching the query will be returned.
///
/// [offset] defines how many items to skip, after which [limit] (or all)
/// items are read from the database.
///
/// ```dart
/// var persons = await Persons.db.find(
/// session,
/// where: (t) => t.lastName.equals('Jones'),
/// orderBy: (t) => t.firstName,
/// limit: 100,
/// );
/// ```
Future<List<OidcIdentity>> find(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<OidcIdentityTable>? where,
int? limit,
int? offset,
_is.OrderByBuilder<OidcIdentityTable>? orderBy,
_is.OrderByListBuilder<OidcIdentityTable>? orderByList,
_is.Transaction? transaction,
OidcIdentityInclude? include,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.find<OidcIdentity>(
where: where?.call(OidcIdentity.t),
orderBy: orderBy?.call(OidcIdentity.t),
orderByList: orderByList?.call(OidcIdentity.t),
limit: limit,
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [OidcIdentity] matching the given query parameters.
///
/// Use [where] to specify which items to include in the return value.
/// If none is specified, all items will be returned.
///
/// To specify the order use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// [offset] defines how many items to skip, after which the next one will be picked.
///
/// ```dart
/// var youngestPerson = await Persons.db.findFirstRow(
/// session,
/// where: (t) => t.lastName.equals('Jones'),
/// orderBy: (t) => t.age,
/// );
/// ```
Future<OidcIdentity?> findFirstRow(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<OidcIdentityTable>? where,
int? offset,
_is.OrderByBuilder<OidcIdentityTable>? orderBy,
_is.OrderByListBuilder<OidcIdentityTable>? orderByList,
_is.Transaction? transaction,
OidcIdentityInclude? include,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<OidcIdentity>(
where: where?.call(OidcIdentity.t),
orderBy: orderBy?.call(OidcIdentity.t),
orderByList: orderByList?.call(OidcIdentity.t),
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [OidcIdentity] by its [id] or null if no such row exists.
Future<OidcIdentity?> findById(
_is.DatabaseSession session,
_is.UuidValue id, {
_is.Transaction? transaction,
OidcIdentityInclude? include,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.findById<OidcIdentity>(
id,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [OidcIdentity]s in the list and returns the inserted rows.
///
/// The returned [OidcIdentity]s will have their `id` fields set.
///
/// This is an atomic operation, meaning that if one of the rows fails to
/// insert, none of the rows will be inserted.
///
/// If [ignoreConflicts] is set to `true`, rows that conflict with existing
/// rows are silently skipped, and only the successfully inserted rows are
/// returned.
///
/// If [noReturn] is set to `true`, the inserted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<OidcIdentity>> insert(
_is.DatabaseSession session,
List<OidcIdentity> rows, {
_is.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<OidcIdentity>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [OidcIdentity] and returns the inserted row.
///
/// The returned [OidcIdentity] will have its `id` field set.
Future<OidcIdentity> insertRow(
_is.DatabaseSession session,
OidcIdentity row, {
_is.Transaction? transaction,
}) async {
return session.db.insertRow<OidcIdentity>(
row,
transaction: transaction,
);
}
/// Upserts all [OidcIdentity]s in the list and returns the resulting rows.
///
/// If a row conflicts on the given [conflictColumns], the existing row is
/// updated with the new values. Otherwise, a new row is inserted.
///
/// If [updateColumns] is provided, only those columns will be updated on
/// conflict. If null, all non-conflict, non-id columns are updated.
///
/// If [updateWhere] is provided, the update only applies to rows matching the
/// given expression. Conflicting rows that don't match are skipped and not
/// returned, so the resulting list may be shorter than [rows].
///
/// The returned [OidcIdentity]s will have their `id` fields set.
///
/// This is an atomic operation, meaning that if one of the rows fails,
/// none of the rows will be affected.
///
/// If [noReturn] is set to `true`, the resulting rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<OidcIdentity>> upsert(
_is.DatabaseSession session,
List<OidcIdentity> rows, {
required _is.ColumnSelections<OidcIdentityTable> conflictColumns,
_is.ColumnSelections<OidcIdentityTable>? updateColumns,
_is.WhereExpressionBuilder<OidcIdentityTable>? updateWhere,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<OidcIdentity>(
rows,
conflictColumns: conflictColumns(OidcIdentity.t),
updateColumns: updateColumns?.call(OidcIdentity.t),
updateWhere: updateWhere?.call(OidcIdentity.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [OidcIdentity] and returns the resulting row.
///
/// If the row conflicts on the given [conflictColumns], the existing row is
/// updated. Otherwise, a new row is inserted.
///
/// If [updateColumns] is provided, only those columns will be updated on
/// conflict. If null, all non-conflict, non-id columns are updated.
///
/// If [updateWhere] is provided, the update only applies when the existing
/// row matches the expression. Returns `null` if no row was affected — for
/// example when [updateWhere] does not match the conflicting row.
///
/// The returned [OidcIdentity] will have its `id` field set.
Future<OidcIdentity?> upsertRow(
_is.DatabaseSession session,
OidcIdentity row, {
required _is.ColumnSelections<OidcIdentityTable> conflictColumns,
_is.ColumnSelections<OidcIdentityTable>? updateColumns,
_is.WhereExpressionBuilder<OidcIdentityTable>? updateWhere,
_is.Transaction? transaction,
}) async {
return session.db.upsertRow<OidcIdentity>(
row,
conflictColumns: conflictColumns(OidcIdentity.t),
updateColumns: updateColumns?.call(OidcIdentity.t),
updateWhere: updateWhere?.call(OidcIdentity.t),
transaction: transaction,
);
}
/// Updates all [OidcIdentity]s in the list and returns the updated rows. If
/// [columns] is provided, only those columns will be updated. Defaults to
/// all columns.
/// This is an atomic operation, meaning that if one of the rows fails to
/// update, none of the rows will be updated.
///
/// If [noReturn] is set to `true`, the updated rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<OidcIdentity>> update(
_is.DatabaseSession session,
List<OidcIdentity> rows, {
_is.ColumnSelections<OidcIdentityTable>? columns,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<OidcIdentity>(
rows,
columns: columns?.call(OidcIdentity.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [OidcIdentity]. The row needs to have its id set.
/// Optionally, a list of [columns] can be provided to only update those
/// columns. Defaults to all columns.
Future<OidcIdentity> updateRow(
_is.DatabaseSession session,
OidcIdentity row, {
_is.ColumnSelections<OidcIdentityTable>? columns,
_is.Transaction? transaction,
}) async {
return session.db.updateRow<OidcIdentity>(
row,
columns: columns?.call(OidcIdentity.t),
transaction: transaction,
);
}
/// Updates a single [OidcIdentity] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<OidcIdentity?> updateById(
_is.DatabaseSession session,
_is.UuidValue id, {
required _is.ColumnValueListBuilder<OidcIdentityUpdateTable> columnValues,
_is.Transaction? transaction,
}) async {
return session.db.updateById<OidcIdentity>(
id,
columnValues: columnValues(OidcIdentity.t.updateTable),
transaction: transaction,
);
}
/// Updates all [OidcIdentity]s matching the [where] expression with the specified [columnValues].
/// Returns the list of updated rows.
///
/// If [noReturn] is set to `true`, the updated rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<OidcIdentity>> updateWhere(
_is.DatabaseSession session, {
required _is.ColumnValueListBuilder<OidcIdentityUpdateTable> columnValues,
required _is.WhereExpressionBuilder<OidcIdentityTable> where,
int? limit,
int? offset,
_is.OrderByBuilder<OidcIdentityTable>? orderBy,
_is.OrderByListBuilder<OidcIdentityTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<OidcIdentity>(
columnValues: columnValues(OidcIdentity.t.updateTable),
where: where(OidcIdentity.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(OidcIdentity.t),
orderByList: orderByList?.call(OidcIdentity.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [OidcIdentity]s in the list and returns the deleted rows.
///
/// To specify the order of the returned rows use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// This is an atomic operation, meaning that if one of the rows fail to
/// be deleted, none of the rows will be deleted.
///
/// If [noReturn] is set to `true`, the deleted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<OidcIdentity>> delete(
_is.DatabaseSession session,
List<OidcIdentity> rows, {
_is.OrderByBuilder<OidcIdentityTable>? orderBy,
_is.OrderByListBuilder<OidcIdentityTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<OidcIdentity>(
rows,
orderBy: orderBy?.call(OidcIdentity.t),
orderByList: orderByList?.call(OidcIdentity.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [OidcIdentity].
Future<OidcIdentity> deleteRow(
_is.DatabaseSession session,
OidcIdentity row, {
_is.Transaction? transaction,
}) async {
return session.db.deleteRow<OidcIdentity>(
row,
transaction: transaction,
);
}
/// Deletes all rows matching the [where] expression.
///
/// To specify the order of the returned rows use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// If [noReturn] is set to `true`, the deleted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<OidcIdentity>> deleteWhere(
_is.DatabaseSession session, {
required _is.WhereExpressionBuilder<OidcIdentityTable> where,
_is.OrderByBuilder<OidcIdentityTable>? orderBy,
_is.OrderByListBuilder<OidcIdentityTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<OidcIdentity>(
where: where(OidcIdentity.t),
orderBy: orderBy?.call(OidcIdentity.t),
orderByList: orderByList?.call(OidcIdentity.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Counts the number of rows matching the [where] expression. If omitted,
/// will return the count of all rows in the table.
Future<int> count(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<OidcIdentityTable>? where,
int? limit,
_is.Transaction? transaction,
}) async {
return session.db.count<OidcIdentity>(
where: where?.call(OidcIdentity.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [OidcIdentity] rows matching the [where] expression.
Future<void> lockRows(
_is.DatabaseSession session, {
required _is.WhereExpressionBuilder<OidcIdentityTable> where,
required _is.LockMode lockMode,
required _is.Transaction transaction,
_is.LockBehavior lockBehavior = _is.LockBehavior.wait,
}) async {
return session.db.lockRows<OidcIdentity>(
where: where(OidcIdentity.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}
class OidcIdentityAttachRowRepository {
const OidcIdentityAttachRowRepository._();
/// Creates a relation between the given [OidcIdentity] and [AuthUser]
/// by setting the [OidcIdentity]'s foreign key `authUserId` to refer to the [AuthUser].
Future<void> authUser(
_is.DatabaseSession session,
OidcIdentity oidcIdentity,
_iacs.AuthUser authUser, {
_is.Transaction? transaction,
}) async {
if (oidcIdentity.id == null) {
throw ArgumentError.notNull('oidcIdentity.id');
}
if (authUser.id == null) {
throw ArgumentError.notNull('authUser.id');
}
var $oidcIdentity = oidcIdentity.copyWith(authUserId: authUser.id);
await session.db.updateRow<OidcIdentity>(
$oidcIdentity,
columns: [OidcIdentity.t.authUserId],
transaction: transaction,
);
}
}

View file

@ -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/serverpod.dart' as _is;
/// Why a sign-in with an OpenID Connect provider was refused.
enum OidcRefusal implements _is.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;
}

View file

@ -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/serverpod.dart' as _is;
import 'oidc_refusal.dart' as _i8gvcysd;
/// A sign-in with an OpenID Connect provider was refused; nothing changed.
abstract class OidcRefusedException
implements
_is.SerializableException,
_is.SerializableModel,
_is.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.
@_is.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.
@_is.useResult
@override
OidcRefusedException copyWith({_i8gvcysd.OidcRefusal? reason}) {
return OidcRefusedException(reason: reason ?? this.reason);
}
}

View 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/serverpod.dart' as _is;
/// 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 _is.SerializableModel, _is.ProtocolSerialization {
OidcStart._({
required this.attempt,
required this.authorizationUrl,
required this.state,
});
factory OidcStart({
required _is.UuidValue attempt,
required Uri authorizationUrl,
required String state,
}) = _OidcStartImpl;
factory OidcStart.fromJson(Map<String, dynamic> jsonSerialization) {
return OidcStart(
attempt: _is.UuidValueJsonExtension.fromJson(
jsonSerialization['attempt'],
),
authorizationUrl: _is.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.
_is.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.
@_is.useResult
OidcStart copyWith({
_is.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 _is.SerializationManager.encode(this);
}
}
class _OidcStartImpl extends OidcStart {
_OidcStartImpl({
required _is.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.
@_is.useResult
@override
OidcStart copyWith({
_is.UuidValue? attempt,
Uri? authorizationUrl,
String? state,
}) {
return OidcStart(
attempt: attempt ?? this.attempt,
authorizationUrl: authorizationUrl ?? this.authorizationUrl,
state: state ?? this.state,
);
}
}

View file

@ -0,0 +1,481 @@
/* 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/protocol.dart' as _isp;
import 'package:serverpod/serverpod.dart' as _is;
import 'package:serverpod_auth_core_server/serverpod_auth_core_server.dart'
as _iacs;
import 'package:serverpod_auth_idp_server/serverpod_auth_idp_server.dart'
as _iais;
import 'oidc_attempt.dart' as _ihieazd8;
import 'oidc_identity.dart' as _iif3vjzw;
import 'oidc_refusal.dart' as _i8gvcysd;
import 'oidc_refused_exception.dart' as _i70wwg4b;
import 'oidc_start.dart' as _i4fk0f61;
export 'oidc_attempt.dart';
export 'oidc_identity.dart';
export 'oidc_refusal.dart';
export 'oidc_refused_exception.dart';
export 'oidc_start.dart';
class Protocol extends _is.DatabaseSerializationManager {
Protocol._();
factory Protocol() => _instance;
static final Protocol _instance = Protocol._();
final Set<_is.SerializationManager> _hostProtocols = {};
static List<_isp.TableDefinition> get targetTableDefinitions => [
_isp.TableDefinition(
name: 'halkyon_oidc_attempt',
dartName: 'OidcAttempt',
schema: 'public',
module: 'halkyon_oidc',
columns: [
_isp.ColumnDefinition(
name: 'id',
columnType: _isp.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue?',
columnDefault: 'random',
),
_isp.ColumnDefinition(
name: 'provider',
columnType: _isp.ColumnType.text,
isNullable: false,
dartType: 'String',
),
_isp.ColumnDefinition(
name: 'state',
columnType: _isp.ColumnType.text,
isNullable: false,
dartType: 'String',
),
_isp.ColumnDefinition(
name: 'nonce',
columnType: _isp.ColumnType.text,
isNullable: false,
dartType: 'String',
),
_isp.ColumnDefinition(
name: 'codeVerifier',
columnType: _isp.ColumnType.text,
isNullable: true,
dartType: 'String?',
),
_isp.ColumnDefinition(
name: 'callerId',
columnType: _isp.ColumnType.uuid,
isNullable: true,
dartType: 'UuidValue?',
),
_isp.ColumnDefinition(
name: 'subject',
columnType: _isp.ColumnType.text,
isNullable: true,
dartType: 'String?',
),
_isp.ColumnDefinition(
name: 'expiresAt',
columnType: _isp.ColumnType.timestampWithoutTimeZone,
isNullable: false,
dartType: 'DateTime',
),
],
foreignKeys: [
_isp.ForeignKeyDefinition(
constraintName: 'halkyon_oidc_attempt_fk_0',
columns: ['callerId'],
referenceTable: 'serverpod_auth_core_user',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isp.ForeignKeyAction.noAction,
onDelete: _isp.ForeignKeyAction.cascade,
matchType: null,
),
],
indexes: [
_isp.IndexDefinition(
indexName: 'halkyon_oidc_attempt_expires_idx',
tableSpace: null,
elements: [
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'expiresAt',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isp.TableDefinition(
name: 'halkyon_oidc_identity',
dartName: 'OidcIdentity',
schema: 'public',
module: 'halkyon_oidc',
columns: [
_isp.ColumnDefinition(
name: 'id',
columnType: _isp.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue?',
columnDefault: 'random_v7',
),
_isp.ColumnDefinition(
name: 'authUserId',
columnType: _isp.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue',
),
_isp.ColumnDefinition(
name: 'provider',
columnType: _isp.ColumnType.text,
isNullable: false,
dartType: 'String',
),
_isp.ColumnDefinition(
name: 'subject',
columnType: _isp.ColumnType.text,
isNullable: false,
dartType: 'String',
),
_isp.ColumnDefinition(
name: 'createdAt',
columnType: _isp.ColumnType.timestampWithoutTimeZone,
isNullable: false,
dartType: 'DateTime',
),
],
foreignKeys: [
_isp.ForeignKeyDefinition(
constraintName: 'halkyon_oidc_identity_fk_0',
columns: ['authUserId'],
referenceTable: 'serverpod_auth_core_user',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isp.ForeignKeyAction.noAction,
onDelete: _isp.ForeignKeyAction.cascade,
matchType: null,
),
],
indexes: [
_isp.IndexDefinition(
indexName: 'halkyon_oidc_identity_subject_idx',
tableSpace: null,
elements: [
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'provider',
),
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'subject',
),
],
type: 'btree',
isUnique: true,
isPrimary: false,
),
_isp.IndexDefinition(
indexName: 'halkyon_oidc_identity_person_idx',
tableSpace: null,
elements: [
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'authUserId',
),
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'provider',
),
],
type: 'btree',
isUnique: true,
isPrimary: false,
),
],
managed: true,
),
..._iais.Protocol.targetTableDefinitions,
..._iacs.Protocol.targetTableDefinitions,
];
void registerHostProtocol(
String projectName,
_is.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 _is.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 == _ihieazd8.OidcAttempt) {
return _ihieazd8.OidcAttempt.fromJson(data) as T;
}
if (t == _iif3vjzw.OidcIdentity) {
return _iif3vjzw.OidcIdentity.fromJson(data) as 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 == _is.getType<_ihieazd8.OidcAttempt?>()) {
return (data != null ? _ihieazd8.OidcAttempt.fromJson(data) : null) as T;
}
if (t == _is.getType<_iif3vjzw.OidcIdentity?>()) {
return (data != null ? _iif3vjzw.OidcIdentity.fromJson(data) : null) as T;
}
if (t == _is.getType<_i8gvcysd.OidcRefusal?>()) {
return (data != null ? _i8gvcysd.OidcRefusal.fromJson(data) : null) as T;
}
if (t == _is.getType<_i70wwg4b.OidcRefusedException?>()) {
return (data != null
? _i70wwg4b.OidcRefusedException.fromJson(data)
: null)
as T;
}
if (t == _is.getType<_i4fk0f61.OidcStart?>()) {
return (data != null ? _i4fk0f61.OidcStart.fromJson(data) : null) as T;
}
try {
return _iais.Protocol().deserialize<T>(data, t);
} on _is.DeserializationTypeNotFoundException catch (_) {}
try {
return _iacs.Protocol().deserialize<T>(data, t);
} on _is.DeserializationTypeNotFoundException catch (_) {}
try {
return _isp.Protocol().deserialize<T>(data, t);
} on _is.DeserializationTypeNotFoundException catch (_) {}
return super.deserialize<T>(data, t);
}
static String? getClassNameForType(Type type) {
return switch (type) {
_ihieazd8.OidcAttempt => 'OidcAttempt',
_iif3vjzw.OidcIdentity => 'OidcIdentity',
_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 _ihieazd8.OidcAttempt():
return 'OidcAttempt';
case _iif3vjzw.OidcIdentity():
return 'OidcIdentity';
case _i8gvcysd.OidcRefusal():
return 'OidcRefusal';
case _i70wwg4b.OidcRefusedException():
return 'OidcRefusedException';
case _i4fk0f61.OidcStart():
return 'OidcStart';
}
className = _isp.Protocol().getClassNameForObject(data);
if (className != null) {
return className.contains('.') ? className : 'serverpod.$className';
}
return null;
}
@override
dynamic deserializeByClassName(Map<String, dynamic> data) {
var dataClassName = data['className'];
if (dataClassName is! String) {
return super.deserializeByClassName(data);
}
if (dataClassName == 'OidcAttempt') {
return deserialize<_ihieazd8.OidcAttempt>(data['data']);
}
if (dataClassName == 'OidcIdentity') {
return deserialize<_iif3vjzw.OidcIdentity>(data['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']);
}
if (dataClassName.startsWith('serverpod.')) {
data['className'] = dataClassName.substring(10);
return _isp.Protocol().deserializeByClassName(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
? _is.SerializationManager.toEncodableForProtocol(wrapped)
: _is.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 _is.DeserializationClassNameNotFoundException catch (_) {}
}
}
return deserializeByClassName(value);
}
@override
_is.Table? getTableForType(Type t) {
{
var table = _iais.Protocol().getTableForType(t);
if (table != null) {
return table;
}
}
{
var table = _iacs.Protocol().getTableForType(t);
if (table != null) {
return table;
}
}
{
var table = _isp.Protocol().getTableForType(t);
if (table != null) {
return table;
}
}
switch (t) {
case _ihieazd8.OidcAttempt:
return _ihieazd8.OidcAttempt.t;
case _iif3vjzw.OidcIdentity:
return _iif3vjzw.OidcIdentity.t;
}
return null;
}
@override
List<_isp.TableDefinition> getTargetTableDefinitions() =>
targetTableDefinitions;
@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 _iais.Protocol().mapRecordToJson(record);
} catch (_) {}
try {
return _iacs.Protocol().mapRecordToJson(record);
} catch (_) {}
throw Exception('Unsupported record type ${record.runtimeType}');
}
}

Some files were not shown because too many files have changed in this diff Show more