web

Drupal

Drupal

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

Prerequisites

This app has been tested with Drupal 10.1.5. This example should work with other compatible versions of Drupal 10. Drupal 10 requires PHP 8.1.0+ and MariaDB 10.3.7+, MySQL 5.7.8+, or Percona 5.7.8+.

General Architecture

By default, Drupal has a built-in login and authentication system. As such, the application will resemble the following 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

In this section, you’ll get FusionAuth up and running on your machine and create a Drupal application.

Clone The Code

First off, grab the code from the repository and change into that directory.

git clone https://github.com/FusionAuth/fusionauth-quickstart-php-drupal-web.git
cd fusionauth-quickstart-php-drupal-web

Create Your Drupal Application

The complete-application directory in the repository already contains a complete Drupal application but for demonstration purposes, this tutorial shows how to create a new application from scratch and then switch the volumes in the docker-compose.yml file to point to the new application.

Start by creating a new Drupal 10 project and changing to the project directory with the following command.

composer create-project drupal/recommended-project:10.1.5 "your-application" && cd your-application

Next, make two changes in the docker-compose.yml file to allow you to use your new Drupal application.

If you want to use the existing application at any point, revert the following two changes and then rebuild the Docker containers and volumes.

On the web service, change volumes from ./complete-application:/opt/drupal:cached to ./your-application:/opt/drupal:cached.

Under the db2 service, change the key MYSQL_DATABASE from drupaldb to changebank. This updates the database name so that you are not using the same database as the existing complete application.

Save your changes and then run the following command to get the application up and running.

docker compose up -d

Once the application is up and running, navigate to http://localhost/ and you should be redirected to the Drupal installation wizard at http://localhost/core/install.php.

Run through the wizard and fill in all required fields. Make sure to enter the following.

On Step 1 , choose your language and click Save and continue.

On Step 2 , choose Standard profile and click Save and continue.

On Step 4 , enter the following database connection details.

  • Database type : Pick the MySQL, MariaDB, Percona Server, or equivalent option.
  • Database name : changebank.
  • Database username : drupal.
  • Database password : verybadpassword.
  • Expand Advanced options.
    • Set Host to db2.
    • Set the port number to 3307.

Click Save and continue.

On Step 6 , enter the following values.

  • Site name : Change Bank.
  • Site email address : admin@example.com.
  • Username : admin.
  • Password : verybadpassword.
  • Confirm password : verybadpassword.
  • Email address : admin@example.com.

Click Save and continue.

Once you’ve completed the installation wizard, you should be taken to the welcome page.

Now you can install OpenID Connect to handle authenticating with FusionAuth and the Admin Toolbar module to add a bit of quality of life when navigating the Drupal administration menu.

In a terminal, run the following command from the your-application directory.

composer require 'drupal/openid_connect:3.x-dev@dev' 'drupal/admin_toolbar:^3.4'

Enabling Extensions Via UI

You can enable the modules by visiting the extensions page of your Drupal application at http://localhost/admin/modules. Search for “OpenID Connect” and “Admin Toolbar” using the Filter field. Select the module checkboxes and click the Install button.

Enabling Extensions With Drush

Alternatively, you can use Drush to enable the modules. Drush is a command line shell and Unix scripting interface for Drupal, available as a composer package. Install it by running the following command.

composer require drush/drush

You might need to restart your containers for the changes to take effect. You can do so by running the following command.

docker compose restart

Once the containers are up and running, connect to the Drupal container terminal to run Drush commands.

docker compose exec web bash

Run the following command to enable the modules.

vendor/bin/drush pm-enable openid_connect admin_toolbar

Confirm with yes when prompted.

Do you want to continue? (yes/no) [yes]:
yes

You can use Drush to run many commands and perform common tasks in Drupal. Other popular commands include:

  • vendor/bin/drush cr clears the cache.
  • vendor/bin/drush pm-enable <module name> enables a module.
  • vendor/bin/drush updb updates the database.
  • vendor/bin/drush uli generates a one-time login link for a user.

For the full list of Drush commands, visit https://www.drush.org/12.2.0/commands/all/.

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.

Authentication

To set up your Drupal application to use FusionAuth for authentication, head to http://localhost/admin/config/people/openid-connect on your Drupal site.

