mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 15:15:24 +00:00
feat: move sso and ldap logins into plugins (C2)
This commit is contained in:
1 parent
5c18435105
commit
925da4a661
223 files changed
+25778
-6820
No files matched your search
@@ -999,11 +999,11 @@ auto-session and trusted devices, plus the base SSH auth types password, key,
|
||||
stored credential, agent and none. Every other login method, second factor and
|
||||
SSH auth method is a plugin through `ctx.auth`.
|
||||
|
||||
In 2.9.0, OIDC (and GitHub, Google), LDAP, OPKSSH, Step-CA, Vault, Tailscale
|
||||
and Warpgate still live in core. They register through the same interfaces
|
||||
from `src/backend/auth/legacy-providers.ts` and
|
||||
`src/ui/auth/legacy-auth-ui.tsx` with `pluginId: "core"`. Phase C moves each
|
||||
one into its plugin by moving its block out of those two files; D1 deletes them.
|
||||
In 2.9.0, OPKSSH, Step-CA, Vault, Tailscale and Warpgate still live in core.
|
||||
They register through the same interfaces from
|
||||
`src/backend/auth/legacy-providers.ts` and `src/ui/auth/legacy-auth-ui.tsx`
|
||||
with `pluginId: "core"`. Phase C moves each one into its plugin by moving its
|
||||
block out of those two files; D1 deletes them.
|
||||
Nothing else in core branches on those type names.
|
||||
|
||||
**C1** moved TOTP and passkeys out:
|
||||
@@ -1043,6 +1043,52 @@ Nothing else in core branches on those type names.
|
||||
`AuthManager.unlockWithSystemKey` (was `authenticateWebAuthnUser`) opens
|
||||
the data key for any identity that carries no password.
|
||||
|
||||
**C2** moved external logins out:
|
||||
|
||||
- `sso` registers the `oidc` login method: OIDC, GitHub and Google, one
|
||||
button per enabled provider (the method's instances), plus the provider
|
||||
configured through the `OIDC_*` environment variables as instance `0`. It
|
||||
adopts `sso_providers` as `p_sso_providers`, ignoring any `ldap` rows left
|
||||
in it. Its public routes are `/plugin-api/sso/start` (sends the browser to
|
||||
the provider), `/callback` (GET and POST), `/backchannel-logout` and
|
||||
`/config`; provider management is `/plugin-api/sso/providers`, gated on
|
||||
`sso.manage`, and renders as the custom `providers` field on the plugin's
|
||||
admin settings page. Login state (nonce, PKCE verifier, return address)
|
||||
lives in `ctx.kv` for ten minutes.
|
||||
- The redirect URI is `<base>/plugin-api/sso/callback`. Providers that
|
||||
existed before 2.9 keep sending `<base>/users/oidc/callback`, the URI their
|
||||
identity provider was registered with, because the sso plugin's second
|
||||
migration sets `legacy_callback` on every adopted row; the env provider
|
||||
always does. Core answers that old URL with a 308 to the plugin's callback,
|
||||
and the token exchange sends the same redirect URI the authorize request
|
||||
did, so strict providers accept it. The admin page shows each provider's
|
||||
URI and switches it to the new one once the admin has registered that.
|
||||
`/users/oidc/backchannel-logout` is a 308 to the plugin as well.
|
||||
- `ldap` registers the `ldap` form method, one form per directory, in its own
|
||||
`p_ldap_providers`. `utils/crypto-migration/ldap-provider-migration.ts`
|
||||
copies the 2.8 LDAP rows out of `sso_providers` (adopted or not) with their
|
||||
ids and deletes them there. It runs from the loader whenever a plugin's
|
||||
migrations applied something (`plugin-data-moves.ts`), so the rows are in
|
||||
place before the ldap plugin first activates, at boot or when it is enabled
|
||||
later. LDAP identities are stored under provider `ldap:<id>` so their ids
|
||||
can never clash with SSO ones; `external-identity-migration.ts` writes them
|
||||
that way and the LDAP migration renames any bare ones.
|
||||
- Both return 2.8's `users.oidc_identifier` value in `legacy`, so pre-2.9
|
||||
accounts are found and new ones keep the column filled. The sso plugin
|
||||
passes its group role map as `roles` and the id token's `sub`/`sid` as
|
||||
`logoutClaims`; `/backchannel-logout` ends those sessions with
|
||||
`ctx.auth.revokeSessions`. LDAP counts failed binds with
|
||||
`ctx.auth.loginRateLimit`, the limiter password login uses.
|
||||
- Core keeps provisioning (the allowed-users matcher is
|
||||
`auth/allowed-users.ts`), `oidc_auto_provision`, silent sign-in (it starts
|
||||
the first redirect method), account linking and the trusted proxy rule:
|
||||
every `external` method is refused while trusted proxy login is on, and
|
||||
`/users/proxy-login` refuses while any external method lists an instance.
|
||||
The 2.8 client routes `/users/oidc/authorize`, `/users/oidc-config` (307 to
|
||||
`/plugin-api/sso/config`), `/users/sso-providers` and `/users/ldap/login`
|
||||
live in `database/routes/auth-compat-routes.ts` and only forward to the
|
||||
registered methods, so with the plugins off they answer 404.
|
||||
|
||||
#### One SSH connect pipeline
|
||||
|
||||
Every SSH connection, core's and plugins', goes through
|
||||
@@ -1171,10 +1217,25 @@ or an external identity. Core then:
|
||||
Routes, all under `/users/auth`: `GET methods` (public), `GET :methodId/start`,
|
||||
`GET|POST :methodId/callback`, `POST :methodId/verify`,
|
||||
`POST second-factor/:factorId/challenge`, `POST second-factor/:factorId/verify`.
|
||||
A redirect method's provider must send the browser back to
|
||||
`/users/auth/<methodId>/callback`. The older routes (`/users/login`,
|
||||
A redirect method's provider sends the browser back to
|
||||
`/users/auth/<methodId>/callback`, or to a public route of the plugin's own
|
||||
that calls `ctx.auth.completeRedirectLogin(methodId, req, res)`, which runs
|
||||
the same callback and pipeline (**C2**, for a URL that has to stay stable in
|
||||
the provider's configuration). The older routes (`/users/login`,
|
||||
`/users/oidc/*`, `/users/ldap/login`) are thin wrappers over the same
|
||||
pipeline. `/users/totp/verify-login` stays for 2.8 clients: it answers with
|
||||
pipeline.
|
||||
|
||||
**Identity extras (C2).** An external identity may also carry `roles`
|
||||
(`{ desired, managed }`: core adds the desired roles and removes managed ones
|
||||
the user no longer maps to), `logoutClaims` (`providerId`, `sub`, `sid`,
|
||||
stored on the session for `ctx.auth.revokeSessions`), `legacy` (the 2.8
|
||||
`oidc_identifier` and `sso_provider_id`, only for a plugin that took over a
|
||||
2.8 method) and `rateLimitKey` (cleared on success; keys are kept apart per
|
||||
plugin). Core copies only these documented fields, so a plugin cannot set
|
||||
core-only ones. `ctx.auth.countLinkedUsers(provider)` answers how many users
|
||||
sign in through a provider, for refusing to delete one still in use.
|
||||
`ctx.http.baseUrl(req)` is the install's public URL (origin plus base path,
|
||||
honouring `OIDC_FORCE_HTTPS`) for building callback URLs. `/users/totp/verify-login` stays for 2.8 clients: it answers with
|
||||
the factor named in `factor`, or the user's first required factor, and core
|
||||
never names one itself.
|
||||
|
||||
@@ -1562,6 +1623,7 @@ Built per plugin in `src/backend/plugins/ctx.ts` and passed to `activate`.
|
||||
| `ctx.db.persist` / `.dialect` | `db:own` (persist only) | **A9**, lazy **B16** |
|
||||
| `ctx.sync.registerEntity` | none | **A3** |
|
||||
| `ctx.http.router` / `ctx.ws.route` / `.upgrade` | `network:serve` | **A4** |
|
||||
| `ctx.http.baseUrl(req)` | none | **C2** |
|
||||
| `ctx.rbac.has` / `.hasFor` / `.require` | own permissions only | **A5** |
|
||||
| `ctx.capabilities.has` / `.require` | the capability itself | **B11** |
|
||||
| `ctx.hosts.*` | `hosts:read` / `hosts:write` | **B4**, **B5**, **B18** |
|
||||
@@ -1570,6 +1632,7 @@ Built per plugin in `src/backend/plugins/ctx.ts` and passed to `activate`.
|
||||
| `ctx.settings.*` | `settings:read-core` (readCore only) | **A6** |
|
||||
| `ctx.notify.channels` / `.send` | `notify:send` | **B17** |
|
||||
| `ctx.auth.*` | `auth:provide` | **A8** |
|
||||
| `ctx.auth.completeRedirectLogin` and the rest | `auth:provide` | **C2** |
|
||||
| `ctx.desktop.openIsolatedWindow` | `desktop:window` | **B8** |
|
||||
| `ctx.desktop.launchNativeRdp` / `.available` | `desktop:window` (launch only) | **B14** |
|
||||
| `ctx.credentials.resolveHostProtocol` | `credentials:read` | **B14** |
|
||||
@@ -2361,8 +2424,8 @@ Then, with the app running:
|
||||
|
||||
The bundled plugins predate the SDK, apart from workspaces (A9), snippets
|
||||
(B2), remote-desktop (B14), docker (B15), host-metrics (B16), automations
|
||||
(B17), ai (B18), homepage (B19), totp and webauthn (C1), which import
|
||||
nothing from core. The others still reach core by relative
|
||||
(B17), ai (B18), homepage (B19), totp and webauthn (C1), sso and ldap (C2),
|
||||
which import nothing from core. The others still reach core by relative
|
||||
path (`../../../../src/backend/...`), which an esbuild plugin,
|
||||
`packages/plugin-sdk/cli/lib/legacy-core-imports.mjs`, keeps out of the bundle
|
||||
and rewrites to the compiled output path (`../../../backend/backend/...`, or
|
||||
|
||||
@@ -465,6 +465,27 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite
|
||||
and `totp_backup_codes` stay in the database (dropped from `schema.ts`, the
|
||||
drizzle drop is a no-op) until 3.0.0 removes them, after the boot
|
||||
migration has run everywhere. Owner: 3.0.0.
|
||||
- **C2 (sso/ldap):** `users.is_oidc`, `oidc_identifier`, `sso_provider_id`
|
||||
and the 2.8 per-user OIDC columns (`client_id`, `issuer_url`, ...), and
|
||||
`sessions.sso_provider_id`, `oidc_sub`, `oidc_sid`, stay in core. Core
|
||||
still reads `is_oidc`/`oidc_identifier` for account linking, `/users/me` and
|
||||
the `$oidc.preferred_username` placeholder, and still writes them from an
|
||||
identity's `legacy` field so a downgrade works. Rename or drop them once
|
||||
nothing needs the 2.8 shape. Owner: 3.0.0.
|
||||
- **C2 (sso/ldap):** `database/routes/auth-compat-routes.ts` keeps the 2.8
|
||||
URLs (`/users/oidc/*`, `/users/oidc-config`, `/users/sso-providers`,
|
||||
`/users/ldap/login`) and so names the `oidc`/`ldap` method ids and the sso
|
||||
plugin's `/plugin-api/sso/` paths. Keep until identity providers have moved
|
||||
to the new redirect URI and Termix-Mobile uses `/users/auth/*`. Owner:
|
||||
3.0.0.
|
||||
- **C2 (sso):** the `oidc_auto_provision` and `oidc_silent_login_default`
|
||||
settings, their routes and admin toggles are generic (every external
|
||||
method, every redirect method) but still carry OIDC in their names and
|
||||
i18n. Rename with a settings migration if wanted. Owner: D0.
|
||||
- **Unrelated, found in C2:** `node scripts/check-shell-plugin-ids.cjs`
|
||||
fails on `src/ui/lib/host-to-ssh-host.ts` naming `"web-endpoint"`
|
||||
(`h.pluginSettings?.["web-endpoint"]`). The file is untouched since before
|
||||
C2. Owner: D0.
|
||||
|
||||
## Manual checks after 2.9.0
|
||||
|
||||
@@ -539,3 +560,15 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite
|
||||
plugin and a TOTP user's login is refused until an admin resets their
|
||||
factors; register a passkey, sign in with it with and without a PIN (no
|
||||
PIN still asks for TOTP), delete it; existing 2.8 passkeys still sign in.
|
||||
- SSO and LDAP: on a 2.8 database every SSO provider and LDAP directory is
|
||||
there, in Settings > Plugins > Single sign-on and > LDAP; an existing
|
||||
provider still signs in through its identity provider without changing
|
||||
anything there (its redirect URI shows `/users/oidc/callback`); add the
|
||||
new redirect URI at the provider, switch it over and sign in again; a new
|
||||
provider, GitHub and Google; an existing SSO user and an existing LDAP
|
||||
user land in their old accounts; the env-configured provider; admin group
|
||||
and role map; back-channel logout from Keycloak ends the session; the
|
||||
desktop app's system browser login and Termix-Mobile's login; silent
|
||||
sign-in; a second factor after an external login with the admin setting
|
||||
on and off; disable each plugin and its buttons go away and its URLs stop
|
||||
signing anyone in, then enable it again.
|
||||
@@ -313,6 +313,12 @@ export interface PluginHttp {
|
||||
* on deactivate.
|
||||
*/
|
||||
router: <T = unknown>(options?: PluginRouterOptions) => T;
|
||||
/**
|
||||
* The public base URL a request came in on: origin plus the install's base
|
||||
* path, no trailing slash. Honours forwarded headers from a trusted proxy,
|
||||
* BASE_PATH and OIDC_FORCE_HTTPS. For building absolute callback URLs.
|
||||
*/
|
||||
baseUrl: (req: unknown) => string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1102,6 +1108,33 @@ export type PluginVerifiedIdentity =
|
||||
/** Redirect methods: where the browser goes back to. */
|
||||
returnTo?: string;
|
||||
rememberMe?: boolean;
|
||||
/**
|
||||
* Provider groups mapped to Termix roles. Core adds the desired roles
|
||||
* and removes managed roles the user no longer maps to, on every login.
|
||||
* Roles outside `managed` are never touched.
|
||||
*/
|
||||
roles?: { desired: string[]; managed: string[] };
|
||||
/**
|
||||
* Stored on the session so `ctx.auth.revokeSessions` can find it later,
|
||||
* for an identity provider's back-channel logout.
|
||||
*/
|
||||
logoutClaims?: {
|
||||
providerId?: number | null;
|
||||
sub?: string | null;
|
||||
sid?: string | null;
|
||||
};
|
||||
/**
|
||||
* The values a 2.8 install kept on the user row (`oidc_identifier`,
|
||||
* `sso_provider_id`). Only for a plugin that took over a 2.8 login
|
||||
* method: core finds pre-2.9 accounts by `identifier` and keeps writing
|
||||
* both for new users so a downgrade still works.
|
||||
*/
|
||||
legacy?: { identifier: string; providerRowId?: number | null };
|
||||
/**
|
||||
* The key this attempt was counted under with
|
||||
* `ctx.auth.loginRateLimit.recordFailure`. Cleared on success.
|
||||
*/
|
||||
rateLimitKey?: string;
|
||||
};
|
||||
|
||||
/** Request shape a login method sees. Structural, like PluginRequestLike. */
|
||||
@@ -1118,6 +1151,11 @@ export interface PluginLoginInstance {
|
||||
id: string;
|
||||
label: string;
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Provider type shown to 2.8 clients by the old `GET /users/sso-providers`
|
||||
* route ("oidc", "github", "google", "ldap"). Leave unset otherwise.
|
||||
*/
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface PluginLoginMethod {
|
||||
@@ -1184,6 +1222,46 @@ export interface PluginAuth {
|
||||
*/
|
||||
recordEnrollment: (userId: string, factorId: string) => Promise<void>;
|
||||
removeEnrollment: (userId: string, factorId: string) => Promise<void>;
|
||||
/**
|
||||
* Finishes a redirect login from one of this plugin's own public routes:
|
||||
* runs the registered method's `callback`, then core finds or provisions
|
||||
* the user, runs second factors and redirects the browser back with a
|
||||
* session, exactly like `/users/auth/<methodId>/callback`. The method must
|
||||
* be one this plugin registered.
|
||||
*/
|
||||
completeRedirectLogin: (
|
||||
methodId: string,
|
||||
req: unknown,
|
||||
res: unknown,
|
||||
) => Promise<void>;
|
||||
/**
|
||||
* Revokes sessions whose login carried matching `logoutClaims`, for a
|
||||
* back-channel logout. Needs `sub` or `sid`; `providerId` narrows it.
|
||||
* Returns how many sessions ended.
|
||||
*/
|
||||
revokeSessions: (match: {
|
||||
providerId?: number | null;
|
||||
sub?: string | null;
|
||||
sid?: string | null;
|
||||
}) => Promise<number>;
|
||||
/**
|
||||
* Core's login rate limiter, the one password login uses. Keys are kept
|
||||
* apart per plugin. Put the key in the identity's `rateLimitKey` so a
|
||||
* successful login clears it.
|
||||
*/
|
||||
loginRateLimit: {
|
||||
isLocked: (
|
||||
ip: string,
|
||||
key: string,
|
||||
) => Promise<{ locked: boolean; remainingTime?: number }>;
|
||||
recordFailure: (ip: string, key: string) => Promise<void>;
|
||||
};
|
||||
/**
|
||||
* How many users are linked to an external identity from `provider` (the
|
||||
* `provider` of the identities this plugin returns). For refusing to
|
||||
* delete a provider people still sign in with.
|
||||
*/
|
||||
countLinkedUsers: (provider: string) => Promise<number>;
|
||||
}
|
||||
|
||||
export interface PluginOpenIsolatedWindowRequest {
|
||||
|
||||
@@ -25,6 +25,13 @@ import type {
|
||||
PluginWebSocketOptions,
|
||||
} from "./backend.js";
|
||||
|
||||
/** The bits of an express Response the fake completeRedirectLogin uses. */
|
||||
interface FakeResponse {
|
||||
status: (code: number) => FakeResponse;
|
||||
json: (body: unknown) => unknown;
|
||||
redirect: (url: string) => unknown;
|
||||
}
|
||||
|
||||
/** A ctx.ws registration as the test doubles record it. */
|
||||
export interface FakeWsRoute {
|
||||
path: string;
|
||||
@@ -37,6 +44,8 @@ export interface FakeWsRoute {
|
||||
}
|
||||
import type {
|
||||
PluginLoginMethod,
|
||||
PluginLoginRequest,
|
||||
PluginVerifiedIdentity,
|
||||
PluginSecondFactor,
|
||||
PluginSshAuthProvider,
|
||||
PluginSshConnectOptions,
|
||||
@@ -123,6 +132,12 @@ export interface FakeContextOptions {
|
||||
* proxy login on, password login off), with this message and a 409.
|
||||
*/
|
||||
refuseEnrollment?: string;
|
||||
/** What ctx.http.baseUrl answers. Defaults to "https://termix.test". */
|
||||
baseUrl?: string;
|
||||
/** What ctx.auth.countLinkedUsers answers, by provider. Defaults to 0. */
|
||||
linkedUsers?: Record<string, number>;
|
||||
/** Failures before ctx.auth.loginRateLimit locks a key. Defaults to 5. */
|
||||
loginAttemptLimit?: number;
|
||||
}
|
||||
|
||||
export interface FakeAuthRegistrations {
|
||||
@@ -136,6 +151,19 @@ export interface FakeAuthRegistrations {
|
||||
string,
|
||||
(userId: string, reference: string) => Promise<string>
|
||||
>;
|
||||
/** Identities handed to ctx.auth.completeRedirectLogin, in order. */
|
||||
completedLogins: Array<{
|
||||
methodId: string;
|
||||
identity: PluginVerifiedIdentity;
|
||||
}>;
|
||||
/** Every ctx.auth.revokeSessions call, in order. */
|
||||
revokedSessions: Array<{
|
||||
providerId?: number | null;
|
||||
sub?: string | null;
|
||||
sid?: string | null;
|
||||
}>;
|
||||
/** Failures recorded through ctx.auth.loginRateLimit, by "<ip>|<key>". */
|
||||
loginFailures: Map<string, number>;
|
||||
}
|
||||
|
||||
export interface FakePluginContext {
|
||||
@@ -347,6 +375,9 @@ export function createFakeContext(
|
||||
secondFactors: [],
|
||||
enrollments: new Set(),
|
||||
secretResolvers: new Map(),
|
||||
completedLogins: [],
|
||||
revokedSessions: [],
|
||||
loginFailures: new Map(),
|
||||
};
|
||||
const sshClient = options.sshClient ?? {};
|
||||
let actor = options.actor;
|
||||
@@ -531,6 +562,7 @@ export function createFakeContext(
|
||||
httpRouters.push(routerOptions);
|
||||
return (options.router ? options.router() : undefined) as never;
|
||||
},
|
||||
baseUrl: () => options.baseUrl ?? "https://termix.test",
|
||||
},
|
||||
|
||||
ws: {
|
||||
@@ -801,6 +833,56 @@ export function createFakeContext(
|
||||
removeEnrollment: async (userId, factorId) => {
|
||||
auth.enrollments.delete(`${userId}:${factorId}`);
|
||||
},
|
||||
// Stands in for core's pipeline: answers with the identity as JSON, or
|
||||
// redirects back with the error the way core does.
|
||||
completeRedirectLogin: async (methodId, req, res) => {
|
||||
const response = res as FakeResponse;
|
||||
const method = auth.loginMethods.find(
|
||||
(candidate) => candidate.id === methodId,
|
||||
);
|
||||
if (!method?.callback) {
|
||||
throw new Error(`No registered redirect method "${methodId}"`);
|
||||
}
|
||||
try {
|
||||
const identity = await method.callback(req as PluginLoginRequest);
|
||||
auth.completedLogins.push({ methodId, identity });
|
||||
response.status(200).json({ completed: methodId, identity });
|
||||
} catch (error) {
|
||||
const returnTo = (error as { returnTo?: string }).returnTo;
|
||||
const message = (error as Error).message;
|
||||
if (returnTo) {
|
||||
const url = new URL(returnTo);
|
||||
url.searchParams.set(
|
||||
"error",
|
||||
(error as { code?: string }).code ?? message,
|
||||
);
|
||||
response.redirect(url.toString());
|
||||
return;
|
||||
}
|
||||
response
|
||||
.status((error as { status?: number }).status ?? 500)
|
||||
.json({ error: message });
|
||||
}
|
||||
},
|
||||
revokeSessions: async (match) => {
|
||||
if (!match.sub && !match.sid) return 0;
|
||||
auth.revokedSessions.push(match);
|
||||
return 1;
|
||||
},
|
||||
loginRateLimit: {
|
||||
isLocked: async (ip, key) => {
|
||||
const failures = auth.loginFailures.get(`${ip}|${key}`) ?? 0;
|
||||
return failures >= (options.loginAttemptLimit ?? 5)
|
||||
? { locked: true, remainingTime: 60 }
|
||||
: { locked: false };
|
||||
},
|
||||
recordFailure: async (ip, key) => {
|
||||
const id = `${ip}|${key}`;
|
||||
auth.loginFailures.set(id, (auth.loginFailures.get(id) ?? 0) + 1);
|
||||
},
|
||||
},
|
||||
countLinkedUsers: async (provider) =>
|
||||
options.linkedUsers?.[provider] ?? 0,
|
||||
},
|
||||
|
||||
desktop: {
|
||||
@@ -943,6 +1025,12 @@ export interface MockContextOptions {
|
||||
fetch?: (url: string, init?: PluginFetchInit) => Promise<Response>;
|
||||
/** See FakeContextOptions. */
|
||||
refuseEnrollment?: string;
|
||||
/** See FakeContextOptions. */
|
||||
baseUrl?: string;
|
||||
/** See FakeContextOptions. */
|
||||
linkedUsers?: Record<string, number>;
|
||||
/** See FakeContextOptions. */
|
||||
loginAttemptLimit?: number;
|
||||
}
|
||||
|
||||
export interface MockPluginContext extends FakePluginContext {
|
||||
@@ -986,6 +1074,9 @@ export function createMockCtx(
|
||||
notificationChannels: options.notificationChannels,
|
||||
fetch: options.fetch,
|
||||
refuseEnrollment: options.refuseEnrollment,
|
||||
baseUrl: options.baseUrl,
|
||||
linkedUsers: options.linkedUsers,
|
||||
loginAttemptLimit: options.loginAttemptLimit,
|
||||
manifest: {
|
||||
capabilities: options.capabilities ?? [],
|
||||
...options.manifest,
|
||||
@@ -1078,6 +1169,7 @@ export function createMockCtx(
|
||||
require("network:serve");
|
||||
return ctx.http.router(routerOptions);
|
||||
},
|
||||
baseUrl: (req) => ctx.http.baseUrl(req),
|
||||
},
|
||||
|
||||
ws: {
|
||||
@@ -1233,6 +1325,28 @@ export function createMockCtx(
|
||||
require("auth:provide");
|
||||
return ctx.auth.removeEnrollment(userId, factorId);
|
||||
},
|
||||
completeRedirectLogin: async (methodId, req, res) => {
|
||||
require("auth:provide");
|
||||
return ctx.auth.completeRedirectLogin(methodId, req, res);
|
||||
},
|
||||
revokeSessions: async (match) => {
|
||||
require("auth:provide");
|
||||
return ctx.auth.revokeSessions(match);
|
||||
},
|
||||
loginRateLimit: {
|
||||
isLocked: async (ip, key) => {
|
||||
require("auth:provide");
|
||||
return ctx.auth.loginRateLimit.isLocked(ip, key);
|
||||
},
|
||||
recordFailure: async (ip, key) => {
|
||||
require("auth:provide");
|
||||
return ctx.auth.loginRateLimit.recordFailure(ip, key);
|
||||
},
|
||||
},
|
||||
countLinkedUsers: async (provider) => {
|
||||
require("auth:provide");
|
||||
return ctx.auth.countLinkedUsers(provider);
|
||||
},
|
||||
},
|
||||
|
||||
desktop: {
|
||||
|
||||
Reference in new issue
Block a user