Skip to content

Kirby 3.9.8

User sign‑up

Requirements

The first example requires at least Kirby 3.6.2, while the second example works with 3.6.x and higher.

Intro

To implement a (frontend) user registration in your Kirby project, you can leverage Kirby's authentication features. To achieve this, we are going to implement the following steps:

  • Enable login via code in Kirby's config
  • Create a new user when user submits registration form
  • Send an authentication challenge once new user is created
  • Redirect the user to the Panel login or a frontend login page
  • Verify code and log user in

This recipe is based on the Kirby Starterkit. Feel free to use a Plainkit or your own project to follow along, though.

Ready? Let's start with the config settings!

Config settings

To be able to send emails with login codes, we need to configure the email transport settings and enable authentication via login code:

/site/config/config.php
return [
  // other settings
  'email' => [
    // see https://getkirby.com/docs/guide/emails#transport-configuration
    'transport' => [
      'type' => 'smtp',
      'host' => 'smtp.company.com',
      'port' => 465,
      'security' => true
    ]
  ],
  // see https://getkirby.com/docs/reference/system/options/auth#login-methods
  'auth' => [
    'methods' => ['password', 'code']
  ],
];

To test locally whether sending the login code via email works, you can use MailHog or a similar tool.

In our first example, we will let the Panel handle the login code verification, and add a login page in the second example, where we handle everything on the frontend.

User registration

Registration page

To handle user registrations, we create a registration page as /registration/registration.txt in the /content folder with a title and optional other information you want to render on the page:

Title: Register

For this page, we will create a Template with a registration form as well as a Controller. But before we get to that, let's create the user Blueprint for the role we want to assign to new users.

User role blueprint

Our newly registered users should get the role of client with no Panel access. We therefore create the following client.yml blueprint:

/site/blueprints/users/client.yml
title: Client
# Set page where you want to redirect the user to after login
home: /
# Disallow panel access
permissions:
  access:
    panel: false

Change permissions or the home option as required. For our example, we don't want to allow Panel access and send the user directly to the home page after login.

If you like, you could allow this role access to their own user account in the Panel, while keeping all other areas locked.

Registration template

In its most minimal form, our registration template would just have a form with an email field. In our example, we also want to require a username. Add any fields you need, always keeping important privacy principles like data minimisation in mind:

/site/templates/registration.php
<?php snippet('header') ?>
<div class="intro">
    <h1><?= $page->title() ?></h1>
</div>

<?php
// if the form has errors, show a list of messages
if (count($errors) > 0) : ?>
<ul class="alert">
    <?php foreach ($errors as $message) : ?>
        <li><?= kirbytext($message) ?></li>
    <?php endforeach ?>
</ul>
<?php endif ?>

<form method="post" action="<?= $page->url() ?>">
    <input type="hidden" name="csrf" value="<?= csrf() ?>">
    <div>
        <label for="name">Name</label>
        <input required type="text" id="name" name="name" value="<?= esc($data['name'] ?? '', 'attr') ?>">
    </div>
    <div>
        <label for="email">Email</label>
        <input required type="email" id="email" name="email" value="<?= esc($data['email'] ?? '', 'attr') ?>">
    </div>
    <div>
        <input type="submit" name="register" value="Register">
    </div>
</form>

<?php snippet('footer') ?>

Registration controller

The controller handles our form validation and, on successful user creation, redirects the new user to the Panel login page. Find the different steps explained in the comments:

/site/controllers/registration.php
<?php

use Kirby\Exception\PermissionException;

return function ($kirby) {
    // send already logged-in user somewhere else
    if ($kirby->user()) {
        go('home');
    }

    // create empty error list
    $errors = [];

    // the form was sent
    if (get('register') && $kirby->request()->is('POST')) {
        // validate CSRF token
        if (csrf(get('csrf')) === true) {
            // get form data
            $data = [
                'email' => get('email'),
                'name' => get('name'),
            ];
            // validation rules
            $rules = [
                'email' => ['required', 'email'],
                'name' => ['required', 'minLength' => 3],
            ];
            // error messages
            $messages = [
                'email' => 'Please enter a valid email address',
                'name' => 'Your name must have at least 3 characters',
            ];
            // check if data is valid
            if ($invalid = invalid($data, $rules, $messages)) {
                $errors = $invalid;

            // the data is fine, let's create a user
            } else {
                // authenticate
                $kirby->impersonate('kirby');
                try {
                    // create new user
                    $user = $kirby->users()->create([
                        'email' => $data['email'],
                        'role' => 'client',
                        'language' => 'en',
                        'name' => $data['name'],
                    ]);
                    if (isset($user) === true) {
                        // create the authentication challenge
                        try {
                            $status = $kirby->auth()->createChallenge($user->email(), false, 'login');
                            go('login');
                        } catch (PermissionException $e) {
                            $errors[] = $e->getMessage();
                        }
                    }
                } catch (Exception $e) {
                    $errors[] = $e->getMessage();
                }
            }
        } else {
            $errors[] = 'Invalid CSRF token.';
        }
    }

    return [
        'errors' => $errors
    ];
};