Add a new client by clicking on + Generic OAuth 2.0 and fill in the details as follows:

  • Name : FusionAuth
  • Client ID : e9fdb985-9173-4e01-9d73-ac2d60d1dc8e
  • Client secret : super-secret-secret-that-should-be-regenerated-for-production
  • Authorization endpoint : http://host.docker.internal:9011/oauth2/authorize
  • Token endpoint : http://host.docker.internal:9011/oauth2/token
  • UserInfo endpoint : http://host.docker.internal:9011/oauth2/userinfo

You may need to update your hosts file to include an entry for 127.0.0.1 host.docker.internal if it’s not already present. On macOS and Linux you can achieve this by running the following command:

echo "127.0.0.1 host.docker.internal" | sudo tee -a /etc/hosts

Scroll down and click Create OpenID Connect client.

While on the page that lists your newly created client, click the Settings tab. Under the section OpenID buttons display in user login form , change the value from Hidden to Below to display the “Login with FusionAuth” button on the login page.

Scroll down and click Save configuration to save the settings.

Before you test the authentication flow, you need to make a small change to allow visitors to create accounts on the Drupal application. Navigate to http://localhost/admin/config/people/accounts and change Who can register accounts? to Visitors and uncheck Require email verification when a visitor creates an account . Save the configuration.

You need to flush the cache for the changes to take effect. You can flush the cache in Drupal in two ways:

Test Authentication

Now you can test the authentication flow.

In the browser, navigate to http://localhost/user/logout to log out of the Drupal application. Navigate to http://localhost/user/login and click on the Login with FusionAuth button.

You should be taken to the FusionAuth login page. Log in with the username richard@example.com and password password.

You should be redirected back to the Drupal application once logged in.

Customization

In this section, you’ll turn your application into a trivial banking application with some styling.

You’re going to create a module called “Changebank” that will handle the logic for making change from dollars, and add a couple of blocks to the application.

Generate Changebank Module

In your terminal, run the following command in the Drupal Docker container.

vendor/bin/drush generate module

You will be presented with the module creation wizard. Fill it out as follows:

Welcome to module generator!

Module name:
Changebank

Module machine name [changebank]:
changebank

Module description:
Provides a way to make change from dollars.

Package [Custom]:
Custom

Dependencies (comma separated):

Would you like to create module file [No]:
Yes

Would you like to create install file [No]:
No

Would you like to create README.md file [No]:
No

The following directories and files have been created or updated:

- /opt/drupal/web/modules/changebank/changebank.info.yml
- /opt/drupal/web/modules/changebank/changebank.module

Now you will expand on the module by generating two blocks, a form, a controller, and a routing file.

To generate the first block, run the following command and complete the wizard.

vendor/bin/drush generate block

You will be presented with the block creation wizard. Fill it out as follows:

Welcome to block generator!

Module machine name:
changebank

Block admin label:
User Email Block

Plugin ID [changebank_user_email_block]:
useremailblock

Plugin class [UseremailblockBlock]:
UserEmailBlock

Block category [Custom]:
Custom

Make the block configurable? [No]:
No

Would you like to inject dependencies? [No]:
No

Create access callback? [No]:
No

The following directories and files have been created or updated:

- /opt/drupal/web/modules/changebank/src/Plugin/Block/UserEmailBlock.php

Generate the second block with the same block generation command as before.

vendor/bin/drush generate block

Enter the following information:

Welcome to block generator!

Module machine name:
changebank

Block admin label:
User Login Logout Block

Plugin ID [changebank_user_login_logout_block]:
userloginlogoutlock

Plugin class [UserloginlogoutlockBlock]:
UserLoginLogoutBlock

Block category [Custom]:
Custom

Make the block configurable? [No]:
No

Would you like to inject dependencies? [No]:
No

Create access callback? [No]:
No

The following directories and files have been created or updated:

- /opt/drupal/web/modules/changebank/src/Plugin/Block/UserLoginLogoutBlock.php

To generate the form, run the following command.

vendor/bin/drush generate form

You will be presented with the form wizard. Fill it out as follows:

Welcome to form generator!

Module machine name:
changebank

Module name [Changebank]:
Changebank

Class [ExampleForm]:
ChangebankForm

Would you like to create a route for this form? [Yes]:
No

The following directories and files have been created or updated:

- /opt/drupal/web/modules/changebank/src/Form/ChangebankForm.php

Next, generate a controller with the following command.

vendor/bin/drush generate controller

You will be presented with the controller wizard. Fill it out as follows:

Welcome to controller generator!

Module machine name:
changebank

Module name [Changebank]:
Changebank

Class [ChangebankController]:
ChangebankController

Would you like to inject dependencies? [No]:
No

Would you like to create a route for this controller? [Yes]:
No

