Add a firewall in your theme

You may need to add a secured area in your website or application, even for none-backend users. Roadiz uses Symfony security components to handle firewalled requests. You will be able to extend the firewall map in your Theme addDefaultFirewallEntry method.

Before create your firewall map entry, you must understand that Roadiz already has 2 firewall areas:

  • ^/rz-admin area, which naturally matches every back-office sections
  • ^/ area which is required for previewing unpublished node and get user information across the whole website

The last firewall request matcher can be tricky to deal with, especially if you want to add another secured area as it listen to every requests. When you’ll add new firewall map entry, you may call parent::addDefaultFirewallEntry($container); before your custom configuration to be sure that ^/ request matcher has the lowest priority. However, if you want to override ^/ request matcher configuration you have to omit the parent method call.

/**
 * {@inheritdoc}
 */
public static function addDefaultFirewallEntry(Container $container)
{
    /*
     * Your custom firewall map entry configuration
     * goes here
     */

    /*
     * Call parent ONLY if you don’t want to create
     * a firewall map at website root level. And call it after
     * your own firewall entry.
     */
    parent::addDefaultFirewallEntry($container);
}

Configuring a non-root firewall map entry with FirewallEntry class

Before copy and pasting the following lines, think about it a little time… A firewall map entry defines severals mandatory routes:

  • A base path for your firewall to be triggered
  • A login path, which can be outside or inside of your firewall map
  • A login_check path, which must be inside of your firewall map
  • A logout path, which must be inside of your firewall map
  • A new role describing your secured area purpose (i.e. ROLE_ACCESS_PRESS for a private press kit area), you should create this role in Roadiz backoffice before.

If this example I will use:

  • /press as my base path for secured area
  • /signin for my login page, notice that it’s not in my firewall
  • /press/login_check
  • /press/logout
  • ROLE_ACCESS_PRESS

Here is the code to add in your theme’ addDefaultFirewallEntry method. Do not forget to add the matching use statement in your file header.

use RZ\Roadiz\Utils\Security\FirewallEntry;
use Pimple\Container;

/**
 * {@inheritdoc}
 */
public static function addDefaultFirewallEntry(Container $container)
{
    /*
     * Call parent ONLY if you don’t want to create
     * a firewall map at website root level.
     */
    parent::addDefaultFirewallEntry($container);

    $firewallBasePattern = '^/press';
    $firewallBasePath = '/press';
    $firewallLogin = '/signin';
    $firewallLogout = '/press/logout';
    $firewallLoginCheck = '/press/login_check';
    $firewallBaseRole = 'ROLE_ACCESS_PRESS';

    $firewallEntry = new FirewallEntry(
        $container,
        $firewallBasePattern,
        $firewallBasePath,
        $firewallLogin,
        $firewallLogout,
        $firewallLoginCheck,
        $firewallBaseRole
        // You can add a special AuthenticationSuccessHandler
        // if you need to do some stuff for your theme at visitor login
        //'Themes\YourTheme\Authentification\AuthenticationSuccessHandler'
    );
    // Allow anonymous authentication
    $firewallEntry->withAnonymousAuthenticationListener();
    // Allow switch user feature
    $firewallEntry->withSwitchUserListener();

    /*
     * Finally add this entry to the Roadiz
     * firewall map.
     */
    $container['firewallMap']->add(
        $firewallEntry->getRequestMatcher(),
        $firewallEntry->getListeners(),
        $firewallEntry->getExceptionListener()
    );
}

Add login routes

After configuring your Firewall, you’ll need to add your routes to your theme routes.yml file. Logout and login_check won’t need any controller setup as they will be handled directly by Roadiz firewall event dispatcher. The only one you need to handle is the login page.

themeLogout:
    path:     /press/logout
themeLoginCheck:
    path:     /press/login_check
themeLoginPage:
    path:     /signin
    defaults:
        _controller: Themes\MySuperTheme\Controllers\LoginController::loginAction

Warning

If your login route is inside your firewall and your access map require an other role than IS_AUTHENTICATED_ANONYMOUSLY you must add a special access map entry to enable your public visitor to access your login page.

$this->container['accessMap']->add(new RequestMatcher('^/press/signin'), ['IS_AUTHENTICATED_ANONYMOUSLY']);

