Integrate Your Laravel API With FusionAuth
Integrate Your Laravel API With FusionAuth
In this article, you are going to learn how to integrate a Laravel API with FusionAuth. This presupposes you've built an application that is going to retrieve an access token from FusionAuth via one of the OAuth grants. The grant will typically be the Authorization Code grant for users or the Client Credentials grant for programmatic access.
The token provided by FusionAuth can be stored by the client in a number of locations. For server side applications, it can be stored in a database or on the file system. In mobile applications, store them securely as files accessible only to your app. For a browser application like a SPA, use a cookie if possible and server-side sessions if not.
Here’s a typical API request flow before integrating FusionAuth with your Laravel API.
Here’s the same API request flow when FusionAuth is introduced.
This document will walk through the use case where a Laravel API validates the token. You can also use an API gateway to verify claims and signatures. For more information on doing that with FusionAuth, visit the API gateway documentation.
Prerequisites
For this tutorial, you’ll need to have Composer, Docker and at least PHP 8.2 installed.
You'll also need Docker, since that is how you’ll install FusionAuth.
The commands below are for macOS, but are limited to mkdir
and cd
, which have equivalents in Windows and Linux.
Download and Install FusionAuth
First, make a project directory:
mkdir integrate-fusionauth && cd integrate-fusionauth
Then, install FusionAuth:
curl -o docker-compose.yml https://raw.githubusercontent.com/FusionAuth/fusionauth-containers/master/docker/fusionauth/docker-compose.yml
curl -o .env https://raw.githubusercontent.com/FusionAuth/fusionauth-containers/master/docker/fusionauth/.env
docker compose up -d
Create a User and an API Key
Next, log into your FusionAuth instance. You’ll need to set up a user and a password, as well as accept the terms and conditions.
Then, you’re at the FusionAuth admin UI. This lets you configure FusionAuth manually. But for this tutorial, you're going to create an API key and then you’ll configure FusionAuth using our PHP client library.
Navigate to + button to add a new API Key. Copy the value of the Key field and then save the key.
It might be a value like CY1EUq2oAQrCgE7azl3A2xwG-OEwGPqLryDRBCoz-13IqyFYMn1_Udjt
.
Doing so creates an API key that can be used for any FusionAuth API call. Save that key value off as you’ll be using it later.
Configure FusionAuth
Next, you need to set up FusionAuth. This can be done in different ways, but we’re going to use the PHP client library. You can use the client library with an IDE of your preference as well.
First, make a directory:
mkdir setup-fusionauth && cd setup-fusionauth
Now, copy and paste the following file into composer.json
.
{
"require": {
"php": "^8.2",
"fusionauth/fusionauth-client": "^1.45"
}
}
Install the dependencies.
composer install
Then copy and paste the following file into setup.php
.
<?php
declare(strict_types=1);
use FusionAuth\ClientResponse;
use FusionAuth\FusionAuthClient;
if (empty($argv[1])) {
echo 'Please generate an Api Key and pass it to this script:' . PHP_EOL;
echo 'Usage: php ' . $argv[0] . ' "your-api-key"' . PHP_EOL;
die(1);
}
require __DIR__ . '/vendor/autoload.php';
const FUSIONAUTH_BASE_URL = 'http://localhost:9011';
const FUSIONAUTH_CLIENT_SECRET = 'change-this-in-production-to-be-a-real-secret';
const APPLICATION_NAME = 'PHP Example App';
const APPLICATION_BASE_URL = 'http://localhost:8080';
const APPLICATION_ID = 'e9fdb985-9173-4e01-9d73-ac2d60d1dc8e';
const RSA_KEY_ID = '356a6624-b33c-471a-b707-48bbfcfbc593';
$mediator = new class(
$argv[1],
FUSIONAUTH_BASE_URL,
FUSIONAUTH_CLIENT_SECRET,
APPLICATION_NAME,
APPLICATION_ID,
RSA_KEY_ID,
APPLICATION_BASE_URL,
) {
private readonly FusionAuthClient $client;
public function __construct(
string $apiKey,
private readonly string $baseUrl,
private readonly string $clientSecret,
private readonly string $applicationName,
private readonly string $applicationId,
private readonly string $rsaKeyId,
private readonly string $applicationBaseUrl,
) {
$this->client = new FusionAuthClient($apiKey, $baseUrl);
}
public function run(): stdClass
{
echo 'Retrieving Tenants... ';
$tenant = $this->getTenant();
echo 'OK' . PHP_EOL;
echo 'Patching Tenant... ';
$this->patchTenant($tenant);
echo 'OK' . PHP_EOL;
echo 'Generating RSA Key... ';
$this->generateKey();
echo 'OK' . PHP_EOL;
echo 'Creating Application... ';
$this->createApplication();
echo 'OK' . PHP_EOL;
echo 'Retrieving Users... ';
$user = $this->getUser();
echo 'OK' . PHP_EOL;
echo "Patching User {$user->email}... ";
$this->patchUser($user);
echo 'OK' . PHP_EOL;
echo 'Registering User... ';
$this->registerUser($user);
echo 'OK' . PHP_EOL;
return $user;
}
private function handleResponse(string $method, ClientResponse $response): stdClass
{
if (!$response->wasSuccessful()) {
$message = 'An unknown error occurred. Please check the API Key and Base URL for your FusionAuth instance.';
$error = $response->errorResponse;
if (!empty($error)) {
$message = PHP_EOL . json_encode($error, JSON_PRETTY_PRINT);
}
throw new RuntimeException("Error while {$method}: {$message}", $response->status ?: 0);
}
return $response->successResponse ?? new stdClass();
}
private function getTenant(): stdClass
{
$response = $this->handleResponse(
'Retrieving Tenants',
$this->client->retrieveTenants()
);
if (empty($response->tenants)) {
throw new RuntimeException("Couldn't find any tenants");
}
return $response->tenants[0];
}
private function patchTenant(stdClass $tenant): void
{
$this->handleResponse(
'Patching Tenant',
$this->client->patchTenant(
$tenant->id,
['tenant' => ['issuer' => $this->baseUrl]]
)
);
}
private function generateKey(): void
{
$this->handleResponse(
'Generating API Key',
$this->client->generateKey(
$this->rsaKeyId,
[
'key' => [
'algorithm' => 'RS256',
'name' => "For {$this->applicationName}",
'length' => 2048,
],
]
)
);
}
private function createApplication(): void
{
$application = [
'name' => $this->applicationName,
'oauthConfiguration' => [
'authorizedRedirectURLs' => [$this->applicationBaseUrl],
'requireRegistration' => true,
'enabledGrants' => ['authorization_code', 'refresh_token'],
'logoutURL' => $this->applicationBaseUrl,
'clientSecret' => $this->clientSecret,
],
// assign key from above to sign our tokens. This needs to be asymmetric
'jwtConfiguration' => [
'enabled' => true,
'accessTokenKeyId' => $this->rsaKeyId,
'idTokenKeyId' => $this->rsaKeyId,
],
// creating roles
'roles' => ['admin'],
];
$this->handleResponse(
'Creating Application',
$this->client->createApplication(
$this->applicationId,
['application' => $application]
)
);
}
private function getUser(): stdClass
{
// should only be one user
$response = $this->handleResponse(
'Retrieving User',
$this->client->searchUsersByQuery([
'search' => [
'queryString' => '*',
],
])
);
if (empty($response->users)) {
throw new RuntimeException("Couldn't find any users");
}
return $response->users[0];
}
private function patchUser(stdClass $user): void
{
$this->handleResponse(
'Patching User',
$this->client->patchUser($user->id, [
'user' => [
'fullName' => "{$user->firstName} {$user->lastName}",
],
])
);
}
private function registerUser(stdClass $user): void
{
$this->handleResponse(
'Registering User',
$this->client->register($user->id, [
'registration' => [
'applicationId' => $this->applicationId,
],
])
);
}
};
try {
$user = $mediator->run();
var_dump($user);
echo 'Setup finished successfully.' . PHP_EOL;
} catch (\Throwable $t) {
echo PHP_EOL;
$code = $t->getCode();
if ($code > 0) {
echo "[HTTP {$code}] ";
}
echo $t->getMessage() . PHP_EOL;
die(2);
}
Then, you can run the setup script.
The setup script is designed to run on a newly installed FusionAuth instance with only one user and no tenants other than Default
. To follow this guide on a FusionAuth instance that does not meet these criteria, you may need to modify the above script.
Refer to the PHP client library documentation for more information.
This will create the FusionAuth configuration for your Laravel API.
php setup.php "YOUR_API_KEY_FROM_ABOVE"
Retrieve JWKS Endpoint
Instead of manually storing the public key to verify JWTs, your application should automatically look it up using the JWKS endpoint.
As both the Laravel application, which you are going to create in the next step, and FusionAuth instance are running in different Docker Compose projects, they can't reach each other. To allow that, expose your FusionAuth instance to the Web using ngrok.
You could also add network connectivity between them by running docker network connect
.
Now, log into your instance using the address ngrok
gave you and browse to . Locate the PHP Example App
application that the setup script created for you and click to view its details. In the OAuth2 & OpenID Connect Integration details section, locate JSON Web Key (JWK) Set and copy it. You'll need this value later.
Create Your Laravel API
Now you are going to create a Laravel application. While this section uses a simple Laravel application, you can use the same steps to integrate any Laravel application with FusionAuth.
First, make a directory:
mkdir ../setup-laravel && cd ../setup-laravel
Next, create a simple Laravel template using Laravel's build script. For this API, you don't need all the tools that the script would normally install (like Redis and Selenium), so we are only requiring MariaDB, one of the most popular relational database management systems used with Laravel.
curl -s "https://laravel.build/fusionauth-example-laravel-api?with=mariadb" | bash
This can take several minutes to complete, so please be patient.
Adding JWT Authentication
After it is done, change into the fusionauth-example-laravel-api
directory and install fusionauth/jwt-auth-webtoken-provider
, a library created by FusionAuth to add JWT validation capabilities to Laravel, and web-token/jwt-signature-algorithm-rsa
, a package to handle RSA algorithms.
cd fusionauth-example-laravel-api
./vendor/bin/sail composer require fusionauth/jwt-auth-webtoken-provider web-token/jwt-signature-algorithm-rsa
Add some authentication routes to routes/api.php
and another endpoint to allow GET
requests to /api/messages
, which you'll create later.
<?php
use App\Http\Controllers\Api\AuthController;
use App\Http\Controllers\Api\MessagesController;
use Illuminate\Contracts\Routing\Registrar;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/
Route::group(['middleware' => 'auth:api'], function (Registrar $router) {
$router->group(['prefix' => 'auth'], function (Registrar $router) {
$router->post('logout', [AuthController::class, 'logout']);
$router->post('me', [AuthController::class, 'me']);
});
$router->get('messages', MessagesController::class);
});
Create app/Http/Controllers/Api/AuthController.php
for all authentication logic.
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use function auth;
use function response;
class AuthController extends Controller
{
/**
* Returns the authenticated User
*
* @return \Illuminate\Http\JsonResponse
*/
public function me(): JsonResponse
{
return response()->json(auth()->user());
}
/**
* Logs the user out by invalidating the token
*
* @return \Illuminate\Http\JsonResponse
*/
public function logout(): JsonResponse
{
auth()->logout();
return response()->json(['message' => 'Successfully logged out']);
}
}
Laravel uses something called Guards to protect your endpoints, so we need to tell it about the new Guard provided from that library by editing config/auth.php
.
<?php
return [
'defaults' => [
'guard' => 'api',
'passwords' => 'users',
],
'guards' => [
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
'providers' => [
'users' => [
'driver' => 'fusionauth_eloquent',
'model' => App\Models\User::class,
],
],
'passwords' => [],
'password_timeout' => 10800,
];
Edit the config/app.php
file to add some information about your FusionAuth instance.
<?php
use Illuminate\Support\Facades\Facade;
use Illuminate\Support\ServiceProvider;
return [
/*
|--------------------------------------------------------------------------
| Application Name
|--------------------------------------------------------------------------
|
| This value is the name of your application. This value is used when the
| framework needs to place the application's name in a notification or
| any other location as required by the application or its packages.
|
*/
'name' => env('APP_NAME', 'Laravel'),
/*
|--------------------------------------------------------------------------
| Application Environment
|--------------------------------------------------------------------------
|
| This value determines the "environment" your application is currently
| running in. This may determine how you prefer to configure various
| services the application utilizes. Set this in your ".env" file.
|
*/
'env' => env('APP_ENV', 'production'),
/*
|--------------------------------------------------------------------------
| Application Debug Mode
|--------------------------------------------------------------------------
|
| When your application is in debug mode, detailed error messages with
| stack traces will be shown on every error that occurs within your
| application. If disabled, a simple generic error page is shown.
|
*/
'debug' => (bool) env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Application URL
|--------------------------------------------------------------------------
|
| This URL is used by the console to properly generate URLs when using
| the Artisan command line tool. You should set this to the root of
| your application so that it is used when running Artisan tasks.
|
*/
'url' => env('APP_URL', 'http://localhost'),
'asset_url' => env('ASSET_URL'),
/*
|--------------------------------------------------------------------------
| Application Timezone
|--------------------------------------------------------------------------
|
| Here you may specify the default timezone for your application, which
| will be used by the PHP date and date-time functions. We have gone
| ahead and set this to a sensible default for you out of the box.
|
*/
'timezone' => 'UTC',
/*
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the default locale that will be used
| by the translation service provider. You are free to set this value
| to any of the locales which will be supported by the application.
|
*/
'locale' => 'en',
/*
|--------------------------------------------------------------------------
| Application Fallback Locale
|--------------------------------------------------------------------------
|
| The fallback locale determines the locale to use when the current one
| is not available. You may change the value to correspond to any of
| the language folders that are provided through your application.
|
*/
'fallback_locale' => 'en',
/*
|--------------------------------------------------------------------------
| Faker Locale
|--------------------------------------------------------------------------
|
| This locale will be used by the Faker PHP library when generating fake
| data for your database seeds. For example, this will be used to get
| localized telephone numbers, street address information and more.
|
*/
'faker_locale' => 'en_US',
/*
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| This key is used by the Illuminate encrypter service and should be set
| to a random, 32 character string, otherwise these encrypted strings
| will not be safe. Please do this before deploying an application!
|
*/
'key' => env('APP_KEY'),
'cipher' => 'AES-256-CBC',
/*
|--------------------------------------------------------------------------
| Maintenance Mode Driver
|--------------------------------------------------------------------------
|
| These configuration options determine the driver used to determine and
| manage Laravel's "maintenance mode" status. The "cache" driver will
| allow maintenance mode to be controlled across multiple machines.
|
| Supported drivers: "file", "cache"
|
*/
'maintenance' => [
'driver' => 'file',
// 'store' => 'redis',
],
/*
|--------------------------------------------------------------------------
| Autoloaded Service Providers
|--------------------------------------------------------------------------
|
| The service providers listed here will be automatically loaded on the
| request to your application. Feel free to add your own services to
| this array to grant expanded functionality to your applications.
|
*/
'providers' => ServiceProvider::defaultProviders()->merge([
/*
* Package Service Providers...
*/
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\FusionAuth\Providers\FusionAuthServiceProvider::class,
])->toArray(),
/*
|--------------------------------------------------------------------------
| Class Aliases
|--------------------------------------------------------------------------
|
| This array of class aliases will be registered when this application
| is started. However, feel free to register as many as you wish as
| the aliases are "lazy" loaded so they don't hinder performance.
|
*/
'aliases' => Facade::defaultAliases()->merge([
// 'Example' => App\Facades\Example::class,
])->toArray(),
'fusionauth' => [
'url' => rtrim(env('FUSIONAUTH_URL'), '/'),
'client_id' => env('FUSIONAUTH_CLIENT_ID'),
],
];
To make the library available for use, publish its configuration by running the command below.
./vendor/bin/sail artisan vendor:publish --provider="FusionAuth\JWTAuth\WebTokenProvider\Providers\WebTokenServiceProvider"
Disabling Username/Password Authentication
The Laravel installer already brings some useful resources for many applications because it usually expects users to have a username and password for authentication. APIs in general should only expect an API key or a token to authenticate a request.
In this quickstart, disable the typical username/password authentication method and only allow JWTs as an authentication method. Start by removing the need for users to have a password by editing app/Models/User.php
.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
use Tymon\JWTAuth\Contracts\JWTSubject;
class User extends Authenticatable implements JWTSubject
{
use HasApiTokens, HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'id',
'name',
'email',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'remember_token',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified' => 'boolean',
];
/**
* Indicates if the IDs are auto-incrementing.
* @var bool
*/
public $incrementing = false;
/**
* The "type" of the primary key ID.
*
* @var string
*/
protected $keyType = 'string';
/**
* Get the identifier that will be stored in the subject claim of the JWT.
*
* @return mixed
*/
public function getJWTIdentifier()
{
return $this->getKey();
}
/**
* Return a key value array, containing any custom claims to be added to the JWT.
*
* @return array
*/
public function getJWTCustomClaims(): array
{
return [];
}
}
Change the created migration at database/migrations/2014_10_12_000000_create_users_table.php
.
<?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::create('users', function (Blueprint $table) {
$table->uuid('id');
$table->string('name');
$table->string('email')->unique();
$table->boolean('email_verified')->default(false);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
}
};
You may remove all other migrations, as you won't need them.
rm database/migrations/2014_10_12_100000_create_password_reset_tokens_table.php
rm database/migrations/2019_08_19_000000_create_failed_jobs_table.php
rm database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php
Run the remaining migrations to create the necessary tables in your database.
./vendor/bin/sail artisan migrate
Provisioning New Users
By default, Laravel only allows JWTs that correspond to users in your database, but one of the greatest benefits of using FusionAuth is to have a single source of truth of user management. So, you want your API to automatically provision new users when it receives a trusted JWT from FusionAuth.
To do so, extend the default User Provider to create new users when receiving valid JWTs from FusionAuth.
<?php
declare(strict_types=1);
namespace App\FusionAuth\Providers;
use App\Models\User;
use Illuminate\Auth\EloquentUserProvider;
use Tymon\JWTAuth\Payload;
class FusionAuthEloquentUserProvider extends EloquentUserProvider
{
/**
* Returns a user from the provided payload
*
* @param \Tymon\JWTAuth\Payload $payload
*
* @return \App\Models\User
*/
public function createModelFromPayload(Payload $payload): User
{
/** @var \App\Models\User $model */
$model = $this->createModel();
$model->id = $payload->get('sub');
$model->email = $payload->get('email');
$model->name = $model->email;
$model->email_verified = !!$payload->get('email_verified');
return $model;
}
}
Now, add an Authentication Guard in app/FusionAuth/FusionAuthJWTGuard.php
to call that custom method from the User Provider created above.
<?php
declare(strict_types=1);
namespace App\FusionAuth;
use App\FusionAuth\Providers\FusionAuthEloquentUserProvider;
use Illuminate\Contracts\Auth\Authenticatable;
use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\JWTGuard;
class FusionAuthJWTGuard extends JWTGuard
{
/**
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function user(): ?Authenticatable
{
// Calling the default method that will retrieve existing users
$user = parent::user();
if ($user !== null) {
return $user;
}
// Otherwise, we'll use the custom FusionAuth user provider to create a user from the JWT
if (!$this->provider instanceof FusionAuthEloquentUserProvider) {
return null;
}
try {
$payload = $this->jwt->getPayload();
if (empty($payload)) {
return null;
}
$this->user = $this->provider->createModelFromPayload($payload);
$this->user->save();
return $this->user;
} catch (JWTException) {
return null;
}
}
}
To override the existing classes with the ones you just added, you must create a Service Provider in app/FusionAuth/Providers/FusionAuthServiceProvider.php
.
<?php
declare(strict_types=1);
namespace App\FusionAuth\Providers;
use App\FusionAuth\Claims\Audience;
use App\FusionAuth\Claims\Issuer;
use App\FusionAuth\FusionAuthJWTGuard;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\ServiceProvider;
use Tymon\JWTAuth\Http\Parser\Cookies;
class FusionAuthServiceProvider extends ServiceProvider
{
public function boot(): void
{
Auth::provider('fusionauth_eloquent', function (Application $app, array $config) {
return new FusionAuthEloquentUserProvider($this->app['hash'], $config['model']);
});
Auth::extend('jwt', function (Application $app, string $name, array $config) {
$guard = new FusionAuthJWTGuard(
$app['tymon.jwt'],
$app['auth']->createUserProvider($config['provider']),
$app['request']
);
$app->refresh('request', $guard, 'setRequest');
return $guard;
});
/** @var \Tymon\JWTAuth\Claims\Factory $factory */
$factory = $this->app['tymon.jwt.claim.factory'];
$factory->extend('iss', Issuer::class);
$factory->extend('aud', Audience::class);
/** @var \Tymon\JWTAuth\Http\Parser\Parser $parsers */
$parsers = $this->app['tymon.jwt.parser'];
foreach ($parsers->getChain() as $parser) {
if ($parser instanceof Cookies) {
$parser->setKey('app_at');
break;
}
}
}
}
Edit the .env
file and add these lines there.
FUSIONAUTH_CLIENT_ID=e9fdb985-9173-4e01-9d73-ac2d60d1dc8e
FUSIONAUTH_URL=http://localhost:9011
JWT_ALGO=RS256
JWT_JWKS_URL=https://address.that.ngrok.gave.you/.well-known/jwks.json
JWT_JWKS_URL_CACHE=86400
You should change the JWT_JWKS_URL
value to the JWKS Endpoint you copied earlier.
Validating Issuer and Audience Claims
Besides verifying the signature, it is also recommended to validate the issuer (iss
) and audience (aud
) claims when receiving a JWT. To do those checks, you need to create two files.
First, create a file named app/FusionAuth/Claims/Audience.php
that will check if the audience (aud
) claim actually contains the Client Id for the application you created in FusionAuth.
<?php
declare(strict_types=1);
namespace App\FusionAuth\Claims;
use Tymon\JWTAuth\Claims\Audience as BaseAudienceClaim;
use Tymon\JWTAuth\Exceptions\TokenInvalidException;
class Audience extends BaseAudienceClaim
{
private string $expectedValue;
/**
* @throws \Tymon\JWTAuth\Exceptions\TokenInvalidException
*/
public function validatePayload(): bool
{
if (!$this->validate()) {
throw new TokenInvalidException('Audience (aud) invalid');
}
return true;
}
private function validate(): bool
{
// Audience must be set
$value = $this->getValue();
if (empty($value)) {
return false;
}
if (!isset($this->expectedValue)) {
$this->expectedValue = \strtolower(config('app.fusionauth.client_id'));
}
// If we have specified valid values, we check if the current audience is present there
if (empty($this->expectedValue)) {
return false;
}
return \strtolower($value) === $this->expectedValue;
}
}
Now, create a file called app/FusionAuth/Claims/Issuer.php
to check if the issuer (iss
) is actually your FusionAuth instance address.
<?php
declare(strict_types=1);
namespace App\FusionAuth\Claims;
use Tymon\JWTAuth\Claims\Issuer as BaseIssuerClaim;
use Tymon\JWTAuth\Exceptions\TokenInvalidException;
class Issuer extends BaseIssuerClaim
{
private string $expectedValue;
/**
* @throws \Tymon\JWTAuth\Exceptions\TokenInvalidException
*/
public function validatePayload(): bool
{
if (!$this->validate()) {
throw new TokenInvalidException('Issuer (iss) invalid');
}
return true;
}
private function validate(): bool
{
// Issuer must be set
$value = $this->getValue();
if (empty($value)) {
return false;
}
if (!isset($this->expectedValue)) {
$this->expectedValue = \strtolower(config('app.fusionauth.url'));
}
// If we have specified valid values, we check if the current issue is present there
if (empty($this->expectedValue)) {
return false;
}
return \strtolower($value) === $this->expectedValue;
}
}
Creating a Controller
Create a controller in app/Http/Controllers/Api/MessagesController.php
to return a JSON message. This is going to be the protected API.
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use function auth;
use function response;
class MessagesController extends Controller
{
/**
* Display a listing of the resource.
*/
public function __invoke(): JsonResponse
{
$messages = [
'Hello, world!',
];
/** @var \Tymon\JWTAuth\Payload $payload */
$payload = auth()->payload();
$roles = (array) $payload->get('roles');
if (in_array('admin', $roles)) {
$messages[] = 'Welcome, admin.';
}
return response()->json([
'messages' => $messages,
]);
}
}
Creating the View
Change the view file located at resources/views/welcome.blade.php
to add a Log in with FusionAuth button when you are logged out and a container for the messages API response when you are logged in. This is of course not really all that typical; usually you'd have the user log into FusionAuth, store the token in the browser or mobile app, and then call the API. But since this is a quickstart to show you how to validate the
<!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 & Laravel API</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,600&display=swap" rel="stylesheet"/>
<!-- Styles -->
<style>
/* ! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com */*,::after,::before{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}::after,::before{--tw-content:''}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;font-family:Figtree, sans-serif;font-feature-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*, ::before, ::after{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgb(59 130 246 / 0.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::-webkit-backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgb(59 130 246 / 0.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgb(59 130 246 / 0.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.relative{position:relative}.mx-auto{margin-left:auto;margin-right:auto}.mx-6{margin-left:1.5rem;margin-right:1.5rem}.ml-4{margin-left:1rem}.mt-16{margin-top:4rem}.mt-6{margin-top:1.5rem}.mt-4{margin-top:1rem}.-mt-px{margin-top:-1px}.mr-1{margin-right:0.25rem}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.h-16{height:4rem}.h-7{height:1.75rem}.h-6{height:1.5rem}.h-5{height:1.25rem}.min-h-screen{min-height:100vh}.w-auto{width:auto}.w-16{width:4rem}.w-7{width:1.75rem}.w-6{width:1.5rem}.w-5{width:1.25rem}.max-w-7xl{max-width:80rem}.shrink-0{flex-shrink:0}.scale-100{--tw-scale-x:1;--tw-scale-y:1;transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.grid-cols-1{grid-template-columns:repeat(1, minmax(0, 1fr))}.items-center{align-items:center}.justify-center{justify-content:center}.gap-6{gap:1.5rem}.gap-4{gap:1rem}.self-center{align-self:center}.rounded-lg{border-radius:0.5rem}.rounded-full{border-radius:9999px}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242 / var(--tw-bg-opacity))}.bg-dots-darker{background-image:url("data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1.22676 0C1.91374 0 2.45351 0.539773 2.45351 1.22676C2.45351 1.91374 1.91374 2.45351 1.22676 2.45351C0.539773 2.45351 0 1.91374 0 1.22676C0 0.539773 0.539773 0 1.22676 0Z' fill='rgba(0,0,0,0.07)'/%3E%3C/svg%3E")}.from-gray-700\/50{--tw-gradient-from:rgb(55 65 81 / 0.5);--tw-gradient-to:rgb(55 65 81 / 0);--tw-gradient-stops:var(--tw-gradient-from), var(--tw-gradient-to)}.via-transparent{--tw-gradient-to:rgb(0 0 0 / 0);--tw-gradient-stops:var(--tw-gradient-from), transparent, var(--tw-gradient-to)}.bg-center{background-position:center}.stroke-red-500{stroke:#ef4444}.stroke-gray-400{stroke:#9ca3af}.p-6{padding:1.5rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.text-center{text-align:center}.text-right{text-align:right}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-sm{font-size:0.875rem;line-height:1.25rem}.font-semibold{font-weight:600}.leading-relaxed{line-height:1.625}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99 / var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39 / var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128 / var(--tw-text-opacity))}.underline{-webkit-text-decoration-line:underline;text-decoration-line:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgb(0 0 0 / 0.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.shadow-gray-500\/20{--tw-shadow-color:rgb(107 114 128 / 0.2);--tw-shadow:var(--tw-shadow-colored)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms}.selection\:bg-red-500 *::selection{--tw-bg-opacity:1;background-color:rgb(239 68 68 / var(--tw-bg-opacity))}.selection\:text-white *::selection{--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity))}.selection\:bg-red-500::selection{--tw-bg-opacity:1;background-color:rgb(239 68 68 / var(--tw-bg-opacity))}.selection\:text-white::selection{--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity))}.hover\:text-gray-900:hover{--tw-text-opacity:1;color:rgb(17 24 39 / var(--tw-text-opacity))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81 / var(--tw-text-opacity))}.focus\:rounded-sm:focus{border-radius:0.125rem}.focus\:outline:focus{outline-style:solid}.focus\:outline-2:focus{outline-width:2px}.focus\:outline-red-500:focus{outline-color:#ef4444}.group:hover .group-hover\:stroke-gray-600{stroke:#4b5563}.z-10{z-index: 10}@media (prefers-reduced-motion: no-preference){.motion-safe\:hover\:scale-\[1\.01\]:hover{--tw-scale-x:1.01;--tw-scale-y:1.01;transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39 / var(--tw-bg-opacity))}.bg-gray-800\/50{background-color:rgb(31 41 55 / 0.5)}.bg-red-800\/20{background-color:rgb(153 27 27 / 0.2)}.bg-dots-lighter{background-image:url("data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1.22676 0C1.91374 0 2.45351 0.539773 2.45351 1.22676C2.45351 1.91374 1.91374 2.45351 1.22676 2.45351C0.539773 2.45351 0 1.91374 0 1.22676C0 0.539773 0.539773 0 1.22676 0Z' fill='rgba(255,255,255,0.07)'/%3E%3C/svg%3E")}.bg-gradient-to-bl{background-image:linear-gradient(to bottom left, var(--tw-gradient-stops))}.stroke-gray-600{stroke:#4b5563}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175 / var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity))}.shadow-none{--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000)}.ring-inset{--tw-ring-inset:inset}.ring-white\/5{--tw-ring-color:rgb(255 255 255 / 0.05)}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity))}.group:hover .group-hover\:stroke-gray-400{stroke:#9ca3af}@media (min-width: 640px){.sm\:fixed{position:fixed}.sm\:top-0{top:0px}.sm\:right-0{right:0px}.sm\:ml-0{margin-left:0px}.sm\:flex{display:flex}.sm\:items-center{align-items:center}.sm\:justify-center{justify-content:center}.sm\:justify-between{justify-content:space-between}.sm\:text-left{text-align:left}.sm\:text-right{text-align:right}}@media (min-width: 768px){.md\:grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}}@media (min-width: 1024px){.lg\:gap-8{gap:2rem}.lg\:p-8{padding:2rem}}
</style>
<style>
pre{white-space: pre-wrap;}
.text-center{text-align:center;}
.mb-3{margin-bottom: 0.755rem;}
.px-4{padding-left: 1rem;padding-right: 1rem;}
.py-3{padding-top: 0.75rem;padding-bottom: 0.75rem;}
</style>
</head>
<body class="antialiased">
<div class="relative sm:flex sm:justify-center sm:items-center min-h-screen bg-dots-darker bg-center bg-gray-100 bg-dots-lighter bg-gray-900 selection:bg-red-500 selection:text-white">
<div class="max-w-7xl mx-auto p-6 lg:p-8">
<div class="flex justify-center">
<img src="/img/fusionauth.svg" alt="FusionAuth" class="h-16 w-auto">
</div>
<div class="mt-16">
@if(session('error'))
<div class="alert alert-danger">
{!! session('error') !!}
</div>
@endif
@auth
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8">
<div class="scale-100 p-6 bg-white bg-gray-800/50 bg-gradient-to-bl from-gray-700/50 via-transparent ring-1 ring-inset ring-white/5 rounded-lg shadow-2xl shadow-gray-500/20 shadow-none">
<div class="flex items-center">
<div class="h-16 w-16 bg-red-50 bg-red-800/20 flex items-center justify-center rounded-full">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"
stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" class="w-7 h-7 stroke-red-500">
<path d="M16 15H8C5.79086 15 4 16.7909 4 19V21H20V19C20 16.7909 18.2091 15 16 15Z"/>
<path d="M12 11C14.2091 11 16 9.20914 16 7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7C8 9.20914 9.79086 11 12 11Z"/>
</svg>
</div>
<h2 class="ml-4 text-xl font-semibold text-gray-900 text-white">
Hello, {{ auth()->user()->name }}
</h2>
</div>
<pre id="profile"
class="mt-4 text-gray-500 text-gray-400 text-sm">Retrieving user data...</pre>
</div>
<div class="scale-100 p-6 bg-white bg-gray-800/50 bg-gradient-to-bl from-gray-700/50 via-transparent ring-1 ring-inset ring-white/5 rounded-lg shadow-2xl shadow-gray-500/20 shadow-none">
<div class="flex items-center">
<div class="h-16 w-16 bg-red-50 bg-red-800/20 flex items-center justify-center rounded-full">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"
stroke-linecap="round" stroke-linejoin="round"
class="w-7 h-7 stroke-red-500">
<path d="M17.98 10.79V14.79C17.98 15.05 17.97 15.3 17.94 15.54C17.71 18.24 16.12 19.58 13.19 19.58H12.79C12.54 19.58 12.3 19.7 12.15 19.9L10.95 21.5C10.42 22.21 9.56 22.21 9.03 21.5L7.82999 19.9C7.69999 19.73 7.41 19.58 7.19 19.58H6.79001C3.60001 19.58 2 18.79 2 14.79V10.79C2 7.86001 3.35001 6.27001 6.04001 6.04001C6.28001 6.01001 6.53001 6 6.79001 6H13.19C16.38 6 17.98 7.60001 17.98 10.79Z"
stroke-width="1.5" stroke-miterlimit="10"/>
<path d="M21.98 6.79001V10.79C21.98 13.73 20.63 15.31 17.94 15.54C17.97 15.3 17.98 15.05 17.98 14.79V10.79C17.98 7.60001 16.38 6 13.19 6H6.79004C6.53004 6 6.28004 6.01001 6.04004 6.04001C6.27004 3.35001 7.86004 2 10.79 2H17.19C20.38 2 21.98 3.60001 21.98 6.79001Z"
stroke-width="1.5" stroke-miterlimit="10"/>
<path d="M13.4955 13.25H13.5045" stroke-width="2"/>
<path d="M9.9955 13.25H10.0045" stroke-width="2"/>
<path d="M6.4955 13.25H6.5045" stroke-width="2"/>
</svg>
</div>
<h2 class="ml-4 text-xl font-semibold text-gray-900 text-white">
Messages
</h2>
</div>
<pre id="api-results"
class="mt-4 text-gray-500 text-gray-400 text-sm">Loading messages...</pre>
</div>
</div>
<div class="text-center mt-16">
<a href="{{ $logoutUrl }}"
class="px-4 py-3 font-semibold bg-gray-100 rounded-full shadow-2xl">
Log out
</a>
</div>
@else
<div class="text-center">
<a href="{{ $loginUrl }}"
class="px-4 py-3 font-semibold bg-gray-100 rounded-full shadow-2xl">
Log in with FusionAuth
</a>
</div>
@endauth
</div>
</div>
</div>
@auth
<script src="/js/fusionauth.js"></script>
<script>FusionAuth("{{ $baseUrl }}");</script>
@endauth
</body>
</html>
Create a file named public/js/fusionauth.js
that will fetch both /api/messages
from the Laravel API and your user details from FusionAuth.
const FusionAuth = (fusionAuthUrl) => {
const catchError = ($container, err) => {
console.error(err);
$container.innerHTML = `Error: ${err.message}`;
};
const getMe = ($container) => {
fetch(
fusionAuthUrl + '/app/me',
{
method: 'GET',
credentials: 'include',
},
).then((response) => {
if (!response.ok) {
return catchError($container, new Error(`Got HTTP ${response.status}`));
}
response.json()
.then(response => $container.innerHTML = JSON.stringify(response, null, 2))
.catch(err => catchError($container, err));
}).catch(err => catchError($container, err));
};
getMe(document.getElementById('profile'));
const loadMessages = ($container) => {
const callMessagesApi = (shouldRetry = false) => {
fetch(
'/api/messages',
{
method: 'GET',
credentials: 'include',
},
).then((response) => {
if (!response.ok) {
if (response.status === 401) {
if (shouldRetry) {
console.log('Received 401 error, trying to refresh token...');
callMessagesApi(false);
return;
}
console.log('Received 401 error again');
}
catchError($container, new Error(`Got HTTP ${response.status}`));
return;
}
response.json()
.then(response => $container.innerHTML = JSON.stringify(response, null, 2))
.catch(err => catchError($container, err));
});
};
callMessagesApi(true);
};
loadMessages(
document.getElementById('api-results'),
);
};
Testing the Authentication Flow
Finally, start your application.
./vendor/bin/sail up -d
Browse to localhost and click the Log in with FusionAuth button.
Log in with username richard@example.com
and password password
. You should be redirected back to your Laravel application with both your user details and the result of the messages API call.
The full code for this guide can be found here.
Feedback
How helpful was this page?
See a problem?
File an issue in our docs repo
Have a question or comment to share?
Visit the FusionAuth community forum.