The following directories and files have been created or updated:

- /opt/drupal/web/modules/changebank/src/Controller/ChangebankController.php

Finally, generate a routing file with the following command.

vendor/bin/drush generate routing

You will be presented with the routing wizard. Fill it out as follows:

Module machine name:
changebank

Module name [Changebank]:
Changebank

The following directories and files have been created or updated:

- /opt/drupal/web/modules/changebank/changebank.routing.yml

Customizing The Module

Now that you have all the files you need in your custom module, you can customize them.

You will be editing the following files:

  • ChangebankForm.php
  • ChangebankController.php
  • changebank.routing.yml
  • UserEmailBlock.php
  • UserLoginLogoutBlock.php
  • changebank.module

Customize The Changebank Form

In your text editor, select the ChangebankForm.php file located at your-application/web/modules/changebank/src/Form/ChangebankForm.php. Clear the existing boilerplate contents and add the following code.

<?php

declare(strict_types = 1);

namespace Drupal\changebank\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;

/**
 * Provides a form for creating change from an amount in US dollars.
 */
class ChangebankForm extends FormBase {

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'changebank_form';
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $result = $form_state->get('result');

    $form['result_wrapper'] = [
      '#type' => 'container',
      '#attributes' => [
        'class' => ['changebank-result-wrapper'],
      ],
    ];

    if ($form_state->get('result')) {
      $form['result_wrapper']['result'] = [
        '#markup' => $result,
      ];
    }

    $form['amount'] = [
      '#type' => 'number',
      '#title' => $this->t('Amount in USD: $'),
      '#required' => TRUE,
      '#min' => 0,
      '#step' => 0.01,
    ];
    $form['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Make Change'),
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $amount = $form_state->getValue('amount');

    $amount_in_cents = round($amount * 100);

    $quarters = floor($amount_in_cents / 25);
    $amount_in_cents %= 25;

    $dimes = floor($amount_in_cents / 10);
    $amount_in_cents %= 10;

    $nickels = floor($amount_in_cents / 5);
    $amount_in_cents %= 5;

    $pennies = $amount_in_cents;

    $coins = [];
    if ($quarters > 0) {
      $coins[] = "@quarters quarters";
    }
    if ($dimes > 0) {
      $coins[] = "@dimes dimes";
    }
    if ($nickels > 0) {
      $coins[] = "@nickels nickels";
    }
    if ($pennies > 0) {
      $coins[] = "@pennies pennies";
    }

    $result = $this->t('We can make change for $@dollars with ' . implode(', ', $coins) . '!', [
      '@dollars' => $amount,
      '@quarters' => $quarters,
      '@dimes' => $dimes,
      '@nickels' => $nickels,
      '@pennies' => $pennies,
    ]);

    $form_state->setRebuild(TRUE);
    $form_state->set('result', $result);

    \Drupal::state()->set('changebank_form_value', $result);
  }

}

Customize The Controller

In your text editor, select the ChangebankController.php file located at your-application/web/modules/changebank/src/Controller/ChangebankController.php and replace the content with the following code.

<?php

declare(strict_types = 1);

namespace Drupal\changebank\Controller;

use Drupal\Core\Controller\ControllerBase;

/**
 * Returns responses for changebank routes.
 */
class ChangebankController extends ControllerBase {

  /**
   * Builds the response.
   */
  public function build() {
    $build = [];
    $form = $this->formBuilder()->getForm('\Drupal\changebank\Form\ChangebankForm');
    $build['form'] = $form;
    return $build;
  }

  /**
   * Displays the form submission value on the account page.
   */
  public function accountBalance() {
    $value = \Drupal::state()->get('changebank_form_value');
    $arguments = ($value) ? $value->getArguments() : [];
    $dollars = $arguments['@dollars'] ?? 0;
    return [
      '#markup' => '<span class="account-balance">$' . $dollars . '</span>',
      '#cache' => [
        'contexts' => [
          'session',
        ],
        'max-age' => 0,
      ],
    ];
  }

}

Customize The Routing

Next you’ll set up two routes, one for the form and one for the account page.

Select the changebank.routing.yml file located at your-application/web/modules/changebank/changebank.routing.yml and replace the content with the following configuration.

changebank.makechange:
  path: '/makechange'
  defaults:
    _title: 'We Make Change'
    _controller: '\Drupal\changebank\Controller\ChangebankController::build'
    _form: 'Drupal\changebank\Form\ChangebankForm'
  requirements:
    _user_is_logged_in: 'TRUE'

