Yesterday we saw how to secure our API with a Token and how to use the Token in VueJs. Today we will see how to authenticate a user in our application in a SPA context using VueJS.
Let’s proceed step by step.
1. Security with Symfony
A) Create User Entity
<?php
namespace App\Entity;
...
/**
* @ApiResource(
* itemOperations={
* "get"={
* "path"="/users/{id}",
* "swagger_context"={
* "tags"={"Account"}
* }
* }
* },
* collectionOperations={
* "post"={
* "path"="/users",
* "method"="POST",
* "swagger_context"={
* "tags"={"Account"},
* "summary"={"Create a new account"}
* }
* },
* "get"={
* "path"="/users",
* "method"="GET",
* "swagger_context"={
* "tags"={"Account"}
* }
* }
* },
* )
*
* @ORM\Entity(repositoryClass="App\Repository\UserAccountRepository")
*/
final class UserAccount implements UserInterface
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=180)
*/
private $firstName;
/**
* @ORM\Column(type="string", length=180)
*/
private $lastName;
/**
* @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($apiToken): void
{
$this->apiToken = $apiToken;
}
public function getFirstName()
{
return $this->firstName;
}
public function setFirstName($firstName): void
{
$this->firstName = $firstName;
}
public function getLastName()
{
return $this->lastName;
}
public function setLastName($lastName): void
{
$this->lastName = $lastName;
}
}
B) Create Authenticator
<?php
namespace App\Security;
...
class JsonAuthenticator extends AbstractFormLoginAuthenticator
{
use TargetPathTrait;
private $entityManager;
private $router;
private $csrfTokenManager;
private $passwordEncoder;
public function __construct(
EntityManagerInterface $entityManager,
RouterInterface $router,
CsrfTokenManagerInterface $csrfTokenManager,
UserPasswordEncoderInterface $passwordEncoder
) {
$this->entityManager = $entityManager;
$this->router = $router;
$this->csrfTokenManager = $csrfTokenManager;
$this->passwordEncoder = $passwordEncoder;
}
public function supports(Request $request)
{
return 'app_account' === $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'],
'csrf_token' => $data['_csrf_token'],
];
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['email']
);
return $credentials;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
$user = $this->entityManager->getRepository(UserAccount::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)
{
return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
return new JsonResponse($exception->getMessage());
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
return new JsonResponse('authenticated');
}
}
C) Update Security configuration
Adding a new provider and a new firewall.
security:
encoders:
App\Entity\UserAccount:
algorithm: bcrypt
cost: 12
App\Entity\UserApi:
algorithm: bcrypt
cost: 12
providers:
app_user_provider:
entity:
class: App\Entity\UserAccount
property: email
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
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
main:
pattern: ^/
anonymous: true
provider: app_user_provider
json_login:
check_path: app_account
username_path: email
password_path: password
guard:
authenticators:
- App\Security\JsonAuthenticator
logout:
# The route name the user can go to in order to logout
path: app_account_logout
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
- { path: '^/api/docs', roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: '^/api', roles: ROLE_API }
D) Create Login Controller
<?php declare(strict_types=1);
namespace App\Controller\Account;
...
final class Account extends AbstractController
{
/**
* @Route("/account", name="app_account")
*/
public function __invoke(AuthenticationUtils $authenticationUtils): Response
{
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
// last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('account/app.html.twig', [
'last_username' => $lastUsername,
'is_authenticated' => json_encode(!empty($this->getUser())),
'error' => $error
]);
}
/**
* @Route("/account/logout", name="app_account_logout", methods={"GET"})
*
* @throws \Exception
*/
public function logout()
{
// controller can be blank: it will never be executed!
throw new \Exception('Don\'t forget to activate logout in security.yaml');
}
}
That’s all for this section. Now we need to take care of the following steps in order to authenticate the user with a VueJS application.
2. Integration with VueJs
A) Install Vue Router
$ ./node_modules/.bin/yarn add vue-router –save
B) Create Components
We will use two components, Login.vue that performs login and Home.vue that will show the personal data of an authenticated user.



If an user is already authenticated, or if the user has just logged in, the component redirects to the Home.
C) Create Routes
//assets/js/routes/routes.js
import Home from '../components/account/Home.vue'
import Login from '../components/account/Login.vue'
const routes = [
{ path: '/', component: Home, meta: { requiresAuth: true } },
{ path: '/login', component: Login, props: true }
]
export default routes
D) Create store
The store is in charge of sharing the status, in this case authenticated, with all the components.
//assets/js/store/authentication-store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export const store = new Vuex.Store({
state: {
authenticated: false
},
mutations: {
change (state, value) {
state.authenticated = value
}
},
getters: {
isAuthenticated: state => {
if (state.authenticated === 'true' || state.authenticated === true) {
return true
}
return false
}
}
})
E) Create app.js
//assets/js/router-app.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import AppRouter from './AppRouter'
import routes from './routes/routes'
import { store } from './store/authentication-store'
Vue.use(VueRouter)
const router = new VueRouter({
routes: routes
})
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
console.log('route need authentication')
if (store.getters.isAuthenticated !== false) {
console.log('user is authenticated: ' + store.getters.isAuthenticated)
next()
} else {
console.log('page should be redirect')
next({
path: '/login',
query: { redirect: to.fullPath }
})
}
} else {
next()
}
})
// eslint-disable-next-line no-unused-vars
var routeVm = new Vue({
el: '#router-app',
store,
data: {
csrf_token: '',
last_email: ''
},
router,
template: '<AppRouter v-bind:csrf_token="csrf_token" v-bind:last_email="last_email"/>',
components: { AppRouter },
beforeMount () {
this.csrf_token = this.$el.attributes['data-token'].value
this.last_email = this.$el.attributes['data-last-email'].value
this.$store.commit('change', this.$el.attributes['data-is-authenticated'].value)
}
})
Every time a route is requested, the app checks if the user hasn’t authenticated yet. If he is not logged in, the app will redirect him to the login page.
When the user logs in, he will be automatically redirected to the home.
Be aware that the controller provides an is_authenticated parameter to the view, that is used from the app to initialize the store.
From this point on, the front office app will handle everything about authentication.
E) Update Webpack.config.js
var Encore = require('@symfony/webpack-encore')
Encore
.setOutputPath('public/build/')
.setPublicPath('/build')
.cleanupOutputBeforeBuild()
.enableSingleRuntimeChunk()
.enableSassLoader()
.enableSourceMaps(!Encore.isProduction())
.enableVersioning(Encore.isProduction())
...
.addEntry('router-app', './assets/js/router-app.js')
...
...
module.exports = Encore.getWebpackConfig()
Et voilà, you can test the result at: https://localhost:8443/account
Conclusions
In my opinion, an important part in the modern web applications is authentication. It enables organizations to keep their networks secure by allowing only authenticated users (or processes) to access its protected resources, which may include computer systems, networks, databases, websites and other network-based applications or services.
In this post we saw how easy it is to implement different types of authentication with Symfony and Api Platform. For the purpose of the article we have used some basic examples. But complex authentication can also be implemented, such as the famous double factor authentication.
We also saw how to implement the process in a front office using VueJs but the logic remains the same if you decide to use another front library such as ReactJs.
I hope this quick tutorial can help you. The sample code used in this post is freely available on GitHub in authentication branch.
A big thank you to Abel Hadjadj, my colleague at France Television, for taking time to review the two posts.
[…] 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 […]