Repository navigation
Commit 1d2a246
fix(plugin-auth)!: admin create-user refuses a password outside the deployment's length range (#25927)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9D35eCXEFgjdFBa5wrMSFZSx)_
Fixes #25824
Clause-②: no (narrowing) — admin create-user refuses one request shape
it used to run: an explicit `password` shorter than the deployment's
`minPasswordLength` (one longer than `maxPasswordLength` was already
refused by better-auth, now with set-user-password's code). Nothing is
widened.
Metadata-protocol: none
## Why
`POST /api/v1/auth/admin/create-user` saved a 1-character password, and
the user then signed in with it (the card's repro, measured on a stock
showcase boot). `set-user-password` refuses the same value with `400
INVALID_REQUEST` "Password must be at least 8 characters", and the
user's own change-password refuses it with `400 PASSWORD_TOO_SHORT`. The
cause is in better-auth 1.7.12 itself: `admin/create-user` runs
`assertPasswordNotTooLong` only, while `admin/set-user-password` runs
both `assertPasswordNotTooShort` and `assertPasswordNotTooLong`
(`better-auth/dist/plugins/admin/routes.mjs`, read in this worktree's
install). Our `runAdminCreateUser` passed the password straight to that
`createUser`. Our `resolvePassword` ran only the complexity check, and
the length check lived only in the set-user-password runner. ADR-0069
records `password_min_length` / `password_max_length` →
`emailAndPassword.minPasswordLength` / `maxPasswordLength` as enforced,
and on this door it was not.
## What changed
(`packages/plugins/plugin-auth/src/admin-user-endpoints.ts`)
- **One length check, in the one place both admin doors resolve a
credential.** `resolvePassword` now takes the deployment's range
(`AuthContextLike['password']['config']`, better-auth's resolved
`minPasswordLength` / `maxPasswordLength`). It refuses a password
outside that range with set-user-password's existing refusal: `400
INVALID_REQUEST`, "Password must be at least N characters" / "Password
must be at most N characters". There is no new error code.
set-user-password's own copy of the check is deleted, so the two doors
cannot drift apart again.
- **create-user reads the range before anything is written.** It reads
`getAuthContext()` before `resolvePassword`, so a refusal comes before
the multi-org join check, the permission-set grant, `createUser`, the
must-change stamp and the audit row. If the context cannot be read, the
request is refused through `mapAuthApiError` (`create-user failed`) and
no account is created. That is the same mapping set-user-password
already used for the same read.
- **A generated password is sized to the range.** It is `min(max(16,
minPasswordLength), maxPasswordLength)` long, and
`generateTemporaryPassword` still never goes below 12. Before, it was
always 16. So with a minimum above 16, set-user-password refused the
password it had just generated, and create-user saved it below the
minimum. The range check runs on generated passwords too, so one that
still cannot fit (a maximum below 12) is refused, never saved.
- Check order on set-user-password is unchanged: `userId`, then
complexity, then length. The one difference is that the context read
moved ahead of password resolution, which is observable only when that
read throws.
Changeset: `.changeset/25824-admin-create-user-password-length.md`
(`@objectstack/plugin-auth` patch, BREAKING banner, ADR-0087
`not-required (no-migration-prescription)`, before / now / fix).
## Verification
Run at `5737e51e` (code) and `d151a987` (code + changeset; the code blob
is identical).
- **New pins** (`admin-user-endpoints.test.ts`, describe "admin doors:
one password length range"), run across both doors from one table:
- Refusal pins: `'a'` (the repro), one short of the minimum, one past
the maximum. Each answers `400`, `INVALID_REQUEST` and the exact
message, and writes nothing (no `createUser`, no hash, no account write,
no engine update, no audit insert).
- Boundary pins: exactly the minimum and exactly the maximum are
applied.
- Configured-range pin: a configured `minPasswordLength: 12` refuses a
10-character password.
- Generated-password pins: a minimum of 24 gives a 24-character
password, and a maximum of 14 gives a 14-character one.
- create-user only: an unreadable auth context refuses with no account.
- `pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2
src/admin-user-endpoints.test.ts src/durability-swallow-repair.test.ts`:
VERDICT command-exit 0, 2 files, 115/115.
- `pnpm --filter @objectstack/plugin-auth test` at `d151a987`: VERDICT
command-exit 0, Test Files 193 passed (193), Tests 3698 passed | 10
skipped (3708).
- `pnpm --filter @objectstack/plugin-auth build && pnpm --filter
@objectstack/plugin-auth typecheck`: VERDICT command-exit 0 (`tsc` +
examples + `check:test-typecheck` OK, ledger unchanged: 10 files / 88
errors / 23 pinned signatures).
- **Ablations.** Each was committed first, mutated through
`scripts/ablation-replace.mjs` or `git restore --source`, and restored
to the HEAD blob `b89b9184` with `git diff HEAD` empty:
- **BASE** (the file put back to `cb0a2fcf`, tests kept): 9 failed / 84
passed. All three create-user refusal pins, create-user's
configured-range pin, all four generated-password pins (both doors) and
the unreadable-context pin went red. set-user-password's refusal pins
stayed green, as expected: that door already refused.
- **A** (the range check deleted from `resolvePassword`): 9 failed / 84
passed. All six refusal pins (both doors), both configured-range pins,
and the pre-existing "enforces better-auth min password length" went
red.
- **B** (`<` → `<=` on the minimum): 4 failed / 89 passed. Both
exactly-the-minimum pins and both minimum-raised generated pins went
red.
- **Gates.** All run at `d151a987`, derived by `node
scripts/pm/dispatch-gates.mjs --commands` (70 families). Exit codes were
captured before any pipe, and `--ran` reconciles 70 derived: 68 run, 2
NOT MEASURED, 0 unrun.
- 66 exited 0 on the first run.
- `check-plugin-teardown-shape --self-test` first exited 3
(prerequisite: the shallow clone could not reach its pinned fixture
commit). It exited 0 after fetching that one commit.
- `pnpm check:dts-closure` first exited 1, naming
`@objectstack/metadata-core` d.ts as missing. That package was being
rebuilt by my own concurrent dogfood-closure build at that moment. It
exited 0 on a re-run once that build finished.
- NOT MEASURED locally (exit 3, prerequisite not met): `pnpm
check:dual-build-cjs-loads` needs every package's `dist/` (39 are not
built in this tree), and `pnpm check:type-check-debt`'s closure build
never got the shared verify lock. CI's `Lint & Repo Gates` runs both.
- `pnpm lint` (repo-wide, `eslint . --no-inline-config`): exit 0, no
output.
- Full dogfood battery (owed for the `(narrowing)` arm): running on
`d151a987` under the verify lock. Its VERDICT line and counts go in the
card's `os-dev-report`.
## Claim race on the card
Two `Claim:` comments landed 88 s apart: this seat's 6407745341
(03:45:06Z) and `domain:server` seat 1's 6407761859 (03:46:34Z, read
against a stale "unassigned" card). Seat 1 withdrew in 6407776655, and
the seat's arbitration is 6407784753. This PR is the 6407745341 claim's.
Seat 1's thread made no edit and opens no PR.
## Acceptance notes
- **Admin import-users is not sized to the range** (noted, not filed:
reach not measured live). With `passwordPolicy: 'temporary'`,
`admin-import-users.ts` calls `generateTemporaryPassword()` with no
length, and its deps carry no auth context. On a deployment whose
`minPasswordLength` is above 16, a temporary-password import would save
16-character credentials below the minimum, the same way create-user
did, because better-auth's `createUser` checks only the maximum. This is
the same defect class, but it is in another file outside this claim's
surface. Dedupe words: import-users temporary password
minPasswordLength, generateTemporaryPassword length import.
- **Two codes for one policy.** The admin doors answer `INVALID_REQUEST`
(set-user-password's existing code, kept per the triage route "with the
same code"), while better-auth's sign-up and change-password answer
`PASSWORD_TOO_SHORT` / `PASSWORD_TOO_LONG`. Aligning them would change a
wire answer, so it is out of scope here.
- On both admin doors the complexity check still runs before the length
check, as on set-user-password before. A short password that also fails
complexity gets the complexity refusal.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Berf7f8pR4mCUBQwMPXUbV)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 073f08b commit 1d2a246
3 files changed
Lines changed: 192 additions & 33 deletions
File tree
- .changeset
- packages/plugins/plugin-auth/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
835 | 835 | | |
836 | 836 | | |
837 | 837 | | |
| 838 | + | |
| 839 | + | |
| 840 | + | |
| 841 | + | |
| 842 | + | |
| 843 | + | |
| 844 | + | |
| 845 | + | |
| 846 | + | |
| 847 | + | |
| 848 | + | |
| 849 | + | |
| 850 | + | |
| 851 | + | |
| 852 | + | |
| 853 | + | |
| 854 | + | |
| 855 | + | |
| 856 | + | |
| 857 | + | |
| 858 | + | |
| 859 | + | |
| 860 | + | |
| 861 | + | |
| 862 | + | |
| 863 | + | |
| 864 | + | |
| 865 | + | |
| 866 | + | |
| 867 | + | |
| 868 | + | |
| 869 | + | |
| 870 | + | |
| 871 | + | |
| 872 | + | |
| 873 | + | |
| 874 | + | |
| 875 | + | |
| 876 | + | |
| 877 | + | |
| 878 | + | |
| 879 | + | |
| 880 | + | |
| 881 | + | |
| 882 | + | |
| 883 | + | |
| 884 | + | |
| 885 | + | |
| 886 | + | |
| 887 | + | |
| 888 | + | |
| 889 | + | |
| 890 | + | |
| 891 | + | |
| 892 | + | |
| 893 | + | |
| 894 | + | |
| 895 | + | |
| 896 | + | |
| 897 | + | |
| 898 | + | |
| 899 | + | |
| 900 | + | |
| 901 | + | |
| 902 | + | |
| 903 | + | |
| 904 | + | |
| 905 | + | |
| 906 | + | |
| 907 | + | |
| 908 | + | |
| 909 | + | |
| 910 | + | |
| 911 | + | |
| 912 | + | |
| 913 | + | |
| 914 | + | |
| 915 | + | |
| 916 | + | |
| 917 | + | |
| 918 | + | |
| 919 | + | |
| 920 | + | |
| 921 | + | |
| 922 | + | |
| 923 | + | |
| 924 | + | |
| 925 | + | |
| 926 | + | |
| 927 | + | |
| 928 | + | |
| 929 | + | |
| 930 | + | |
| 931 | + | |
| 932 | + | |
| 933 | + | |
| 934 | + | |
| 935 | + | |
| 936 | + | |
| 937 | + | |
| 938 | + | |
| 939 | + | |
838 | 940 | | |
839 | 941 | | |
840 | 942 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
238 | 238 | | |
239 | 239 | | |
240 | 240 | | |
241 | | - | |
242 | | - | |
243 | | - | |
244 | | - | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
245 | 246 | | |
246 | 247 | | |
247 | 248 | | |
| |||
317 | 318 | | |
318 | 319 | | |
319 | 320 | | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
320 | 333 | | |
321 | 334 | | |
322 | 335 | | |
323 | 336 | | |
324 | 337 | | |
325 | 338 | | |
326 | 339 | | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
327 | 346 | | |
328 | 347 | | |
329 | 348 | | |
330 | 349 | | |
331 | 350 | | |
332 | 351 | | |
| 352 | + | |
333 | 353 | | |
334 | 354 | | |
335 | 355 | | |
| 356 | + | |
336 | 357 | | |
337 | | - | |
338 | | - | |
339 | | - | |
340 | | - | |
341 | | - | |
342 | | - | |
343 | | - | |
344 | | - | |
345 | | - | |
346 | | - | |
347 | | - | |
348 | | - | |
349 | | - | |
350 | | - | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
351 | 373 | | |
352 | | - | |
353 | | - | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
354 | 384 | | |
355 | | - | |
| 385 | + | |
356 | 386 | | |
357 | 387 | | |
358 | 388 | | |
| |||
688 | 718 | | |
689 | 719 | | |
690 | 720 | | |
691 | | - | |
| 721 | + | |
| 722 | + | |
| 723 | + | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
692 | 729 | | |
693 | 730 | | |
694 | 731 | | |
| |||
1026 | 1063 | | |
1027 | 1064 | | |
1028 | 1065 | | |
1029 | | - | |
| 1066 | + | |
| 1067 | + | |
| 1068 | + | |
| 1069 | + | |
| 1070 | + | |
| 1071 | + | |
| 1072 | + | |
1030 | 1073 | | |
1031 | 1074 | | |
1032 | 1075 | | |
1033 | | - | |
1034 | | - | |
1035 | | - | |
1036 | | - | |
1037 | | - | |
1038 | | - | |
1039 | | - | |
1040 | | - | |
1041 | | - | |
1042 | | - | |
1043 | 1076 | | |
1044 | 1077 | | |
1045 | 1078 | | |
| |||
0 commit comments