Authentication and users
laravel-auth 6.1 handles authentication: it answers JSON under /auth, in the web group. The SPA screens are just the face. The user is a model generated by LaraPack, and ADMIN_EMAILS decides who administers.
The session
The SPA uses no tokens: it signs in with Sanctum's session cookie.
bootstrap/app.phpcallsstatefulApi().- axios sends
withCredentialsandwithXSRFToken(since axios 1.6,X-XSRF-TOKENis not sent without it). - Before a POST that regenerates the session (log in, register), it requests
GET /sanctum/csrf-cookie. - A 419 (expired CSRF token) fetches a new cookie and retries the request once.
- A 401 outside the session load clears the session and goes to
/auth/login?redirect=<route>.
At boot the interface requests auth.get.auth and stores:
{
"user": { "id": 1, "name": "Ana", "email": "ana@example.com", "payload": { "avatar": "/lu/upload/…" } },
"authenticated": true,
"is_admin": true,
"verified": true,
"impersonating": false
}| Field | Where it comes from |
|---|---|
authenticated | There is a user under the Sanctum guard |
is_admin | $user->isAdmin() |
verified | true if the user does not implement MustVerifyEmail or has verified their email |
impersonating | The session has impersonate_token |
APP_URL and the session domains
If the cookie does not arrive, login answers fine but get-auth says there is no session. Check the common problems in Install.
The flows
Log in
/auth/login (guests only).
- GET the CSRF cookie.
- POST
auth.loginwith{ email, password, remember }. load()the session.- Navigate to
?redirect=if it is an internal path (starts with/, not//or/\). Otherwise to/admin.
A 422 shows under each field. A 429 says "Too many attempts". In Vue, if the POST succeeded but the session did not stick, it says "You signed in, but the session was not kept. Check the session and Sanctum domains.". In React, the /admin guard sends the user back to login.
Register
/auth/register (guests only). It sends { name, email, password, password_confirmation } to auth.register after fetching the CSRF cookie, and loads the session. Then it navigates to ?redirect= or to /admin.
- With
allow-registrationset tofalseinconfig/laravel-auth.php, laravel-auth answers 403 and the screen says "New accounts are not being accepted right now.". - The password rules are in that config's
passwordkey:length(8),uppercase(false) andnumber(false).
Log out
POST auth.logout. The local session is cleared even if the request fails.
Password reset
/auth/forgot-passwordsends{ email }toauth.forgot.password. laravel-auth answers the same whether or not the account exists, and the screen shows that message.- The email links to laravel-auth's
frontend.reset-password:auth/reset-password/{token}/{email}, with the email URL-encoded. That is the SPA route/auth/reset-password/:token/:email. - That screen sends
{ token, email, password, password_confirmation }toauth.reset.passwordand goes to login with a toast.
In Vue the email comes from the URL and is read-only. In React it can be edited. React also fetches the CSRF cookie before both POSTs.
Email verification
- With a session and
verified === false, the admin panel shows a notice with "Resend the email", which callsauth.email.verification.notification(at most 6 per minute). - If it answers
status: 'already-verified', the interface reloads the session. - The email link goes to laravel-auth's route
auth/email/verify/{id}/{hash}(signed, session required), which redirects to/admin.
Verification is not required by default
The User LaraPack generates does not implement MustVerifyEmail, so verified is always true and the notice never shows. To require verification, implement Illuminate\Contracts\Auth\MustVerifyEmail in app/Models/User.php. LaraPack keeps the file when regenerating with --force, because its hash no longer matches.
Change password
From the profile: POST auth.update.password with { old_password, password, password_confirmation }. With laravel-auth 6.0.3 or later, changing it does not sign you out of the admin panel.
Impersonation
An admin can sign in as another user to see what they see.
Who can. The laravel-auth.impersonate Gate ability. By default: a user with isAdmin(), never on themselves or another admin. Turn it off entirely with allow-impersonate set to false, and redefine it in your AuthServiceProvider or with ImpersonateRequest::authorizeUsing().
The flow:
- POST
auth.impersonatewith{ target_user_id }→{ token, url }. The token encrypts who asked, for whom and when. - GET
url(auth/impersonate/{token}): signs in as the user, regenerates the session and storesimpersonate_token. If the request does not expect JSON, it redirects to/admin. The token is valid for two minutes and only for whoever asked for it. get-authanswersimpersonating: trueand the admin panel shows "You are viewing the account of<name>" with "Back to my account".- "Back to my account" does a POST to
auth.revert.impersonateand reloads the session. Vue goes to/admin, or to login if the session was closed; React goes to/admin. - An impersonation lasts at most two hours. After that, reverting signs out instead of restoring the original account.
Reverting is a POST since laravel-auth 6.1
As a GET, another site could end an impersonation with an <img> and no CSRF token. laravel-auth 6.1.0 only accepts POST, and laravel-setup 7.0.1 uses it in both interfaces. That is why the application requires innoboxrr/laravel-auth ^6.1.0: with 6.0 that POST would answer 405.
There is no button to start
The interfaces only include the notice and "Back to my account". To start an impersonation, call the endpoint yourself, for example from an action in the users table:
import { apiUrl, http } from '@app/http.js'
const { data } = await http.post(apiUrl('auth.impersonate'), { target_user_id: 42 })
// Full navigation: laravel-auth signs in as the user and redirects to /admin.
window.location.assign(data.url)import axios from 'axios'
import route from 'innoboxrr-route-resolver'
const { data } = await axios.post(route('auth.impersonate'), { target_user_id: 42 })
// Full navigation: laravel-auth signs in as the user and redirects to /admin.
window.location.assign(data.url)Who administers: ADMIN_EMAILS
.env ADMIN_EMAILS=ana@example.com,luis@example.com
↓
config/auth.php 'admins' => array_values(array_filter(array_map('trim', explode(',', (string) env('ADMIN_EMAILS', '')))))
↓
User::isAdmin() compares the email, case-insensitivelyThe isAdmin() LaraPack generates with authenticatable:
public function isAdmin(): bool
{
$admins = array_map('strtolower', (array) config('auth.admins', []));
return in_array(strtolower((string) $this->email), $admins, true);
}Who reads it:
| Where | What it decides |
|---|---|
auth.get.auth → is_admin | The "Administration" group, adminOnly and /admin/site in the interface |
before() in the policies LaraPack generates | An admin passes everything except forceDelete |
The base application's UserPolicy | The same |
| laravel-options policy | Only an admin writes options (the site) |
admin middleware (EnsureUserIsAdmin) | /env-editor |
viewLogViewer gate | /log-viewer |
laravel-auth.impersonate gate | Who can impersonate |
- With the configuration cached (
php artisan config:cache), a change toADMIN_EMAILSdoes not show untilphp artisan config:clear, or until you cache it again. - The interface reads
is_adminat boot: reload the page. ADMIN_EMAILSis a starting point. For roles, changeisAdmin()inapp/Models/User.php: everything in the table keeps working.
The generated user
laraimport.json, as app:setup leaves it:
{
"$schema": "vendor/innoboxrr/larapack-generator/schema/laraimport.schema.json",
"models": [
{
"name": "User",
"authenticatable": true,
"metas": true,
"editable_metas": ["avatar"],
"routes": { "except": ["create"] },
"props": [
{ "name": "name", "type": "string", "datatable": true, "form": true, "form_component": "TextInputComponent", "form_submit": true },
{ "name": "email", "type": "string", "datatable": true, "form": true, "form_component": "TextInputComponent", "form_submit": true },
{ "name": "email_verified_at", "type": "timestamp", "nullable": true, "fillable": false, "creatable": false, "updatable": false, "datatable": true },
{ "name": "password", "type": "string", "updatable": false, "exports_cols": false }
],
"requests": [
{
"name": "Update",
"rules": {
"user_id": "required|numeric",
"name": "sometimes|required|string|max:255",
"email": "sometimes|required|email|max:255"
}
}
]
}
]
}| Declaration | What it generates |
|---|---|
authenticatable: true | The model extends Illuminate\Foundation\Auth\User with Notifiable and HasApiTokens; hides password and remember_token; casts email_verified_at to a date and password to hashed; and adds isAdmin(). |
metas: true, editable_metas: ["avatar"] | The user's metas table, its UserMeta model, the metas() relation and the copy in payload. The form only writes the avatar meta. |
routes.except: ["create"] | No create endpoint or form: accounts are born at registration. Index, show, update, delete, restore, forceDelete, export and the bulk actions remain. |
requests | The update rules: the profile sends name and email separately from avatar, hence the sometimes. |
- The API lives in
routes/api/models/withapi.app.user.*names. - The interface module has the
AdminUsersroute, which is inadminOnly. - Laravel creates the
userstable, so LaraPack does not alter it. The base application's0001_01_01_000010_add_payload_and_soft_deletes_to_users_table.phpmigration addspayloadanddeleted_at.
How to declare and regenerate a model is in The contract: laraimport.json.
The base application's UserPolicy
The policy LaraPack generates is closed by default: only the admin passes. The profile needs each person to view and edit their own account, so app:setup copies this policy over it:
| Ability | Admin | The user themselves | Another user |
|---|---|---|---|
index, viewAny | Yes | No | No |
view | Yes | Yes | No |
update | Yes | Yes | No |
create | Yes | No | No |
delete, restore, export | Yes | No | No |
forceDelete | No | No | No |
forceDelete is in before()'s $exceptAbilities: permanent deletion is off by default, even for admins. LaraPack keeps the file when regenerating with --force, because its hash does not match the manifest's, and larapack:verify reports it as customised.
The profile
/admin/profile, for any signed-in user.
| Card | Request |
|---|---|
| Account (name and email) | PUT api.app.user.update with { user_id, name, email }, then reloads the session |
| Photo: upload | POST lu.upload.file (multipart with file and visibility=public); then PUT api.app.user.update with { user_id, avatar: <uri> } |
| Photo: remove | PUT api.app.user.update with { user_id, avatar: '' }. An empty meta is deleted. |
| Password | POST auth.update.password with { old_password, password, password_confirmation } |
- What is stored. The upload's relative
uri(the publiclu.upload.displayroute), not the absoluteurl: it keeps working if the domain changes. - How it is read. As
payload.avatar, whetherpayloadarrives as an object or as JSON text. Without a photo, or if it fails to load in React, the initials show. - Which files. Images only (
accept="image/*", checked before uploading). Size and type limits are laravel-uploads', which does not allow SVG. The disk isLARAVEL_UPLOADS_DISK, which the base application sets topublic. - Why not
AvatarInputComponent. In Vue the upload goes through axios because that component usesfetchwithout the XSRF header, and the laravel-uploads route requires it with the session. - Where the code is. In Vue, the calls are in
admin/ProfileView.vue. In React, in the session store:updateProfile,updateAvatarandremoveAvatar.
Social login
Not wired in the interfaces
app:setup writes VITE_GOOGLE_LOGIN, VITE_FACEBOOK_LOGIN and VITE_MICROSOFT_LOGIN, but neither the Vue nor the React interface reads any VITE_* variable, and no screen has provider buttons.
laravel-auth does include the Socialite routes: auth.socialite.redirect (auth/social/{provider}/redirect) and auth.socialite.callback, which redirects to /admin. Providers are configured in config/services.php. Wiring it means adding the buttons to the authentication screens yourself; see laravel-auth.