Authentication

Nette provides a way to implement authentication on our sites, but it doesn't force any specific approach. The implementation is entirely up to us. Nette includes the Nette\Security\Authenticator interface, which requires only one method, authenticate, to verify the user in any way we choose.

There are many ways a user can be authenticated. The most common method is password-based authentication (where the user provides their name or email and a password), but other methods exist. You might be familiar with “Login with Facebook” buttons or logging in via Google/Twitter/GitHub on some sites. With Nette, we can have any login method, or even combine them. It's entirely our decision.

Typically, we would write our own authenticator. However, for this simple blog, we'll use the built-in authenticator that logs in based on a username and password stored in the configuration file. This is suitable for testing purposes. Let's add the following security section to the configuration file config/common.neon:

security:
	users:
		admin: secret  # user 'admin', password 'secret'

Nette automatically creates a service for this authenticator in the DI container.

Sign-In Form

Now that authentication is set up, we need to create a user interface for logging in. Let's create a new presenter named SignPresenter that will:

  • display a login form (with username and password fields)
  • authenticate the user when the form is submitted
  • provide a sign-out option

We'll start with the login form. We already know how forms work in presenters. Let's create the SignPresenter and write the createComponentSignInForm method. It should look something like this:

<?php
namespace App\Presentation\Sign;

use Nette;
use Nette\Application\UI\Form;

final class SignPresenter extends Nette\Application\UI\Presenter
{
	protected function createComponentSignInForm(): Form
	{
		$form = new Form;
		$form->addText('username', 'Username:')
			->setRequired('Please enter your username.');

		$form->addPassword('password', 'Password:')
			->setRequired('Please enter your password.');

		$form->addSubmit('send', 'Sign in');

		$form->onSuccess[] = $this->signInFormSucceeded(...);
		return $form;
	}
}

There are fields for the username and password.

Template

The form will be rendered in the in.latte template:

{block content}
<h1 n:block=title>Sign in</h1>

{control signInForm}

Login Callback

Next, we'll add the callback method for logging in the user, which will be called immediately after the form is successfully submitted.

The callback simply takes the username and password entered by the user and passes them to the authenticator. After successful login, we redirect to the homepage.

private function signInFormSucceeded(Form $form, \stdClass $data): void
{
	try {
		$this->getUser()->login($data->username, $data->password);
		$this->redirect('Home:');

	} catch (Nette\Security\AuthenticationException $e) {
		$form->addError('Incorrect username or password.');
	}
}

The User::login() method throws an exception if the username and password do not match the credentials in the configuration file. As we know, this could result in a Tracy red screen or, in production mode, a server error message. We don't want that. Therefore, we catch the exception and add a nice, user-friendly error message to the form.

When a form error occurs, the page with the form is re-rendered, and a message appears above the form informing the user that they entered the wrong username or password.

Securing Presenters

We will secure the form for adding and editing posts, which is defined in the EditPresenter. The goal is to prevent users who are not logged in from accessing the page.

We'll create a startup() method, which runs immediately at the beginning of the presenter life cycle. This method will redirect non-logged-in users to the login form.

public function startup(): void
{
	parent::startup();

	if (!$this->getUser()->isLoggedIn()) {
		$this->redirect('Sign:in');
	}
}

An unauthorized user can no longer see the create or edit pages, but they can still see the links pointing to them. We should hide these as well. One such link is in the app/Presentation/Home/default.latte template and should only be visible to logged-in users.

We can hide it using an n:attribute called n:if. If the condition is false, the entire <a> tag, including its content, will not be displayed.

<a n:href="Edit:create" n:if="$user->isLoggedIn()">Create post</a>

which is a shortcut for the following notation (do not confuse it with tag-if):

{if $user->isLoggedIn()}<a n:href="Edit:create">Create post</a>{/if}

Hide the edit link in the app/Presentation/Post/show.latte template in the same way.

How do we actually get to the sign-in page? There's no link leading to it. Let's add one to the @layout.latte template. Try to find a suitable place – it can be almost anywhere.

...
<ul class="navig">
	<li><a n:href="Home:">Home</a></li>
	{if $user->isLoggedIn()}
		<li><a n:href="Sign:out">Sign out</a></li>
	{else}
		<li><a n:href="Sign:in">Sign in</a></li>
	{/if}
</ul>
...

If the user is not logged in, the “Sign in” link is displayed. Otherwise, the “Sign out” link is shown. We'll also add this action to SignPresenter.

Since we redirect the user immediately after signing out, no template is needed. The sign-out action looks like this:

public function actionOut(): void
{
	$this->getUser()->logout();
	$this->flashMessage('You have been signed out.');
	$this->redirect('Home:');
}

It simply calls the logout() method and then displays a confirmation message to the user.

Summary

We have links for signing in and signing out the user. We used the built-in authenticator for authentication, and the login credentials are in the configuration file, as this is a simple test application. We also secured the editing forms so that only logged-in users can add and edit posts.

You can read more about user login and authorization here.