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:
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:
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:
<?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:
<?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.
<?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.
<?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:
<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
:
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.
Add parameters to navigation links
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.
<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.
<?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.
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.
// 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:
// …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.
// 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!