changebank.account:
  path: '/account'
  defaults:
    _title: 'Your Balance'
    _controller: '\Drupal\changebank\Controller\ChangebankController::accountBalance'
  requirements:
    _user_is_logged_in: 'TRUE'

Customize The User Email Block

You’ll use the User Email Block to display the current user’s email address in a block. Select the UserEmailBlock.php file located at your-application/web/modules/changebank/src/Plugin/Block/UserEmailBlock.php and replace the content with the following code.

<?php

declare(strict_types = 1);

namespace Drupal\changebank\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provides a block that displays the current user's email address.
 *
 * @Block(
 *   id = "user_email_block",
 *   admin_label = @Translation("User Email Block")
 * )
 */
class UserEmailBlock extends BlockBase implements ContainerFactoryPluginInterface {

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $currentUser;

  /**
   * Constructs a new UserEmailBlock object.
   *
   * @param array $configuration
   *   The block configuration.
   * @param string $plugin_id
   *   The block plugin ID.
   * @param mixed $plugin_definition
   *   The block plugin definition.
   * @param \Drupal\Core\Session\AccountInterface $current_user
   *   The current user.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, AccountInterface $current_user) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->currentUser = $current_user;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('current_user')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function build() {
    $email = $this->currentUser->getEmail();
    return [
      '#markup' => $this->t('@email', ['@email' => $email]),
    ];
  }

}

Customize The User Login And Logout Block

You’ll use the User Login And Logout Block to display a login button for anonymous users and a logout button for authenticated users.

Select the UserLoginLogoutBlock.php file located at your-application/web/modules/changebank/src/Plugin/Block/UserLoginLogoutBlock.php and replace the content with the following code.

<?php

declare(strict_types = 1);

namespace Drupal\changebank\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provides a block that displays linked text for anonymous users.
 *
 * @Block(
 *   id = "user_login_logout_block",
 *   admin_label = @Translation("User Login Logout Block")
 * )
 */
class UserLoginLogoutBlock extends BlockBase implements ContainerFactoryPluginInterface {

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $currentUser;

