Registration
lukk ships a first-party POST /auth/register that mirrors login: it creates the user, fires Laravel's Registered event (so email verification can send its link), and returns the same token pair a login yields — so a new user is signed in immediately, exactly like logging in. It's opt-in, and you keep full control of the user's fields, validation, and creation. Without it, an app has to hand-roll a controller that calls startSession() and re-implements token emission, the 2FA / email-verification interception, and BFF/cookie handling; registration does all of that for you.
NOTE
Registration must be enabled on the server (features.registration). You own the user's shape via Lukk::registerUsing() / Lukk::registerValidation().
Server (Laravel)
How it works
Registration reuses the exact login machinery. POST /auth/register:
- Validates the input (
RegisterRequest— email + a confirmed password by default; customizable). - Creates the user — via your
Lukk::registerUsing()hook, or the default (the configured model withemail+ a hashedpassword). - Fires
Illuminate\Auth\Events\Registered, so — whenfeatures.email_verificationis on and your model isMustVerifyEmail— the framework listener sends the (lukk-signed) verification link. - Responds the same way login does:
- a token pair (auto-login), honoring BFF vs cookie mode, or
- a 2FA challenge if the new user is already enrolled (
{ two_factor: true, challenge_token }), or - a
201{ registered: true, requires_verification: true }with no session whenemail_verification.block_unverified_loginis on — the user verifies their email, then logs in.
Setup
Enable the feature:
// config/lukk.php
'features' => [
'registration' => true,
// ...
],The built-in default create works with the stock Laravel users shape out of the box — it writes name + the identifier (email by default) + a hashed password. If your table has a different shape (no name, extra columns, a username), point Lukk::registerUsing() at your own create (below); lukk doesn't presume your columns beyond the stock default.
Two settings tune the built-in flow:
// config/lukk.php
'registration' => [
'login' => true, // auto-login after registering (false → create-only, 201, sign in separately)
],
'username' => 'email', // the identifier column for login + registration (see "Username instead of email")Endpoints
Registered only when features.registration is enabled. Per-IP throttled by the lukk-register limiter (rate_limits.registration).
| Method | Path | Middleware | Purpose |
|---|---|---|---|
POST | /auth/register | throttle | Create an account. Body { name, email, password, password_confirmation } by default. |
Responses — identical to login, so a client treats a successful register exactly like a login:
200with a token pair ({ access_token, refresh_token?, expires_in };refresh_tokenonly in BFF/body mode).200with{ two_factor: true, challenge_token }if the new user is already 2FA-enrolled — complete it at/auth/two-factor-challenge.201with{ registered: true, requires_verification }and no tokens, when the account can't log in yet — eitherregistration.loginis off (sign in separately;requires_verification: false) oremail_verification.block_unverified_loginis on (verify first;requires_verification: true).422on validation failure (including a duplicate identifier).
All responses are Cache-Control: no-store.
Customizing the user's fields
Point Lukk::registerUsing() at a closure (or an invokable class-string) that creates and returns the new user. It receives the validated payload (with the plaintext password — hash it yourself) and must return a brand-new Authenticatable:
use Illuminate\Support\Facades\Hash;
use Lukk\Lukk;
Lukk::registerUsing(function (array $input) {
return User::create([
'name' => $input['name'],
'email' => $input['email'],
'password' => Hash::make($input['password']),
]);
});WARNING
The hook must create a user — never return an existing one (e.g. firstOrCreate). lukk signs in whatever you return, so handing back an existing account would be a credential-free takeover. lukk guards the common Eloquent case (it rejects a returned model that wasn't just created), but keep your hook a pure create.
Declare the matching validation rules with Lukk::registerValidation() (or rebind RegisterRequest to a subclass):
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Password;
Lukk::registerValidation(fn ($request) => [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', Rule::unique(User::class)],
'password' => ['required', 'confirmed', Password::defaults()],
// add a captcha field, a terms checkbox, etc.
]);The default rules (RegisterRequest) are name, email (a valid, unique address), and password (confirmed, meeting Password::defaults()). Tune password strength app-wide with Password::defaults().
Username instead of email
lukk.username (default email) is the identifier column, Fortify-style — it governs both login and registration. Set it once and the whole stack follows: the login/register request field, the constant-time credential lookup, the per-account login throttle, and the default create + validation all use that column.
// config/lukk.php
'username' => 'username', // or env('LUKK_USERNAME')Now POST /auth/login and POST /auth/register take username instead of email (registration validates it as a plain unique string rather than an email address). Your users table needs that column, and if you drop email entirely, make it nullable or use Lukk::registerUsing() for the create.
Register-only (no auto-login)
By default a successful register signs the user in (returns a token pair). Set registration.login to false to create the account only — the endpoint returns a 201 and the user logs in separately:
'registration' => ['login' => false],Use this when you want a deliberate "account created → now log in" step, or a manual-approval flow. (Email verification has its own gate — block_unverified_login — which also withholds the session until the address is verified.)
Customizing the response
Contracts\RegisterResponse is rebindable like the other response contracts — bind your own implementation to reshape the body or cookies:
$this->app->bind(\Lukk\Contracts\RegisterResponse::class, MyRegisterResponse::class);Security notes
- Passwords are hashed (bcrypt via
Password::defaults()), never stored or logged in plaintext;confirmedis enforced server-side, and the length is bounded (max:255) so an unauthenticated caller can't force unbounded verifier work. - Enumeration: a registration form inherently reveals whether an identifier is taken (the
uniquerule →422). lukk adds no faster oracle — same status, same shape for taken vs. new beyond that rule.
WARNING
Add a captcha before you expose a public sign-up form. The endpoint is per-IP throttled, but that alone does not stop a distributed attacker (rotating IPs, each under the cap) from mass-creating accounts and — when email verification is on — making your app email arbitrary victim addresses (a spam-relay / blacklisting risk). Add a captcha or proof-of-work as a field validated via registerValidation, and keep the throttle.
- Auto-login trusts an unverified identity. With
registration.loginon (default) and email verification off, a successful register issues a session for an unverified email/identifier — enabling account squatting (registering someone else's address first). If any authorization keys off the identifier, enable email verification withblock_unverified_loginso the session is withheld until the address is proven. - No bypass: registration runs the same 2FA and unverified-email checks as login before issuing a session — a new user can't skip a 2FA challenge or the
block_unverified_logingate.
Client (Nuxt)
The client exposes a register() that mints a session, parallel to login() — same transport (BFF or direct), same outcomes (token pair, 2FA challenge, or verify-first), so your sign-up form is as simple as your login form.
const { register } = useLukkAuth()
async function submit() {
const result = await register({
email: email.value,
password: password.value,
password_confirmation: passwordConfirmation.value,
})
if (result?.two_factor) {
// new user is 2FA-enrolled — send them to your challenge screen (same as login)
}
else {
await navigateTo('/dashboard') // signed in
}
}Because a successful register returns the same token pair as login, the session is established for you — no separate login call. When block_unverified_login is on, register() resolves without a session and the client surfaces the requires_verification signal so you can route to a "check your inbox" page; pair it with useLukkEmailVerification to resend the link.
For a form bound to Laravel's validation errors (per-field messages on the 422), use the lukk-js form helper.
Next: The User