During my experience as a software developer, I have observed that one of the most delicate issues is authentication. So I wanted to add a brick to my previous post, in order to explain how to set up a basic authentication process.
Starting from the Bookstore application that we created during the previous PoC, let’s say we now want to add a user account that allows the authenticated user to view and change his personal data. We also want to protect the API that provides the data to the front office.
Thus there are two distinct types of authentication to manage. The first one authorizes a user to use the application and the second one authorizes a client (Web, Mobile or an external source) to use our APIs. We will also see how to integrate both authentication processes into a VueJs application.
Let’s proceed step by step.
1. Install Symfony Security component
$ composer require symfony/security-bundle
2. Protect yours APIs
Adding authentication with Api Platform is very simple. The official documentation explains how to implement it, using a JWT authentication and the excellent LexikJWTAuthenticationBundle. But for simplicity we will use a basic authentication, with a simple md5 token that will be generated at the user’s creation.
A) Create User Entity
<?php declare(strict_types = 1);
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* @ApiResource(
* itemOperations={
* "get"={
* "path"="/users-api/{id}",
* "swagger_context"={
* "tags"={"User"}
* }
* }
* },
* collectionOperations={
* "post"={
* "path"="/users-api",
* "method"="POST",
* "swagger_context"={
* "tags"={"Authentication"},
* "summary"={"User registration"}
* }
* },
* "get"={
* "path"="/users-api",
* "method"="GET",
* "swagger_context"={
* "tags"={"User"}
* }
* }
* },
* )
*
* @ORM\Entity(repositoryClass="App\Repository\UserApiRepository")
* @ORM\HasLifecycleCallbacks()
*/
final class UserApi implements UserInterface
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=180, unique=true)
*/
private $email;
/**
* @ORM\Column(type="json")
*/
private $roles = [];
/**
* @var string The hashed password
* @ORM\Column(type="string")
*/
private $password;
/**
* @ORM\Column(type="string", unique=true, nullable=true)
*/
private $apiToken;
public function getId(): ?int
{
return $this->id;
}
/**
* A visual identifier that represents this user.
*
* @see UserInterface
*/
public function getUsername(): string
{
return (string)$this->email;
}
public function getEmail(): string
{
return (string)$this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
/**
* @see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
/**
* @see UserInterface
*/
public function getPassword(): string
{
return (string)$this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
/**
* @see UserInterface
*/
public function getSalt()
{
// not needed when using the "bcrypt" algorithm in security.yaml
}
/**
* @see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
public function getApiToken(): ?string
{
return $this->apiToken;
}
public function setApiToken(?string $apiToken): void
{
$this->apiToken = $apiToken ?? md5(uniqid(rand(), true));
}
/**
* @ORM\PrePersist
*/
public function createToken(): void
{
if(!$this->apiToken) {
$this->apiToken = md5(uniqid(rand(), true));
}
}
}
B) Create Authenticator
<?php declare(strict_types = 1);
namespace App\Security;
...
class TokenAuthenticator extends AbstractGuardAuthenticator
{
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
/**
* Called on every request to decide if this authenticator should be
* used for the request. Returning false will cause this authenticator
* to be skipped.
*/
public function supports(Request $request)
{
return 'authentication_token' !== $request->attributes->get('_route');
}
/**
* Called on every request. Return whatever credentials you want to
* be passed to getUser() as $credentials.
*/
public function getCredentials(Request $request)
{
$authorisationHeader = $request->headers->get('Authorization');
preg_match('/API-X-TOKEN (.*)/', $authorisationHeader, $matches);
return [
'token' => $matches[1] ?? null,
];
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$apiToken = $credentials['token'];
if (null === $apiToken) {
return;
}
// if a User object, checkCredentials() is called
$user = $this->em->getRepository(User::class)
->findOneBy(['apiToken' => $apiToken]);
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
return true;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
// on success, let the request continue
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$data = [
'message' => strtr($exception->getMessageKey(), $exception->getMessageData())
// or to translate this message
// $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
];
return new JsonResponse($data, Response::HTTP_FORBIDDEN);
}
/**
* Called when authentication is needed, but it's not sent
*/
public function start(Request $request, AuthenticationException $authException = null)
{
$data = [
// you might translate this message
'message' => 'Authentication Required'
];
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
public function supportsRememberMe()
{
return false;
}
}
C) Update Security configuration
#config/packages/security.yaml
security:
encoders:
App\Entity\UserApi:
algorithm: bcrypt
cost: 12
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
# used to reload user from session & other features (e.g. switch_user)
api_user_provider:
entity:
class: App\Entity\UserApi
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
api:
pattern: ^/api/
stateless: true
anonymous: true
provider: api_user_provider
guard:
authenticators:
- App\Security\TokenAuthenticator
# Easy way to control access for large sections of your site
# Note: Only the *first* matching access control will be used
access_control:
- { path: '^/api/docs', roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: '^/api', roles: ROLE_API }
D) Update routes configuration
#config/routes.yaml
authentication_token:
path: /api/authentication_token
methods: ['POST']
E) Update Api platform configuration
To activate authentication management in the Api Platform swagger.
#config/packages/api-platform.yaml
api_platform:
...
swagger:
api_keys:
apiKey:
name: Authorization
type: header
F) Create fixtures
#fixtures/api-users.yaml
App\Entity\UserApi:
user_api_1:
email: api-user@bookstore.com
password: <('$2y$12$wiN7w.K4bj9HWa/qwtLUb.EPvzgMyXgJpd9Ar4yw1rJDz7G9Lvvny')> # admin
roles: ['ROLE_API']
apiToken: eb0bcb7b7b9c1f4636538663f74ee7a6
$ bin/console doctrine:schema:drop --force --no-interaction $ bin/console doctrine:schema:create --no-interaction $ bin/console hautelook:fixtures:load --no-interaction --purge-with-truncate -vvv
Now we need the token in order to use APIs. Go to api doc to see the behavior.

