How to Build a Blog with Laravel - Part 4 - Authentication with Fortify
At this point we've built the basic CRUD for our blog app, but now we want to add some functionality that will prevent unauthorized access to certain areas of the blog. For instance, we want to prevent visitors from creating and editing posts unless they are logged in. We will do this using Laravel Fortify, a comprehensive authorization package that is built and maintained by the Laravel core team.
If you have been following along, we already installed Laravel, setup our database, and created some controller routes. Or, if you want to skip ahead, you can clone the git repo and get right to work:
git clone https://github.com/fstrube/blog-app.git
cd blog-app
git checkout part-3
npm install
composer install
composer run dev
Install Dependencies
First things first, bust out that commandline and install Fortify.
composer require laravel/fortify
Now, we want to install Fortify's database migrations and config.
php artisan fortify:install
Before we move on, let's take a look at the changes that Fortify introduced.
git status
Fortify added a service provider in our bootstrap/providers.php
file, added some PHP classes in app/Actions
, created a configuration file located at config/fortify.php
, and a database migration in database/migrations
for setting up two-factor auth.
Go ahead and run the migrations.
php artisan migrate
Configuring Fortify
Fortify comes with some pretty great features out of the box, but of course we want to deviate slightly from their opinionated defaults. What's nice is that Fortify was built with this in mind and has a customizable configuration file and various hooks where we can tweak its functionality.
Let's take a look at the default Fortify config.
config/fortify.php
<?php
use Laravel\Fortify\Features;
return [
/*
|--------------------------------------------------------------------------
| Fortify Guard
|--------------------------------------------------------------------------
|
| Here you may specify which authentication guard Fortify will use while
| authenticating users. This value should correspond with one of your
| guards that is already present in your "auth" configuration file.
|
*/
'guard' => 'web',
/*
|--------------------------------------------------------------------------
| Fortify Password Broker
|--------------------------------------------------------------------------
|
| Here you may specify which password broker Fortify can use when a user
| is resetting their password. This configured value should match one
| of your password brokers setup in your "auth" configuration file.
|
*/
'passwords' => 'users',
/*
|--------------------------------------------------------------------------
| Username / Email
|--------------------------------------------------------------------------
|
| This value defines which model attribute should be considered as your
| application's "username" field. Typically, this might be the email
| address of the users but you are free to change this value here.
|
| Out of the box, Fortify expects forgot password and reset password
| requests to have a field named 'email'. If the application uses
| another name for the field you may define it below as needed.
|
*/
'username' => 'email',
'email' => 'email',
/*
|--------------------------------------------------------------------------
| Lowercase Usernames
|--------------------------------------------------------------------------
|
| This value defines whether usernames should be lowercased before saving
| them in the database, as some database system string fields are case
| sensitive. You may disable this for your application if necessary.
|
*/
'lowercase_usernames' => true,
/*
|--------------------------------------------------------------------------
| Home Path
|--------------------------------------------------------------------------
|
| Here you may configure the path where users will get redirected during
| authentication or password reset when the operations are successful
| and the user is authenticated. You are free to change this value.
|
*/
'home' => '/home',
/*
|--------------------------------------------------------------------------
| Fortify Routes Prefix / Subdomain
|--------------------------------------------------------------------------
|
| Here you may specify which prefix Fortify will assign to all the routes
| that it registers with the application. If necessary, you may change
| subdomain under which all of the Fortify routes will be available.
|
*/
'prefix' => '',
'domain' => null,
/*
|--------------------------------------------------------------------------
| Fortify Routes Middleware
|--------------------------------------------------------------------------
|
| Here you may specify which middleware Fortify will assign to the routes
| that it registers with the application. If necessary, you may change
| these middleware but typically this provided default is preferred.
|
*/
'middleware' => ['web'],
/*
|--------------------------------------------------------------------------
| Rate Limiting
|--------------------------------------------------------------------------
|
| By default, Fortify will throttle logins to five requests per minute for
| every email and IP address combination. However, if you would like to
| specify a custom rate limiter to call then you may specify it here.
|
*/
'limiters' => [
'login' => 'login',
'two-factor' => 'two-factor',
],
/*
|--------------------------------------------------------------------------
| Register View Routes
|--------------------------------------------------------------------------
|
| Here you may specify if the routes returning views should be disabled as
| you may not need them when building your own application. This may be
| especially true if you're writing a custom single-page application.
|
*/
'views' => true,
/*
|--------------------------------------------------------------------------
| Features
|--------------------------------------------------------------------------
|
| Some of the Fortify features are optional. You may disable the features
| by removing them from this array. You're free to only remove some of
| these features or you can even remove all of these if you need to.
|
*/
'features' => [
Features::registration(),
Features::resetPasswords(),
// Features::emailVerification(),
Features::updateProfileInformation(),
Features::updatePasswords(),
Features::twoFactorAuthentication([
'confirm' => true,
'confirmPassword' => true,
// 'window' => 0,
]),
],
];
This file is commented so well that I don't really need to explain what it is doing. We only want to make a small change. I want all of my routes prefixed with /admin
. Below is a diff of my changes to the configuration file.
diff --git a/config/fortify.php b/config/fortify.php
index cfe8272..8638deb 100644
--- a/config/fortify.php
+++ b/config/fortify.php
@@ -73,7 +73,7 @@
|
*/
- 'home' => '/home',
+ 'home' => '/admin',
/*
|--------------------------------------------------------------------------
@@ -86,7 +86,7 @@
|
*/
- 'prefix' => '',
+ 'prefix' => 'admin',
'domain' => null,
Let's take a look at the routes that Fortify configures for us.
php artisan route:list
Fortify installs around 20 routes for login, registration, and user account management.
Now, we can head on over to http://localhost:8000/admin/login and see what we see.
Oops. I get a cryptic error.
Target [Laravel\Fortify\Contracts\LoginViewResponse] is not instantiable.
Looks like we need to register a login view. Let's open up the FortifyServiceProvider.
app/Providers/FortifyServiceProvider.php
<?php
namespace App\Providers;
use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\ResetUserPassword;
use App\Actions\Fortify\UpdateUserPassword;
use App\Actions\Fortify\UpdateUserProfileInformation;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
use Laravel\Fortify\Fortify;
class FortifyServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Fortify::createUsersUsing(CreateNewUser::class);
Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
RateLimiter::for('login', function (Request $request) {
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip());
return Limit::perMinute(5)->by($throttleKey);
});
RateLimiter::for('two-factor', function (Request $request) {
return Limit::perMinute(5)->by($request->session()->get('login.id'));
});
}
}
We're going to register our Fortify responses in the boot()
function.
diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php
index 2d741e3..a1ff13f 100644
--- a/app/Providers/FortifyServiceProvider.php
+++ b/app/Providers/FortifyServiceProvider.php
@@ -33,6 +33,8 @@ public function boot(): void
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
+ Fortify::loginView('auth.login');
+
RateLimiter::for('login', function (Request $request) {
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip());
And, of course, we will also need to make our login view.
php artisan make:view auth.login
resources/views/auth/login.blade.php
<form action="{{ route('login.store') }}" method="POST">
@csrf
<div>
<label for="email">Email</label>
<input id="email" name="email">
</div>
<div>
<label for="password">Password</label>
<input id="password" name="password" type="password">
</div>
<input type="submit" value="Login">
<div>
<a href="{{ route('register') }}">Create an account.</a>
</div>
<div>
<a href="{{ route('password.request') }}">Forgot password?</a>
</div>
@foreach ($errors->all() as $error)
<div class="error">
<p>{{ $error }}</p>
</div>
@endforeach
</form>
We just created a basic login form with email and password. There are a couple links to signup and forgot password pages as well. We'll need to create the views and register them in our FortifyServiceProvider
.
php artisan make:view auth.register
php artisan make:view auth.password-request
php artisan make:view auth.password-reset
resources/views/auth/register.blade.php
<form action="{{ route('register.store') }}" method="POST">
@csrf
<div>
<label for="name">Name</label>
<input id="name" name="name" value="{{ old('name') }}">
</div>
<div>
<label for="email">Email</label>
<input id="email" name="email" value="{{ old('email') }}">
</div>
<div>
<label for="password">Password</label>
<input id="password" name="password" type="password" value="{{ old('password') }}">
</div>
<div>
<label for="password_confirmation">Confirm Password</label>
<input id="password_confirmation" name="password_confirmation" type="password" value="{{ old('password_confirmation') }}">
</div>
<input type="submit" value="Submit">
<div>
<a href="{{ route('login') }}">Back to login.</a>
</div>
@foreach ($errors->all() as $error)
<div class="error">
<p>{{ $error }}</p>
</div>
@endforeach
</form>
resources/views/auth/password-request.blade.php
<form action="{{ route('password.email') }}" method="POST">
@csrf
<div>
<label for="email">Email</label>
<input id="email" name="email" value="{{ old('email') }}">
</div>
<input type="submit" value="Submit">
<div>
<a href="{{ route('login') }}">Back to login.</a>
</div>
@foreach ($errors->all() as $error)
<div class="error">
<p>{{ $error }}</p>
</div>
@endforeach
</form>
resources/views/auth/password-reset.blade.php
<form action="{{ route('password.update') }}" method="POST">
@csrf
<input name="token" type="hidden" value="{{ request('token') }}">
<div>
<label for="email">Email</label>
<input id="email" name="email" readonly value="{{ request('email') }}">
</div>
<div>
<label for="password">Password</label>
<input id="password" name="password" type="password" value="{{ old('password') }}">
</div>
<div>
<label for="password_confirmation">Confirm Password</label>
<input id="password_confirmation" name="password_confirmation" type="password" value="{{ old('password_confirmation') }}">
</div>
<input type="submit" value="Submit">
@foreach ($errors->all() as $error)
<div class="error">
<p>{{ $error }}</p>
</div>
@endforeach
</form>
app/Providers/FortifyServiceProvider.php
The last thing we need to do is tell Fortify which views to use on each page.
diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php
index a1ff13f..a7e0c0c 100644
--- a/app/Providers/FortifyServiceProvider.php
+++ b/app/Providers/FortifyServiceProvider.php
@@ -34,6 +34,9 @@ public function boot(): void
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
Fortify::loginView('auth.login');
+ Fortify::registerView('auth.register');
+ Fortify::requestPasswordResetLinkView('auth.password-request');
+ Fortify::resetPasswordView('auth.password-reset');
RateLimiter::for('login', function (Request $request) {
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip());
Alright, time to test. Go to http://localhost:8000/admin/login. You should see the login form.
We don't have an account yet, so let's create one. Click on "Create an account."
Fill out and submit the form to create your account. You will be logged in and redirected to the home
path as configured in config/fortify.php
, which at this point doesn't exist. Here is the error that I see.
Let's create a controller and a view for our admin page.
php artisan make:controller -i Admin\\HomeController
Unlike the last time we made a controller, this time we are passing the -i
flag, which creates an invokable controller. Invokable controllers are responsible for serving a single route, unlike resource controllers, which serve many routes. This new controller will do one thing and one thing only: serve our Blade view for the admin home page.
app/Http/Controllers/Admin/HomeController.php
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class HomeController extends Controller
{
/**
* Handle the incoming request.
*/
public function __invoke(Request $request)
{
return view('admin.home');
}
}
php artisan make:view admin.home
resources/views/admin/home.blade.php
<div>
<h1>Greetings, {{ auth()->user()->name }}</h1>
<a href="{{ route('logout') }}">Logout</a>
</div>
Our home page will simply display a greeting for now. Oh, and don't forget your routes!
routes/web.php
<?php
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
Route::resource('/posts', App\Http\Controllers\PostController::class);
Route::get('/admin', App\Http\Controllers\Admin\HomeController::class);
Route::get('/admin/logout', [Laravel\Fortify\Http\Controllers\AuthenticatedSessionController::class, 'destroy']);
This is what you should see at http://localhost:8000/admin now.
Adding a logout route
The last line in that routes/web.php
file is a bit of a hack around Fortify's default behavior. Fortify has a logout route, but it only supports POST
requests for some reason. I want my logout to simply be a link, so that I don't need to submit a <form>
in order to log out of the app. Without it, we'd see the following when clicking the logout link on our admin page.
The GET method is not supported for route admin/logout. Supported methods: POST.
Re-routing our PostController
Now that we have a password-protected admin section, it makes sense to move around some of the resource routes on our PostController
. We're going to change our /posts
resource to only route the index
and show
actions. Then we will add an /admin/posts
resource routing to everything else and put them behind the auth
middleware.
routes/web.php
<?php
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
Route::resource('/posts', App\Http\Controllers\PostController::class)
->only('index', 'show');
Route::resource('/admin/posts', App\Http\Controllers\PostController::class)
->except('index', 'show')
->middleware('auth');
Route::get('/admin', App\Http\Controllers\Admin\HomeController::class);
Route::get('/admin/logout', [Laravel\Fortify\Http\Controllers\AuthenticatedSessionController::class, 'destroy']);
This is our first time talking about middleware. Middleware provides a mechanism to hook into the Laravel request lifecycle in order to redirect or manipulate the response to a request. The auth
middleware in this case will intercept the request and redirect to the login page if the visitor is unauthenticated.
Navigate around for a bit to test things out. Can you get to the /posts page? What happens when you click the "New Post" link? It should redirect you to the login page. Once logged in, you can create and edit posts as yourself.
As always, don't forget to git
.
git add .
git commit -m 'laravel fortify'
Coming up next
Okay, so let's be honest, our blog is not very nice looking. In the next part of this series I will take you on a journey, transforming this bare-bones blog into a frontend masterpiece, so stay tuned!