Authenticating Users
Little to none web applications need no mechanism for user login or checking user privileges. In this chapter, we'll talk about:
- user login and logout
- custom authenticators and authorizators
In the examples, we will use an object of class Nette\Security\User, which represents the current user
and which you get by passing it using dependency
injection. In presenters simply call $user = $this->getUser()
.
Authentication
Authentication means user login, ie. the process during which a user's identity is verified. The user usually
identifies himself using username and password. Verification is performed by the so-called authenticator. If the login fails, it throws
Nette\Security\AuthenticationException
.
try {
$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
$this->flashMessage('The username or password you entered is incorrect.');
}
This is how to log out the user:
$user->logout();
And checking if user is logged in:
echo $user->isLoggedIn() ? 'yes' : 'no';
Simple, right? And all security aspects are handled by Nette for you.
In presenter, you can verify login in the startup()
method and redirect a non-logged-in user to the
login page.
protected function startup()
{
parent::startup();
if (!$this->getUser()->isLoggedIn()) {
$this->redirect('Sign:in');
}
}
Expiration
The user login expires along with expiration of repository, which is usually a
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. Provide a string with a relative time as a parameter:
Besides logging the user out with the logout()
method, it can be done automatically based on a specified time
interval or closing the browser window. For this configuration, we have to call setExpiration()
during the login
process. As an argument, it takes a relative time in seconds, UNIX timestamp, or textual representation of time.
// login expires after 30 minutes of inactivity
$user->setExpiration('30 minutes');
// cancel set expiration
$user->setExpiration(0);
The $user->getLogoutReason()
method tells if the user has been logged out because the time interval has
expired. It returns either the constant Nette\Security\UserStorage::LOGOUT_INACTIVITY
if the time expired or
UserStorage::LOGOUT_MANUAL
when the logout()
method was called.
Authenticator
It is an object that verifies the login data, ie usually the name and password. The trivial implementation is the class Nette\Security\SimpleAuthenticator, which can be defined in configuration:
security:
users:
# name: password
johndoe: secret123
kathy: evenmoresecretpassword
This solution is more suitable for testing purposes. We will show you how to create an authenticator that will verify credentials against a database table.
An authenticator is an object that implements the Nette\Security\IAuthenticator interface with
method authenticate()
. Its task is either to return the so-called identity or to throw an
exception Nette\Security\AuthenticationException
. It would also be possible to provide an fine-grain error code
IAuthenticator::IDENTITY_NOT_FOUND
or IAuthenticator::INVALID_CREDENTIAL
.
use Nette;
class MyAuthenticator implements Nette\Security\IAuthenticator
{
private $database;
public function __construct(Nette\Database\Connection $database)
{
$this->database = $database;
}
/** @return Nette\Security\IIdentity */
public function authenticate(array $credentials)
{
list($username, $password) = $credentials;
$row = $this->database->table('users')
->where('username', $username)
->fetch();
if (!$row) {
throw new Nette\Security\AuthenticationException('User not found.');
}
if (!Nette\Security\Passwords::verify($password, $row->password)) {
throw new Nette\Security\AuthenticationException('Invalid password.');
}
return new Nette\Security\Identity(
$row->id,
$row->role, // or array of roles
['name' => $row->username]
);
}
}
The MyAuthenticator class communicates with the database through Nette
Database Explorer and works with table users
, where column username
contains the user's login name
and column password
contains hash. After verifying the
name and password, it returns the identity with user's ID, role (column role
in the table), which we will mention 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
Object Nette\Security\User
has events
$onLoggedIn
and $onLoggedOut
, so you can add callbacks that are triggered after a successful login or
after the user logs out.
$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 then stored in a session
and retrieved using $user->getIdentity()
. So we can get the id, roles and other user data as we passed them in the
authenticator:
$user->getIdentity()->getId();
// also works shortcut $user->getId();
$user->getIdentity()->getRoles();
// user data can be access as properties
// the name we passed on in MyAuthenticator
$user->getIdentity()->name;
Importantly, when user logs out using $user->logout()
, identity is not deleted and is still available.
So, if identity exists, it by itself does not grant that the user is also logged in. If we want to explicitly delete the identity,
we logout the user by logout(true)
.
Thanks to this, you can still assume which user is at the computer and, for example, display personalized offers in the e-shop, however, you can only display his personal data after logging in.
Identity is an object that implements the Nette\Security\IIdentity interface, the default implementation is Nette\Security\Identity. And as mentioned, identity is stored in the session, so if, for example, we change the role of some of the logged-in users, old data will be kept in the identity until he logs in again.
Multiple Independent Authentications
It is possible to have several independent logged users within one site and one session at a time. For example, if we want to have separate authentication for frontend and backend, we will just set a unique session namespace for each of them:
$user->getStorage()->setNamespace('backend');
It's necessary to keep in mind that this must be set at all places belonging to the same segment. When using presenters, we will set the namespace in the common ancestor – usually the BasePresenter. In order to do so we will extend the checkRequirements() method:
public function checkRequirements($element)
{
$this->getUser()->getStorage()->setNamespace('backend');
parent::checkRequirements($element);
}
Multiple Authenticators
Dividing an application into segments with independent authentication generally requires different authenticators. However,
registering two classes that implement IAuthenticator into config services would trigger an error because Nette wouldn't know
which of them should be autowired to the Nette\Security\User
object. Which is why we must limit autowiring for them
with autowired: self
so that it's activated only when their class is specifically requested:
services:
-
factory: FrontAuthenticator
autowired: self
class SignPresenter extends Nette\Application\UI\Presenter
{
/** @var FrontAuthenticator */
private $authenticator;
public function __construct(FrontAuthenticator $authenticator)
{
$this->authenticator = $authenticator;
}
}
We only need to set our authenticator to the User object before calling method login() which typically means in the login form callback:
$form->onSuccess[] = function (Form $form, \stdClass $data) {
$user = $this->getUser();
$user->setAuthenticator($this->authenticator);
$user->login($data->username, $data->password);
// ...
};