Using the Authorize button, at the right, we put the token:



3. Integration in VueJs
Now all requests to the APIs must have their Authentication header with a valid token. So we have to put the token in the .env file, and via Webpack Encore it will be passed to the VueJs application.
To do this we start by installing dotenv:
$ ./node_modules/.bin/yarn add dotenv
and update the webpack.config.js file:
var Encore = require('@symfony/webpack-encore')
Encore
.setOutputPath('public/build/')
.setPublicPath('/build')
.cleanupOutputBeforeBuild()
.enableSingleRuntimeChunk()
.enableSassLoader()
.enableSourceMaps(!Encore.isProduction())
.enableVersioning(Encore.isProduction())
...
...
.enableVueLoader()
.configureDefinePlugin(options => {
var dotenv = require('dotenv')
const env = dotenv.config()
if (env.error) {
throw env.error
}
options['process.env'].API_TOKEN = JSON.stringify(env.parsed.API_TOKEN)
})
module.exports = Encore.getWebpackConfig()
And do not forget to put the token into .env file
API_TOKEN=eb0bcb7b7b9c1f4636538663f74ee7a6
Watching assets
./node_modules/yarn/bin/yarn encore dev --env-file .env
The token is now readable by Javascript
import Vue from 'vue'
import Hello from './components/Hello'
require('../css/app.scss')
// eslint-disable-next-line no-new
new Vue({
el: '#welcome',
template: '<Hello/>',
components: { Hello },
beforeMount: function () {
console.log(process.env.API_TOKEN)
}
})

