Composer
While this sample application doesn't have login functionality without FusionAuth, a more typical integration will replace an existing login system with FusionAuth.
In that case, the system might look like this before FusionAuth is introduced.
Request flow during login before FusionAuth
The login flow will look like this after FusionAuth is introduced.
Request flow during login after FusionAuth
In general, you are introducing FusionAuth in order to normalize and consolidate user data. This helps make sure it is consistent and up-to-date as well as offloading your login security and functionality to FusionAuth.
Start with getting FusionAuth up and running and creating a new Laravel application.
First, grab the code from the repository and change into that folder.
git clone https://github.com/FusionAuth/fusionauth-quickstart-php-laravel-web.git
cd fusionauth-quickstart-php-laravel-web
All shell commands in this guide can be entered in a terminal in this folder. On Windows, you need to replace forward slashes with backslashes in paths.
All the files you’ll create in this guide already exist in the complete-application
subfolder, if you prefer to copy them.
You'll find a Docker Compose file (docker-compose.yml
) and an environment variables configuration file (.env
) in the root directory of the repo.
Assuming you have Docker installed, you can stand up FusionAuth on your machine with the following.
docker compose up -d
Here you are using a bootstrapping feature of FusionAuth called Kickstart. When FusionAuth comes up for the first time, it will look at the kickstart/kickstart.json
file and configure FusionAuth to your specified state.
If you ever want to reset the FusionAuth application, you need to delete the volumes created by Docker Compose by executing docker compose down -v
, then re-run docker compose up -d
.
FusionAuth will be initially configured with these settings:
E9FDB985-9173-4E01-9D73-AC2D60D1DC8E
.super-secret-secret-that-should-be-regenerated-for-production
.richard@example.com
and the password is password
.admin@example.com
and the password is password
.http://localhost:9011/
.You can log in to the FusionAuth admin UI and look around if you want to, but with Docker and Kickstart, everything will already be configured correctly.
If you want to see where the FusionAuth values came from, they can be found in the FusionAuth app. The tenant Id is found on the Tenants page. To see the Client Id and Client Secret, go to the Applications page and click the View
icon under the actions for the ChangeBank application. You'll find the Client Id and Client Secret values in the OAuth configuration
section.
The .env
file contains passwords. In a real application, always add this file to your .gitignore
file and never commit secrets to version control.
While this guide builds a new Laravel project, you can use the same method to integrate your existing project with FusionAuth.
To create a new Laravel project run the following command. There are other ways of creating the project as suggested in the Laravel documentation.
composer create-project laravel/laravel your-application && cd your-application
Composer will initialize the project and add all the starter app files in your-application
directory.
If you are not familiar with Laravel, here’s a brief explanation of its structure:
app
— contains the lowest level code: models, controllers, and middleware.bootstrap
— the starting point for execution.config
— contains all settings.database
— contains all database migration scripts.public
— contains static resources (images, CSS) available to anyone on the web.resources
— similar to public
, but contains Blade templates, and files are adapted by the server before serving.routes
— contains the server code to respond to URLs called by a browser.storage
— cache folder managed by Laravel when running.tests
— where you write your tests.vendor
— contains all Composer packages.Authentication in Laravel is managed by Socialite. For this application, you need the FusionAuth Socialite provider. To install it, run the following command.
composer require socialiteproviders/fusionauth
In your-application/.env
, insert the following lines.
FUSIONAUTH_CLIENT_ID="E9FDB985-9173-4E01-9D73-AC2D60D1DC8E"
FUSIONAUTH_CLIENT_SECRET="super-secret-secret-that-should-be-regenerated-for-production"
FUSIONAUTH_BASE_URL="http://localhost:9011"
FUSIONAUTH_REDIRECT_URL="http://localhost:8000/auth/callback"
# FUSIONAUTH_TENANT_ID=
This tells Laravel where to find and connect to FusionAuth.
This quickstart will use a SQLite database. SQLite is a small, fast, self-contained database engine, but you can use MySQL or Postgres if you have them installed on your machine.
Create a SQLite database by running the following command from your-application
directory.
touch database/database.sqlite
In the your-application/.env
file, comment out the values for the following keys as shown here.
# DB_CONNECTION=mysql
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
Add the following key above the keys you just commented out.
DB_CONNECTION=sqlite
This enables Laravel to connect to the SQLite database.
In the file your-application/config/services.php
, add one more item to the array being returned with the code below.
'fusionauth' => [
'client_id' => env('FUSIONAUTH_CLIENT_ID'),
'client_secret' => env('FUSIONAUTH_CLIENT_SECRET'),
'base_url' => env('FUSIONAUTH_BASE_URL'),
'redirect' => env('FUSIONAUTH_REDIRECT_URL'),
'tenant_id' => env('FUSIONAUTH_TENANT_ID'),
],
Now the configuration files have all the settings you need. Next, you’ll change your provider to be aware of FusionAuth.
In the file your-application/app/Providers/EventServiceProvider.php
, replace the $listen
array with the code below.
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
\SocialiteProviders\Manager\SocialiteWasCalled::class => [
\SocialiteProviders\FusionAuth\FusionAuthExtendSocialite::class . '@handle',
],
];
Finally, update your User model to have FusionAuth fields.
In the file your-application/app/Models/User.php
, add the FusionAuth items to the $fillable
and $hidden
properties with the code below.
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
'fusionauth_id',
'fusionauth_access_token',
'fusionauth_refresh_token',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
'fusionauth_id',
'fusionauth_access_token',
'fusionauth_refresh_token',
];
Every model in Laravel uses the database, so you need to update the database table to match your new model.
Create a file named your-application/database/migrations/2023_11_03_112258_add_fusionauth_fields_user_table.php
or run the command below to create it.
Running the command below will create the file with a slightly different name depending on the date you are running the command.
php artisan make:migration add_fusionauth_fields_user_table
The result is the same whether you create the database scripts manually or with this command, so you can use your preferred method.
Add the code below to the file.
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('fusionauth_id', 36)->unique();
$table->text('fusionauth_access_token');
$table->text('fusionauth_refresh_token')->nullable();
$table->string('password')->nullable()->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('fusionauth_id');
$table->dropColumn('fusionauth_access_token');
$table->dropColumn('fusionauth_refresh_token');
$table->string('password')->nullable(false)->change();
});
}
};
Install the following package required to allow manipulation of columns.
composer require doctrine/dbal
Run the command below to update your database with the new User definition.
php artisan migrate
Now that authentication is done, the last task is to create example pages that a user can access only when logged in.
Copy over some CSS and images from the example app.
cp ../complete-application/public/changebank.css public/changebank.css
cp ../complete-application/public/money.jpg public/money.jpg
cp ../complete-application/public/changebank.svg public/changebank.svg
Next, create an index.blade.php
file.
touch resources/views/index.blade.php
Paste the following code into the file.
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>FusionAuth OpenID and PKCE example</title>
<link rel="stylesheet" href="changebank.css" />
</head>
<body>
<div id="page-container">
<div id="page-header">
<div id="logo-header">
<img
src="changebank.svg"
/>
<a class="button-lg" href="/login">Login</a>
</div>
<div id="menu-bar" class="menu-bar">
<a class="menu-link">About</a>
<a class="menu-link">Services</a>
<a class="menu-link">Products</a>
<a class="menu-link" style="text-decoration-line: underline">Home</a>
</div>
</div>
<div style="flex: 1">
<div class="column-container">
<div class="content-container">
<div style="margin-bottom: 100px">
<h1>Welcome to Changebank</h1>
<p>
To get started,
<a href="/login">log in or create a new account</a>.
</p>
</div>
</div>
<div style="flex: 0">
<img src="money.jpg" style="max-width: 800px" />
</div>
</div>
</div>
</div>
</body>
</html>
The index page contains nothing to note except a link to the login page <a href="/login">
.
Next, create an account.blade.php
file.
touch resources/views/account.blade.php
Paste the following code into the file.
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>FusionAuth OpenID and PKCE example</title>
<link rel="stylesheet" href="changebank.css" />
</head>
<body>
<div id="page-container">
<div id="page-header">
<div id="logo-header">
<img src="changebank.svg" />
<div class="h-row">
<p class="header-email">{{$email}}</p>
<a class="button-lg" href="/logout"> Logout </a>
</div>
</div>
<div id="menu-bar" class="menu-bar">
<a class="menu-link inactive" href="/change">Make Change</a>
<a class="menu-link" href="/account">Account</a>
</div>
</div>
<div style="flex: 1;">
<!-- Application page -->
<div class="column-container">
<div class="app-container">
<h3>Your balance</h3>
<div class="balance">$0.00</div>
</div>
</div>
</div>
</div>
</body>
</html>
The account page displays the user’s email from FusionAuth, as you’ll see later when you create the route <p class="header-email">{{$email}}</p>
.
Next, create a change.blade.php
file.
touch resources/views/change.blade.php
Paste the following code into the file.
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>FusionAuth OpenID and PKCE example</title>
<link rel="stylesheet" href="changebank.css" />
</head>
<body>
<div id="page-container">
<div id="page-header">
<div id="logo-header">
<img src="changebank.svg" />
<div class="h-row">
<p class="header-email">{{$email}}</p>
<a class="button-lg" href="/logout"> Logout </a>
</div>
</div>
<div id="menu-bar" class="menu-bar">
<a class="menu-link" href="/change">Make Change</a>
<a class="menu-link inactive" href="/account">Account</a>
</div>
</div>
<div style="flex: 1;">
<div class="column-container">
<div class="app-container change-container">
<h3>We Make Change</h3>
@if ($state['error'] && $state['hasChange'])
<div class="error-message">Please enter a dollar amount</div>
@endif
@if (!$state['hasChange'])
<div class="error-message"></div>
@endif
@if (!$state['error'] && $state['hasChange'])
<div class="change-message">
We can make change for {{ $state['total'] }} with {{ $state['nickels'] }} nickels and {{ $state['pennies'] }} pennies!
</div>
@endif
<form method="post" action="/change">
@csrf
<div class="h-row">
<div class="change-label">Amount in USD: $</div>
<input class="change-input" name="amount" value="0.00" />
<input class="change-submit" type="submit" value="Make Change" />
</div>
</form>
</div>
</div>
</div>
</div>
</body>
</html>
The change page uses some Blade conditional statements to display different div
s depending on the state returned by the form POST @if ($state['error'] && $state['hasChange'])
.
The change page also includes a @csrf
anti-forgery token in the form at the bottom to prevent Laravel throwing page expired errors.
In the file your-application/routes/web.php
, overwrite everything with the code below.
<?php
use Illuminate\Support\Facades\Route;
use Illuminate\Http\Request;
use Laravel\Socialite\Facades\Socialite;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
Route::get('/', function () {
if (auth()->check())
return redirect('/account');
return view('index');
});
Route::get('/login', function () {
return Socialite::driver('fusionauth')->redirect();
})->name('login');
Route::get('/auth/callback', function () {
/** @var \SocialiteProviders\Manager\OAuth2\User $user */
$user = Socialite::driver('fusionauth')->user();
$user = User::updateOrCreate([
'fusionauth_id' => $user->id,
], [
'name' => $user->name,
'email' => $user->email,
'fusionauth_access_token' => $user->token,
'fusionauth_refresh_token' => $user->refreshToken,
]);
Auth::login($user);
return redirect('/account');
});
Route::get('/logout', function () {
Auth::logout();
return redirect('/');
});
Route::get('/account', function () {
return view('account', ['email' => Auth::user()->email]);
})->middleware('auth');
Route::get('/change', function () {
$state = [
'error' => false,
'hasChange' => false,
'total' => '',
'nickels' => '',
'pennies' => '',
];
return view('change', ['state' => $state, 'email' => Auth::user()->email]);
})->middleware('auth');
Route::post('/change', function (Request $request) {
$amount = $request->input('amount');
$state = [
'error' => false,
'hasChange' => true,
'total' => '',
'nickels' => '',
'pennies' => '',
];
$total = floor(floatval($amount) * 100) / 100;
$state['total'] = is_nan($total) ? '' : number_format($total, 2);
$nickels = floor($total / 0.05);
$state['nickels'] = number_format($nickels);
$pennies = ($total - (0.05 * $nickels)) / 0.01;
$state['pennies'] = ceil(floor($pennies * 100) / 100);
$state['error'] = !preg_match('/^(\d+(\.\d*)?|\.\d+)$/', $amount);
return view('change', ['state' => $state, 'email' => Auth::user()->email]);
})->middleware('auth');
The homepage route /
checks whether you are logged in and redirects you to your account page if you are.
After login, FusionAuth redirects you to the /auth/callback
route. Use Socialite to update the user model in the database with $user = User::updateOrCreate([
, then start a session for the user with Auth::login($user);
.
Logout ends the session with Auth::logout();
.
The account and change routes both include the user’s email in the page template data, return view('account', ['email' => Auth::user()->email]);
. Both routes use middleware to prevent users without a session from seeing the page, })->middleware('auth');
.
Finally, the change route has both a GET and a POST version. Both versions return the same view, but the POST route does a calculation and includes more template data.
From the your-application
directory, run the following command.
php artisan serve
Browse to the app at http://localhost:8000. Log in using richard@example.com
and password
. The change page will allow you to enter a number. Log out and verify that you can’t browse to the account page.
This quickstart is a great way to get a proof of concept up and running quickly, but to run your application in production, there are some things you're going to want to do.
FusionAuth gives you the ability to customize just about everything to do with the user's experience and the integration of your application. This includes:
Ensure FusionAuth is running in the Docker container. You should be able to log in as the admin user admin@example.com
with a password of password
at http://localhost:9011/admin.
Browse to the homepage, log out, and try to log in again. If that still doesn’t work, delete and restart all the containers.
You can always pull down a complete running application and compare what’s different.
git clone https://github.com/FusionAuth/fusionauth-quickstart-php-laravel-web.git
cd fusionauth-quickstart-php-laravel-web
docker compose up -d
cd complete-application
composer install
touch database/database.sqlite
php artisan migrate
php artisan serve
Browse to the app at http://localhost:8000.