With the page all set, we are ready for some testing. Make sure you are logged out of the Panel. Open the registration page at example.com/registration and enter an email address. You should be redirected to the Panel login page with the code form field.

Once you enter the login code you received via email in the form field, you are logged in and automatically redirected to the location set via the home option in the client.yml blueprint.

Great, we now have a working user registration page with very little code.

While Kirby limits automatic registrations from the same IP (you can set the limit in your config.php), consider using some sort of spam protection in a production environment (honeypot, captcha etc. ).

If you want to handle everything on the frontend instead of sending the user to the Panel login page for authentication, you could instead redirect the user to an authentication page. In the next steps, we will look into this option.

Frontend login with code

For our second example, we have to adapt our code from above a little and then add a login page.

Registration controller

But let's first change the redirect link in the registration.php controller from…

go('panel/login');

to…

go('login')`;

and create a new login page with a content file called login.txt.

The login page serves two purposes:

  • Get a user email and send an authentication challenge if there is no valid challenge
  • Get and validate the authentication code if a challenge is active

In the next two steps, we create the template and controller for the login page.

Login template

In the template, we create the login form. Depending on the current authentication status (pending or inactive, see controller below), the form shows either the code or the email field. Users with an active auth status are already logged in and therefore redirected.

The general procedure is therefore the same as in our first example above.

/site/templates/login.php
<?php snippet('header') ?>

<div class="intro">
<h1><?= $page->title()->html() ?></h1>
</div>

<article>
    <div class="text">
        <?php if($error): ?>
        <div class="alert"><?= $error ?></div>
        <?php endif ?>

        <form method="post" action="<?= $page->url() ?>">
            <input type="hidden" name="csrf" value="<?= csrf() ?>">
            <?php if($status === 'inactive'): ?>
            <div>
                <label for="email">Email:</label>
                <input id="email" name="email" required type="email"  value="<?= esc($email, 'attr') ?>">
            </div>
            <?php endif ?>

            <?php if ($status === 'pending'): ?>
            <div>
                <label for="name">Login Code</label>
                <input id="code" name="code" placeholder="000 000" required type="text">
                <p><small>If your email address is registered, the requested code was sent via email.</small></p>
            </div>
            <?php endif ?>
            <div>
                <input type="submit" name="login" value="Log in">
            </div>

        </form>
    </div>
</article>

<?php snippet('footer') ?>

Login controller

The login.php controller is a bit more complex because depending on which field is submitted via the form, we either validate the provided code or send an new authentication challenge. Again I have explained all steps in the comments.

/site/controllers/login.php
<?php

use Kirby\Cms\Auth\Status;
use Kirby\Exception\PermissionException;
use Kirby\Toolkit\V;

return function ($kirby) {
    $error = null;

    // get authentication status
    $status = $kirby->auth()->status();

    // user is already logged in, send them elsewhere
    if ($status->status() === 'active') {
        go('home');
    }

    // form is submitted
    if (get('login') && $kirby->request()->is('POST') ) {
        // check CSRF token
        if (csrf(get('csrf')) === true) {

            // if we get an email address, we send an authentication challenge
            if (get('email')) {
                $email = get('email');
                if (V::email($email)) {
                    try {
                        $status = $kirby->auth()->createChallenge($email, false, 'login');
                    } catch (PermissionException $e) {
                        $error = $e->getMessage();
                    }
                } else {
                    $error = 'Please provide a valid email address';
                }

            // if we get a code, we validate the code
            } elseif (get('code')) {
                $code = get('code');
                try {
                    // if successful, the user will be logged in
                    // `verifyChallenge()` either returns a user or throws an exception
                    $user = $kirby->auth()->verifyChallenge($code);
                    // if the user is logged-in, redirect them
                    if ($user) {
                        go('home');
                    }
                } catch (Exception $e) {
                    $error = $e->getMessage();
                    // set new status object with inactive status
                    $status = new Status([
                        'kirby' => $kirby,
                        'status' => 'inactive'
                    ]);
                }
            }

        } else {
            $error = 'Invalid CSRF token';
        }
    }

    return [
        'email'  => $email ?? '',
        'error'  => $error,
        'status' => $status->status(),
    ];
};

Time for testing. Clear the sessions folder to make sure you don't have an active sessions anymore. Register a new user. You should now land on the new login page. Enter the code in the form field and the new user should be logged in again.

Perfect!

File structure

By now, we have created the following files in the project:

  • content
    • login
      • login.txt
    • registration
      • registration.txt
  • site
    • blueprints
      • users
        • client.yml
    • controllers
      • login.php
      • registration.php
    • templates
      • login.php
      • registration.php

Fine tuning

Navigation & logout route

Currently, we have to access all pages via the browser's address bar. Let's make it more comfortable, and add links to the registration and login pages in the navigation, and also add a logout link for logged-in users.

To this purpose, we extend the navigation item in the header snippet as follows:

/site/snippets/header.php
<nav class="menu">
    <?php foreach ($site->children()->listed() as $item): ?>
    <a <?php e($item->isOpen(), 'aria-current ') ?> href="<?= $item->url() ?>"><?= $item->title()->html() ?></a>
    <?php endforeach ?>

    <?php if (!$kirby->user()) : ?>
    <?php foreach($site->children()->find('registration', 'login') as $item): ?>
        <a <?php e($item->isOpen(), 'aria-current ') ?> href="<?= $item->url() ?>"><?= $item->title()->html() ?></a>
    <?php endforeach ?>
    <?php endif ?>

    <?php if ($kirby->user()) : ?>
        <a href="<?= url('logout') ?>">Logout</a>
    <?php endif ?>

    <?php snippet('social') ?>
</nav>

And additionally, add a logout route in your config:

&quot;/site/config/config.php
return [
    // …other settings
    'routes' => [
        [
            'pattern' => 'logout',
            'action'  => function() {
                if ($user = kirby()->user()) {
                    $user->logout();
                }

                go('login');

            }
        ]
    ],
];

Optional blueprints

Currently, the login and registration pages will be opened in the Panel with the default.yml blueprint. As long as we don't need specific fields in those pages, that's totally fine. Feel free to add blueprints for these pages as you see fit.

Bonus: dynamic redirects

In many cases, we want to redirect the user to the location where they left off before they hit the registration/login page. For this purpose, we are going to store the original page in the session.

The first step is to add the current page id as parameter to the navigation links. And while we are at it, we can do the same for the logout link.

/site/snippets/header.php
<nav class="menu">
    <?php foreach ($site->children()->listed() as $item): ?>
    <a <?php e($item->isOpen(), 'aria-current ') ?> href="<?= $item->url() ?>"><?= $item->title()->html() ?></a>
    <?php endforeach ?>

    <?php if (!$kirby->user()) : ?>
    <?php foreach($site->children()->find('registration', 'login') as $item): ?>
        <a <?php e($item->isOpen(), 'aria-current ') ?> href="<?= $item->url(['params' => ['cookbook.referrer' => $page->id()]]) ?>"><?= $item->title()->html() ?></a>
    <?php endforeach ?>
    <?php endif ?>

    <?php if ($kirby->user()) : ?>
        <a href="<?= url('logout', ['params' => ['referrer' => $page->id()]]) ?>">Logout</a>
    <?php endif ?>

    <?php snippet('social') ?>
</nav>

Site method

We create a site method that allows us to fetch the referrer value from the session, so that we can easily retrieve that value in our code. for this purpose, we create a little plugin in the /plugins folder.

&quot;/site/plugins/site-method/index.php
<?php
Kirby::plugin('cookbook/site-method', [
    'siteMethods' => [
        'loginReferrer' => function () {
            return kirby()->session()->pull('login.referrer') ?? '/';
        }
    ]
]);

If a value is stored in the session, we return this value, otherwise we redirect to the home page.

User blueprint

We can now use this method in the client.yml user blueprint to set the home option dynamically.

/site/blueprints/users/client.yml
title: Client
home: "{{ site.loginReferrer }}"
permissions:
  access:
    panel: false

Modify registration controller

At the top of the registration controller we fetch the referrer parameter and store it in the session. We also use the referrer value to redirect already logged-in users.

/site/controllers/registration.php
// add the $site variable as parameter
return function ($kirby, $site) {
    // store parameter in session
    if ( $referrer = param('referrer')) {
        $kirby->session()->set('login.referrer', $referrer);
    }

    // send already logged-in user to the referrer page
    if ($kirby->user()) {
        go($site->loginReferrer());
    }

    // …rest of code
};

We also have to fix the redirect link to the login page and add the original referrer as parameter:

/site/controllers/registration.php
// …rest of code
try {
    $status = $kirby->auth()->createChallenge($user->email(), false, 'login');

    // add referrer to redirect url
    go('login/referrer:' . $site->loginReferrer());
} catch (PermissionException $e) {
    $errors[] = $e->getMessage();
}
// …rest of code

Modify login controller

We also check if the parameter is set in the login controller.

/site/controllers/login.php
// add the $site variable as parameter
return function ($kirby, $site) {
    $error = null;

    if ($referrer = param('referrer')) {
        $kirby->session()->set('login.referrer', $referrer);
    }

And replace the two instances of hard-coded redirects to the home page with

go($site->loginReferrer());

That was it. Happy coding!