Composer
While this sample application doesn't have login functionality without FusionAuth, a more typical integration will replace an existing login system with FusionAuth.
In that case, the system might look like this before FusionAuth is introduced.
Request flow during login before FusionAuth
The login flow will look like this after FusionAuth is introduced.
Request flow during login after FusionAuth
In general, you are introducing FusionAuth in order to normalize and consolidate user data. This helps make sure it is consistent and up-to-date as well as offloading your login security and functionality to FusionAuth.
Start with getting FusionAuth up and running and creating a new PHP application.
First, grab the code from the repository and change to that directory.
git clone https://github.com/FusionAuth/fusionauth-quickstart-php-web.git
cd fusionauth-quickstart-php-web
All shell commands in this guide can be entered in a terminal in this directory. 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
subdirectory, if you prefer to copy them to your application.
You'll find a Docker Compose file (docker-compose.yml
) and an environment variables configuration file (.env
) in the root directory of the repo.
Assuming you have Docker installed, you can stand up FusionAuth on your machine with the following.
docker compose up -d
Here you are using a bootstrapping feature of FusionAuth called Kickstart. When FusionAuth comes up for the first time, it will look at the kickstart/kickstart.json
file and configure FusionAuth to your specified state.
If you ever want to reset the FusionAuth application, you need to delete the volumes created by Docker Compose by executing docker compose down -v
, then re-run docker compose up -d
.
FusionAuth will be initially configured with these settings:
E9FDB985-9173-4E01-9D73-AC2D60D1DC8E
.super-secret-secret-that-should-be-regenerated-for-production
.richard@example.com
and the password is password
.admin@example.com
and the password is password
.http://localhost:9011/
.You can log in to the FusionAuth admin UI and look around if you want to, but with Docker and Kickstart, everything will already be configured correctly.
If you want to see where the FusionAuth values came from, they can be found in the FusionAuth app. The tenant Id is found on the Tenants page. To see the Client Id and Client Secret, go to the Applications page and click the View
icon under the actions for the ChangeBank application. You'll find the Client Id and Client Secret values in the OAuth configuration
section.
The .env
file contains passwords. In a real application, always add this file to your .gitignore
file and never commit secrets to version control.
While this guide builds a new PHP project, you can use the same method to integrate your existing project with FusionAuth.
If you simply want to run the application and not create your own, there is a completed version in the complete-application
directory. You can use the following commands to get it up and running.
cd complete-application
composer install
php -S localhost:9012 -t public
View the application at http://localhost:9012.
First create the directory for your application and change to that directory.
mkdir your-application
cd your-application
Before you start coding, you need to install the PHP module for your application to communicate with FusionAuth. Run the following command.
composer require vlucas/phpdotenv jerryhopper/oauth2-fusionauth
Authentication in PHP is managed by FusionAuth community member Jerry Hopper’s FusionAuth Provider for The League’s OAuth library.
Create an .env
file within your-application
directory and 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_SERVER_URL="http://localhost:9011"
FUSIONAUTH_BROWSER_URL="http://localhost:9011"
FUSIONAUTH_REDIRECT_URL="http://localhost:9012/login.php"
This tells PHP where to find and connect to FusionAuth.
In this application authentication is handled by two files: login.php
and logout.php
.
The login file is based almost exactly on Jerry Hopper’s code, but is split into neater functions and does two extra things: Starts a session and saves the user details to the session after login.
Create a public
directory within your-application
.
mkdir public
In the public
directory, create a login.php
file and insert the following code.
<?php
hideErrorsInBrowser();
loadAllModules();
loadEnvironmentVariables();
$provider = getFusionAuthProvider();
startSafeSession();
redirectToAccountPageIfAlreadyLoggedIn();
redirectUserToFusionAuthIfNotLoggedIn($provider);
checkCSRFToken();
handleFusionAuthCallback($provider);
exit;
function hideErrorsInBrowser() {
ini_set('display_errors', 0);
ini_set('log_errors', '1');
ini_set('error_log', 'php://stderr');
}
function loadAllModules() {
require_once __DIR__ . '/../vendor/autoload.php';
}
function loadEnvironmentVariables() {
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/..');
$dotenv->load();
}
function startSafeSession() {
$cookieParams = [
'lifetime' => 0, // 0 means "until the browser is closed"
'path' => '/', // entire site can use this cookie
// 'domain' => '', // Set your domain here
'secure' => false, // true for HTTPS, false for HTTP
'httponly' => true, // true to make the cookie accessible only through the HTTP protocol
'samesite' => 'Strict' // None, Lax, or Strict
];
session_set_cookie_params($cookieParams);
session_start();
}
function getFusionAuthProvider(): object {
$fusionAuthClientId = $_ENV['FUSIONAUTH_CLIENT_ID'] ?? getenv('FUSIONAUTH_CLIENT_ID');
$fusionAuthClientSecret = $_ENV['FUSIONAUTH_CLIENT_SECRET'] ?? getenv('FUSIONAUTH_CLIENT_SECRET');
$fusionAuthServerUrl = $_ENV['FUSIONAUTH_SERVER_URL'] ?? getenv('FUSIONAUTH_SERVER_URL');
$fusionAuthBrowserUrl = $_ENV['FUSIONAUTH_BROWSER_URL'] ?? getenv('FUSIONAUTH_BROWSER_URL');
$fusionAuthRedirectUrl = $_ENV['FUSIONAUTH_REDIRECT_URL'] ?? getenv('FUSIONAUTH_REDIRECT_URL');
$provider = new \JerryHopper\OAuth2\Client\Provider\FusionAuth([
'clientId' => $fusionAuthClientId,
'clientSecret' => $fusionAuthClientSecret,
'redirectUri' => $fusionAuthRedirectUrl,
'urlAuthorize' => $fusionAuthBrowserUrl . '/oauth2/authorize',
'urlAccessToken' => $fusionAuthServerUrl . '/oauth2/token',
'urlResourceOwnerDetails' => $fusionAuthServerUrl . '/oauth2/userinfo',
]);
return $provider;
}
function redirectToAccountPageIfAlreadyLoggedIn() {
if (isset($_SESSION['id'])) {
header('Location: account.php');
exit;
}
}
function redirectUserToFusionAuthIfNotLoggedIn($provider) {
if (isset($_GET['code']))
return;
$options = [
'scope' => ['openid email profile']
];
$authUrl = $provider->getAuthorizationUrl($options);
$_SESSION['oauth2state'] = $provider->getState();
header('Location: '.$authUrl);
exit;
}
function checkCSRFToken() {
if (empty($_GET['state']) || (!\hash_equals($_SESSION['oauth2state'], $_GET['state']))) {
unset($_SESSION['oauth2state']);
exit('Invalid CSRF state');
}
}
function handleFusionAuthCallback($provider) {
$token = $provider->getAccessToken('authorization_code', ['code' => $_GET['code']]);
try {
$user = $provider->getResourceOwner($token);
$userArray = $user->toArray();
$email = $user->getEmail();
$name = $userArray['given_name'];
session_regenerate_id();
$_SESSION['id'] = $user->getId();
$_SESSION['email'] = $email;
$_SESSION['name'] = $name;
header('Location: account.php');
}
catch (Exception $e) {
exit('Failed to get user details from FusionAuth');
}
}
This setup code:
.env
file.Next comes the FusionAuth handlers. Starting from the getFusionAuthProvider()
function, the code:
PHP automatically links the user’s session to their browser by returning a cookie for the site, which is then included in every subsequent request.
The login page handles both the initial user request to start the login process and the server callback request from FusionAuth to complete the authentication.
Logging out is much simpler. Create a logout.php
file in the public
directory and insert the following code.
<?php
logOut();
function logOut() {
session_start();
session_unset();
session_destroy();
header('Location: index.php');
exit;
}
This code terminates the session on the server for this user and redirects them back to the home page.
Now that authentication is done, the last task is to create example pages that a user can browse.
Create a static
directory within your-application/public
directory.
mkdir public/static
Copy images from the example app.
cp ../complete-application/public/static/money.jpg public/static/money.jpg
cp ../complete-application/public/static/changebank.svg public/static/changebank.svg
Create a changebank.css
stylesheet file in your-application/public/static
directory and add the following code to it.
h1 {
color: #096324;
}
h3 {
color: #096324;
margin-top: 20px;
margin-bottom: 40px;
}
a {
color: #096324;
}
p {
font-size: 18px;
}
.header-email {
color: #096324;
margin-right: 20px;
}
.fine-print {
font-size: 16px;
}
body {
font-family: sans-serif;
padding: 0px;
margin: 0px;
}
.h-row {
display: flex;
align-items: center;
}
#page-container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
#page-header {
flex: 0;
display: flex;
flex-direction: column;
}
#logo-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
}
.menu-bar {
display: flex;
flex-direction: row-reverse;
align-items: center;
height: 35px;
padding: 15px 50px 15px 30px;
background-color: #096324;
font-size: 20px;
}
.menu-link {
font-weight: 600;
color: #FFFFFF;
margin-left: 40px;
}
.menu-link {
font-weight: 600;
color: #FFFFFF;
margin-left: 40px;
}
.inactive {
text-decoration-line: none;
}
.button-lg {
width: 150px;
height: 30px;
background-color: #096324;
color: #FFFFFF;
font-size: 16px;
font-weight: 700;
border-radius: 10px;
text-align: center;
padding-top: 10px;
text-decoration-line: none;
}
.column-container {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.content-container {
flex: 1;
display: flex;
flex-direction: column;
padding: 60px 20px 20px 40px;
}
.balance {
font-size: 50px;
font-weight: 800;
}
.change-label {
font-size: 20px;
margin-right: 5px;
}
.change-input {
font-size: 20px;
height: 40px;
text-align: end;
padding-right: 10px;
}
.change-submit {
font-size: 15px;
height: 40px;
margin-left: 15px;
border-radius: 5px;
}
.change-message {
font-size: 20px;
margin-bottom: 15px;
}
.error-message {
font-size: 20px;
color: #FF0000;
margin-bottom: 15px;
}
.app-container {
flex: 0;
min-width: 440px;
display: flex;
flex-direction: column;
margin-top: 40px;
margin-left: 80px;
}
.change-container {
flex: 1;
}
Next, you’ll create three more pages in the your-application/public
directory. First create the home page, index.php
, and paste the following code into it.
<html lang="en">
<head>
<meta charset="utf-8" />
<title>FusionAuth OpenID and PKCE example</title>
<link rel="stylesheet" href="static/changebank.css">
</head>
<body>
<div id="page-container">
<div id="page-header">
<div id="logo-header">
<img src="static/changebank.svg" alt="logo"/>
<a class="button-lg" href="login.php">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.php">log in or create a new account</a>.</p>
</div>
</div>
<div style="flex: 0;">
<img src="static/money.jpg" style="max-width: 800px;" alt="coins"/>
</div>
</div>
</div>
</div>
</body>
</html>
The index page contains nothing to note except a link to the login page <a href="login.php">
.
Next, create an account.php
file and paste the following code into it.
<?php
verifySession();
function verifySession() {
session_start();
if (!isset($_SESSION['id'])) {
header('Location: login.php');
exit;
}
}
?>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>FusionAuth OpenID and PKCE example</title>
<link rel="stylesheet" href="static/changebank.css">
</head>
<body>
<div id="page-container">
<div id="page-header">
<div id="logo-header">
<img src="static/changebank.svg" alt="logo"/>
<div class="h-row">
<p class="header-email"><?= $_SESSION['email'] ?></p>
<a class="button-lg" href="logout.php" onclick="">Logout</a>
</div>
</div>
<div id="menu-bar" class="menu-bar">
<a class="menu-link inactive" href="change.php">Make Change</a>
<a class="menu-link" href="account.php">Account</a>
</div>
</div>
<div style="flex: 1;">
<div class="column-container">
<div class="app-container">
<h3>Your balance</h3>
<div class="balance">$0.00</div>
</div>
</div>
</div>
</body>
</html>
The account page displays the user’s email from FusionAuth with <p class="header-email"><?= $_SESSION['email'] ?></p>
.
The account page is only visible to logged in users. If a session Id is not found, the user is redirected to login.
Finally, create a change.php
file and paste the following code into it.
<?php
verifySession();
handleCSRFToken();
$state = calculateChange();
function verifySession() {
session_start();
if (!isset($_SESSION['id'])) {
header('Location: login.php');
exit;
}
}
function handleCSRFToken() {
if ($_SERVER['REQUEST_METHOD'] === 'GET')
$_SESSION["csrftoken"] = bin2hex(random_bytes(32));
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !\hash_equals($_SESSION["csrftoken"], $_POST["csrftoken"]))
exit;
elseif ($_SERVER['REQUEST_METHOD'] !== 'POST' && $_SERVER['REQUEST_METHOD'] !== 'GET')
exit;
}
function calculateChange(): array {
if ($_SERVER['REQUEST_METHOD'] !== 'POST')
return [];
$amount = $_POST["amount"];
$state = [
'iserror' => 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['iserror'] = !preg_match('/^(\d+(\.\d*)?|\.\d+)$/', $amount);
return $state;
}
?>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>FusionAuth OpenID and PKCE example</title>
<link rel="stylesheet" href="static/changebank.css">
</head>
<body>
<div id="page-container">
<div id="page-header">
<div id="logo-header">
<img src="static/changebank.svg" alt="logo"/>
<div class="h-row">
<p class="header-email"><?= $_SESSION['email'] ?></p>
<a class="button-lg" href="logout.php" onclick="">Logout</a>
</div>
</div>
<div id="menu-bar" class="menu-bar">
<a class="menu-link" href="change.php">Make Change</a>
<a class="menu-link inactive" href="account.php">Account</a>
</div>
</div>
<div style="flex: 1;">
<div class="column-container">
<div class="app-container change-container">
<h3>We Make Change</h3>
<!-- GET REQUEST ------------------------------------------------>
<?php if ($_SERVER['REQUEST_METHOD'] === 'GET'): ?>
<div class="change-message">Please enter a dollar amount:</div>
<form method="post" action="change.php">
<input type="hidden" name="csrftoken" value="<?= $_SESSION["csrftoken"] ?>" />
<div class="h-row">
<div class="change-label">Amount in USD: $</div>
<input class="change-input" name="amount" value="" />
<input class="change-submit" type="submit" value="Make Change" />
</div>
</form>
<?php else: ?>
<!-- POST REQUEST ----------------------------------------------->
<?php if ($state['iserror']): ?>
<div class="error-message">Please enter a dollar amount:</div>
<?php else: ?>
<div class="change-message">
We can make change for <?= $state['total'] ?> with <?= $state['nickels'] ?> nickels and <?= $state['pennies'] ?> pennies!
</div>
<?php endif; ?>
<form method="post" action="change.php">
<input type="hidden" name="csrftoken" value="<?= $_SESSION["csrftoken"] ?>" />
<div class="h-row">
<div class="change-label">Amount in USD: $</div>
<input class="change-input" name="amount" value="<?= htmlspecialchars($_POST["amount"]) ?>" />
<input class="change-submit" type="submit" value="Make Change" />
</div>
</form>
<?php endif; ?>
</div>
</div>
</div>
</body>
</html>
In addition to verifying login, the code at the top of the change page checks if the CSRF token is valid on POST requests. The CSRF token is hidden in the form in the HTML, <input type="hidden" name="csrftoken" value="<?= $_SESSION["csrftoken"] ?>" />
. Although any attacker can make a POST request on behalf of a logged in user, a server will reject a GET request if it’s not made from the same origin (URL). Including this CSRF token in your page requires an attacker to make a GET request to obtain it before making the POST request, which isn’t possible.
The calculateChange()
method takes the amount given in the form and returns a $state
array with the amount in nickels and pennies.
The HTML at the bottom of the file displays a blank form when the page first loads (GET) or the result of the calculation when returning (POST).
From your-application
directory run the following command to serve the application using the built-in PHP server.
php -S localhost:9012 -t public
Browse to the app at http://localhost:9012. Log in using richard@example.com
and password
. The change page allows you to enter a number.
This quickstart is a great way to get a proof of concept up and running quickly, but to run your application in production, there are some things you're going to want to do.
FusionAuth gives you the ability to customize just about everything to do with the user's experience and the integration of your application. This includes:
Ensure FusionAuth is running in the Docker container. You should be able to log in as the admin user admin@example.com
with the password password
at http://localhost:9011/admin.
Open the app in an incognito browser window or clear your browser cache and cookies data.
Browse to the home page, log out, and try to log in again. If that still doesn’t work, delete and restart all the containers.
You can always pull down a complete running application and compare what’s different.
git clone https://github.com/FusionAuth/fusionauth-quickstart-php-web.git
cd fusionauth-quickstart-php-web
docker compose up
cd complete-application
composer install
php -S localhost:9012 -t public
Browse to the app at http://localhost:9012.