News in Nette Security 3.1

4 years ago by David Grudl  

You'll be amazed at the horizons of this new version, and how easy it is to build web applications that don't need a session.

Clarity

Before we look at the main news, we must say that Nette is gradually leaving the prefix I in the interface names, so it disappears from the names IAuthenticator, IAuthorizator, IResource, IRole, IUserStorage. Also class Identity was renamed to SimpleIdentity.

Of course, the original names still work as aliases.

It's not always just about renaming. For example Nette\Security\UserStorage is a completely newly designed interface. Also Nette\Security\Authenticator differs slightly from IAuthenticator in that the name and password (and any other parameters) are not passed in the array, but as regular parameters:

class Authenticator implements Nette\Security\Authenticator
{
	public function authenticate(string $username, string $password): SimpleIdentity
	{
		...
	}
}

Storage for Logged User

Nette maintains two basic information about the user: whether he is logged in and his identity as an object implementing the interface Nette\Security\IIdentity. This information is usually stored in a session. Which you can easily and fundamentally influence in Nette Security 3.1. You will soon find out what it is all for.

Where the mentioned information is stored is determined by the so-called storage, which is an object implementing the interface Nette\Security\UserStorage. There are two standard implementations, the first stores data in a session and the second, which is a novelty, in a cookie. These are classes Nette\Bridges\SecurityHttp\SessionStorage and CookieStorage.

You can choose the storage and configure it very conveniently in the security › authentication configuration.

By default, Nette serializes the identity into a session after the user logs in and reads it in subsequent requests. You can now control what else happens when you serialize your identity (sleep) and restore it (wakeup). It is enough for the authenticator to implement the interface Nette\Security\IdentityHandler and it has the ability to influence this.

As an example, we will show a solution to a common question on how to update identity roles right after restoring from a session:

final class Authenticator implements Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
	public function sleepIdentity(IIdentity $identity): IIdentity
	{
		// here you can change the identity before storing after logging in,
		// but we don't need that now
		return $identity;
	}

	public function wakeupIdentity(IIdentity $identity): ?IIdentity
	{
		// updating roles in identity
		$userId = $identity->getId();
		$identity->setRoles($this->facade->getUserRoles($userId));
		return $identity;
	}

In the method wakeupIdentity() we pass the current roles to the identity, eg from the database. It's that easy now.

Of course, in addition to the roles, we can update other information, or return the entire new identity. You can even return null if the user has been banned, for example, to log him out.

And now we return to the cookie-based storage. It allows you to create a website where users can log in without the need to use sessions. So it does not need to write to disk. After all, this is how the website you are now 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 user in the cookie.

We will add a column authtoken in the database, in which each user will have a completely random, unique and unguessable string of sufficient length (at least 13 characters). The repository CookieStorage stores only the value $identity->getId() in the cookie, so in sleepIdentity() we replace the original identity with a proxy with authtoken in the ID, on the contrary in the method wakeupIdentity() we restore whole identity from the database according 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);
		// check password
		...
		// we return the identity with all the data from the database
		return new SimpleIdentity($row->id, null, (array) $row);
	}

	public function sleepIdentity(IIdentity $identity): SimpleIdentity
	{
		// we return a proxy identity, where in the ID is authtoken
		return new SimpleIdentity($identity->authtoken);
	}

	public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
	{
		// replace the proxy identity with a 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;
	}
}

You can switch the storage in the configuration with security › authentication › storage: cookie.

Compatibility

As mentioned, the interface Nette\Security\UserStorage is completely different from the original IUserStorage. It is implemented by the above-mentioned repositories SessionStorage and CookieStorage. Method Nette\Security\User::getStorage() also returns this new storage. However, you can use the configuration to use to the old storage Nette\Http\UserStorage, which is deprecated and will be removed in version 4:

services:
	security.userStorage: false

The minimum required version is PHP 7.2.