docker compose installed.
A client wants access to an API resource at /resource
. However, it is denied this resource until it acquires an access token from FusionAuth.
Resource Server Authentication with FusionAuth
While the access token is acquired via the Login API above, this is for simplicity of illustration. The token can be, and typically is, acquired through one of the OAuth grants.
In this section, you’ll get FusionAuth up and running and create a resource server which will serve the API.
First off, grab the code from the repository and change into that directory.
git clone https://github.com/FusionAuth/fusionauth-quickstart-php-laravel-api.git
cd fusionauth-quickstart-php-laravel-api
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
.teller@example.com
and the password is password
. They will have the role of teller
.customer@example.com
and the password is password
. They will have the role of customer
.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.
To make sure your local FusionAuth instance is accessible to your Laravel app, you need to expose it to the Internet. Write down the URL ngrok gave you as you’ll need it soon.
Now you are going to create a Laravel API application. While this section builds a simple API, you can use the same configuration to integrate an existing API with FusionAuth.
We are going to be building an API backend for a banking application called ChangeBank. This API will have two endpoints:
make-change
: This endpoint will allow you to call GET with a total
amount and receive a response indicating how many nickels and pennies are needed to make change. Valid roles are customer
and teller
.panic
: Tellers may call this endpoint to call the police in case of an incident. The only valid role is teller
.Both endpoints will be protected such that a valid JSON web token (JWT) will be required in an app.at
cookie or the Authorization
header in order to be accessed. Additionally, the JWT must have a roles
claim containing the appropriate role to use the endpoint.
If you simply want to run the application, there is a completed version in the complete-application
directory. You can install PHP and Composer and use the following commands to get it up and running if you do not want to create your own.
cd complete-application
composer install
./vendor/bin/sail up -d
You can then follow the instructions in the “Run the API” section.
To create the application from scratch, you’ll use Laravel Sail to set up and run your application using Docker.
curl "https://laravel.build/your-application?with=mariadb" | bash
cd your-application
This may ask for your user password to run commands with sudo
.
Initialize the Laravel application using the following command (it’ll run things in background, just like docker compose up -d
would).
./vendor/bin/sail up -d
Laravel Sail will use port 80, so make sure you don’t have anything listening on that. Otherwise, you can pass an APP_PORT
environment variable to specify another port, like shown below.
APP_PORT=3000 ./vendor/bin/sail up -d
Include FusionAuth’s library to add JWT support to the authentication system.
./vendor/bin/sail composer require fusionauth/jwt-auth-webtoken-provider web-token/jwt-signature-algorithm-rsa
When installing Laravel, it will automatically make a migration to create a users
table in the database. You’ll have to edit the file database/migrations/2014_10_12_000000_create_users_table.php
to change the default behavior and use UUID for the primary key, remove the need for a password and the remember token.
<?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')->unique();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
}
};
You’ll also need to edit app/Models/User.php
to make the database changes consistent with your model.
<?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 should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'remember_token',
];
/**
* 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 [];
}
}
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
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, first create a folder named FusionAuth
inside app/
, and another one named Providers
inside FusionAuth
, so you can have app/FusionAuth/Providers
.
mkdir -p app/FusionAuth/Providers
Add a new User Provider at app/FusionAuth/Providers/FusionAuthEloquentUserProvider.php
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
{
$email = $payload->get('email');
/** @var \App\Models\User $model */
$model = $this->createModel()
->setAttribute('id', $payload->get('sub'));
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 as TymonJWTGuard;
class FusionAuthJwtGuard extends TymonJWTGuard
{
/**
* @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;
}
}
}
Let Laravel know about these two classes by editing config/auth.php
.
First, change the driver
for the web
Guard to jwt
:
'guards' => [
'web' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
Now, change the default provider
to fusionauth_eloquent
:
'providers' => [
'users' => [
'driver' => 'fusionauth_eloquent',
'model' => App\Models\User::class,
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
Finally, create a Service Provider in app/FusionAuth/Providers/FusionAuthServiceProvider.php
to make sure Laravel will use the newly created classes instead of its defaults.
<?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;
}
}
}
}
Change routes/api.php
to add both /api/make-change
and /api/panic
endpoints for your ChangeBank application.
<?php
use Illuminate\Http\Request;
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!
|
*/
$middleware = Route::middleware('auth:sanctum');
$middleware->get('/user', function (Request $request) {
return $request->user();
});
$middleware->post('/panic', \App\Http\Controllers\ChangeBank\PanicController::class);
$middleware->get('/make-change', \App\Http\Controllers\ChangeBank\MakeChangeController::class);
Change the base controller at app/Http/Controllers/Controller.php
to include a helper function to check for roles.
<?php
namespace App\Http\Controllers;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\Gate;
class Controller extends BaseController
{
use AuthorizesRequests, ValidatesRequests;
protected function checkRoles(string ...$roles): void
{
/** @var \Tymon\JWTAuth\Payload $payload */
$payload = auth('web')->payload();
$rolesFromJwt = (array) $payload->get('roles');
$hasAtLeastOneRole = false;
foreach ($roles as $role) {
foreach ($rolesFromJwt as $roleFromJwt) {
if ($roleFromJwt === $role) {
$hasAtLeastOneRole = true;
break;
}
}
}
if (!$hasAtLeastOneRole) {
throw new AuthorizationException('Proper role not found for user.');
}
}
}
Create a folder named ChangeBank
inside app/Http/Controllers
to hold the two controllers you’re going to create.
mkdir app/Http/Controllers/ChangeBank
To respond the /api/make-change
endpoint, create a file at app/Http/Controllers/ChangeBank/MakeChangeController.php
. This controller will verify that the authenticated user has either the teller
or customer
role. It then takes in the URL parameter total
to calculate which coins will be returned in the JSON payload.
<?php
namespace App\Http\Controllers\ChangeBank;
use App\Http\Controllers\Controller;
use Symfony\Component\HttpFoundation\Response;
use function response;
class MakeChangeController extends Controller
{
/**
* Make Change entrypoint for the ChangeBank API.
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function __invoke(): Response
{
$this->checkRoles('teller', 'customer');
$total = (float) request()->query('total', 0);
$output = $this->makeChange($total);
return response()->json($output);
}
protected function makeChange(float $total): array
{
if ($total <= 0) {
return [
'Message' => 'Please provider a total parameter greater than 0.',
];
}
$message = 'We can make change using';
$remainingAmount = $total;
$coins = [
'quarters' => 0.25,
'dimes' => 0.10,
'nickels' => 0.05,
'pennies' => 0.01,
];
$output = [
'Message' => $message,
'Change' => [],
];
foreach ($coins as $coinName => $value) {
$coinCount = intval($remainingAmount / $value);
$remainingAmount = round(($remainingAmount - $coinCount * $value) * 100) / 100;
$output['Message'] .= " {$coinCount} {$coinName}";
$output['Change'][] = ['Denomination' => $coinName, 'Count' => $coinCount];
}
return $output;
}
}
To finish, create a controller at app/Http/Controllers/ChangeBank/PanicController.php
to handle the /api/panic
endpoint for users with the teller
role.
<?php
namespace App\Http\Controllers\ChangeBank;
use App\Http\Controllers\Controller;
use Symfony\Component\HttpFoundation\Response;
use function intval;
use function request;
use function response;
use function round;
class PanicController extends Controller
{
/**
* Panic entrypoint for the ChangeBank API.
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function __invoke(): Response
{
$this->checkRoles('teller');
return response()->json([
'message' => "We've called the police!",
]);
}
}
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.
Create a folder named Claims
inside app/FusionAuth
.
mkdir app/FusionAuth/Claims
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 (empty($this->expectedValue)) {
return false;
}
// If we have specified valid values, we check if the current audience is present there
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;
}
}
Now that you created all these files, you need to pass some details about the FusionAuth configuration to them.
Start by adding the environment variables below to your .env
file, changing https://The-Address-Ngrok-Gave-You
to the actual address you copied from ngrok.
FUSIONAUTH_CLIENT_ID=e9fdb985-9173-4e01-9d73-ac2d60d1dc8e
FUSIONAUTH_URL=http://localhost:9011
JWT_ALGO=RS256
JWT_JWKS_URL_CACHE=86400
JWT_JWKS_URL=https://The-Address-Ngrok-Gave-You/.well-known/jwks.json
Now open the config/app.php
file and browse to the providers
section. This is a Laravel configuration to automatically load these files when booting your application. Add App\FusionAuth\Providers\FusionAuthServiceProvider::class
to the end of the providers
array, like shown below.
'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(),
In that same config/app.php
file, add these lines at the end (just before the last line) to pass values from the environment variables to the application.
/*
|--------------------------------------------------------------------------
| FusionAuth instance config
|--------------------------------------------------------------------------
|
| Retrieving FusionAuth instance settings.
|
*/
'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"
There are several ways to acquire a token in FusionAuth, but for this example you will use the Login API to keep things simple.
First, try requests as the teller@example.com
user. Based on the configuration this user has the teller
role and should be able to use both /api/make-change
and /api/panic
.
teller@example.com
by making the following requestcurl --location 'http://localhost:9011/api/login' \
--header 'Authorization: this_really_should_be_a_long_random_alphanumeric_value_but_this_still_works' \
--header 'Content-Type: application/json' \
--data-raw '{
"loginId": "teller@example.com",
"password": "password",
"applicationId": "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e"
}'
Copy the token
from the response, which should look like this:
{
"token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InVOYl9iQzFySHZZTnZMc285VzRkOEprZkxLWSJ9.eyJhdWQiOiJlOWZkYjk4NS05MTczLTRlMDEtOWQ3My1hYzJkNjBkMWRjOGUiLCJleHAiOjE2ODkzNTMwNTksImlhdCI6MTY4OTM1Mjk5OSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MDExIiwic3ViIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMTExMTExMTExMTExIiwianRpIjoiY2MzNWNiYjUtYzQzYy00OTRjLThmZjMtOGE4YWI1NTI0M2FjIiwiYXV0aGVudGljYXRpb25UeXBlIjoiUEFTU1dPUkQiLCJlbWFpbCI6InRlbGxlckBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhcHBsaWNhdGlvbklkIjoiZTlmZGI5ODUtOTE3My00ZTAxLTlkNzMtYWMyZDYwZDFkYzhlIiwicm9sZXMiOlsiY3VzdG9tZXIiLCJ0ZWxsZXIiXSwiYXV0aF90aW1lIjoxNjg5MzUyOTk5LCJ0aWQiOiJkN2QwOTUxMy1hM2Y1LTQwMWMtOTY4NS0zNGFiNmM1NTI0NTMifQ.WLzI9hSsCDn3ZoHKA9gaifkd6ASjT03JUmROGFZaezz9xfVbO3quJXEpUpI3poLozYxVcj2hrxKpNT9b7Sp16CUahev5tM0-4_FaYlmUEoMZBKo2JRSH8hg-qVDvnpeu8nL6FXxJII0IK4FNVwrQVFmAz99ZCf7m5xruQSziXPrfDYSU-3OZJ3SRuvD8bMopSiyRvZLx6YjWfBsvGSmMXeh_8vHG5fVkq5w1IkaDdugHnivtJIivHuCfl38kQBgw9rAqJLJoKRHHW0Ha7vHIcS6OCWWMDIIVspLyQNcLC16pL9Nss_5v9HMofow1OvQ9sUSMrbbkipjKq2peSjG7qA",
"tokenExpirationInstant": 1689353059670,
"user": {
...
}
}
Make a request to /api/make-change
with a query parameter total=5.12
and pass the token
in an app.at
cookie (or as a Bearer
token in the Authorization
header).
curl --cookie 'app.at=YOUR_TOKEN' 'http://localhost/api/make-change?total=5.12'
You can replace --cookie 'app.at=<your_token>'
with --header 'Authorization: Bearer <your_token>'
if you wish to use the Authorization
header instead of the app.at
cookie.
The response should look similar to below, however it has been formatted here for readability:
{
"Message": "We can make change using 20 quarters 1 dimes 0 nickels 2 pennies",
"Change": [
{
"Denomination": "quarters",
"Count": 20
},
{
"Denomination": "dimes",
"Count": 1
},
{
"Denomination": "nickels",
"Count": 0
},
{
"Denomination": "pennies",
"Count": 2
}
]
}
You were authorized, success! You can try making the request without the cookie or with a different string rather than a valid token, and see that you are denied access.
Next call the /api/panic
endpoint because you are in trouble!
curl --cookie 'app.at=YOUR_TOKEN' --request POST 'http://localhost/api/panic'
This is a POST
(not a GET
) because you want all your emergency calls to be non-idempotent.
Your response should look like this:
{"message":"We've called the police!"}
Nice, help is on the way!
Now let’s try as customer@example.com
who has the role customer
. Acquire a token for customer@example.com
.
curl --location 'http://localhost:9011/api/login' \
--header 'Authorization: this_really_should_be_a_long_random_alphanumeric_value_but_this_still_works' \
--header 'Content-Type: application/json' \
--data-raw '{
"loginId": "customer@example.com",
"password": "password",
"applicationId": "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e"
}'
Your response should look like this:
{
"token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InVOYl9iQzFySHZZTnZMc285VzRkOEprZkxLWSJ9.eyJhdWQiOiJlOWZkYjk4NS05MTczLTRlMDEtOWQ3My1hYzJkNjBkMWRjOGUiLCJleHAiOjE2ODkzNTQxMjMsImlhdCI6MTY4OTM1MzUyMywiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MDExIiwic3ViIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMjIyMjIyMjIyMjIyIiwianRpIjoiYjc2YWMwMGMtMDdmNi00NzkzLTgzMjgtODM4M2M3MGU4MWUzIiwiYXV0aGVudGljYXRpb25UeXBlIjoiUEFTU1dPUkQiLCJlbWFpbCI6ImN1c3RvbWVyQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImFwcGxpY2F0aW9uSWQiOiJlOWZkYjk4NS05MTczLTRlMDEtOWQ3My1hYzJkNjBkMWRjOGUiLCJyb2xlcyI6WyJjdXN0b21lciJdLCJhdXRoX3RpbWUiOjE2ODkzNTM1MjMsInRpZCI6ImQ3ZDA5NTEzLWEzZjUtNDAxYy05Njg1LTM0YWI2YzU1MjQ1MyJ9.T1bELQ6a_ItOS0_YYpvqhIVknVMNeamcoC7BWnPjg2lgA9XpCmFA2mVnycoeuz-mSOHbp2cCoauP5opxehBR2lCn4Sz0If6PqgJgXKEpxh5pAxCPt91UyfjH8hGDqE3rDh7E4Hqn7mb-dFFwdfX7CMdKvC3dppMbXAGCZTl0LizApw5KIG9Wmt670339pSf5lzD38P9WAG5Wr7fAmVrIJPVu6yv2FoR-pMYD2lnAF63HWKknrWB-khmhr9ZKRLXKhP1UK-ThY1FSnmpp8eNblsBqCxf6WaYxYkdp5_F2e56M4sQwHzrg4P9tZGVCmMri4dShF3Ck7OGa7hel-iIPew",
"tokenExpirationInstant": 1689354123118,
"user": {
...
}
}
Now use that token to call /api/make-change
with a query parameter total=3.24
.
curl --cookie 'app.at=YOUR_TOKEN' --location 'http://localhost/api/make-change?total=3.24'
The response should look similar to below, however it has been formatted here for readability:
{
"Message": "We can make change using 12 quarters 2 dimes 0 nickels 4 pennies",
"Change": [
{
"Denomination": "quarters",
"Count": 12
},
{
"Denomination": "dimes",
"Count": 2
},
{
"Denomination": "nickels",
"Count": 0
},
{
"Denomination": "pennies",
"Count": 4
}
]
}
So far so good. Now call the /api/panic
endpoint with -i
flag to see the headers of the response).
curl --cookie 'app.at=YOUR_TOKEN' -i --request POST -H "Accept: application/json" 'http://localhost/api/panic'
Your should receive a 403 Forbidden
response with the following message and a huge stack trace (because you are in development mode):
{
"message": "Proper role not found for user.",
"exception": "Symfony\\Component\\HttpKernel\\Exception\\AccessDeniedHttpException",
...
}
Looks like this user does not have access to this function. Enjoy your secured resource server!
This quickstart is a great way to get a proof of concept up and running quickly, but to run your API in production, there are some things you're going to want to do.
This site can’t be reached localhost refused to connect.
when I call the Login API.Ensure FusionAuth is running in the Docker container. You should be able to login as the admin user, admin@example.com
with a password of password
at http://localhost:9011/admin.
/api/panic
endpoint doesn’t work when I call it.Make sure you are making a POST
call and using a token with the teller
role.
You can always pull down a complete running application and compare what’s different.
git clone https://github.com/FusionAuth/fusionauth-quickstart-php-laravel-api.git
cd fusionauth-quickstart-php-laravel-api
docker compose up -d
cd complete-application
composer install
./vendor/bin/sail up -d