This way, it’s easy to fetch requests, for exemple with Vuex. Let’s look at a simple example.
Install Vuex:
$ ./node_modules/.bin/yarn add vuex
Using the store for stock value:
//assets/js/store/store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export const store = new Vuex.Store({
state: {
token: ''
},
mutations: {
token (state, token) {
state.token = token
}
},
getters: {
getToken: state => {
return 'API-X-TOKEN ' + state.token
}
}
})
In the app.js we read the token and we commit a mutation to store
import Vue from 'vue'
import Booklist from './Booklist'
import { store } from './store/store'
// eslint-disable-next-line no-new
new Vue({
el: '#book-list',
store,
template: '<Booklist/>',
components: { Booklist },
beforeMount: function () {
store.commit('token', process.env.API_TOKEN)
}
})
The value will be used for perform request into component
//assets/js/components/Search.vue
...
...
var headers = new Headers();
headers.append('Authorization', this.$store.getters.getToken);
fetch('/api/books', {headers: headers})
.then(response => response.json())
.then(data => {
....
})
That’s all. The token stored in the .env file, is used by the front office and allows to properly communicate with the API.
4. Get Token with api (Optional)
Maybe you want to create an endpoint to remind yourself what your token is, by logging in with your username and password.
To do this the easiest way is to add a second authenticator:
<?php
namespace App\Security;
...
final class ApiJsonAuthenticator extends AbstractFormLoginAuthenticator
{
use TargetPathTrait;
private $token;
private $entityManager;
private $passwordEncoder;
public function __construct(
EntityManagerInterface $entityManager,
UserPasswordEncoderInterface $passwordEncoder
) {
$this->entityManager = $entityManager;
$this->passwordEncoder = $passwordEncoder;
}
public function supports(Request $request)
{
return 'authentication_token' === $request->attributes->get('_route')
&& $request->isMethod('POST');
}
public function getCredentials(Request $request)
{
if ($request->getContentType() !== 'json') {
throw new BadRequestHttpException();
}
$data = json_decode($request->getContent(), true);
$credentials = [
'email' => $data['email'],
'password' => $data['password'],
];
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['email']
);
return $credentials;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$user = $this->entityManager->getRepository(UserApi::class)->findOneBy(['email' => $credentials['email']]);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('Email could not be found.');
}
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
if ($this->passwordEncoder->isPasswordValid($user, $credentials['password'])) {
$this->token = $user->getApiToken();
return true;
}
return false;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
return new JsonResponse('failed');
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
return new JsonResponse('token: '. $this->token);
}
}
Update the security.yaml file:
security:
...
...
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
api:
pattern: ^/api/
stateless: true
anonymous: true
provider: api_user_provider
json_login:
check_path: /api/authentication_token
username_path: email
password_path: password
guard:
authenticators:
- App\Security\TokenAuthenticator
- App\Security\ApiJsonAuthenticator
entry_point: App\Security\ApiJsonAuthenticator
...
...
Let’s use a normalizer in order to make the endpoint visible to the Swagger
<?php declare(strict_types = 1);
namespace App\Normalizer;
...
final class SwaggerDecorator implements NormalizerInterface
{
private $decorated;
public function __construct(NormalizerInterface $decorated)
{
$this->decorated = $decorated;
}
public function normalize($object, $format = null, array $context = [])
{
$customDocumentation = $this->getCustomDocumentation();
$docs = $this->decorated->normalize($object, $format, $context);
return array_merge_recursive($customDocumentation, $docs);
}
public function supportsNormalization($data, $format = null)
{
return $this->decorated->supportsNormalization($data, $format);
}
private function getCustomDocumentation(): array
{
$customDocumentation = [
'paths' => [
'/api/authentication_token' => [
'post' => [
'tags' => ['Authentication'],
'summary' => 'Performs a login attempt, returning a valid token on success',
'parameters' => [
[
'in' => 'body',
'name' => 'body',
'description' => 'Login payload',
'required' => true,
'schema' => [
'type' => 'object',
'required' => [
'email',
'password'
],
'properties' => [
'email' => [
'type' => 'string'
],
'password' => [
'type' => 'string'
]
]
],
],
],
'responses' => [
200 => [
'description' => 'token'
],
401 => [
'description' => 'Bad credentials'
],
400 => [
'description' => 'Bad request'
],
],
]
]
]
];
return $customDocumentation;
}
}
And here’s how the end point in the swagger is represented.


This is the end of the first part on this subject. In the next part, we’ll see how to perform a simple user authentication using VueJs, VueRouter and obviously Symfony.
Hi,
I have a problem with the final part : swagger
« Circular reference detected for service « serializer », path: « serializer -> App\Normalizer\SwaggerDecorator -> serializer ». »
Any idea ?
Thanks,
Best regards
OK, it’s for new version in symfony.
You must declare Swagger in services.yaml :
# Swagger Decorator
App\Normalizer\SwaggerDecorator:
decorates: ‘api_platform.swagger.normalizer.documentation’