Authenticating Users
Almost no web application can do without a mechanism for logging users in and out and verification of user permissions. In this chapter, we'll talk about:
- logging users in and out
- custom authenticators
→ Installation and requirements
In the examples, we will use an object of the class Nette\Security\User, which represents the current user
and which you get by having it passed to you using dependency injection. In presenters, just call
$user = $this->getUser()
.
Authentication
Authentication means user login, i.e., the process during which a user's identity is verified. The user usually
identifies themselves using a username and password. Verification is done by the so-called authenticator. If the login fails, a Nette\Security\AuthenticationException
is
thrown.
try {
$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
$this->flashMessage('The username or password you entered is incorrect.');
}
This is how you log out the user:
$user->logout();
And to find out if the user is logged in:
echo $user->isLoggedIn() ? 'yes' : 'no';
Very simple, isn't it? And Nette takes care of all the security aspects for you.
In presenters, you can verify login in the startup()
method and redirect non-logged-in users to the
login page.
protected function startup()
{
parent::startup();
if (!$this->getUser()->isLoggedIn()) {
$this->redirect('Sign:in');
}
}
Expiration
User login expires together with the storage expiration, which is usually the
session (see the session expiration setting). However, you
can also set a shorter time interval after which the user is logged out. The setExpiration()
method, which is called
before login()
, is used for this purpose. Pass a string with a relative time as the argument:
// login expires after 30 minutes of inactivity
$user->setExpiration('30 minutes');
// cancel the set expiration
$user->setExpiration(null);
The $user->getLogoutReason()
method reveals whether the user was logged out because the time interval expired.
It returns either the constant Nette\Security\UserStorage::LogoutInactivity
(the time limit expired) or
UserStorage::LogoutManual
(logout()
method was called).
Authenticator
This is an object that verifies login credentials, typically username and password. A trivial form is the class Nette\Security\SimpleAuthenticator, which can be defined in the configuration:
security:
users:
# username: password
johndoe: secret123
kathy: evenmoresecretpassword
This solution is more suitable for testing purposes. We will show you how to create an authenticator that verifies login credentials against a database table.
An authenticator is an object implementing the Nette\Security\Authenticator interface with the
authenticate()
method. Its task is to either return an identity or throw a
Nette\Security\AuthenticationException
. It would also be possible to specify an error code to distinguish the
situation more finely: Authenticator::IdentityNotFound
or Authenticator::InvalidCredential
.
use Nette;
use Nette\Security\SimpleIdentity;
class MyAuthenticator implements Nette\Security\Authenticator
{
public function __construct(
private Nette\Database\Explorer $database,
private Nette\Security\Passwords $passwords,
) {
}
public function authenticate(string $username, string $password): SimpleIdentity
{
$row = $this->database->table('users')
->where('username', $username)
->fetch();
if (!$row) {
throw new Nette\Security\AuthenticationException('User not found.');
}
if (!$this->passwords->verify($password, $row->password)) {
throw new Nette\Security\AuthenticationException('Invalid password.');
}
return new SimpleIdentity(
$row->id,
$row->role, // or an array of roles
['name' => $row->username],
);
}
}
The MyAuthenticator
class communicates with the database via Nette Database Explorer and works with the users
table, where
the username
column contains the user's login name and the password
column contains the password hash. After verifying the name and password, it returns the
identity containing the user's ID, their role (the role
column in the table), which we will discuss more later, and an array with additional data (in our case, the username).
We will add the authenticator to the configuration as a service of the DI container:
services:
- MyAuthenticator
$onLoggedIn, $onLoggedOut Events
The Nette\Security\User
object has events
$onLoggedIn
and $onLoggedOut
, so you can add callbacks that are triggered after a successful login or
after the user logs out, respectively.
$user->onLoggedIn[] = function () {
// user has just logged in
};
Identity
An identity is a set of information about a user that is returned by the authenticator and which is subsequently stored in the
session and can be retrieved using $user->getIdentity()
. This allows us to get the ID, roles, and other user data,
just as we passed them in the authenticator:
$user->getIdentity()->getId();
// shortcut $user->getId() also works
$user->getIdentity()->getRoles();
// user data are accessible as properties
// the username we passed in MyAuthenticator
$user->getIdentity()->name;
What's important is that when logging out using $user->logout()
, the identity is not deleted and it is
still available. So, even if a user has an identity, they do not have to be logged in. If we want to delete the identity
explicitly, we log the user out by calling logout(true)
.
Thanks to this, you can still assume which user is at the computer and, for example, display personalized offers in an e-shop, but you can only display their personal information after they log in.
An identity is an object implementing the Nette\Security\IIdentity interface. The default implementation is Nette\Security\SimpleIdentity. And as mentioned, it is maintained in the session, so if, for example, we change the role of one of the logged-in users, the old data will remain in their identity until they log in again.
Storage for Logged-in User
The two basic information about the user, namely whether they are logged in and their identity, are
usually transmitted in the session. Which can be changed. An object implementing the Nette\Security\UserStorage
interface is responsible for storing this information. Two standard implementations are available:
Nette\Bridges\SecurityHttp\SessionStorage
, which transmits data in the session, and CookieStorage
, which
transmits data in a cookie. You can choose the storage and configure it very conveniently in the security › authentication configuration.
Furthermore, you can influence how exactly the identity saving (sleep) and restoring (wakeup) will proceed. All
that is needed is for the authenticator to implement the Nette\Security\IdentityHandler
interface. This interface has
two methods: sleepIdentity()
is called before the identity is written to storage, and wakeupIdentity()
after it's read. These methods can modify the identity content, or replace it with a new object that it returns. The
wakeupIdentity()
method can even return null
, which logs the user out.
As an example, let's show a solution to the frequent question of how to update roles in the identity right after loading from
the session. In the wakeupIdentity()
method, we pass the current roles, e.g., from a database, into the identity:
final class Authenticator implements
Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
public function sleepIdentity(IIdentity $identity): IIdentity
{
// here you can modify the identity before writing it to storage after login,
// but we don't need that now
return $identity;
}
public function wakeupIdentity(IIdentity $identity): ?IIdentity
{
// update roles in the identity
$userId = $identity->getId();
$identity->setRoles($this->facade->getUserRoles($userId));
return $identity;
}
Now let's return to storage based on cookies. It allows you to create a website where users can log in while not needing
sessions. Thus, it does not need to write to the disk. This is how the website you are currently reading works, including the
forum. In this case, the implementation of IdentityHandler
is a necessity. We will only store a random token
representing the logged-in user in the cookie.
First, set the required storage in the configuration using security › authentication › storage: cookie
.
In the database, create the authtoken
column, where each user will have a completely random, unique, and unguessable string of sufficient length (at least
13 characters). The CookieStorage
transmits only the value $identity->getId()
in the cookie, so in
sleepIdentity()
, we replace the original identity with a proxy identity containing the authtoken
in the
ID. Conversely, in the wakeupIdentity()
method, we read the entire identity from the database based on the
authtoken:
final class Authenticator implements
Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
public function authenticate(string $username, string $password): SimpleIdentity
{
$row = $this->db->fetch('SELECT * FROM user WHERE username = ?', $username);
// verify password
...
// return the identity with all data from the database
return new SimpleIdentity($row->id, null, (array) $row);
}
public function sleepIdentity(IIdentity $identity): SimpleIdentity
{
// return a proxy identity where the ID contains the authtoken
return new SimpleIdentity($identity->authtoken);
}
public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
{
// replace the proxy identity with the full identity, as in authenticate()
$row = $this->db->fetch('SELECT * FROM user WHERE authtoken = ?', $identity->getId());
return $row
? new SimpleIdentity($row->id, null, (array) $row)
: null;
}
}
Multiple Independent Logins
It is possible to have multiple independent users logging in within one website and one session simultaneously. For example, if we want to have separate authentication for the administration and the public-facing part of the website, we just need to set a unique namespace for each:
$user->getStorage()->setNamespace('backend');
It's important to remember to set the namespace always in all places belonging to the respective part. If we use presenters, we set the namespace in the common ancestor for that part – usually BasePresenter. We do this by extending the checkRequirements() method:
public function checkRequirements($element): void
{
$this->getUser()->getStorage()->setNamespace('backend');
parent::checkRequirements($element);
}
Multiple Authenticators
Dividing an application into parts with independent login usually also requires different authenticators. However, if we
registered two classes implementing Authenticator in the service configuration, Nette would not know which one to automatically
assign to the Nette\Security\User
object and would display an error. Therefore, we must restrict autowiring for authenticators so that it works only when
someone requests a specific class, e.g., FrontAuthenticator
. This is achieved by choosing
autowired: self
:
services:
-
create: FrontAuthenticator
autowired: self
class SignPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private FrontAuthenticator $authenticator,
) {
}
}
We set the authenticator of the User object before calling the login() method, so usually in the code of the form that logs them in:
$form->onSuccess[] = function (Form $form, \stdClass $data) {
$user = $this->getUser();
$user->setAuthenticator($this->authenticator);
$user->login($data->username, $data->password);
// ...
};