@happyvertical/smrt-users 0.54.0 → 0.55.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +16 -0
- package/README.md +167 -3
- package/agents/oidc-provisioning.md +6 -0
- package/dist/chunks/{TerminalAuthService-BfxG493c.js → TerminalAuthService-DdV-Rm4h.js} +969 -155
- package/dist/chunks/TerminalAuthService-DdV-Rm4h.js.map +1 -0
- package/dist/collections/PasswordCredentialCollection.d.ts +53 -0
- package/dist/collections/PasswordCredentialCollection.d.ts.map +1 -0
- package/dist/collections/SessionCollection.d.ts +7 -2
- package/dist/collections/SessionCollection.d.ts.map +1 -1
- package/dist/collections/index.d.ts +1 -0
- package/dist/collections/index.d.ts.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -4
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +462 -7
- package/dist/models/PasswordCredential.d.ts +36 -0
- package/dist/models/PasswordCredential.d.ts.map +1 -0
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.d.ts.map +1 -1
- package/dist/services/DeviceCredentialService.d.ts.map +1 -1
- package/dist/services/PasswordCredentialService.d.ts +302 -0
- package/dist/services/PasswordCredentialService.d.ts.map +1 -0
- package/dist/services/SessionService.d.ts +3 -1
- package/dist/services/SessionService.d.ts.map +1 -1
- package/dist/services/credential-hash.d.ts +76 -0
- package/dist/services/credential-hash.d.ts.map +1 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +215 -10
- package/dist/sveltekit/index.d.ts +46 -0
- package/dist/sveltekit/index.d.ts.map +1 -1
- package/dist/sveltekit/password-handlers.d.ts +91 -0
- package/dist/sveltekit/password-handlers.d.ts.map +1 -0
- package/dist/sveltekit.js +250 -68
- package/dist/sveltekit.js.map +1 -1
- package/package.json +12 -12
- package/dist/chunks/TerminalAuthService-BfxG493c.js.map +0 -1
package/AGENTS.md
CHANGED
|
@@ -14,6 +14,7 @@ are not prerequisites for unrelated user-package work.
|
|
|
14
14
|
| `src/services/MobileAuthService.ts` | mobile handshake, bearer sessions, bootstrap extension boundary | [agents/mobile-auth.md](agents/mobile-auth.md) |
|
|
15
15
|
| `src/services/TerminalAuthService.ts`, `src/services/LoginAttemptLimiter.ts`, `src/collections/LoginAttemptCollection.ts` | terminal approval concurrency, shared login budget and lockout (#3273), retry-safe database fixtures | [agents/terminal-auth-testing.md](agents/terminal-auth-testing.md) |
|
|
16
16
|
| `src/services/DeviceCredentialService.ts` | per-person PIN layered on a device session (#3276), verifier seam for other credential kinds | this file, "Security boundaries" |
|
|
17
|
+
| `src/services/PasswordCredentialService.ts`, `src/sveltekit/password-handlers.ts` | email + password sign-in and password lifecycle (#3274); shared scrypt in `credential-hash.ts` | this file, "Security boundaries"; README "Password sign-in" |
|
|
17
18
|
| `src/retention.ts` | expired session/token/CLI-auth reaping and retention sweep wiring | [agents/retention.md](agents/retention.md) |
|
|
18
19
|
|
|
19
20
|
## Models and authority
|
|
@@ -90,6 +91,21 @@ are not prerequisites for unrelated user-package work.
|
|
|
90
91
|
sign-in snapshot; `singleOccupant` revokes other children only AFTER a
|
|
91
92
|
successful mint. A `mustReset` sign-in mints with an empty ceiling and
|
|
92
93
|
`setPin` ends that session; do not grant it authority another way.
|
|
94
|
+
- `UsersPasswordCredential` is one row per person, sensitive, with no
|
|
95
|
+
generated surface; `PasswordCredentialService` and its handlers are its only
|
|
96
|
+
read/write paths. Sign-in normalizes the email like `emailKey`, reserves
|
|
97
|
+
before work, always runs one scrypt verify (dummy hash when absent), never
|
|
98
|
+
creates a user, and maps every non-rate-limit refusal to one
|
|
99
|
+
`PasswordCredentialError`. It re-checks the credential after the mint (a
|
|
100
|
+
rehash that keeps row and version is confirmed by re-verifying); the
|
|
101
|
+
parameter-upgrade write is a guarded UPDATE that must never replace a
|
|
102
|
+
rotated password, and verification against a weaker stored hash is padded
|
|
103
|
+
to the policy's scrypt cost. Self-service writes are guarded on the proven
|
|
104
|
+
credential (first set insert-only); session sweeps run before and after
|
|
105
|
+
each write. Self-service needs the current password except for a first
|
|
106
|
+
set; `users.password.manage` follows the PIN's single-tenant target
|
|
107
|
+
rule and never resets the actor's own. Layered sessions never manage
|
|
108
|
+
passwords; a `mustChange` sign-in mints an empty ceiling.
|
|
93
109
|
- A layered session whose tenant differs from its parent's never loads (null
|
|
94
110
|
is allowed: narrower). Limiter rows carry `retain_until`; retention must
|
|
95
111
|
not fall back to a process-wide horizon.
|
package/README.md
CHANGED
|
@@ -378,6 +378,43 @@ export const GET = createOidcCallbackHandler({
|
|
|
378
378
|
});
|
|
379
379
|
```
|
|
380
380
|
|
|
381
|
+
#### Register the client: redirect URL and landing URL
|
|
382
|
+
|
|
383
|
+
Register two URLs with the IdP, not one. `getOidcClientRegistration()` derives
|
|
384
|
+
both from the same `callbackPath`/`loginPath` options the route handlers use
|
|
385
|
+
(defaults `/auth/<provider>/callback` and `/auth/<provider>/login`):
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
import { getOidcClientRegistration } from '@happyvertical/smrt-users/sveltekit';
|
|
389
|
+
|
|
390
|
+
const { redirectUri, landingUrl } = getOidcClientRegistration(
|
|
391
|
+
'https://app.example.com',
|
|
392
|
+
{ provider: 'kanidm' },
|
|
393
|
+
);
|
|
394
|
+
// redirectUri: https://app.example.com/auth/kanidm/callback
|
|
395
|
+
// landingUrl: https://app.example.com/auth/kanidm/login
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
For Kanidm:
|
|
399
|
+
|
|
400
|
+
```sh
|
|
401
|
+
kanidm system oauth2 add-redirect-url <client> https://app.example.com/auth/kanidm/callback
|
|
402
|
+
kanidm system oauth2 set-landing-url <client> https://app.example.com/auth/kanidm/login
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
The landing URL must be the login **start** route, not the app's home page.
|
|
406
|
+
Kanidm (verified through 1.11) holds the app's pending authorization in a
|
|
407
|
+
short-lived cookie. It discards that cookie whenever the user passes through its
|
|
408
|
+
own `/ui/login` page, for example by pressing "Return to Login" after a
|
|
409
|
+
mistyped password, and also after 15 minutes. After signing in, the user then
|
|
410
|
+
lands in Kanidm's apps panel instead of returning to the app. The app never
|
|
411
|
+
sees this happen, so the app tile in that panel, which links to the landing URL,
|
|
412
|
+
is the only way back. When the tile points at the login start route, one click
|
|
413
|
+
signs the user in, because their Kanidm session already exists. When it points at
|
|
414
|
+
the home page, the user arrives signed out and has to start sign-in again. Other
|
|
415
|
+
IdPs that offer an "initiate login URI" or app-launcher URL should use the same
|
|
416
|
+
`landingUrl`.
|
|
417
|
+
|
|
381
418
|
The callback verifies `state`, PKCE, issuer, audience, nonce, and the provider
|
|
382
419
|
JWKS-signed ID token, falling back to the OIDC UserInfo endpoint when the ID
|
|
383
420
|
token omits required profile claims like `email`. Temporary transaction cookies
|
|
@@ -602,8 +639,8 @@ the current tenant in application code.
|
|
|
602
639
|
|
|
603
640
|
### Login rate limiting and lockout
|
|
604
641
|
|
|
605
|
-
Every credential-based sign-in (terminal approval, device PIN,
|
|
606
|
-
|
|
642
|
+
Every credential-based sign-in (terminal approval, device PIN, password
|
|
643
|
+
sign-in, and any passkey flow an app adds) draws from one shared budget,
|
|
607
644
|
`LoginAttemptLimiter` (#3273). The budget lives in `users_login_attempts`, so it
|
|
608
645
|
holds across every replica on Postgres; keys are hashed, so the table never
|
|
609
646
|
becomes an index of emails or IPs.
|
|
@@ -775,6 +812,127 @@ Other credential kinds (a fob or badge reader, say) implement
|
|
|
775
812
|
`DeviceCredentialVerifier` and go through `service.signIn(verifier, input)`;
|
|
776
813
|
`PinVerifier` is the reference implementation.
|
|
777
814
|
|
|
815
|
+
### Password sign-in
|
|
816
|
+
|
|
817
|
+
`PasswordCredentialService` (#3274) gives an app without an identity provider
|
|
818
|
+
email + password sign-in for **existing, active** users. It never creates a
|
|
819
|
+
user. The password lives in its own object, `UsersPasswordCredential`
|
|
820
|
+
(`users_password_credentials`, one row per person), not on `User`: it is
|
|
821
|
+
`sensitive`, has no generated REST/MCP/CLI surface, never enters the change
|
|
822
|
+
feed, and `toPublicJSON()` omits the hash. The service and the handlers below
|
|
823
|
+
are its only read and write paths.
|
|
824
|
+
|
|
825
|
+
**Hashing.** scrypt from `node:crypto` (no native dependency) with a random
|
|
826
|
+
per-hash salt; the parameters are stored in each hash
|
|
827
|
+
(`scrypt$N$r$p$salt$hash`) and compared with `timingSafeEqual`. Raising
|
|
828
|
+
`password.scrypt` never breaks existing passwords: a weaker hash still
|
|
829
|
+
verifies and is rehashed under the current parameters on the next successful
|
|
830
|
+
sign-in, by a guarded update that never overwrites a password changed in the
|
|
831
|
+
meantime. A sign-in for an unknown email, or a user with no password, runs
|
|
832
|
+
the same scrypt verification against a dummy hash, and a check against a hash
|
|
833
|
+
still carrying weaker parameters is padded with extra scrypt work up to the
|
|
834
|
+
current policy's cost, so timing does not reveal whether the account or
|
|
835
|
+
credential exists. Only ever raise the parameters: hashes stronger than the
|
|
836
|
+
policy are not rehashed downward and would verify slower than the dummy.
|
|
837
|
+
Passwords are NFKC-normalized.
|
|
838
|
+
|
|
839
|
+
**Sign-in.** The submitted email is normalized exactly as `User.emailKey` is
|
|
840
|
+
and matched against that column (legacy users need `backfillUserEmailKeys()`
|
|
841
|
+
first), and the attempt is reserved through the shared `LoginAttemptLimiter` *before*
|
|
842
|
+
any credential work: per account (the email key, whether or not it exists) and
|
|
843
|
+
per client address. A request missing its email or password is reserved and
|
|
844
|
+
failed like a wrong password. The person must be `ACTIVE` and, when a tenant is given,
|
|
845
|
+
hold an `ACTIVE` membership in it. A successful sign-in mints a first-class
|
|
846
|
+
session with `authMethod: 'password'` through `SessionService.createSession`,
|
|
847
|
+
re-checks that the verified password is still current (so a concurrent change
|
|
848
|
+
or reset cannot leave a session behind), and revokes the browser's previous
|
|
849
|
+
cookie session. Every refusal — unknown email, no password, wrong password,
|
|
850
|
+
inactive user, no membership, malformed or over-long input — is the same
|
|
851
|
+
`PasswordCredentialError` / 401; a rate limit is `LoginRateLimitError` / 429
|
|
852
|
+
with `Retry-After`, and it is reached the same way for an unknown email as for
|
|
853
|
+
a real one. Audit rows carry hashed keys and the tenant only: never the email,
|
|
854
|
+
the password, the hash, or the user id.
|
|
855
|
+
|
|
856
|
+
**Policy** (`password` option): `minLength` (default 10), `maxLength`
|
|
857
|
+
(default 256, hard ceiling 1024 — longer input is refused before hashing,
|
|
858
|
+
at sign-in too), a small built-in `DEFAULT_PASSWORD_DENYLIST` extended by
|
|
859
|
+
`denylist`, a refusal of the person's own email, and a `reject(password,
|
|
860
|
+
{ userId, email })` hook for anything else (a breached-password check, shop
|
|
861
|
+
words). There are no composition rules. Optional `pepper`. Violations are
|
|
862
|
+
`PasswordPolicyError` (400) with a message fit to display.
|
|
863
|
+
|
|
864
|
+
**Lifecycle and authority.**
|
|
865
|
+
|
|
866
|
+
| Call | Who | Sessions ended |
|
|
867
|
+
|---|---|---|
|
|
868
|
+
| `setPassword` | the person, for their **first** password; or an admin with `users.password.manage` | none; all of the person's if an admin replaced an existing one |
|
|
869
|
+
| `changePassword` | the person, proving the current password (from the sign-in budget) | every other session of theirs |
|
|
870
|
+
| `resetPassword` | an admin with `users.password.manage`, never for themself; `mustChange` defaults to true | every session of the person |
|
|
871
|
+
| `clearPassword` | an admin, or the person proving the current password | the person's password sessions |
|
|
872
|
+
|
|
873
|
+
Self-service writes are guarded on the exact credential the person proved
|
|
874
|
+
(or, for a first password, insert-only), so a reset that lands while a change
|
|
875
|
+
or clear is in flight is never overwritten with a password chosen under the
|
|
876
|
+
old one; the slower request fails with `PasswordCredentialError`. Every write
|
|
877
|
+
that ends sessions ends them both before and after the credential write, so
|
|
878
|
+
a failure between the two leaves the person signed out rather than signed in
|
|
879
|
+
under a password that is gone.
|
|
880
|
+
|
|
881
|
+
A reset with `mustChange` makes the next sign-in a restricted session: it
|
|
882
|
+
resolves to no permissions (an empty permission ceiling) and can only call
|
|
883
|
+
`changePassword`, which ends it; the person then signs in with the new
|
|
884
|
+
password. A device PIN (layered) session never manages passwords. Like the
|
|
885
|
+
PIN, the password is one per person across tenants, so `users.password.manage`
|
|
886
|
+
reaches only people whose every active membership is in the administrator's
|
|
887
|
+
session tenant. It is impersonation-equivalent — grant it like an owner-level
|
|
888
|
+
permission. It is registered in the runtime catalog
|
|
889
|
+
(`ensurePasswordPermissionsRegistered()`, called by the service), so
|
|
890
|
+
`syncPermissionCatalog()` creates it and the default role matrix
|
|
891
|
+
(`seedRolePermissions()`) grants it to `owner` and `admin` (`*`), not to
|
|
892
|
+
`member` or `viewer`.
|
|
893
|
+
|
|
894
|
+
#### Consumer wiring (SvelteKit)
|
|
895
|
+
|
|
896
|
+
```ts
|
|
897
|
+
// src/lib/server/password-auth.ts
|
|
898
|
+
import { createPasswordCredentialHandlers } from '@happyvertical/smrt-users/sveltekit';
|
|
899
|
+
|
|
900
|
+
export const passwordAuth = createPasswordCredentialHandlers({
|
|
901
|
+
db,
|
|
902
|
+
tenantId: () => SHOP_TENANT_ID, // value or (event) => …; omit for no tenant context
|
|
903
|
+
cookieName: 'sid', // the same cookie createSessionHandler reads
|
|
904
|
+
sessionTtl: 12 * 60 * 60, // default 7 days
|
|
905
|
+
loginLimiter, // optional: share one limiter with PIN/terminal
|
|
906
|
+
password: { minLength: 12, denylist: ['teamworks fabrication'] },
|
|
907
|
+
});
|
|
908
|
+
// POST /auth/password → passwordAuth.signIn ({ email, password }, JSON or form)
|
|
909
|
+
// PUT /account/password → passwordAuth.changePassword ({ currentPassword, newPassword })
|
|
910
|
+
// POST /account/password → passwordAuth.setPassword ({ password } or admin { userId, password })
|
|
911
|
+
// DELETE /account/password → passwordAuth.clearPassword ({ currentPassword } or admin { userId })
|
|
912
|
+
// POST /admin/users/password → passwordAuth.resetPassword ({ userId, password, mustChange? })
|
|
913
|
+
// sign out → destroySessionCookie(event, { db })
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
What the app must do:
|
|
917
|
+
|
|
918
|
+
- Mount `createSessionHandler` with the same `cookieName`; the management
|
|
919
|
+
handlers act as the session it puts in `event.locals` and refuse (401) when
|
|
920
|
+
`locals.authMethod` is absent.
|
|
921
|
+
- Put the sign-in route on the signed-out allowlist; leave CSRF protection on
|
|
922
|
+
(SvelteKit's origin check covers form posts, JSON needs CORS preflight).
|
|
923
|
+
- On `mustChange: true` (or a 200 from `changePassword` with
|
|
924
|
+
`signedOut: true`), send the person to the change-password page or back to
|
|
925
|
+
sign-in.
|
|
926
|
+
- Run `syncPermissionCatalog()` (or `seedRolePermissions()`) after the
|
|
927
|
+
service is first constructed, and grant `users.password.manage` to any
|
|
928
|
+
other role that administers passwords.
|
|
929
|
+
- Run `smrt db:migrate` once for the new `users_password_credentials` table.
|
|
930
|
+
- For a form action instead of an endpoint, call
|
|
931
|
+
`(await passwordAuth.service()).signIn({ … })` and set the cookie with the
|
|
932
|
+
returned `sessionId`; keep the session id out of page data.
|
|
933
|
+
|
|
934
|
+
Passkeys/WebAuthn are a separate credential (#3275).
|
|
935
|
+
|
|
778
936
|
### Request-scoped database access
|
|
779
937
|
|
|
780
938
|
Generated SvelteKit helpers and custom server code can read the current
|
|
@@ -1011,6 +1169,7 @@ TenantService supports three modes: `flexible` (no auto-create), `personal` (aut
|
|
|
1011
1169
|
| `Session` | Server-side session. Secure UUID. TTL in seconds. `authMethod` records the channel; `parentSessionId` makes it a layered session valid only while its parent is. Reserved `data` keys (`SESSION_DATA_KEYS`) carry a permission ceiling, sliding idle timeout, and absolute expiry. |
|
|
1012
1170
|
| `UsersLoginAttempt`, `UsersLoginAuditEvent` | Hashed-key login budget rows and durable sign-in audit events (#3273). Closed generated surface. |
|
|
1013
1171
|
| `UsersPinCredential` | Per-person scrypt PIN hash for enrolled-device sign-in (#3276). Closed generated surface. |
|
|
1172
|
+
| `UsersPasswordCredential` | Per-person scrypt password hash for email + password sign-in (#3274). Sensitive; closed generated surface. |
|
|
1014
1173
|
| `Group` | Team within a tenant. Gains permissions via GroupRole. |
|
|
1015
1174
|
| `Membership` | User + Tenant + Role junction. UNIQUE(userId, tenantId). |
|
|
1016
1175
|
| `MembershipOverride` | Per-user permission grant/deny on a membership. |
|
|
@@ -1028,7 +1187,7 @@ TenantService supports three modes: `flexible` (no auto-create), `personal` (aut
|
|
|
1028
1187
|
| `MembershipOverrideCollection`, `TenantPermissionOverrideCollection` | Override management at membership and tenant levels |
|
|
1029
1188
|
| `GroupCollection`, `GroupMemberCollection`, `GroupRoleCollection`, `RolePermissionCollection` | Group and role-permission junction management |
|
|
1030
1189
|
| `AccessRequestCollection` | AccessRequest queries: `findByEmail()`, `findOpenByEmail()`, `findByStatus()`, `findOpen()` |
|
|
1031
|
-
| `UsersLoginAttemptCollection`, `UsersLoginAuditEventCollection`, `UsersPinCredentialCollection` | Atomic limiter primitives, audit storage, and PIN credential rows behind the services below |
|
|
1190
|
+
| `UsersLoginAttemptCollection`, `UsersLoginAuditEventCollection`, `UsersPinCredentialCollection`, `UsersPasswordCredentialCollection` | Atomic limiter primitives, audit storage, and PIN and password credential rows behind the services below |
|
|
1032
1191
|
|
|
1033
1192
|
### Services
|
|
1034
1193
|
|
|
@@ -1042,6 +1201,7 @@ TenantService supports three modes: `flexible` (no auto-create), `personal` (aut
|
|
|
1042
1201
|
| `SessionService` | High-level session management. `createSession()`, `loadSessionContext()`, `destroySession()`; tenant contexts include direct or inherited membership provenance, `authMethod`, and `parent` for layered sessions. |
|
|
1043
1202
|
| `LoginAttemptLimiter` | Shared sign-in budget with exponential lockout and audit (#3273): `reserve()` → lease `.fail()`/`.succeed()`/`.release()`. `LoginRateLimitError`, `InvalidCredentialsError`, `LoginAuditSink`, `DurableLoginAuditSink`. |
|
|
1044
1203
|
| `DeviceCredentialService` | Per-person sign-in layered on an enrolled device session (#3276): `signInWithPin()`, `signIn(verifier, input)`, `signOut()`, `setPin()`, `resetPin()`, `clearPin()`. Person authority with optional `deviceCeiling`, `singleOccupant` hand-over, `personIdleSeconds` / `personMaxSeconds`. `PinVerifier`, `DeviceCredentialVerifier`. |
|
|
1204
|
+
| `PasswordCredentialService` | Email + password sign-in for existing active users (#3274): `signIn()`, `setPassword()`, `changePassword()`, `resetPassword()`, `clearPassword()`, `hasPassword()`, `validatePassword()`. `PasswordCredentialError`, `PasswordCredentialForbiddenError`, `PasswordPolicyError`. |
|
|
1045
1205
|
| `OidcLoginService` | Generic OIDC authorization-code login with PKCE for Kanidm, Dex, and other standards-compliant providers. |
|
|
1046
1206
|
| `backfillLegacyUserProfiles` | Transactionally create and link canonical global Person Profiles for legacy Users; never creates OIDC identities or infers ownership. |
|
|
1047
1207
|
| `backfillUserEmailKeys` | Idempotently populate durable normalized-email keys after migrating legacy Users; fails closed on duplicates. |
|
|
@@ -1065,8 +1225,10 @@ TenantService supports three modes: `flexible` (no auto-create), `personal` (aut
|
|
|
1065
1225
|
| `switchSessionTenant` | Change tenant context for current session |
|
|
1066
1226
|
| `beginOidcLogin`, `completeOidcLogin` | Low-level SvelteKit helpers for custom OIDC login routes |
|
|
1067
1227
|
| `createOidcLoginHandler`, `createOidcCallbackHandler` | Ready-to-use SvelteKit route handlers for OIDC login and callback |
|
|
1228
|
+
| `getOidcClientRegistration` | Redirect URL and landing (login start) URL to register with the IdP for an OIDC client |
|
|
1068
1229
|
| `createMobileAuthHandlers` | Mountable `/api/mobile` PKCE, bearer session, bootstrap, logout, and route-guard handlers |
|
|
1069
1230
|
| `createDeviceCredentialHandlers` | Mountable PIN sign-in, sign-out, and PIN management handlers for enrolled devices |
|
|
1231
|
+
| `createPasswordCredentialHandlers` | Mountable password sign-in (sets the session cookie) and password set/change/reset/clear handlers |
|
|
1070
1232
|
| `resolveMobileUploadDedupKey` | Resolves `clientCaptureId` with `Idempotency-Key` fallback for app-owned multipart routes |
|
|
1071
1233
|
| `SessionLocals` | Type for `event.locals` (extend in `app.d.ts`) |
|
|
1072
1234
|
|
|
@@ -1089,6 +1251,8 @@ for authentication, deduplication, and status semantics.
|
|
|
1089
1251
|
| `DEFAULT_SESSION_TTL`, `MAX_TENANT_HIERARCHY_DEPTH` | 604800 (7 days in seconds), 10 |
|
|
1090
1252
|
| `DEFAULT_LOGIN_MAX_ATTEMPTS`, `DEFAULT_LOGIN_ATTEMPT_WINDOW_SECONDS`, `DEFAULT_LOGIN_LOCKOUT_*` | 5 attempts per 300 s window; lockout 60 s × 2ⁿ, capped at 3600 s |
|
|
1091
1253
|
| `DEFAULT_PIN_MANAGE_PERMISSION`, `PIN_LOGIN_KIND` | `users.pin.manage`, `pin` |
|
|
1254
|
+
| `DEFAULT_PASSWORD_MANAGE_PERMISSION`, `PASSWORD_LOGIN_KIND` | `users.password.manage`, `password` |
|
|
1255
|
+
| `DEFAULT_PASSWORD_MIN_LENGTH`, `DEFAULT_PASSWORD_MAX_LENGTH`, `PASSWORD_MAX_LENGTH_CEILING`, `DEFAULT_PASSWORD_DENYLIST` | 10, 256, 1024, built-in common-password list |
|
|
1092
1256
|
| `TenantHierarchyError` | Thrown on a missing parent, a cycle, or exceeding the hierarchy depth limit (`code`) |
|
|
1093
1257
|
| `TenantHierarchyMaterializationError` | Thrown by `materializeTenantHierarchy` when any tenant's parent chain is broken; lists `problems` |
|
|
1094
1258
|
|
|
@@ -12,6 +12,12 @@ OIDC handlers, or identity migrations. The canonical scenario matrix is
|
|
|
12
12
|
`iss` against discovered metadata with exact string comparison before trusting
|
|
13
13
|
a code or error. If metadata advertises issuer-response support, `iss` is
|
|
14
14
|
required.
|
|
15
|
+
- **The login start route is the IdP landing URL (#3569).** Kanidm drops a
|
|
16
|
+
pending authorization after any detour through its `/ui/login` and strands the
|
|
17
|
+
user in its apps panel; the client landing URL is the only way back.
|
|
18
|
+
`getOidcClientRegistration()` derives it from `loginPath` (default
|
|
19
|
+
`/auth/<provider>/login`) alongside the callback URL; keep the login handler
|
|
20
|
+
safe to enter with no query string.
|
|
15
21
|
- **Verified-email Profile reuse is fail-closed.** The typed canonical scenarios
|
|
16
22
|
live in
|
|
17
23
|
`packages/profiles/src/testing/oidcProvisioningDecisionMatrix.ts`; both
|