  /**
   * Constructs a new UserLoginLogoutBlock object.
   *
   * @param array $configuration
   *   The block configuration.
   * @param string $plugin_id
   *   The block plugin ID.
   * @param mixed $plugin_definition
   *   The block plugin definition.
   * @param \Drupal\Core\Session\AccountInterface $current_user
   *   The current user.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, AccountInterface $current_user) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->currentUser = $current_user;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('current_user')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function build() {
    $roles = $this->currentUser->getRoles();
    if (in_array('anonymous', $roles)) {
      return [
        '#markup' => $this->t('<button class="btn btn-primary"><a href="/user/login">Log in</a></button>'),
      ];
    }
    else {
        return [
            '#markup' => $this->t('<button class="btn btn-primary"><a href="/user/logout">Logout</a></button>'),
        ];
    }
  }

}

Customize The changebank.module File

To make the blocks available to the application, you need to make some changes to the changebank.module file located at your-application/web/modules/changebank/changebank.module. Add the following.

<?php

declare(strict_types = 1);

/**
 * @file
 * Primary module hooks for changebank module.
 */

/**
 * Implements hook_block_info().
 */
function changebank_block_info() {
    $blocks = [];

    $blocks['user_email_block'] = [
      'info' => t('User Email Block'),
      'cache' => DRUPAL_CACHE_PER_ROLE,
    ];

    $blocks['user_login_logout_block'] = [
      'info' => t('User Login Logout Block'),
      'cache' => DRUPAL_CACHE_PER_ROLE,
    ];

    return $blocks;
  }

Enable The Changebank Module

To enable the Changebank module, navigate to http://localhost/admin/modules while logged in as admin and select the checkbox next to the Changebank module item. Click the Install button.

You can also enable the module by running the following command in the Drupal Docker container.

vendor/bin/drush pm-enable changebank

Add Styling

In this section, you’re going to add a theme to the application, along with a couple of templates and some basic styling.

Download A Theme

A theme called “Barrio Bootstrap 5” will give you a good starting point for styling the application. Run the following command from a terminal in the your-application directory to add the theme to the project.

composer require 'drupal/bootstrap_barrio:^5.5'

Create A Subtheme

Next, you’ll create a subtheme of the Barrio Bootstrap 5 theme.

You will notice a folder called subtheme in the your-application/web/themes/contrib/bootstrap_barrio directory.

First, create a directory called custom in your-application, and a directory called subtheme in custom.

mkdir -p web/themes/custom/subtheme

Copy the your-application/web/themes/contrib/bootstrap_barrio/subtheme directory into your-application/web/themes/custom/subtheme.

cp -r web/themes/contrib/bootstrap_barrio/subtheme/* web/themes/custom/subtheme

Enter the subtheme folder and edit the bootstrap_barrio_subtheme.info.yml file. Change the name of the theme from Bootstrap Barrio Subtheme to Changebank, and change the description from Basic structure for a Bootstrap Barrio SubTheme to Subtheme for Changebank.

Enable The Theme

To enable the theme, navigate to http://localhost/admin/appearance and click the Install button for both “Bootstrap Barrio 5.5.14” and “Changebank 5.5.14”.

Click the Set as default button for “Changebank 5.5.14”.

While you are here, let’s add the Changebank logo to the application.

Click Settings on Changebank 5.5.14, then scroll down and click Logo image. Uncheck the Use the logo supplied by the theme checkbox, and upload the Changebank logo in the Upload logo image field. Save the configuration.

Install And Enable Twig Tweak

The Twig Tweak module will make it easier to render content in the templates you create later. Twig Tweak is a small module that provides a Twig extension with some useful functions and filters that can improve the developer experience.

Run the following command in the your-application directory to download the module.

composer require 'drupal/twig_tweak:^3.2'

Enable the module by navigating to http://localhost/admin/modules, selecting the checkbox next to Twig Tweak, and clicking the Install button.

You can also enable the module with the following command in the Drupal Docker container.

vendor/bin/drush pm-enable twig_tweak

Add Templates

In the subtheme directory of your application, you’ll add a couple of templates to override the default templates provided by the Bootstrap Barrio theme. This will allow you to customize the markup of the application.

The page--front.html.twig template file you will create uses an image that is currently not in your application. To copy the image to your application, run the following command from your-application directory.

cp ../complete-application/web/sites/default/files/money-new.jpg  web/sites/default/files/money-new.jpg

Navigate to your-application/web/themes/custom/subtheme/templates/. If the templates directory is not there, you may need to create it.

In the templates directory, create the following files:

  • page--front.html.twig
  • block--system-branding-block.html.twig
  • region--secondary-menu.html.twig

In the page template for the front page (called page--front.html.twig), add the following code.

{% extends "@bootstrap_barrio/layout/page.html.twig" %}

{#
/**
 * @file
 * Bootstrap Barrio's theme implementation to display a single page.
 *
 * The doctype, html, head and body tags are not in this template. Instead they
 * can be found in the html.html.twig template normally located in the
 * core/modules/system directory.
 *
 * Available variables:
 *
 * General utility variables:
 * - base_path: The base URL path of the Drupal installation. Will usually be
 *   "/" unless you have installed Drupal in a sub-directory.
 * - is_front: A flag indicating if the current page is the front page.
 * - logged_in: A flag indicating if the user is registered and signed in.
 * - is_admin: A flag indicating if the user has permission to access
 *   administration pages.
 *
 * Site identity:
 * - front_page: The URL of the front page. Use this instead of base_path when
 *   linking to the front page. This includes the language domain or prefix.
 * - logo: The url of the logo image, as defined in theme settings.
 * - site_name: The name of the site. This is empty when displaying the site
 *   name has been disabled in the theme settings.
 * - site_slogan: The slogan of the site. This is empty when displaying the site
 *   slogan has been disabled in theme settings.

 * Page content (in order of occurrence in the default page.html.twig):
 * - node: Fully loaded node, if there is an automatically-loaded node
 *   associated with the page and the node ID is the second argument in the
 *   page's path (e.g. node/12345 and node/12345/revisions, but not
 *   comment/reply/12345).
 *
 * Regions:
 * - page.top_header: Items for the top header region.
 * - page.top_header_form: Items for the top header form region.
 * - page.header: Items for the header region.
 * - page.header_form: Items for the header form region.
 * - page.highlighted: Items for the highlighted region.
 * - page.primary_menu: Items for the primary menu region.
 * - page.secondary_menu: Items for the secondary menu region.
 * - page.featured_top: Items for the featured top region.
 * - page.content: The main content of the current page.
 * - page.sidebar_first: Items for the first sidebar.
 * - page.sidebar_second: Items for the second sidebar.
 * - page.featured_bottom_first: Items for the first featured bottom region.
 * - page.featured_bottom_second: Items for the second featured bottom region.
 * - page.featured_bottom_third: Items for the third featured bottom region.
 * - page.footer_first: Items for the first footer column.
 * - page.footer_second: Items for the second footer column.
 * - page.footer_third: Items for the third footer column.
 * - page.footer_fourth: Items for the fourth footer column.
 * - page.footer_fifth: Items for the fifth footer column.
 * - page.breadcrumb: Items for the breadcrumb region.
 *
 * Theme variables:
 * - navbar_top_attributes: Items for the header region.
 * - navbar_attributes: Items for the header region.
 * - content_attributes: Items for the header region.
 * - sidebar_first_attributes: Items for the highlighted region.
 * - sidebar_second_attributes: Items for the primary menu region.
 *
 * @see template_preprocess_page()
 * @see bootstrap_barrio_preprocess_page()
 * @see html.html.twig
 */
#}
{# see https://www.drupal.org/project/drupal/issues/953034#comment-14192130 #}
{% set sidebar_first_exists = page.sidebar_first|render|striptags('<img><video><audio><drupal-render-placeholder>')|trim is not empty %}
{% set sidebar_second_exists = page.sidebar_second|render|striptags('<img><video><audio><drupal-render-placeholder>')|trim is not empty %}

{% block head %}
        {% if page.secondary_menu or page.top_header or page.top_header_form %}
          <nav{{ navbar_top_attributes }}>
          {% if container_navbar %}
          <div class="container">
          {% endif %}
              {{ page.secondary_menu }}
              {{ page.top_header }}
              {% if page.top_header_form %}
                <div class="form-inline navbar-form float-right">
                  {{ page.top_header_form }}
                </div>
              {% endif %}
          {% if container_navbar %}
          </div>
          {% endif %}
          </nav>
        {% endif %}
        <nav{{ navbar_attributes }}>
          {% if container_navbar %}
          <div class="container">
          {% endif %}
            {{ page.header }}
            {% if page.primary_menu or page.header_form %}
              <button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#CollapsingNavbar" aria-controls="CollapsingNavbar" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button>
              <div class="collapse navbar-collapse" id="CollapsingNavbar">
                {{ page.primary_menu }}
                {% if page.header_form %}
                  <div class="form-inline navbar-form float-right">
                    {{ page.header_form }}
                  </div>
                {% endif %}
	          </div>
            {% endif %}
            {% if sidebar_collapse %}
              <button class="navbar-toggler navbar-toggler-left collapsed" type="button" data-toggle="collapse" data-target="#CollapsingLeft" aria-controls="CollapsingLeft" aria-expanded="false" aria-label="Toggle navigation"></button>
            {% endif %}
          {% if container_navbar %}
          </div>
          {% endif %}
        </nav>
{% endblock %}

{% block content %}
        <div id="main" class="container-fluid">
          {{ page.breadcrumb }}
          <div class="row row-offcanvas row-offcanvas-left clearfix">
              <main{{ content_attributes }}>
                <section class="section">
                  <a href="#main-content" id="main-content" tabindex="-1"></a>
                  <div class="row">
                    <div class="col col-sm-3 welcome">
                      <div class="sidebar" style="text-align: center;">
                        <h1 style="color: #096324; font-weight: 700;">Welcome to Changebank</h1>
                        <p><strong>To get started, <a href="/user/login" style="color: #096324;">log in</a> or<br/> <a href="/user/login" style="color: #096324;">create a new account</a>.</strong></p>
                      </div>
                    </div>
                    <div class="col col-sm-9" style="height: calc(100vh - 148px); overflow: hidden;">
                      <img src="/sites/default/files/money-new.jpg" alt="Image" width="100%" height="auto">
                    </div>
                  </div>
                  {# {{ page.content }} #}
                </section>
              </main>
            {% if sidebar_first_exists %}
              <div{{ sidebar_first_attributes }}>
                <aside class="section" role="complementary">
                  {{ page.sidebar_first }}
                </aside>
              </div>
            {% endif %}
            {% if sidebar_second_exists %}
              <div{{ sidebar_second_attributes }}>
                <aside class="section" role="complementary">
                  {{ page.sidebar_second }}
                </aside>
              </div>
            {% endif %}
          </div>
        </div>
{% endblock %}

Now add markup to create a custom layout for the site logo, the user email block, and the user login and logout block.

First, add the following to the region--secondary-menu.html.twig template.

{#
/**
 * @file
 * Theme override to display a region.
 *
 * Available variables:
 * - content: The content for this region, typically blocks.
 * - attributes: HTML attributes for the region div.
 * - region: The name of the region variable as defined in the theme's
 *   .info.yml file.
 *
 * @see template_preprocess_region()
 */
#}
{%
  set classes = [
    'region',
    'region-' ~ region|clean_class,
  ]
%}
{% if content %}
  <section{{ attributes.addClass(classes) }}>
    <div class="row">
      <div class="col col-xs-9">
        {{ drupal_block('system_branding_block') }}
      </div>
      <div class="col col-xs-3 d-flex justify-content-end login-container">
        <div class="email-container">
          {{ drupal_block('user_email_block') }}
        </div>
        <div class="button-container">
          {{ drupal_block('user_login_logout_block') }}
        </div>
      </div>
    </div>
    {# {{ content }} #}
  </section>
{% endif %}

If the user email and login and logout blocks do not display or appear broken, make sure that the Twig Tweak module is enabled as it provides the naming convention to embed the blocks in the template.

Now, as the code adds the System Branding block to the secondary menu region template, you need to manually hide the site name.

Add the following to the block--system-branding-block.html.twig template.

{#
/**
 * @file
 * Bootstrap Barrio's theme implementation for a branding block.
 *
 * Each branding element variable (logo, name, slogan) is only available if
 * enabled in the block configuration.
 *
 * Available variables:
 * - site_logo: Logo for site as defined in Appearance or theme settings.
 * - site_name: Name for site as defined in Site information settings.
 * - site_slogan: Slogan for site as defined in Site information settings.
 */
#}
{% set attributes = attributes.addClass('site-branding') %}
  {% if site_logo or site_name %}
    <a href="{{ path('<front>') }}" title="{{ 'Home'|t }}" rel="home" class="navbar-brand">
      {% if site_logo %}
        <img src="{{ site_logo }}" alt="{{ 'Home'|t }}" class="img-fluid d-inline-block align-top" />
      {% endif %}
      {# {{ site_name }} #}
    </a>
  {% endif %}
  {% if site_slogan %}
    <div class="d-inline-block align-top site-name-slogan">
      {{ site_slogan }}
    </div>
  {% endif %}

This code hides the site_name variable by commenting it out, but you can delete it if you prefer.

Customize The Stylesheet

Next you’ll add some styling to the application to make it look a little nicer. Select the style.css file located at your-application/web/themes/custom/subtheme/css/style.css and add the following.

/**
 * @file
 * Subtheme specific CSS.
 */

#navbar-top {
    background-color: #fff!important;
}

#navbar-main {
    background-color: #096324!important;
}

.sidebar h2 {
    margin: 0 0 0.5em;
    padding-bottom: 5px;
    text-shadow: 0 1px 0 #fff;
    font-size: 2.071em;
}

.sidebar .block {
    border-style: solid;
    border-width: 1px;
    padding: 15px 15px;
    margin: 0 0 20px;
    background-color: #fff!important;
    border-color: #fff!important;
}

.main-content.col {
    padding-right: 0px;
}

.welcome {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 320px;
}

body {
    overflow-x: hidden;
}

.site-footer {
    display: none;
}

#main-wrapper {
    height: calc(100vh - 148px);
}

.navbar {
    display: block;
}

.email-container {
    display: inline-block;
    vertical-align: middle;
    margin-right: 20px;
}

.button-container {
    display: inline-block;
    vertical-align: middle;
}

.login-container {
    align-items: center;
}

button,
.btn,
.btn-primary {
    background-color: #096324!important;
}

button a {
    color: #fff;
    font-weight: 700;
    text-decoration: none;
    padding-left: 30px;
    padding-right: 30px;
}

button a:hover {
    color: #fff;
    text-decoration: underline;
}

.block-user-email-block {
    color: #096324;
    font-weight: 400;
}

.navbar-brand img {
    padding: 5px 0px 5px 10px;
    height: 70px;
}

#block-bootstrap-barrio-subtheme-page-title {
    margin-top: 70px;
}

h1, h2, h3, h4, h5, h6 {
    color: #096324;
}

.nav-link {
    font-weight: 500;
    font-size: 20px;
}

.nav-item .active {
    text-decoration: underline;
}

.nav {
    margin-right: 7px;
}

.account-balance {
    font-size: 32px;
    font-weight: 700;
}

Add A Menu

Previously, you created the routes for the form and account page. Now you’ll add a menu to the application to make it easier to navigate between the two pages.

Go to http://localhost/admin/structure/menu in the administration menu and click Add menu.

Set Title to Changebank Menu and click Save.

Click Add link and enter the following:

  • Menu link title : Account
  • Link : /account

Scroll down and click Save.

Click on Add link again and enter the following:

  • Menu link title : Make Change
  • Link : /makechange

Scroll down and click Save.

Enable Blocks

Now that you have the User Email, User Login and Logout, and the Changebank Menu blocks, you need to enable them.

Navigate to http://localhost/admin/structure/block in the administration menu.

Enable The Changebank Menu Block

Scroll down to the Primary Menu section and click Place block.

  • Type Changebank Menu in the Filter field.
  • Click Place block to place the block in the region.
  • Uncheck the Display title checkbox.
  • Click on the Roles tab under Visibility and enable the Authenticated user role.
  • Click on Save block.

In your custom module, you set access to the account and makechange routes to be restricted to authenticated (logged in) users. You want to make sure that the menu is also only visible to authenticated users.

Enable The User Email Block

Scroll down to the Secondary Menu section and click Place block.

  • Type User Email Block in the Filter field.
  • Click Place block” to place the block in the region.
  • Uncheck the Display title checkbox.
  • Click the Roles tab under Visibility and enable the Authenticated user role.
  • Click on Save block.

Enable The User Login And Logout Block

Scroll down to the Secondary Menu section and click Place block.

  • Type User Login Logout Block in the Filter field.
  • Click Place block to place the block in the region.
  • Uncheck the Display title checkbox.
  • Leave the Roles tab under Visibility set to any role by not selecting any of the options (the visibility of the login and logout links is controlled in your custom module).
  • Click on Save block.

Default Blocks

By default, Drupal will have a Site Branding block, a Main Navigation block, and a User Account block.

You already have custom login and logout links, so you can remove the User account menu block. Click the dropdown arrow next to the User account menu block and select Remove.

To disable Breadcrumbs, click the dropdown arrow next to the Breadcrumbs block and select Disable.

The theme you used also has default blocks, like Search Form (wide) in the top header form and Search Form (narrow) in the sidebar first section. You don’t need search functionality in this application, so remove these blocks by clicking on the dropdown arrow next to each and selecting Remove.

Now let’s set the Main Navigation block to only be visible to anonymous users. Click the Configure link next to the Main Navigation block. Click the Roles tab under Visibility and select the Anonymous user role. Click Save block.

Move the Site Branding block to the Secondary Menu section by dragging the block and dropping it into the region above the other two blocks. Scroll down and click Save blocks.

Finally, configure the Site Branding block to only display the logo by unchecking the Site name and Site slogan checkboxes. Click Save block.

Set Login Redirect

Navigate to http://localhost/admin/config/people/openid-connect and click the Settings tab.

Since you have the /account route set up in your custom module, let’s make sure that the user is redirected to that page after logging in and not the standard user page.

Scroll down to the redirects section and enter account in the Login redirect field.

Save the configuration and clear the cache. Log out from the application.

Run The Application

You can now open the application at http://localhost/user/login.

Log in by clicking the Login with FusionAuth button and using the username richard@example.com and password password.

Made it this far? Get a free t-shirt, on us!

Thank you for spending some time getting familiar with FusionAuth.

*Offer only valid in the United States and Canada, while supplies last.

fusionauth tshirt

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

Drupal Customization

Drupal is a powerful CMS, and there are many ways to customize it. Here are a few things you may want to do:

  • Increase the functionality of the make change form, such as adding a field for the amount of change to make or specifying the type of change to make (quarters, dimes, nickels, pennies).
  • Add additional functionality to the application, such as a way to deposit and withdraw money from the account and keep track of all transactions on the account page.
  • Customize the FusionAuth login button.

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 an error /usr/bin/env: 'php\r': No such file or directory when trying to run Drush commands.

Incorrect line endings are known to cause this issue. To fix the issue, convert the line endings of the file from Windows to Unix. You can use the dos2unix command. Install dos2unix by running the following command in the Drupal container.

apt-get update && apt-get install -y dos2unix

Once installed, convert the line endings of the file by running the following command in the Drupal container.

dos2unix /opt/drupal/vendor/bin/drush
  • I get the following error during the Drupal installation wizard: The Settings file does not exist.

Drupal will try to automatically create a settings.php configuration file, which is normally in the directory sites/default (to avoid problems when upgrading, Drupal is not packaged with this file). If auto-creation fails, you will need to create this file yourself, using the file sites/default/default.settings.php as a template.

Create a settings file by navigating to your-application/web/sites/default/, copying default.settings.php, and renaming it to settings.php.