Symfony on a lambda: authentication
Tags: php, symfony, lambda, serverless
Published: 2020-04-10 07:08:51

---
title: Symfony on a lambda: authentication
published: true
description: Learn how to handle authentication in a serverless environment 
tags: PHP, Symfony, Lambda, Serverless
series: Symfony on a lambda
---

#Symfony on a lambda: connections

If you are lost, you can find the project on Github: [link](https://github.com/hyoa/symfony-on-lambda)
Each branch will match a chapter.

### Authentications

Now that we know how to handle our sessions, we can work with the authentication.

#### Creation of the user table

Like for the session, we need a table to store our users. Let's create one !
In our **serverless.yml** will add the following resource:

```yaml
//serverless.yml
service: [name of your project]

provider:
	// ...
custom:
	// ...
plugins:
	// ...

functions:
	// ...
	
resources:
	Resources:
		// ...
    UsersTable:
      Type: AWS::DynamoDB::Table
      Properties:
      	TableName: ${self:custom.prefix}-usersTable
     		AttributeDefinitions:
      		- AttributeName: email
      			AttributeType: S
     		KeySchema:
      		- AttributeName: email
      			KeyType: HASH
      	ProvisionedThroughput:
      		ReadCapacityUnits: 1
      		WriteCapacityUnits: 1
```

We add a table to store our users, and define the field `email` as the index. We can now just run `serverless deploy` to create the table.

Once the table is created, we can now code the authentication system !

#### Forms 

Symfony has a simple tool to produce what we need to login, lets use it. But first we need to create a class for our user.
In **src/Entity**, add the following class:

```php
<?php

namespace App\Entity;

use Symfony\Component\Security\Core\User\UserInterface;

class User implements UserInterface
{
    /** @var string */
    protected $email;

    /** @var string */
    protected $password;

    /** @var array */
    protected $roles;

    public function getEmail(): ?string
    {
        return $this->email;
    }

    public function setEmail(string $email): string
    {
        $this->email = $email;

        return $this;
    }

    public function setRoles(array $roles): self
    {
        $this->roles = $roles;

        return $this;
    }

    public function getRoles(): array
    {
        return $this->roles;
    }

    public function getPassword(): ?string
    {
        return $this->password;
    }

    public function setPassword(string $password): self
    {
        $this->password = $password;

        return $this;
    }

    public function getSalt()
    {
        return null;
    }

    public function getUsername(): string
    {
        return $this->email;
    }

    /**
     * Removes sensitive data from the user.
     *
     * This is important if, at any given point, sensitive information like
     * the plain-text password is stored on this object.
     */
    public function eraseCredentials()
    {
        // TODO: Implement eraseCredentials() method.
    }
}
```

Now, run the command `php bin/console make:auth`, then type `1`, `LoginFormAuthenticator` and press enter for the other questions.

One small problem is, the template used for login has no style. Let's add some (so we can at least see the fields). Because I'm lazy (and that I have no idea for the design of this exercise), I will borrow what has been done on the landing page.

Here is my template:

```html
{% extends 'base.html.twig' %}

{% block title %}Log in!{% endblock %}

{% block body %}
    <section id="landing">
        <div id="landing-background">
        </div>
        <section class="w-1/2 mx-auto -mt-10 bg-white shadow-md py-2 px-4 rounded">
            <form method="post">
                {% if error %}
                    <div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
                {% endif %}

                {% if app.user %}
                    <div class="mb-3">
                        You are logged in as {{ app.user.username }}, <a href="{{ path('app_logout') }}">Logout</a>
                    </div>
                {% endif %}

                <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
                <label for="inputEmail">Email</label>
                <input
                    type="email"
                    value="{{ last_username }}"
                    name="email" id="inputEmail"
                    required
                    autofocus
                >

                <label for="inputPassword">Password</label>
                <input
                    type="password"
                    name="password"
                    id="inputPassword"
                    required
                >

                <input type="hidden" name="_csrf_token"
                       value="{{ csrf_token('authenticate') }}"
                >

                {#
                    Uncomment this section and add a remember_me option below your firewall to activate remember me functionality.
                    See https://symfony.com/doc/current/security/remember_me.html

                    <div class="checkbox mb-3">
                        <label>
                            <input type="checkbox" name="_remember_me"> Remember me
                        </label>
                    </div>
                #}

                <button class="bg-blue-400 text-white p-2 rounded-sm mt-2" type="submit">
                    Sign in
                </button>
            </form>
        </section>
    </section>
{% endblock %}

```

To style the input, create **assets/scss/components/form.scss** with the following style:

```scss
form {
  input[type="text"], input[type="email"], input[type="password"] {
    @apply block border border-gray-400 p-2 w-full;
  }
}
```

And dont forget to import it in **assets/scss/app.scss**

We will now do the same for the registration.
Run `php bin/console make:registration-form`, just answer no to `Do you want to add a @UniqueEntity validation annotation on your User class to make sure duplicate accounts aren't created? (yes/no) [yes]`, we will handle that ourself. For this one we will also change the design, here is my template

```html
{% extends 'base.html.twig' %}

{% block title %}Register{% endblock %}

{% block body %}
    <section id="landing">
        <div id="landing-background">
        </div>
        <section class="w-1/2 mx-auto -mt-10 bg-white shadow-md py-2 px-4 rounded">
          	Please register
            {{ form_start(registrationForm) }}
                {{ form_row(registrationForm.email) }}
                {{ form_row(registrationForm.plainPassword) }}
                {{ form_row(registrationForm.agreeTerms) }}

              	<button class="bg-blue-400 text-white p-2 rounded-sm mt2">Register</button>
            {{ form_end(registrationForm) }}
        </section>
    </section>
{% endblock %}

```

Great, now that we have some visual, let's work connect everything !

#### Registration

First thing we want to work with is the registration.
We need an encoder to encode our password. Let's add it, open **config/packages/security.yaml** and add the following lines:

```yaml
security:
    //....
    encoders:
        Symfony\Component\Security\Core\User\UserInterface: auto
```

Now, because we don't use an SQL database we can't use doctrine to persist our user. Let's create our own repository to work with.
In **src/Repository/UserRepository.php** add the following lines

```php
<?php

namespace App\Repository;

use App\Entity\User;
use App\Services\AwsClient;
use Aws\DynamoDb\Marshaler;

class UserRepository
{
    protected $dynamoDbClient;

    protected $tableName;

    public function __construct(AwsClient $awsClient, string $tableName)
    {
        $this->dynamoDbClient = $awsClient->dynamoDbClient();
        $this->tableName = $tableName;
    }

    public function insert(User $user): User
    {
        $item = (new Marshaler())
            ->marshalJson((string) json_encode([
                'email' => $user->getEmail(),
                'password' => $user->getPassword(),
                'roles' => $user->getRoles()
            ]));

        $this->dynamoDbClient->putItem([
            'TableName' => $this->tableName,
            'Item' => $item,
        ]);

        return $user;
    }
}
```

We just create a service with the **dynamoDbClient** we used for our session handling. We also pass the table name to the constructor as it change depending of the stage and project name. Because of that we need to update our **config/services.yaml**

```yaml
//...

App\Repository\UserRepository:
	arguments:
	$tableName: '%env(APP_AWS_RESOURCE_PREFIX)%-usersTable'
```

To come back to our repository, we just create a simple **insert**  function. DynamoDb has its own format so we have to transform the data with the help of `Marshaler` .

We should now update the **RegistrationController.php** to use this repository. The function **register** should now look like this:

```php
    public function register(
        Request $request,
        UserPasswordEncoderInterface $passwordEncoder,
        GuardAuthenticatorHandler $guardHandler,
        LoginFormAuthenticator $authenticator,
        UserRepository $userRepository
    ): Response {
        $user = new User();
        $form = $this->createForm(RegistrationFormType::class, $user);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            // encode the plain password
            $user->setPassword(
                $passwordEncoder->encodePassword(
                    $user,
                    $form->get('plainPassword')->getData()
                )
            );

            $user->setRoles(['ROLE_USER']);
            $userRepository->insert($user);

//            return $guardHandler->authenticateUserAndHandleSuccess(
//                $user,
//                $request,
//                $authenticator,
//                'main' // firewall name in security.yaml
//            );
        }

```

We pass our repository and use it. We also give a role to our user so once login it can do something. As we don't have connect the authentication, we comment the rule that auto-connect once logging. We will uncomment it later.

#### Login

We now have a user in the database, so we can connect. Still, because we don't use a SQL database, we will need to have or own user provider that will fetch our user from our dynamoDb table.

Create a new class **src/Security/UserProvider.php**:

```php
<?php

namespace App\Security;

use App\Entity\User;
use App\Repository\UserRepository;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

class UserProvider implements UserProviderInterface
{
    /** @var UserRepository */
    protected $userRepository;

    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    public function loadUserByUsername($username): UserInterface
    {
        $user = $this->userRepository->findOneByEmail($username);

        if ($user === null) {
            throw new UsernameNotFoundException();
        }

        return $user;
    }

    public function refreshUser(UserInterface $user)
    {
        return $user;
    }

    public function supportsClass($class)
    {
        return User::class === $class;
    }
}

```

The class does not do a lot of thing: 

* `loadUserByUsername` will get our user using the repository (don't worry, we will implement the method just after).
* `refreshUser` is used once the page change and that we are logged, we can refresh some data if we want.
* `supportsClass` is only use to determine if this provider can be use with the entity we retrieved.

Let's create our method find in the repository.

```php
// src/Repository/UserRepository
public function findOneByEmail(string $email): ?User
{
    $key = (new Marshaler())
      	->marshalJson((string) json_encode(['email' => $email]));

    $userDocument = $this->dynamoDbClient
        ->getItem([
          'TableName' => $this->tableName,
          'Key' => $key,
        ])
        ->get('Item')
    ;

    if ($userDocument === null) {
      	return null;
    }

    $userArray = (new Marshaler())->unmarshalItem($userDocument);

    return (new User())
        ->setEmail($userArray['email'])
        ->setPassword($userArray['password'])
        ->setRoles($userArray['roles'])
    ;
}

```

Here, we search the user using its email. Because we set `email` as the index of the table, we can query on it easily.
If we don't have any user we just return `null`, otherwise we hydrate a new `User` object.

We can now tell Symfony to use our custom provider. To do that, we have to edit **src/config/packages/security.yaml**:

```yaml
security:
		providers:
        user_provider:
            id: App\Security\UserProvider
    //...

```

Now it's time to update **src/Security/LoginFormAuthenticator.php**. Once create using the make command, some methods are let empty, so we have to complete them.

First, we have to update the constructor:

```php
protected $userPasswordEncoder;

public function __construct(
  	UrlGeneratorInterface $urlGenerator,
  	CsrfTokenManagerInterface $csrfTokenManager,
  	UserPasswordEncoderInterface $userPasswordEncoder
) {
  	$this->urlGenerator = $urlGenerator;
  	$this->csrfTokenManager = $csrfTokenManager;
  	$this->userPasswordEncoder = $userPasswordEncoder;
}

```

We will use the `UserPasswordEncoderInterface` to check that the password entered in the login form match the user that we found in the table.

Next, let's edit the `checkCredentials` method:

```php 
public function checkCredentials($credentials, UserInterface $user)
{
  	return $this->userPasswordEncoder->isPasswordValid($user, $credentials['password']);
}

```

The method talk for itself.

Then, we have to edit `onAuthenticationSuccess` to have a correct redirection once the user is authenticated:

```php 
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
  	if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
      	return new RedirectResponse($targetPath);
    }

  	return new RedirectResponse($this->urlGenerator->generate('app_user_index'));
}

```

`app_user_index` is a new route, we will have to create it. To do so, create a new class  **src/Controller/UserController** :

```php
<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class UserController extends AbstractController
{
    /**
     * @Route("/user", name="app_user_index")
     * @IsGranted("ROLE_USER")
     */
    public function index(): Response
    {
        return $this->render('user/index.html.twig', ['user' => $this->getUser()]);
    }
}

```

And then create the template **templates/user/index.html.twig**:

```html
{% extends 'base.html.twig' %}

{% block body %}
    <main id="landing" class="w-screen h-screen bg-blue-100">
        <div id="landing-background">
        </div>
        <section class="w-1/2 mx-auto -mt-10 bg-white shadow-md py-2 text-center rounded">
            <h1 class="text-4xl">Welcome {{ user.email }}</h1>
        </section>
    </main>
{% endblock %}

```

You can now log in, you will be redirect to the template above.

It's time to uncomment the lines that we commented in **src/Controller/RegistrationController.php**, now, once we register we will be automatically log in.

You can logout with the `/logout` route, it has been created automatically earlier.

----

Thanks to the session, we can now navigate without losing the connection. See you in the last chapters for some tips and information that can be helpful.