> For the complete documentation index, see [llms.txt](https://fusionauth.io/docs/llms.txt)

# Drupal | FusionAuth Docs

Quickstart integration of Drupal web application with FusionAuth.

# Drupal

[Edit on GitHub](https://github.com/FusionAuth/fusionauth-site/blob/main/astro/src/content/docs/get-started/quickstarts/web/quickstart-php-drupal-web.mdx)

[View Markdown](https://fusionauth.io/docs/get-started/quickstarts/web/quickstart-php-drupal-web.md)

In this quickstart, you are going to build an application with Drupal and integrate it with FusionAuth. You'll be building it for [ChangeBank](https://www.youtube.com/watch?v=CXDxNCzUspM), 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](https://github.com/FusionAuth/fusionauth-quickstart-php-drupal-web).

## Prerequisites[#](#prerequisites)

*   [PHP](https://www.php.net/downloads.php) 8.1.0+
*   [Composer](https://getcomposer.org/)
*   [GD extension](https://www.php.net/manual/en/book.image.php)
*   [Docker](https://www.docker.com): The quickest way to stand up FusionAuth. (There are [other ways](https://fusionauth.io/docs/get-started/download-and-install.md).)

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[#](#general-architecture)

Here's a typical application login flow before FusionAuth:

```mermaid
sequenceDiagram
    participant User
    participant App as Application

    User ->> App : View Homepage
    User ->> App : Click Login Link
    App ->> User : Show Login Form
    User ->> App : Fill Out and Submit Login Form
    App ->> App : Authenticates User
    App ->> User : Display User's Account or Other Info
```

Request flow during login before FusionAuth

And here's the same application login flow after introducing FusionAuth:

```mermaid
sequenceDiagram
    participant User
    participant App as Application
    participant FusionAuth

    User ->> App : View Homepage
    User ->> App : Click Login Link (to FusionAuth)
    User ->> FusionAuth : View Login Form
    FusionAuth ->> User : Show Login Form
    User ->> FusionAuth : Fill Out and Submit Login Form
    FusionAuth ->> FusionAuth : Authenticates User
    FusionAuth ->> User: Go to Redirect URI
    User ->> App: Request the Redirect URI
    App ->> FusionAuth : Is User Authenticated?
    FusionAuth ->> App : User is Authenticated
    App ->> User : Display User's Account or Other Info
```

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[#](#getting-started)

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

### Clone The Code[#](#clone-the-code)

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

```shell-session
$ git clone https://github.com/FusionAuth/fusionauth-quickstart-php-drupal-web.git
```

```shell-session
$ cd fusionauth-quickstart-php-drupal-web
```

### Create Your Drupal Application[#](#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.

```shell-session
$ 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.

```shell-session
$ docker compose up -d
```

Once the application is up and running, navigate to [http://localhost/](http://localhost/) and you should be redirected to the Drupal installation wizard at [http://localhost/core/install.php](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.

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

#### Enabling Extensions Via UI[#](#enabling-extensions-via-ui)

You can enable the modules by visiting the extensions page of your Drupal application at [http://localhost/admin/modules](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[#](#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](https://packagist.org/packages/drush/drush). Install it by running the following command.

```shell-session
$ 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.

```shell-session
$ docker compose restart
```

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

```shell-session
$ docker compose exec web bash
```

Run the following command to enable the modules.

```shell-session
$ vendor/bin/drush pm-enable openid_connect admin_toolbar
```

Confirm with yes when prompted.

```plaintext
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/](https://www.drush.org/12.2.0/commands/all/).

### Run FusionAuth Via Docker[#](#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.

```shell-session
docker compose up -d
```

Here you are using a bootstrapping feature of FusionAuth called [Kickstart](https://fusionauth.io/docs/get-started/download-and-install/development/kickstart.md). 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](http://localhost:9011/admin) 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](http://localhost:9011/admin). 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[#](#authentication)

To set up your Drupal application to use FusionAuth for authentication, head to [http://localhost/admin/config/people/openid-connect](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:

```plaintext
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](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:

*   By navigating to [http://localhost/admin/config/development/performance](http://localhost/admin/config/development/performance) and clicking the Clear all caches button.
*   Or by running the `vendor/bin/drush cr` command in the Drupal Docker container terminal.

### Test Authentication[#](#test-authentication)

Now you can test the authentication flow.

In the browser, navigate to [http://localhost/user/logout](http://localhost/user/logout) to log out of the Drupal application. Navigate to [http://localhost/user/login](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[#](#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[#](#generate-changebank-module)

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

```shell-session
$ vendor/bin/drush generate module
```

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

```plaintext
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.

```shell-session
$ vendor/bin/drush generate block
```

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

```plaintext
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.

```shell-session
$ vendor/bin/drush generate block
```

Enter the following information:

```plaintext
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.

```shell-session
$ vendor/bin/drush generate form
```

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

```plaintext
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.

```shell-session
$ vendor/bin/drush generate controller
```

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

```plaintext
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.

```shell-session
$ vendor/bin/drush generate routing
```

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

```plaintext
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[#](#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[#](#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
<?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[#](#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
<?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[#](#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.

```yaml
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[#](#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
<?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[#](#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
<?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[#](#customize-the-changebankmodule-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
<?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[#](#enable-the-changebank-module)

To enable the Changebank module, navigate to [http://localhost/admin/modules](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.

```shell-session
$ vendor/bin/drush pm-enable changebank
```

### Add Styling[#](#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[#](#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.

```shell-session
$ composer require 'drupal/bootstrap_barrio:^5.5'
```

#### Create A Subtheme[#](#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`.

```shell-session
$ mkdir -p web/themes/custom/subtheme
```

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

```shell-session
$ 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[#](#enable-the-theme)

To enable the theme, navigate to [http://localhost/admin/appearance](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](https://github.com/FusionAuth/fusionauth-quickstart-php-drupal-web/blob/main/complete-application/web/sites/default/files/changebank-logo.png) in the **Upload logo image** field. Save the configuration.

#### Install And Enable Twig Tweak[#](#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.

```shell-session
$ composer require 'drupal/twig_tweak:^3.2'
```

Enable the module by navigating to [http://localhost/admin/modules](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.

```shell-session
$ vendor/bin/drush pm-enable twig_tweak
```

#### Add Templates[#](#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.

```shell-session
$ 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.

```twig
{% 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.

```twig
{#
/**
 * @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.

```twig
{#
/**
 * @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[#](#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.

```css
/**
 * @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[#](#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](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[#](#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](http://localhost/admin/structure/block) in the administration menu.

#### Enable The Changebank Menu Block[#](#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[#](#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[#](#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[#](#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[#](#set-login-redirect)

Navigate to [http://localhost/admin/config/people/openid-connect](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[#](#run-the-application)

You can now open the application at [http://localhost/user/login](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? Want a free t-shirt? We got ya.

Thank you for spending some time getting familiar with FusionAuth.

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

## Next Steps[#](#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:

*   [Hosted pages](https://fusionauth.io/docs/customize/look-and-feel.md) such as login, registration, email verification, and many more.
*   [Email templates](https://fusionauth.io/docs/customize/email-and-messages/email-templates.md).
*   [User data and custom claims in access token JWTs](https://fusionauth.io/articles/tokens/jwt-components-explained.md).

### Security

*   You may want to customize the [token expiration times and policies](https://fusionauth.io/docs/lifecycle/authenticate-users/oauth.md) in FusionAuth.
*   Choose [password rules](https://fusionauth.io/docs/get-started/core-concepts/tenants.md#password) and a [hashing algorithm](https://fusionauth.io/docs/reference/password-hashes.md) that meet your security needs.

### Tenant and Application Management

*   Model your application topology using [Applications](https://fusionauth.io/docs/get-started/core-concepts/applications.md), [Roles](https://fusionauth.io/docs/get-started/core-concepts/roles.md), [Groups](https://fusionauth.io/docs/get-started/core-concepts/groups.md), [Entities](https://fusionauth.io/docs/get-started/core-concepts/groups.md), and more.
*   Set up [MFA](https://fusionauth.io/docs/lifecycle/authenticate-users/multi-factor-authentication.md), [Social login](https://fusionauth.io/docs/lifecycle/authenticate-users/identity-providers.md), or [SAML](https://fusionauth.io/docs/lifecycle/authenticate-users/identity-providers/overview-samlv2.md) integrations.
*   Integrate with external systems using [Webhooks](https://fusionauth.io/docs/extend/events-and-webhooks.md), [SCIM](https://fusionauth.io/docs/lifecycle/migrate-users/scim.md), and [Lambdas](https://fusionauth.io/docs/extend/code/lambdas.md).

### Drupal Customization[#](#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[#](#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](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.

```shell-session
$ 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.

```shell-session
$ 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`.