Skip to content

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.

The passwordless login flow

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:

php
// 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\OneTimePasswordCheckResult

In 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:

php
// - authenticated user       -> link the provider to their account
// - guest, connection exists -> log in the connection's owner
// - guest, no connection     -> register a new account

To 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/)

php
// 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\OauthConnection

OauthLogin 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:

php
Features::canManagePasskeys() : bool

The 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

php
// Delete one session by id, or every session, for a user...
App\Actions\Sessions\DeleteSessions::delete(User $user, ?string $sessionId = null) : void

How 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.