Authentication
Reshape's authentication is passwordless by default. Instead of asking users to create and remember a password, they sign in with a one-time code emailed to them. Passwords still exist under the hood (Fortify manages them, and they back two-factor confirmation), but nothing in the default UI asks a new user to set one.

Signing in with a one-time code
Codes are generated and verified by spatie/laravel-one-time-passwords, attached to User via the HasOneTimePasswords trait:
// Email a fresh one-time code to the user...
$user->sendOneTimePassword();
// Verify a submitted code, logging nobody in automatically...
$user->attemptLoginUsingOneTimePassword(string $code) : Spatie\OneTimePasswords\Actions\OneTimePasswordCheckResultIn local development with MAIL_MAILER=log, the emailed code shows up in your terminal via php artisan pail — see Development.
Registration
Registering follows the same request-then-verify shape as login, but creates the user first with a Pending status. Verifying the code both confirms the email address and activates the account in one step — there's no separate "verify email" link to build a page for. A code can be resent, independently throttled from the initial request.
Social login
Reshape uses Laravel Socialite for Google, GitHub, and LinkedIn, configured in config/oauth.php (slug, label, active, pkce).
A single callback handles three different situations, inferred entirely from server-side state rather than anything the client claims:
// - authenticated user -> link the provider to their account
// - guest, connection exists -> log in the connection's owner
// - guest, no connection -> register a new accountTo enable a provider, add its client credentials to .env and flip the matching VITE_OAUTH_* flag on the frontend — see Installation.
Actions (app/Actions/Auth/OAuth/)
// Log in the owner of an existing OAuth connection...
OauthLogin::handle(OauthConnection $connection, SocialiteUser $socialiteUser) : App\Models\User
// Register a brand new account from a provider's profile...
OauthRegister::handle(string $provider, SocialiteUser $socialiteUser) : App\Models\User
// Attach a provider to the currently authenticated user...
LinkOauthProvider::handle(User $user, string $provider, SocialiteUser $socialiteUser) : App\Models\User
// Create or refresh the stored connection for a user + provider...
UpsertOauthConnection::handle(User $user, string $provider, SocialiteUser $socialiteUser) : App\Models\OauthConnectionOauthLogin and LinkOauthProvider both delegate to UpsertOauthConnection to keep the stored provider id and token fresh.
Two-factor authentication
2FA is a Fortify feature (Features::twoFactorAuthentication), with confirmation required both to enable it and to re-enter it after a period. User uses Laravel\Fortify\TwoFactorAuthenticatable, which adds an encrypted secret and a set of recovery codes. Since Fortify's own views are disabled ('views' => false in config/fortify.php), the frontend talks to Fortify's JSON endpoints directly and builds its own screens — see web/src/routes/_auth/two-factor-challenge.
Passkeys
Passkeys (WebAuthn) are also a Fortify feature:
Features::canManagePasskeys() : boolThe frontend uses @laravel/passkeys to drive the browser's WebAuthn ceremony. The relying party ID is derived from FRONTEND_URL, so passkeys registered against reshape.test will not work if you later change that domain.
Sessions
Users can see every active session and revoke individual ones or all of them at once.
Action
// Delete one session by id, or every session, for a user...
App\Actions\Sessions\DeleteSessions::delete(User $user, ?string $sessionId = null) : voidHow the frontend fits together
All of the auth screens live under web/src/routes/_auth/* — login (and login/code), register, two-factor-challenge, plus the OAuth callback handler at routes/oauth/callback/$provider. The current user and their active tenant are exposed through the useAuth() composable.
Sanctum, not tokens
The frontend authenticates over a stateful Sanctum session, not API tokens. As long as FRONTEND_URL and Sanctum's stateful domains are configured correctly (see Installation), the browser's session cookie is all that's needed — there's nothing to store in local storage.