web

Laravel

Laravel

In this quickstart, you are going to build an application with Laravel and integrate it with FusionAuth. You’ll be building it for ChangeBank, a global leader in converting dollars into coins. It’ll have areas reserved for users who have logged in as well as public facing sections.

The Docker Compose file and source code for a complete application are available at https://github.com/FusionAuth/fusionauth-quickstart-php-laravel-web.

Prerequisites

For this Quickstart, you’ll need:

General Architecture

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.

UserApplicationView HomepageClick Login LinkShow Login FormFill Out and Submit Login FormAuthenticates UserDisplay User's Account or OtherInfoUserApplication

Request flow during login before FusionAuth

The login flow will look like this after FusionAuth is introduced.

UserApplicationFusionAuthView HomepageClick Login Link (to FusionAuth)View Login FormShow Login FormFill Out and Submit Login FormAuthenticates UserGo to Redirect URIRequest the Redirect URIIs User Authenticated?User is AuthenticatedDisplay User's Account or OtherInfoUserApplicationFusionAuth

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.

Getting Started

Start with getting FusionAuth up and running and creating a new Laravel application.

Clone The Code

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.

Run FusionAuth Via Docker

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:

  • Your client Id is E9FDB985-9173-4E01-9D73-AC2D60D1DC8E.
  • Your client secret is super-secret-secret-that-should-be-regenerated-for-production.
  • Your example username is richard@example.com and the password is password.
  • Your admin username is admin@example.com and the password is password.
  • The base URL of FusionAuth is 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.

The Basic Laravel Application

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

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

Customization

Now that authentication is done, the last task is to create example pages that a user can access only when logged in.

CSS And HTML

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

Route

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.

Run The Application

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.

Next Steps

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 Customization

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:

Security

Tenant and Application Management

Laravel Authentication

Troubleshooting

  • I get “This site can’t be reached localhost refused to connect” when I click the Login button.

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.

  • Laravel says there is an invalid state exception.

Browse to the homepage, log out, and try to log in again. If that still doesn’t work, delete and restart all the containers.

  • It still doesn’t work.

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.