Add this line with your login page pattern before adding your firewall entry. Access map entries order is important!

In your LoginController, just add error handling from the securityAuthenticationUtils service to display a feedback on your login form:

/**
 * {@inheritdoc}
 */
public function loginAction(
    Request $request,
    $_locale = 'en'
) {
    $translation = $this->bindLocaleFromRoute($request, $_locale);
    $this->prepareThemeAssignation(null, $translation);
    $helper = $this->get('securityAuthenticationUtils');
    $this->assignation['last_username'] = $helper->getLastUsername();
    $this->assignation['error'] = $helper->getLastAuthenticationError();

    return $this->render('press/login.html.twig', $this->assignation);
}

Then, you can create your login form as you want. Just use the required fields:

  • _username
  • _password

And do not forget to set your form action to {{ path('themeLoginCheck') }} and to use POST method.

{% if error %}
    <div class="alert alert-danger"><i class="fa fa-warning"></i> {{ error.message|trans }}</div>
{% endif %}
<form id="login-form" class="form" action="{{ path('themeLoginCheck') }}" method="post">
    <div class="form-group">
        <label class="control-label" for="_username">{% trans %}username{% endtrans %}</label>
        <input class="form-control" type="text" name="_username" id="_username" placeholder="{% trans %}username{% endtrans %}" value="" />
    </div>
    <div class="form-group">
        <label class="control-label" for="_password">{% trans %}password{% endtrans %}</label>
        <input class="form-control" type="password" name="_password" id="_password" placeholder="{% trans %}password{% endtrans %}" value="" />
    </div>
    <div class="form-group">
        <label class="control-label" for="_remember_me">{% trans %}keep_me_logged_in{% endtrans %}</label>
        <input class="form-control" type="checkbox" name="_remember_me" id="_remember_me" value="1" />
    </div>
    <div class="form-group">
        <button class="btn btn-primary" type="submit"><i class="fa fa-signin"></i> {% trans %}login{% endtrans %}</button>
    </div>
</form>

Configuring a root firewall map entry with FirewallEntry class

You may want to offer authentication for every pages of your website and manage access control manually within your node-type controllers. In that case you need to override default front-end Firewall map entry with your own and defined login/logout paths.

use RZ\Roadiz\Utils\Security\FirewallEntry;
use Pimple\Container;

/**
 * {@inheritdoc}
 */
public static function addDefaultFirewallEntry(Container $container)
{
    /*
     * Do not call parent method
     */

    $firewallBasePattern = '^/';
    $firewallBasePath = '/';
    $firewallLogin = '/accounts';
    $firewallLogout = '/accounts/logout';
    $firewallLoginCheck = '/accounts/login_check';

    /*
     * You MUST use IS_AUTHENTICATED_ANONYMOUSLY base role not to prevent
     * users to access your website
     */
    $firewallBaseRole = 'IS_AUTHENTICATED_ANONYMOUSLY';

    $firewallEntry = new FirewallEntry(
        $container,
        $firewallBasePattern,
        $firewallBasePath,
        $firewallLogin,
        $firewallLogout,
        $firewallLoginCheck,
        $firewallBaseRole
    );
    // Allow anonymous authentication
    $firewallEntry->withAnonymousAuthenticationListener()
                    ->withSwitchUserListener()
                    // Automatically redirect to themeLoginPage route
                    // if AccessDeniedException is thrown
                    ->withAccessDeniedHandler('themeLoginPage')
                    ->withReferer();

    /*
     * Finally add this entry to the Roadiz
     * firewall map.
     */
    $container['firewallMap']->add(
        $firewallEntry->getRequestMatcher(),
        $firewallEntry->getListeners(),
        $firewallEntry->getExceptionListener()
    );
}

For the moment, every pages of your website will be public. You’ll need to use is_granted Twig filter and $this->denyAccessUnlessGranted($role) method to manage access control to your contents.

Multi-theme website

If your website has more than one theme you must disable firewall entries on every non-main theme app class not to register duplicated firewall entries with the same access-map rules.

For example, if you registered a MainTheme and a SecondaryTheme, add the following lines to your themes/SecondaryTheme/SecondaryThemeApp.php class:

/**
 * {@inheritdoc}
 */
public static function addDefaultFirewallEntry(Container $container)
{
    /*
     * Do not register any firewall entry
     */
}