Appearance
Authentication
PODCAST
- Laravel makes implementing authentication very simple as almost everything is configured for you out of the box
- We already installed the Laravel Jetstream starterkit which comes with a full authentication scaffolding ready to use
- The authentication configuration file is located at config/auth.php and contains several well documented options for tweaking the behavior of the authentication services
Authentication Quickstart
Routing
- Laravel provides a quick way to list all vendor routes using one simple command:
php artisan route:list --only-vendor
- The routes that we need for our navigation are:
Method | URI | Route name | Name in our navigation |
---|---|---|---|
GET | login | login | Login |
GET | register | register | Register |
POST | logout | logout | Logout |
GET | dashboard | dashboard | Dashboard |
GET | user/profile | profile.show | Update Profile |
- Only the login and register routes are already defined in our navigation component
Public views
- Jetstream added 7 (unprotected) view files to the resources/views/auth folder
(login, register, verify-email, forgot-password, reset-password, confirm-password and two-factor-challenge) - All these views are based on the
<x-guest-layout>
layout and have no navigation
Test the full auth cycle
Login
- Click on Login and use one of the users we created earlier
Forgot password
- Logout again and click on Login
- Click on Forgot your password?
- Check your email and click on the link in the email
REMARK
- First, check the mail configuration in the .env file
- Check if Mailpit is running on http://localhost:8025/, if not, open a new terminal window and run the command
mailpit
- Enter the email address of one of the users, e.g. john.doe@example.com and click on Email Password Reset Link
- A message is shown that an email has been sent
Register
- Click on Logout
- Click in the navigation on Register
- Register a new user, e.g. demo_vinylshop@mailinator.com with password demo1234
(The password must be at least 8 characters and must be entered twice ) - The user will be created in the database (open phpMyAdmin and check the table users) and is immediately logged in
REMARK
In the authentication cycle illustrated above only one error occurred. Obviously, your Laravel application will also react "correctly" in the following scenarios:
- Login with an unknown email or with a wrong password
- When registering or resetting the password, the entered password is too short or the two passwords are not equal
- A password reset link is requested for a non-existing user
- ...
Update the public views
- Let's do some customization of the auth views
- Replace the Jetstream logo with our own logo
- Change the layout to our own layout
Replace the Jetstream logo with our own logo
- Open one of the auth views, e.g. resources/views/auth/forgot-password.blade.php
Crtl + Click
on<x-authentication-card-logo />
to open the file
resources/views/components/authentication-card-logo.blade.php- Comment out the original SVG logo and place the code for our own logo above it
php
<a href="/">
<x-tmk.logo class="size-20 fill-current text-gray-500 animate-spin" />
{{-- <svg class="w-16 h-16" viewbox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"> ... </svg> --}}
</a>
1
2
3
4
2
3
4
- Log out and go to the forgot-password page to see the result
Update the guest layout
- Open the resources/views/layouts/guest.blade.php file
- Open the resources/views/components/layout/authentication-card.blade.php file
- Comment out the original code and place the code for our vinylshop layout above it
php
<x-vinylshop-layout>
@php
$slug = request()->segment(count(request()->segments())); // Get the last segment of the URL
$slug = str($slug)->replace('-', ' ')->title(); // Replace hyphens with spaces and capitalize each word
@endphp
<x-slot name="title">{{ $slug }}</x-slot>
{{ $slot }}
</x-vinylshop-layout>
{{--<!DOCTYPE html> ... </html> --}}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
Update the app layout
- The dashboard and profile pages are based on the
<x-app-layout>
layout component - Whe can use the same approach as with the guest layout to update the app layout
- Open the resources/views/layouts/app.blade.php file
- Comment out the original code and place the code for our vinylshop layout above it
- This time we keep the default title and add set the subtitle to an empty string
php
<x-vinylshop-layout>
<x-slot name="subtitle"></x-slot>
{{ $slot }}
</x-vinylshop-layout>
{{--<!DOCTYPE html> ... </html> --}}
1
2
3
4
5
6
2
3
4
5
6
Logout
- When you look at the Logout link in the navigation component, you will see that it is a link to the
under-construction
route - Let's change this to a link to the
logout
route
php
<x-slot name="content">
{{-- all users --}}
...
<div class="border-t border-gray-100"></div>
<x-dropdown-link href="{{ route('logout') }}">Logout</x-dropdown-link>
<div class="border-t border-gray-100"></div>
...
</x-slot>
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
- This will not work because the
logout
route is a POST route (not a GET route like the other routes) and we can't use a POST route in a link - We need to use a form to submit the POST request
- Let's try to replace the link with a form that submits the POST request to the
logout
route
(The classes on the submit button are copied from thex-dropdown-link
component)
php
<x-slot name="content">
{{-- all users --}}
...
<div class="border-t border-gray-100"></div>
<form method="POST" action="{{ route('logout') }}">
<button type="submit" class="block w-full text-left px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition">Logout</button>
</form>
<div class="border-t border-gray-100"></div>
...
</x-slot>
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
CSRF protection
- If you test/submit the form, you get a 419 | Page Expired error, because Laravel automatically protects your application from cross-site request forgery (CSRF) attacks
- Laravel generates a "token" for each active user session
- When you submit a form (with a POST request), Laravel checks whether this token corresponds to the mandatory token of the form
- Add a CSRF token to the form using the
@csrf
Blade directive, which injects a hidden field (check the source code in the browser) - Now the form will work
php
<x-slot name="content">
{{-- all users --}}
...
<div class="border-t border-gray-100"></div>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="block w-full text-left px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition">Logout</button>
</form>
<div class="border-t border-gray-100"></div>
...
</x-slot>
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
Retrieving the authenticated user
- You may access the authenticated user via the
auth()
helper function or via theAuth
facade- For example, the code below can be added to a controller of choice (you don't need to do this -> we will use/illustrate this further on in our navigation component and our admin middleware)
php
// Get the currently authenticated user
$user = auth()->user(); // or Auth::user();
// Get one attribute off the currently authenticated user (e.g. name, email, id, ...)
$name = auth()->user()->name; // or Auth::user()->name;
// Shortcut for the currently authenticated user's id
$id = auth()->id(); // or Auth::id();
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
Authentication directives and helpers inside Blade
- The
@auth
and@guest
Blade directives may be used to quickly determine if the current user is authenticated or is a guest - You can also use the
auth()
helper function orAuth
facade (see above) inside a Blade template
php
@guest
// only visible for guests and NOT for authenticated users (= not logged in)
@endguest
@auth
// only visible for authenticated users and NOT for guests (= logged in)
@endauth
1
2
3
4
5
6
7
2
3
4
5
6
7
Update navigation component
- Open the resources/views/components/layout/nav.blade.php file and add the
@auth
and@guest
directives- If the user is not logged in, the navigation component should:
- show the Login and Register links
- hide the dropdown menu on te right
- If the user is logged in, the navigation component should:
- show the dropdown menu on the right
- hide the Login and Register links
- If the user is not logged in, the navigation component should:
php
{{-- right navigation --}}
<div class="relative flex items-center space-x-2">
@guest
<x-nav-link href="{{ route('login') }}" :active="request()->routeIs('login')">
Login
</x-nav-link>
<x-nav-link href="{{ route('register') }}" :active="request()->routeIs('register')">
Register
</x-nav-link>
@endguest
{{-- shopping cart --}}
<x-nav-link href="{{ route('under-construction') }}" :active="request()->routeIs('under-construction')">
<x-fas-shopping-basket class="w-4 h-4"/>
</x-nav-link>
{{-- dropdown navigation--}}
@auth
<x-dropdown align="right" width="48">
{{-- avatar --}}
<x-slot name="trigger"> ... </x-slot>
<x-slot name="content">
{{-- all users --}}
...
{{-- admins only --}}
...
</x-slot>
</x-dropdown>
@endauth
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Update the navigation component
- Open the resources/views/components/layout/nav.blade.php file:
- Line 6: replace
Vinyl+Shop
at the end of thesrc
attribute with{{urlencode(auth()->user()->name)}}
- the
urlencode() PHP function
converts the user's name to a slug (e.g.John Doe
->john+doe
) - this encoded string will be used to generate an avatar image, based on the initials of the user's name
- the
- Line 7: replace the
alt
attribute withauth()->user()->name
- Line 11: replace
My Name
withauth()->user()->name
php
@auth
<x-dropdown align="right" width="48">
{{-- avatar --}}
<x-slot name="trigger">
<img class="rounded-full h-8 w-8 cursor-pointer"
src="https://ui-avatars.com/api/?name={{ urlencode(auth()->user()->name) }}"
alt="{{ auth()->user()->name }}">
</x-slot>
<x-slot name="content">
{{-- all users --}}
<div class="block px-4 py-2 text-xs text-gray-400">{{ auth()->user()->name }}</div>
<x-dropdown-link href="{{ route('dashboard') }}">Dashboard</x-dropdown-link>
<x-dropdown-link href="{{ route('profile.show') }}">Update Profile</x-dropdown-link>
...
</x-slot>
</x-dropdown>
@endauth
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Protecting Routes with Middleware
- Middleware can be used to allow only the authenticated users access to a given route
- Laravel ships with built in auth middleware
- Since this middleware is already registered in your HTTP kernel, all you need to do is attach the middleware to a route or route group definition
- Open routes/web.php
- Protect the 'admin' route group with the auth middleware
middleware(['auth'])
, such that these routes are only available to authenticated users
php
Route::middleware(['auth'])->prefix('admin')->name('admin.')->group(function () {
Route::redirect('/', '/admin/records');
Route::get('records', Demo::class)->name('records.index');
});
1
2
3
4
2
3
4
- Check the URL http://vinyl_shop.test/admin/records
- As a guest, you are directed to the login page
- As an authenticated (logged in) user, you see the URL, regardless whether you have admin rights or not
Make admin middleware
- But, our site has normal users and admins and the records page should only be accessible by admins
- Create a new admin middleware with the command
php artisan make:middleware Admin
- Open the file app/Http/Middleware/Admin.php
- Update the
handle()
method- The
abort()
helper function sends you to an error page. The first argument determines which specific error page must be shown, and the second (optional) argument can be used to provide a specific error message.
- The
- Create a new admin middleware with the command
php
class Admin
{
public function handle($request, Closure $next): Response
{
if (auth()->user()->admin) {
return $next($request);
}
return abort(403, 'Only administrators can access this page');
}
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
NAMING CONVENTIONS
- The name of a middleware starts with a capital letter (e.g.
Admin
) as it corresponds to a class
Make active user middleware
- Additionaly, we want to ban all the auth pages from users that have a profile status of
active
isfalse
- Create a new active user middleware with the command
php artisan make:middleware ActiveUser
- Open the file app/Http/Middleware/ActiveUser.php
- Update the
handle()
method
- Create a new active user middleware with the command
php
class ActiveUser
{
public function handle(Request $request, Closure $next): Response
{
if (auth()->user()->active) {
return $next($request);
}
// logout the user from the web guard
auth('web')->logout();
// abort the request with a message
return abort(403, 'Your account is not active. Please contact the administrator.');
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
Add middleware to the route group
- Line 1: add the Admin and ActiveUser middleware, AFTER the auth middleware, to the 'admin' route group
- Line 10: add the ActiveUser middleware to already protected pages from Jetstream
- Don't forget to import the
use App\Http\Middleware\Admin;
anduse App\Http\Middleware\ActiveUser;
at the top of the file
php
Route::middleware(['auth', Admin::class, ActiveUser::class])->prefix('admin')->name('admin.')->group(function () {
Route::redirect('/', '/admin/records');
Route::get('records', Demo::class)->name('records.index');
});
Route::middleware([
'auth:sanctum',
config('jetstream.auth_session'),
'verified',
ActiveUser::class,
])->group(function () {
Route::get('/dashboard', function () {
return view('dashboard');
})->name('dashboard');
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- Log in as
jane.doe@example.com
and passworduser1234
(Jane has no admin rights) - Open http://vinyl_shop.test/admin/records
- You'll see the 403 error page (with a customized message)
Hide admin menu item for non-admins
- Open resources/views/layouts/nav.blade.php
- Add a
@if(auth()->user()->admin) ... @endif
Blade directive surround the admin menu item to hide it for non-admins
- Jane Doe is a normal user and does not have admin rights
- John Doe is a user with admin rights
php
@auth
<x-dropdown align="right" width="48">
{{-- avatar --}}
...
<x-slot name="content">
{{-- all users --}}
...
@if(auth()->user()->admin)
<div class="border-t border-gray-100"></div>
{{-- admins only --}}
<div class="block px-4 py-2 text-xs text-gray-400">Admin</div>
<x-dropdown-link href="{{ route('under-construction') }}">Genres</x-dropdown-link>
<x-dropdown-link href="{{ route('admin.records') }}">Records</x-dropdown-link>
<x-dropdown-link href="{{ route('under-construction') }}">Covers</x-dropdown-link>
<x-dropdown-link href="{{ route('under-construction') }}">Users</x-dropdown-link>
<x-dropdown-link href="{{ route('under-construction') }}">Orders</x-dropdown-link>
@endif
</x-slot>
</x-dropdown>
@endauth
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20