Skip to content

feat(cli): wheels generate auth — session/token/jwt scaffold on the auth primitives - #3291

Merged
bpamiri merged 4 commits into
developfrom
peter/issue-3155-generate-auth
Jul 6, 2026
Merged

feat(cli): wheels generate auth — session/token/jwt scaffold on the auth primitives#3291
bpamiri merged 4 commits into
developfrom
peter/issue-3155-generate-auth

Conversation

@bpamiri

@bpamiri bpamiri commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Closes #3155. Part of #2962.

Implements wheels generate auth — a one-command authentication scaffold built on the existing wheels.auth primitives (PasswordHasher, Authenticator, SessionStrategy/TokenStrategy/JwtStrategy), following the Phoenix/Rails-8 shape recommended by the wheels-bot cross-framework research on the issue.

Stacked PR: this branch is based on #3288 (peter/issue-3155-password-hasher), which is still open. The PasswordHasher commit will fall out of this diff automatically once #3288 merges. Merge #3288 first.

Approved design decisions

The maintainer signed off on 2026-07-06 on the design calls flagged in the issue and the bot's research comment:

  • Session flow as the default strategy (--strategy=session).
  • --strategy=token and --strategy=jwt API variants included in v1 — both emit app/controllers/api/Sessions.cfc (no views).
  • Registration ON by default, opt out with --no-registration (session strategy only; noted as not-applicable for token/jwt).
  • CFML session-scope storage via SessionStrategy — no DB sessions table.
  • Code-you-own upgrade policy: stamped header on every generated file; upgrade by re-running with --force and reviewing git diff. Marker-delimited config blocks are replaced in place, never duplicated. Migrations are never overwritten.
  • PBKDF2 hashing via the PasswordHasher service (from feat(auth): PBKDF2 password hashing service (PasswordHasher) #3288) — resolves the cross-engine bcrypt gap the bot's fix-hold flagged.

What it generates

Session (default):

  • app/models/User.cfc — transient password property validated (presence on create, unique+format email, confirmation, 12-char minimum), hashed into passwordDigest in beforeSave and scrubbed; authenticate() with transparent rehash-on-login; single-use SHA-256-digested reset tokens (2h expiry, burned on use); protectedProperties() guards mass assignment.
  • app/controllers/Sessions.cfc, Passwords.cfc, Registrations.cfc — every config() calls super.config() first (the CSRF default-on decision (5.0): flip the default after the detect-and-warn deprecation window #2960 CSRF footgun), all-named verifies(...), injection-safe query-builder finders.
  • startFormTag-based views (CSRF token automatic), cfparam'd, plain semantic HTML.
  • app/migrator/migrations/<ts>_create_users_table.cfct.string(columnNames=...) style, timestamps(), unique email index.
  • Marked blocks (// wheels:generate-auth:*) injected into config/routes.cfm (before root/wildcard), config/services.cfm (created if absent — passwordHasher/authenticator/sessionStrategy singletons), and app/events/onapplicationstart.cfm (strategy registration, per the documented wiring).
  • App specs: tests/specs/models/UserAuthSpec.cfc, tests/specs/controllers/SessionsControllerSpec.cfc.

Token: same model + migration plus apiTokenDigest; api/Sessions.cfc mints an opaque token (plaintext returned once, only the SHA-256 digest stored), DELETE revokes; TokenStrategy wired with a hoisted validator closure (Cross-Engine Invariant 5).

JWT: api/Sessions.cfc mints JWTs via JwtService; WHEELS_JWT_SECRET read from the environment and startup fails loudly when missing/shorter than 32 bytes; no-server-side-revocation documented in the generated header.

Tests

  • New cli/lucli/tests/specs/services/GenerateAuthSpec.cfc (31 specs): dispatch reaches generateAuth via the generate() switch, per-strategy file sets, --no-registration omissions, force-overwrites/refuses-without-force, marker idempotency across re-runs, comment-stripped super.config()-first scans of every emitted controller, and a no-inline-closure-as-constructor-arg guard.
  • Full CLI suite against this branch (worktree server, /wheels/cli/tests?format=json): 1122 pass, 0 fail, 0 error.
  • Hand-verified end-to-end by running the generator (all three strategies, plus --no-registration) into temp project dirs through the dev server and eyeballing the emitted files and injected blocks (routes land before .wildcard()/.root(), services/bootstrap blocks correctly wrapped). A full wheels new boot test was not run.

Docs

  • wheels generate auth section + type-table row in the CLI code-generation reference.
  • "Scaffold it in one command" lead-in (with flags table) at the top of digging-deeper/authentication-patterns.mdx.
  • Changelog fragment changelog.d/3155-generate-auth.added.md.

🤖 Generated with Claude Code

Peter Amiri and others added 2 commits July 6, 2026 07:14
Adds wheels.auth.PasswordHasher, the cross-engine password hashing service
that unblocks the wheels generate auth scaffold (#3155, child of #2962).

- PBKDF2-HMAC-SHA256 via javax.crypto.SecretKeyFactory
  (PBKDF2WithHmacSHA256) — byte-identical on Lucee, Adobe CF, and
  BoxLang by construction, so hashes survive engine migrations.
- Defaults: 600000 iterations (OWASP 2023+), 16-byte SecureRandom salt,
  256-bit derived key.
- Self-describing modular-crypt storage format:
  $pbkdf2-sha256$i=<iterations>$<base64(salt)>$<base64(derivedKey)>
- verify() re-derives with the stored salt/iterations and compares raw
  digest bytes in constant time (MessageDigest.isEqual); returns false,
  never throws, on malformed/empty/unknown-format hashes.
- needsRehash() flags hashes below the configured iteration count or
  with an unrecognized format for transparent work-factor upgrades.
- init() validates iterations as a positive integer and throws
  Wheels.PasswordHasher.InvalidConfiguration otherwise.
- Unicode passwords round-trip (UTF-8); empty password hashing is
  allowed by design — minimum-length policy lives in app validations.
- Not auto-registered in DI (matches Authenticator): the generator will
  wire it in config/services.cfm.

TDD: 24-spec PasswordHasherSpec written first and confirmed failing for
the right reason before implementation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <petera@pai.com>
…uth primitives

Implements #3155 (child of #2962): a one-command authentication scaffold built on
the wheels.auth primitives (PasswordHasher, Authenticator, Session/Token/Jwt
strategies).

Dispatch: new 'auth' case in generate() -> generateAuth() in Module.cfc,
orchestrated by Scaffold.generateAuth() with templates under
cli/lucli/templates/auth/. Flags: --model=User (default), --strategy=session|
token|jwt (default session), --registration/--no-registration (default on,
session only), --force.

Session strategy (default) emits a User model (PBKDF2 hashing via the
passwordHasher service, transient password property validated then hashed and
scrubbed in beforeSave, authenticate() with transparent rehash-on-login,
single-use SHA-256-digested reset tokens expiring after 2h), Sessions/
Passwords/Registrations controllers (super.config() first line, private
filters, all-named verifies, injection-safe query-builder finders),
startFormTag-based views, a create-users migration with a unique email index,
and generated app specs. Token/jwt emit app/controllers/api/Sessions.cfc
instead (opaque digested bearer tokens with revocation, or JwtService-signed
JWTs whose WHEELS_JWT_SECRET fails loudly at startup; no server-side
revocation, documented in the generated header).

Route, service, and strategy wiring are injected between
// wheels:generate-auth:* markers in config/routes.cfm, config/services.cfm
(created if absent), and app/events/onapplicationstart.cfm, always before
root/wildcard, and replaced in place on --force — never duplicated. Generated
code is code-you-own: stamped headers, re-run --force + git diff to upgrade.
Migrations are never overwritten. Token validator and strategy constructors
hoist closures (Cross-Engine Invariant 5).

Tests: cli/lucli/tests/specs/services/GenerateAuthSpec.cfc (31 specs) covers
dispatch, all three strategies' file sets, --no-registration, force/refuse
semantics, marker idempotency, comment-stripped super.config() scans, and the
hoisted-validator guard. Full CLI suite: 1122 pass, 0 fail, 0 error.

Docs: generate-auth section in the CLI code-generation reference and a
'Scaffold it in one command' lead-in on the authentication-patterns guide.

Closes #3155

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <petera@pai.com>

@wheels-bot wheels-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wheels Bot — Reviewer

TL;DR — This PR adds wheels generate auth, a one-command session/token/jwt authentication scaffold built on the existing wheels.auth primitives. The implementation is careful and unusually well-tested: injection-safe finders, digested tokens, super.config()-first controllers, hoisted validator closures (Cross-Engine Invariant 5), marker-delimited idempotent config injection, and 31 CLI specs covering all three strategies plus force/idempotency. I verified every framework API the generated code leans on (QueryBuilder.first(), JwtService, Authenticator.hasStrategy/registerStrategy, env(), injector(), application.wheelsdi, $cgiScope) — they all exist and match. No blocking findings. Verdict: comment — one runtime-verification note and two minor observations below, none of which need to gate the merge. (Note: the PasswordHasher commit belongs to the stacked #3288 and will fall out of this diff once that merges — reviewed there, not here.)

Correctness

  • cli/lucli/templates/auth/controller-api-sessions-token.txt:43 (token) DELETE revoke — verify the Authorization header actually reaches the strategy at runtime. The token delete() calls service("authenticator").authenticate(request). TokenStrategy.$extractFromHeader (vendor/wheels/auth/TokenStrategy.cfc:148-164) reads the token from request.headers.authorization or request.cgi.http_authorization. But Wheels never sets request.headers on the request scope, and the per-request request.cgi is built by $cgiScope() (vendor/wheels/Global.cfc:2470-2471) from a fixed key allowlist that does not include http_authorization. If that holds at runtime, the revoke endpoint (and any user filter that protects token/jwt actions the same way) can never see the bearer token and will always 401. This mirrors the documented guide pattern (digging-deeper/authentication-patterns.mdx:259,327,367), so it's plausibly a pre-existing framework/guide gap rather than something this PR introduced — and the PR body already notes the token/jwt runtime auth flows were not booted end-to-end. Flagging as please verify, not as a confirmed defect: exercise DELETE /api/session with a real Authorization: Bearer header against a booted app and confirm the token resolves.

Conventions

  • cli/lucli/services/Scaffold.cfc $injectAuthBlock routes anchor — .end() fallback can land routes after the wildcard. First-time routes insertion prefers // CLI-Appends-Here, then an uncommented .root(, then falls back to inserting before the last .end(). The stock config/routes.cfm ships // CLI-Appends-Here before .wildcard()/.root(), so the common path is correct (a test pins begin < .wildcard()). But in a hand-edited routes file with no anchor comment and a commented-out .root(, the .end() fallback would place the auth routes after .wildcard(), where they'd never match (anti-pattern #6). Low real-world likelihood, but consider emitting a manual-insert skip note instead of the .end() fallback, since that fallback is the one case that can produce wrong ordering.

Tests

  • Strong coverage: cli/lucli/tests/specs/services/GenerateAuthSpec.cfc exercises all three strategies, --no-registration, custom model name, force/refuse semantics, marker idempotency (exactly-once under re-run and --force), comment-stripped super.config()-first scans, and the hoisted-closure guard. Generated app specs correctly extend wheels.WheelsTest. No gaps worth blocking on. The token/jwt runtime auth round-trip (the Correctness note above) is the one behavior the suite can't reach from the CLI layer — an app-level spec that boots the API sessions controller would close that.

Docs

  • code-generation.mdx and authentication-patterns.mdx updated; changelog fragments changelog.d/3155-generate-auth.added.md (+ 3155-password-hasher.added.md for the stacked commit) follow the <slug>.<type>.md convention. Good.

Commits

  • Both commits are commitlint-clean: valid types/scopes (feat(cli), feat(auth)), subjects under 100 chars, DCO signed-off. Messages explain the "why." No issues.

Nice work — the marker-block injection design and the code-you-own stamping are exactly right, and the test suite is thorough. The single thing I'd genuinely want confirmed before shipping the token/jwt paths is the Authorization-header reachability in the Correctness section.

…reset hole

Review hardening for the wheels generate auth scaffold (#3155):

- BLOCKER: all three bootstrap templates emitted template-level `var` into
  app/events/onapplicationstart.cfm. That file is $include()d from a framework
  function and Adobe CF rejects top-level `var` at compile time (the #3063
  class), so every generated app 500'd on every request on Adobe 2018-2025.
  All bootstrap variables are now `local.`-scoped (valid on every engine);
  the guide's four init-hook snippets were teaching the same pattern and are
  fixed too, with a caution aside.
- HIGH: submitting the reset form with a blank password burned the token,
  reported success, and left the old password valid (presence is onCreate-only
  and the hash callback skips blanks). Passwords##update now rejects blanks
  with a field error before clearing the token.
- Revoke endpoint could never see the bearer token: request.cgi's allowlist
  (Global.cfc $cgiScope) omits http_authorization and request.headers never
  exists, so authenticate(request) always 401'd. The token controller now
  hands the Authorization header to the authenticator explicitly.
- Login timing oracle: unknown emails skipped the PBKDF2 derivation entirely,
  leaking account existence. All three login controllers now run a dummy
  derivation when the account is missing, and their headers plus the CLI
  next-steps recommend wheels.middleware.RateLimiter on credential endpoints.
- Generated Sessions spec called processAction("create", params) — the only
  parameter is includeFilters, so "create" silently disabled before-filters.
  Now calls processAction() bare.
- Routes injection: dropped the last-.end() fallback, which could park routes
  after .wildcard() where they never match (anti-pattern #6); with no safe
  anchor the generator now skips with a manual-insert note. Scaffold rollback
  now runs on any failure, not just typed ScaffoldErrors.
- Passwords controller header documents that reset does not invalidate live
  sessions; next steps call out wiring reset-email delivery.
- GenerateAuthSpec grows from 31 to 43 specs: local.-scope guards for all
  three bootstraps, blank-reset guard ordering, timing dummy, explicit-header
  revoke, processAction shape, and the three routes-anchor edge paths.

CLI suite: 1134 pass / 0 fail / 0 error locally (Lucee 7 + SQLite).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <petera@pai.com>
@bpamiri

bpamiri commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

Shepherd round 1 — review hardening pushed (2313810)

Addressed the wheels-bot review plus an internal adversarial pass. CLI suite locally: 1134 pass / 0 fail / 0 error (GenerateAuthSpec 31 → 43 specs).

Bot Correctness item (token revoke Authorization reachability) — verified, it was real. Global.cfc::$cgiScope() builds request.cgi from a fixed key allowlist that does not include http_authorization, and Wheels never sets request.headers, so authenticate(request) could never see the bearer token and DELETE /api/session always 401'd. Fixed template-side: the token controller now reads GetHttpRequestData(false).headers and hands the strategy an explicit {cgi: {http_authorization: ...}} context (shape verified against TokenStrategy.$extractFromHeader). The same gap in the pre-existing guide filter pattern is a framework/docs follow-up outside this PR's scope.

Bot Conventions item (.end() routes fallback) — taken. The last-.end() fallback could park routes after .wildcard() where they never match; the generator now skips with a manual-insert note instead, and the three routes-anchor edge paths are covered by new specs.

Internal review fixes in the same push:

  • Blocker (cross-engine, Documented config/services.cfm snippet (var di = injector();) crashes the entire app at boot on Adobe CF #3063 class): all three bootstrap templates emitted template-level var into app/events/onapplicationstart.cfm — an Adobe CF compile error that 500'd every request of every generated app on Adobe 2018–2025. Bootstrap variables are now local.-scoped, with regression specs per strategy; the guide's four init-hook snippets taught the same pattern and were fixed with a caution aside.
  • High: blank-password reset burned the token, reported success, and kept the old password valid. Passwords##update now rejects blanks with a field error before clearing the token (spec pins the guard ordering).
  • Login timing oracle: dummy PBKDF2 derivation on unknown emails in all three login controllers; headers + CLI next-steps now recommend wheels.middleware.RateLimiter on credential endpoints.
  • Generated Sessions spec called processAction("create", params) — the only parameter is includeFilters, so it silently disabled before-filters; now processAction().
  • Scaffold rollback now runs on any failure (not just typed ScaffoldErrors), so an IO error mid-run can't leave a half-generated scaffold.
  • Passwords header documents that reset does not invalidate live sessions; next-steps call out wiring reset-email delivery (the create() TODO).

@wheels-bot wheels-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wheels Bot — Reviewer

TL;DR — This is the review-hardening pass on wheels generate auth (the session/token/jwt scaffold). The new commit 2313810 cleanly resolves the single "please verify" item from the previous review (Authorization-header reachability on the token revoke endpoint) and self-hardens four more paths: Adobe-safe local.-scoped bootstraps, a blank-password reset hole, a login timing oracle, and the routes .end()-fallback ordering risk. I re-verified the framework APIs the changed code leans on against source. No blocking findings. Verdict: comment — the code paths are correct as written; the one residual is a runtime confirmation the CLI suite structurally cannot reach.

Correctness — prior findings, now resolved

  • Token revoke can now see the bearer token. The previous review flagged that service("authenticator").authenticate(request) could not reach the Authorization header because request.cgi's allowlist (Global.cfc $cgiScope) omits http_authorization and request.headers is never set. controller-api-sessions-token.txt:59-62 now reads the header via GetHttpRequestData(false).headers and hands it to the authenticator as {cgi: {http_authorization: ...}}. I traced this end-to-end: TokenStrategy.$extractFromHeader (vendor/wheels/auth/TokenStrategy.cfc:159-163) reads request.cgi["http_" & Replace(headerName,"-","_","ALL")] = http_authorization, strips the Bearer scheme (:171-179, default scheme "Bearer", headerName lowercased to "authorization"), and Authenticator.$authenticate (:135, :150-151) tries every supports()-true strategy with no filter. The mechanism is correct.

    • Residual (please verify, not blocking): the token/jwt paths still are not booted end-to-end (PR body acknowledges this). Worth one manual DELETE /api/session with a real Authorization: Bearer header against a running app to confirm the round-trip — the CLI spec layer cannot exercise it.
  • Blank-password reset hole closed. controller-passwords.txt:75-79 now rejects a blank password with addError() + renderView("edit") before clearing resetTokenDigest. Correct: presence is when="onCreate" (model.txt:24) and hashPasswordProperty skips blanks (model.txt:75), so without this guard a blank submit would burn the token yet leave the old hash intact. The token is only zeroed at :81-82 after the guard, and since it is set on the in-memory object and persisted by the same save() (:83), a later validation failure (e.g. validatesConfirmationOf) leaves the DB token intact. Sound.

Cross-engine

  • Adobe top-level var fixed. All three bootstrap templates (bootstrap-session.txt, bootstrap-token.txt, bootstrap-jwt.txt) now use local.-scoped variables, with an inline note tying it to #3063. Correct — these files are $include()d from inside a framework function, so local resolves to that function's scope on every engine while template-level var compile-fails on Adobe. The validator closure stays hoisted into local.tokenValidator before the new wheels.auth.TokenStrategy(...) named-arg call (bootstrap-token.txt:13-22), honoring Cross-Engine Invariant #5. The four guide init-hook snippets got the same treatment plus a caution <Aside> — consistent.

Conventions

  • Routes .end() fallback removed. Scaffold.cfc:982-991 drops the last-.end() insertion fallback that could park auth routes after .wildcard() (anti-pattern #6); with no // CLI-Appends-Here or uncommented .root( anchor the generator now skips with a manual-insert note instead of injecting dead routes. $findCodePosition (:1021-1032) correctly ignores commented-out .root( lines. Minor: it strips // line comments only, not /* */ block comments — a .root( inside a block comment could still false-match — but that is a pre-existing helper and a negligible edge for an app-owned routes file.

Tests

  • GenerateAuthSpec.cfc grows 31 to 43 specs covering the new behavior: local.-scope guards for all three bootstraps, blank-reset ordering, the timing dummy, explicit-header revoke, the processAction() shape fix, and the three routes-anchor edge paths. The generated spec-sessions-controller.txt:43 fix (processAction() bare, not processAction("create", loginParams)) is right — processAction's only param is includeFilters, so the old call was silently disabling before-filters. Generated specs extend wheels.WheelsTest. Good coverage for the CLI layer.

Commits

  • fix(cli): make generate auth Adobe-safe and close the blank-password reset hole — commitlint-clean (fix type, cli scope, header under 100, DCO signed-off), and the body explains the "why" for each change. No issues.

Careful, well-tested hardening pass. Everything I could verify against source checks out; the only open item is booting the token/jwt API paths once against a live app, which is a confirmation step rather than a defect.

…amespaced extends

Booting generated apps end-to-end (the runtime confirmation wheels-bot asked
for on all three strategies) surfaced two defects that string-level CLI specs
cannot reach:

1. Every new-account creation failed validation. passwordDigest is an
   allowNull=false column, so Wheels auto-registers validatesPresenceOf for
   it — but the generated model only populates the digest in the beforeSave
   callback, which runs AFTER validation. Registration, programmatic
   creation, seeds, and the generated UserAuthSpec all failed with
   'Password Digest can't be empty' (the exact trap the authentication
   guide documents at its beforeValidation stopgap). Fixed with
   property(name="passwordDigest", automaticValidations=false) — presence
   stays guaranteed by validatesPresenceOf(password) on create feeding the
   hashing callback, with the NOT NULL constraint as backstop. This keeps
   the validate-plaintext-then-hash ordering.

2. The token/jwt controllers live in app/controllers/api/ but extended the
   bare 'Controller', which cannot resolve from a subfolder — every request
   500'd with 'invalid component definition, can't find component
   [Controller]'. Now extends app.controllers.Controller, matching the
   admin generator's namespaced-controller convention.

Verified live against three freshly scaffolded apps (Lucee 7 + SQLite):
token (201+token, 401 wrong/unknown, Bearer revoke 200 then 401), jwt
(valid HS256 with sub/email/iat/exp, 401, stateless delete note), session
(CSRF register 303+flash, wrong-password re-render, login 303). Three new
specs pin both fixes. CLI suite: 1137 pass / 0 fail / 0 error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <petera@pai.com>
@bpamiri

bpamiri commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

Round 2 — runtime verification done (the bot's residual ask), two real defects found and fixed

Commit 2be96af05. Per the reviewer's residual item ("boot the token/jwt API paths once against a live app"), I scaffolded three fresh apps with the worktree CLI (wheels newwheels generate authserver run, Lucee 7 + SQLite) and drove every strategy end-to-end. The exercise caught two runtime defects that the string-level CLI specs structurally cannot see — exactly why the reviewer asked for it.

Defect 1 — every new-account creation failed validation (all three strategies)

passwordDigest is an allowNull=false column, so Wheels auto-registers validatesPresenceOf(passwordDigest) (Model.cfc:224-257). The generated model populates the digest in the beforeSave callback — which runs after validation — so every save() of a new record failed with Password Digest can't be empty. Registration (POST /register), programmatic creation, wheels seed, and the generated UserAuthSpec round-trip spec were all broken out of the box. This is the exact trap the authentication-patterns guide documents (its stopgap hashes in beforeValidation; the generator hashes in beforeSave so plaintext length/confirmation validate first — hence the different fix).

Fix: property(name="passwordDigest", automaticValidations=false) in the model template — the documented per-property opt-out. Presence stays guaranteed transitively: validatesPresenceOf(password) on create feeds the hashing callback, and the DB NOT NULL constraint is the backstop. Validate-plaintext-then-hash ordering is preserved.

Defect 2 — token/jwt controllers 500'd on every request

app/controllers/api/Sessions.cfc extended the bare "Controller", which cannot resolve from a subfolder: every request failed with invalid component definition, can't find component [Controller]. Fix: extends="app.controllers.Controller", matching the admin generator's namespaced-controller convention (pinned by AdminGeneratorTest).

Verified live (after fixes, from regenerated templates — no hand patching)

Strategy Checks
token POST /api/session valid → 201 + opaque token; wrong password → 401; unknown email → 401 (timing-dummy path, comparable latency); DELETE /api/session with Authorization: Bearer{revoked: true} (confirms the Authorization-header handoff works at runtime); same token again → 401 Invalid or expired token; no header → 401
jwt valid creds → 201 + 3-segment HS256 JWT with sub/email/iat/exp claims (exp − iat = 3600); wrong password → 401; DELETE → stateless-revocation doc message
session GET /register → CSRF-tokened form; POST /register → 303 + Welcome! flash (account created + logged in); wrong-password login → re-render with uniform error; correct login → 303 + Welcome back. flash

Also verified along the way: wheels migrate latest applies the generated migration cleanly, the routes block lands at the // CLI-Appends-Here anchor before .wildcard(), and the bootstrap registers the strategy on first boot.

Tests

Three new specs pin the fixes (digest auto-validation opt-out asserted for all three strategy fixtures; app.controllers.Controller extends asserted for token and jwt). Full CLI suite against the worktree: 1137 pass / 0 fail / 0 error (+3 vs. the previous round's 1134).

@wheels-bot wheels-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wheels Bot — Reviewer

TL;DR — This is the make-it-bootable pass on wheels generate auth. The new head commit 2be96af resolves the exact residual the two prior reviews left open (the token/jwt paths were never booted end-to-end) by actually running all three strategies against freshly scaffolded Lucee 7 + SQLite apps, and fixes the two defects that boot surfaced: (1) passwordDigest is populated in beforeSave after validation, so Wheels' auto NOT-NULL presence validation on the allowNull=false column rejected every new record — now suppressed with property(name="passwordDigest", automaticValidations=false); (2) the token/jwt controllers live in app/controllers/api/ but extended a bare Controller, which cannot resolve from a subfolder — now extends="app.controllers.Controller". I verified both fixes against framework source. No blocking findings. Verdict: comment — the delta is clean; the only non-code note is the stacked-PR merge ordering.

Correctness — both fixes verified against source

  • model.txt:25 — automaticValidations=false correctly suppresses the auto presence validation without opening a hole. The per-property gate is real: Model.cfc:232-238 skips automatic validation registration when the per-property automaticValidations flag on the class mapping is false, and property() (vendor/wheels/model/properties.cfc:119-121) sets exactly that key. Presence is still guaranteed for the create path — model.txt:32 keeps validatesPresenceOf(property="password", when="onCreate"), which feeds hashPasswordProperty (:82-89), with the DB allowNull=false (migration.txt:15) as backstop. The config()-time ordering is proven by the existing framework asset vendor/wheels/tests/_assets/models/UserAutomaticValidationsOff.cfc:6 (property(name="id", automaticValidations=false)). No length concern from losing the auto max-length check either: passwordDigest is limit=500 (migration.txt:15), well above any PBKDF2 digest.

  • API session controllers (jwt and token) — namespaced extends matches the admin generator convention. The API controllers land in app/controllers/api/, and the fix to extends="app.controllers.Controller" mirrors cli/lucli/templates/admin/controller.txt:1, which already uses extends="app.controllers.Controller" for its own subfolder controller. The base app/controllers/Controller.cfc extends wheels.Controller, so the mapping path resolves. The top-level session controllers keep extends="Controller" (correct — they resolve relative to app/controllers/).

Tests

  • Three new specs in GenerateAuthSpec.cfc pin both fixes: the automaticValidations=false line across all three strategies (:168-179) and the namespaced extends for token (:349-357) and jwt (:399-402). These are string-level assertions, which is the ceiling of the CLI spec layer — the actual boot round-trip is covered by the commit's documented live verification (token 201/401/revoke, jwt HS256, session CSRF 303). Appropriate coverage for the layer.

Commits

  • fix(cli): make generated auth apps bootable — digest validation and namespaced extends — commitlint-clean (fix type, cli scope, header under 100, DCO signed-off), and the body explains the "why" for both defects with the runtime evidence. No issues.

Non-blocking note (already stated in the PR body, repeating for the merge checklist): this branch is stacked on #3288 and still carries the PasswordHasher commit + spec in the diff — merge #3288 first so that commit falls out cleanly. That code was reviewed in #3288, not here.

Nice work closing the loop with a real boot — that's precisely the confirmation the previous two passes asked for.

@bpamiri
bpamiri merged commit bb6bc41 into develop Jul 6, 2026
19 checks passed
@bpamiri
bpamiri deleted the peter/issue-3155-generate-auth branch July 6, 2026 17:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feature: wheels generate auth — session auth scaffold on the existing wheels.auth primitives

1 participant