web

PHP

PHP

In this quickstart, you are going to build an application with PHP 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-web.

When developing PHP web applications, it’s advisable to consider using a framework. Frameworks provide a structured environment, streamlining development and enhancing code organization. With built-in security features, community support, and time-saving tools, frameworks offer a robust foundation for efficient and maintainable projects.

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 PHP application.

Clone The Code

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.

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 PHP Application

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

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;
    $authUrl = $provider->getAuthorizationUrl();
    $_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:

  • Prevents errors from being displayed in the browser, for security.
  • Loads all vendor modules into PHP so you can use them.
  • Reads your variables from the .env file.
  • Starts a new session using HTTP (not HTTPS since you’re on localhost) and ensures your user Id cookie will not be accessible by JavaScript.

Next comes the FusionAuth handlers. Starting from the getFusionAuthProvider() function, the code:

  • Creates a FusionAuth provider with your environment variables.
  • Sends the user to the account page if they already have a login cookie.
  • Sends the user to the FusionAuth URL if they are not logged in.
  • Checks the user’s FusionAuth token if they are not logged in but are returning from FusionAuth.
  • Gets the user details from the FusionAuth token and saves them to the server-side session. This also generates a new session or cookie Id for the user for security. The user is then redirected to their account page.

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.

Customization

Now that authentication is done, the last task is to create example pages that a user can browse.

CSS And HTML

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

Run The Application

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.

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

PHP 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 the password password at http://localhost:9011/admin.

  • I get “Your browser sent a request that this server could not understand. Size of a request header field exceeds server limit”.

Open the app in an incognito browser window or clear your browser cache and cookies data.

  • PHP says there is an invalid state exception.

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.

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