Skip to content

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

Description

@bpamiri

Child of #2962, sequenced FIRST (highest leverage per effort). CLI-only surface (cli/lucli templates + scaffolding) over existing vendor/wheels/auth primitives (Authenticator, Session/Token/JWT strategies, AuthMiddleware). Phoenix/Rails-8 shape: User model + sessions/passwords controllers + views + routes + migrations. Hard requirements from the 2026-06 campaigns: generated controllers MUST call super.config() (the #2960 CSRF footgun), all-named args, generated specs. Design calls needing the maintainer: registration on/off flag, --strategy= selection, generated-code-you-own upgrade policy.

Activity

  1. wheels-bot commented on Jun 12, 2026

    @wheels-bot
    Contributor

    Wheels Bot — Triage

    Classified as framework-design (API/feature design question).
    Cross-framework research will follow shortly — Rails, Laravel, Django,
    and other modern frameworks will be consulted before any path forward
    is proposed.

    <!-- wheels-bot:triage:3155 -->
    <!-- wheels-bot:triage-class:framework-design -->

  2. wheels-bot commented on Jun 12, 2026

    @wheels-bot
    Contributor

    Wheels Bot — Cross-Framework Research

    Question

    Add a wheels generate auth CLI command that scaffolds session-based authentication over the existing vendor/wheels/auth primitives (Authenticator, SessionStrategy/TokenStrategy/JwtStrategy, AuthMiddleware) — emitting a User model, sessions + passwords controllers, views, routes, migrations, and generated specs as editable "code you own." Hard requirements: super.config() in generated controllers (#2960 CSRF), all-named args, generated specs. Maintainer design calls: registration on/off flag, --strategy= selector, generated-code upgrade policy.

    How other frameworks handle this

    Framework Naming Surface Trade-offs Edge cases
    Rails 8 generate authentication; Sessions/Passwords controllers, Current, Authentication concern, DB Session ~6 owned files + 2 migrations + routes; has_secure_password Own code, no upgrades; DB sessions => per-device logout No sign-up; reset destroys all sessions
    Laravel Breeze breeze:install; AuthenticatedSessionController, PasswordReset*, RegisteredUser Publishes controllers + routes/auth.php + views; thin controller + LoginRequest Owns/edits code; package disposable No --no-registration; reuses default User
    Django Library (no generator); LoginView/PasswordResetView, swappable AUTH_USER_MODEL contrib.auth + URL includes; customize via subclass Central CVE patches; nothing to own No registration/templates; set user model pre-migration
    Phoenix phx.gen.auth phx.gen.auth Accounts User users; UserAuth plug, User*Controller, generated tests context+schema+controllers+views+migration+specs; refuses dirty tree Gold-standard owned scaffold; reviewable diff users_tokens = 3 token kinds; pw change kills sessions
    Spring Security Beans: SecurityFilterChain, UserDetailsService, BCryptPasswordEncoder Declarative DSL (formLogin()); nothing generated Auto-patched; logic hidden in DSL Auto /login; no registration/reset
    RedwoodJS dbAuth (+1) setup auth dbAuth + generate dbAuth; DbAuthHandler, Login/Signup/Forgot/Reset pages Two-command split (server vs UI); per-flow config toggles Owned pages; crypto stays in lib Prints migration fields; --force re-run + git-diff

    +1 — RedwoodJS: modern "generate the code you own" session/cookie auth; adds a server-setup vs UI-gen split and per-flow config toggles while keeping crypto in the framework.

    Dominant pattern

    Four of six (Rails 8, Phoenix, Breeze, Redwood) converge on "scaffold editable code you own, no upgrades" and agree on the shape: a User model with a hashed-password field + authenticate(); a session resource controller (login = create a session, logout = destroy it); a separate passwords controller splitting reset into request (new/create) and confirm (edit/update); server views; a migration; an injected route block. Phoenix makes generated specs first-class — exactly Wheels' requirement. Django/Spring are the counter-model (library, central CVE patching, nothing generated). Divergences: registration (Rails omits; Phoenix/Breeze/Redwood include) and session storage (Rails/Phoenix use a DB table; others use session/cookie scope).

    Recommended path for Wheels

    Adopt the Phoenix/Rails shape — it maps ~1:1 onto existing primitives, so the generator is pure glue, not new framework infrastructure. SessionStrategy already gives login()/logout()/currentUser() with sessionRotate() fixation defense. Emit plural-PascalCase controllers (Sessions, Passwords, Registrations), a singular User model, server views, a migration, generated specs; inject routes + config/services.cfm strategy wiring. --strategy= selects the registered strategy; registration defaults on (3 of 4 include it) via --no-registration. This promotes the existing wheels generate snippets auth output (already super.config() + injection-safe builder) into a fully-wired scaffold.

    // cli/lucli/Module.cfc — new case under generate():
    //   wheels generate auth [ModelName=User] [--strategy=session|token|jwt]
    //                        [--registration | --no-registration] [--force]
    case "auth": return generateAuth(remaining);
    
    // app/controllers/Sessions.cfc — login is a "session" you create and destroy
    component extends="Controller" {
        function config() {
            super.config();                          // #2960: inherit CSRF
            verifies(only="create", params="user");  // all-named args (anti-pattern #1)
        }
        function new() {}
        function create() {                          // injection-safe builder, never a where string
            var user = model("User").where("email", params.user.email).first();
            if (IsObject(user) && user.authenticate(params.user.password)) {
                service("authenticator").getStrategy("session")
                    .login(principal = { id = user.key(), email = user.email });
                redirectTo(route="root");
            } else {
                flashInsert(error="Invalid email or password.");
                renderView(action="new");
            }
        }
        function delete() {
            service("authenticator").getStrategy("session").logout();
            redirectTo(route="login");
        }
    }
    // routes injected into config/routes.cfm:
    //   .get(name="login", to="sessions##new").post(name="signIn", to="sessions##create")
    //   .delete(name="logout", to="sessions##delete").resources(name="passwords", only="new,create,edit,update")

    Lives in: cli/lucli/Module.cfc (dispatch) + cli/lucli/services/Scaffold.cfc (orchestration); templates under cli/lucli/templates/auth/. Composes with the Authenticator strategy registry, AuthMiddleware, the chainable query builder (where().first()), and model beforeSave password hashing.

    Cross-engine notes

    The generator runs on the bundled Lucee CLI, but emitted templates run on every engine — keep them clean: super.config() (#2960), all-named args, private filters, CURRENT_TIMESTAMP (not NOW()), injection-safe where(). Two traps: (1) services.cfm must hoist any SessionStrategy(onLogin=...) closure out of the constructor call — an inline closure as a named constructor arg throws ArrayStoreException: ASTcffunction on Adobe CF and can drop a TestBox bundle (Cross-Engine Invariant #5); (2) bcrypt isn't uniformly built-in across Lucee/Adobe/BoxLang, so the generated hashing needs an all-engine approach (or a declared dependency).

    Open questions

    • Registration default — Rails omits; Phoenix/Breeze/Redwood include. Proposed: on by default, --no-registration to opt out. Confirm.
    • --strategy= semantics — wire only the selected strategy, or scaffold session + emit token/jwt commented? Token/JWT are header-based — do they get views?
    • Upgrade policy — all four scaffolders freeze generated code. Confirm a stamped header + "re-run --force and git-diff", no framework patching.
    • Session storage — CFML-session-scope now, or an optional DB sessions table (per-device logout) behind a flag?
    • Password hashing — which hash works across Lucee 5/6/7, Adobe 2018-2025, BoxLang?

    Confidence: medium

    Dominant pattern is unambiguous and maps cleanly onto existing primitives, but the medium downgrade fired: I adapted two conventions (singular-resource routing -> Wheels named routes; DB-tracked sessions -> session-scope), the +1 (Redwood's two-command split) diverges non-trivially, and registration-default is a real Rails-vs-rest split — not pristine high; the buildable core spec is clear, so not low.

    Sources

  3. wheels-bot commented on Jun 12, 2026

    @wheels-bot
    Contributor

    Wheels Bot — Fix on hold for human review

    The proposed fix touches a sensitive area (generated password/token auth code with an unresolved cross-engine hashing decision) and the bot's safety net requires a human in the loop before any code is written.

    Specifically, the research-recommended path would have the new wheels generate auth scaffold emit a User model and sessions/passwords controllers that hash and verify passwords and mint reset tokens — and those templates run on every supported engine. The research comment itself flags that the core security primitive is unresolved:

    bcrypt isn't uniformly built-in across Lucee/Adobe/BoxLang, so the generated hashing needs an all-engine approach (or a declared dependency).

    That hits two stop-triggers at once: password/token code and a cross-engine concern requiring different code paths. The research landed at confidence medium with five open maintainer design calls still outstanding — registration default, --strategy= semantics, generated-code upgrade policy, session storage (session-scope vs DB table), and the password-hashing algorithm itself. The issue body names three of these as "Design calls needing the maintainer."

    A maintainer should settle at minimum the password-hashing approach (which API is correct and uniform across Lucee 5/6/7, Adobe 2018–2025, and BoxLang) and the registration / --strategy= / upgrade-policy calls before the scaffold templates are authored. Once those are decided, this is good to proceed as a normal TDD fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementroadmapRoadmap / tracking issue grouping scheduled work

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions