Backend API Authentication with Symfony 4

This will be the first of a series of posts where you will learn how to create a full blown web app from scratch with Symfony 4 and VueJS. In today’s post we will go over setting up your environment and creating the registration, login and logout endpoints.

Before we begin this tutorial, there are some prerequisites:

  • You should be comfortable with PHP
  • You should have access to a web server that supports PHP 7.2 and MySQL 5.7
  • You need to have composer installed on your server
  • You will need Postman or some way to make requests to the API we’ll be building. Get it here: https://www.getpostman.com

With that out of the way, let’s begin!

Step 1: Setting up your environment/Installing Symfony

To begin, open a terminal, navigate to where you want to store your project and type:

composer create-project symfony/skeleton api

This command will set up the base Symfony project for you in a folder called api. Symfony is a PHP framework and I really like it because out of the box, it doesn’t come with a million things you will never use. You’re able to pick and choose the things need by requiring other libraries using composer.

Once it’s finished installing, move into the api folder by typing:

cd api

Now we need to install some packages that will help us in creating our API.

Run the following commands and I’ll explain what each does.

composer require annotations

Symfony uses an annotations system. If you’ve ever programmed in Java, you’ll be familiar with the concept. But essentially, annotations, in Symfony at least, help in configuring your app.

composer require maker --dev

This installs a helpful command line tool that creates basic skeleton classes for you. I’ll go more into this as we use it in the tutorial. And if you notice the –dev is included so that it does not get installed when in a production environment.

composer require doctrine

Doctrine is used to interact with the database. It is the recommended database library to use when working with Symfony.

composer require security

This is a Symfony specific package that adds user authentication to our app.

Once those packages are finished installing, there’s just one more thing we need to do before we start coding. Open the .env file in the root directory. This is where any environment variables would go.

You need to set the DATABASE_URL variable to use your actual database login information. If you’re running the database on the same server as your web server, the only thing you need to change here is the db_user, db_password and db_name part of the url. You don’t need to have the database created yet, but if you do that’s no problem. And as an example if you’re not sure what to do, if your database username is “app_user”, password is “12345” and database name is “app_db”, the DATABASE_URL line would look like this:

DATABASE_URL=mysql://app_user:[email protected]:3306/app_db

And that’s it! We’ve set up our app and we can begin creating our API.

Step 2: Creating the User Entity

Up until this point we haven’t really taken a look at what we installed in step 1. So if you want, take a look through the src and config folders. Those are the two folders we’re going to mainly be working in. The src folder holds all of your code, while the config folders hold, well, configuration files :D. I’ll go over the different configuration files that are important as we build the API.

So the first thing we need to do is create the User entity. Entities, if you are coming from an MVC background are essentially the model. Doctrine kind of splits the work of a model into a few different classes. The first being the entity which is a basic object with properties and methods. It shouldn’t have any methods that interact with the database. It just represents an actual object in your code. Repositories, which are used to run queries that retrieve entities, and an entity manager which saves the models/entities to the database. Going forward I’m going to refer to models as entities since that’s what Doctrine calls them and to not confuse anyone.

To create the User entity, make sure you’re still in the api folder in your terminal and type:

php bin/console make:user

This is a special command that comes from the security package that we installed earlier. Once you run that command it will ask you some questions and will create the entity and repository class for your User entity.

The first question the prompt will ask is what to name your user class. For simplicity sake, let’s just call it User.

Next it will ask if you want to store the user in the database. Yes, we do want to store it in the database, so type yes to that one.

For the display name of the user, we’re going to use the email.

When it asks if your app needs to hash/check user passwords, type yes.

The last question asks if you want to use Argon2i for a password hasher. This one doesn’t matter as much. If you’re environment supports that encryption method (the prompt will tell you if it does), type yes if not, type no.

Now, if you open your src folder inside the Entity and Repository folders you will see there are new files in there. Open the src/Entity/User.php file and take look. Symfony has set up the entire class for you! If you scroll through it, you’ll see it really only has some private properties and getter and setter methods. And that’s really all you need for your User entity. There is one method called getSalt towards the bottom. If the comment says “not needed when using the bcrypt algorithm in security.yaml”. This is also true for the Argon2i algorithm if you chose that in the prompt when creating the User entity, so nothing needs to be done there.

If you look inside the UserRepository class its mostly empty except for a constructor and some commented out methods that are examples.

The final place you should take a look at is the security.yaml file in the config/packages folder. Under the security > encoders section you’ll see App\Entity\User and then the algorithm is set. If you are using argon2i, you’ll see that there, if you’re using bcrypt you’ll see that. If you ever want to change algorithms, that’s where you will set it. Though, after going live with a project, unless you have to for security reasons, you shouldn’t really change the encoder algorithm. That would require all of your users to change their password.

That’s all you really need to know about this file for now. We’ll come back to it later when we set up our authentication system.

Step 3: Creating the User table with migrations

Now that we have our User entity, we need the ability to create a User in our system. Before we start, at this point you will need the database set up with the user table. Symfony and Doctrine makes this process really easy to do. If you don’t already have a database set up. Make sure you’re in the api folder still and in your terminal type:

php bin/console doctrine:database:create

If you have your .env file configured correctly from the first step, your database should be created now.

Next type:

php bin/console make:migration

This command will create a migration file based on the entities in your Entity folder. Essentially, Doctrine looks at your current database, compares it to your entities and comes up with the sql needed to create and modify your database tables. Open up the src/Migrations folder and you’ll see the file in there.

In the up function, you’ll see a CREATE TABLE query that creates your user table with the properties that are defined in your User entity class. In the down function, it drops the user table. The down function would be ran if you ever wanted to move down a version in your code. If that’s a little confusing, here’s an example of when you would use it. Say you have your app finished. You decide to create a new feature that requires some database modifications. So you use the make:migration command and create the migration files and go live with your new feature. Then a few hours go by and you notice a huge bug in the feature and you need to go back to what you previously had. Going down in a migration will help to bring your database back to when your code was working. Most of the time though, you are really only going to be using the up function’s queries.

Now that the migration files are created, we need to run them! To do that there is another command that, type in your terminal:

php bin/console doctrine:migrations:migrate

After running that command it will warn you that there will be schema changes and it could cause data to be lost. We don’t need to worry about this since it’s the very first time we’re running a migration and we have no data to lose! You do want to be mindful of this however once you have real data stored.

So to continue type y and then press enter. Your database should now have a user table. If you have MySQL workbench installed, phpMyAdmin, or if you run MySQL from the command line you’ll be able to see your database with the user table created.

Step 4: Creating a way for a User to register

We have our User entity, we have the database and table it’s going to be stored in, now let’s add a way for someone to sign up to our app!

To do that we will need a controller. If you haven’t guessed it already, yes, there is also a command to create controllers! So let’s type:

php bin/console make:controller

It will ask what you’d like to call it. Let’s call ours UserController and press enter.

Now open up the src/Controller folder and you’ll see a UserController.php file was created. It’s pretty bare bones. It just has an index function that returns some json.

If you know how to setup your server so someone can access the api folder from the web, do that now and you can skip the part in blue below and continue on.

Setting Up your Development Web Server
To be able to access your app from the web we’ll need to set up the server to load your app. Symfony comes with a helpful package that does this for you. I would suggest only using this when developing and moving to use either Apache or nginx when in production.

Type the following into your terminal:

composer require symfony/web-server-bundle --dev

This will install a web server that will run your app when in development. Once it’s installed, type:

php bin/console server:run

It will then show you the IP and port the server is listening on. Copy that full url with the IP and port and paste it into your web browser.

You should see a “Welcome to Symfony” page. If you add a /user to the url, some json should show with the message that’s in our UserController.php file! Going forward we’ll refer to the IP and port combo as your app’s url for simplicity sake.


Before we continue, I’ll explain this UserController file a little. If you notice above the index function is:

/**
 * @Route("/user", name="user")
 */

If you haven’t guessed already, this is how the routes are configured. It’s using the annotations package we installed earlier. The first parameter is the actual route that someone would use to access the index function from the web. The name part is a way to name your routes and refer to them throughout your code. Rather than typing /user you’d type user so that if the route ever needed to change, every spot where you used the name would not have to also be updated.

With that out of the way, lets delete that index function along with the route annotation so we have an empty class. We’re going to create a register function that has a route of /register and a name of api_register. If you want to try to write it yourself first go ahead, if not see my code below.

/**
 * @Route("/register", name="api_register")
 */
public function register()
{

}

To see that it’s working lets output some temporary json by making the body of the function the following:

return $this->json(['result' => true]);

Now go to the /register route in your browser, you should see a json object that says result is true.

We’re going to add one more thing to the annotation though so that this route can only be accessed when someone POSTs data to it. To do that we need to add another parameter to the annotation:

@Route("/register", name="api_register", methods={"POST"})

The methods parameter lets you control which specific request methods are allowed to be used in order for this route to run. As an example, if you also wanted to include a GET request method it would look like: {“POST”,”GET”}

Save and then refresh your /register page. Now it shows an error about the method not being allowed. It works! But how will we POST data to the page without some kind of form? Remember in the prerequisites section I said you would need something called Postman. That’s a desktop app that essentially lets you make all types of requests like POST, GET, DELETE, etc. So if you haven’t already done so, open or install Postman.

When it opens up, if a popup shows with a tab that says Create New, you can close it. Now, if you’ve never used Postman before, you should see a tab that says Untitled Request and then below it a text box to enter a url and the word GET next to it.

Type your app’s url and the /register route and press the Send button. When it’s completed loading, click the Preview button and you should see the same error that you saw when accessing from the browser. Click on the word GET and you will see a dropdown with a bunch of options. Choose POST and then click the Send button again. Now you should see the result is true json that we saw earlier before we made the request method restriction.

Now let’s do something real inside the register function and actually create a user. The first thing we need to do is give this class the ability to access the User entity. To do that we need to add a use statement at the top of our UserController.php file under the 2 use statements that Symfony added for us automatically. So type:

use App\Entity\User;

We will also need something called an Object Manager. This is what Doctrine uses to save entities to the database. To import that underneath the other use statements type:

use Doctrine\Common\Persistence\ObjectManager;

Next we will need Symfony’s Request object so we can access the data that’s posted to us. Here is the use statement you need to add for that:

use Symfony\Component\HttpFoundation\Request;

The final thing we’ll need is a password encoder. This is going to be used to encrypt the password that the user passes to us when we store it in the database. You never want to store a plaintext password in your database. It should always be encrypted. The class we need to import for that is called UserPasswordEncoderInterface. This one has a long namespace so if your IDE is able to auto import classes based on the name, use that, otherwise type in the code below:

use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;

Now, onto the register function. When working with controllers in Symfony, each method is able to take parameters and Symfony will automatically create them and pass them into your function if Symfony knows what they are. This is called dependency injection. So for example, we are going to need an ObjectManager object, a UserPasswordEncoderInterface object and a Request object passed to us in the register function. To do that, add three parameters to the register function, one with a type hint for the ObjectManager, one type hinted for UserPasswordEncoderInterface and one for Request. See my code below if you need help:

public function register(ObjectManager $om, UserPasswordEncoderInterface $passwordEncoder, Request $request)
{
   return $this->json(['result' => true]);
}

We don’t have to initialize those three objects in our register function since Symfony will do it for us.

Next, let’s create our User object. Do this just as you would with any other object in PHP:

$user = new User();

Now we need to set the properties of our new user. We are going to be posting an email, a password and a password_confirmation to the /register endpoint. You can access that from the Request object that is passed to us. For POST data, the Request object has a property attached to it called (a little confusingly) request, which holds this information. Then you use the get function of that request property to access the POST data. That might sound a little confusing so here is the code which should hopefully make a little more sense.

$user = new User();

$email                  = $request->request->get("email");
$password               = $request->request->get("password");
$passwordConfirmation   = $request->request->get("password_confirmation");

To make sure it’s working so far, let’s remove that initial json that we are returning and return the email, password and password_confirmation variables in the json instead:

return $this->json([
   'email' => $email,
   'password' => $password,
   'password_confirmation' => $passwordConfirmation
]);

Your entire function should now look like this:

/**
 * @Route("/register", name="api_register", methods={"POST"})
 */
public function register(ObjectManager $om, UserPasswordEncoderInterface $passwordEncoder, Request $request)
{

   $user = new User();

   $email                  = $request->request->get("email");
   $password               = $request->request->get("password");
   $passwordConfirmation   = $request->request->get("password_confirmation");

   return $this->json([
	   'email' => $email,
	   'password' => $password,
	   'password_confirmation' => $passwordConfirmation
   ]);
}

Let’s head back over to Postman. If you look at the middle part of the screen you should see some tabs that say Params, Authorization, Headers, Body, etc… Click on the Body tab. Then in the key type email, for value type [email protected]. In the next line type password in the key and put 12345 for the value, and do the same in the next line except change password to password_confirmation. Click send, and you should see some json with the data that we posted. Test it out using other values for email, password and password_confirmation so you can see it working.

Ok, back to the register function. Before we save the user we want to make sure that the password and the password_confirmation are equal. So let’s create an empty array and name it $errors. Then create an if statement that checks if $password does not equal $passwordConfirmation. If that’s true add a message to the $errors array saying that the password does not match the confirmation. Try writing that code yourself and when your done or need help take a look at my code:

$errors = [];
if($password != $passwordConfirmation)
{
     $errors[] = "Password does not match the password confirmation.";
}

Next we want to also check that the password is at least 6 characters long. If it’s less than 6 characters, add a message saying so in the $errors array. Try writing that check on your own. If you need help or are finished, here’s my code:

if(strlen($password) < 6)
{
   $errors[] = "Password should be at least 6 characters.";
}

Next we need to encrypt the password. To do that we are going to use the $passwordEncoder that we passed into our register function. The password encoder has a method called encodePassword that takes two parameters. The first is the User entity we are working with, and the next is the plaintext password. Like always, if you can, try to write that code yourself, but here’s how if your not sure:

$encodedPassword = $passwordEncoder->encodePassword($user, $password);

The remainder of the function is just using the setter methods to set the email and password properties of the user. Also, we only want to create the user if there are no errors, so assigning values to the user and encoding the password should be wrapped in an if statement that checks to make sure there are no errors. Here is that code if you’re not sure:

if(!$errors)
{
   $encodedPassword = $passwordEncoder->encodePassword($user, $password);
   $user->setEmail($email);
   $user->setPassword($encodedPassword);
}

Next we need to actually save this new user. To do that we will use the ObjectManager. There are two things we need to do. The first is call the persist function of the ObjectManager and pass the $user to it. Then we need to call the flush function of the ObjectManager. Here is what that looks like:

$om->persist($user);
$om->flush();

This is how Doctrine saves entities to the database. First you call persist, then you call flush. You can persist multiple objects at once and call flush once at the end to save them all at the same time.

Next let’s return the User object that we created in json by doing the following:

return $this->json([
   'user' => $user
]);

Before we finish let’s also return the $errors as json if there are any, along with a HTTP status code of 400. The second parameter in the json method takes the status code.

Here is what your register function should look like now:

/**
 * @Route("/register", name="api_register", methods={"POST"})
 */
public function register(ObjectManager $om, UserPasswordEncoderInterface $passwordEncoder, Request $request)
{

   $user = new User();

   $email                  = $request->request->get("email");
   $password               = $request->request->get("password");
   $passwordConfirmation   = $request->request->get("password_confirmation");

   $errors = [];
   if($password != $passwordConfirmation)
   {
	   $errors[] = "Password does not match the password confirmation.";
   }

   if(strlen($password) < 6)
   {
	   $errors[] = "Password should be at least 6 characters.";
   }

   if(!$errors)
   {
	   $encodedPassword = $passwordEncoder->encodePassword($user, $password);
	   $user->setEmail($email);
	   $user->setPassword($encodedPassword);

	   $om->persist($user);
	   $om->flush();

	   return $this->json([
		   'user' => $user
	   ]);
   }
  
   return $this->json([
	   'errors' => $errors
   ], 400);
}

Let’s try it out! Head back over to Postman. You should have the /register page with the email, password and password_confirmation loaded up already. Let’s first try to get it to fail. So put two different things for the password and password_confirmation. It should come back with the errors array.

Now make the passwords the same with at least 6 characters and click send. No errors, wooo! But the user object looks empty. If you open MySQL workbench or phpMyAdmin, you will see though that the user was saved to the database. The reason nothing shows in the user object is because the properties are private. In order to see them, we need to install one other helpful Symfony package called Serializer. You’ll need to stop your server if you’re using Symfony’s web server by pressing ctrl-c in your terminal, then type:

composer require serializer

When that installs, if you need to restart your server type:

php bin/console server:run

Back in Postman, change the email to something different and press send again. Boom! You see the entire User object. We’ll talk about how we can control what you output from the User entity or any entity in json, but first we need to do one more thing. Try pressing send again without changing the email. Error! That’s because we have the email being unique. To check for this, we will create a try/catch block around the persist, flush and return user lines of the code. We will also need to add another use statement to the top of our class:

use Doctrine\DBAL\Exception\UniqueConstraintViolationException;

This is the exception that’s thrown when a unique constraint fails. We will catch that exception and add an error to the $errors array. And to be safe, we’ll catch a general Exception and add a general error message to the $errors array if that happens. Here is what that part of the code should look like now:

try
{
   $om->persist($user);
   $om->flush();

   return $this->json([
	   'user' => $user
   ]);
}
catch(UniqueConstraintViolationException $e)
{
   $errors[] = "The email provided already has an account!";
}
catch(\Exception $e)
{
   $errors[] = "Unable to save new user at this time.";
}

Now if someone tries to create a user with an email that already has an account, it will fail gracefully. Try to press send one more time in Postman. You should see the errors array saying that there is already an account!

Here is the final register function:

/**
 * @Route("/register", name="api_register", methods={"POST"})
 */
public function register(ObjectManager $om, UserPasswordEncoderInterface $passwordEncoder, Request $request)
{

   $user = new User();

   $email                  = $request->request->get("email");
   $password               = $request->request->get("password");
   $passwordConfirmation   = $request->request->get("password_confirmation");

   $errors = [];
   if($password != $passwordConfirmation)
   {
	   $errors[] = "Password does not match the password confirmation.";
   }

   if(strlen($password) < 6)
   {
	   $errors[] = "Password should be at least 6 characters.";
   }

   if(!$errors)
   {
	   $encodedPassword = $passwordEncoder->encodePassword($user, $password);
	   $user->setEmail($email);
	   $user->setPassword($encodedPassword);

	   try
	   {
		   $om->persist($user);
		   $om->flush();

		   return $this->json([
			   'user' => $user
		   ]);
	   }
	   catch(UniqueConstraintViolationException $e)
	   {
		   $errors[] = "The email provided already has an account!";
	   }
	   catch(\Exception $e)
	   {
		   $errors[] = "Unable to save new user at this time.";
	   }

   }


   return $this->json([
	   'errors' => $errors
   ], 400);

}

We’ve finished the register function! It’s a little on the large side. If you come from an MVC background you’ll know that your controller functions should be on the smaller side. But for the sake of this part of the tutorial, I think it’s fine. We accomplished what we needed to accomplish, and that’s allowing for user registration. We will however, be going over a way of making this function look smaller in a future tutorial!

Step 5: Creating the /login route

Now that users can register, they need to be able to login! To do that lets create a /login route and login function in our UserController.php file. Set up a function similar to the register function except don’t put anything inside and we don’t need any parameters. Make the route /login and name it api_login. Leave the method as POST only. Try writing that function yourself and then have a look at my code:

/**
 * @Route("/login", name="api_login", methods={"POST"})
 */
public function login()
{
   
}

Let’s have it return some simple json so we can check that its working:

return $this->json(['result' => true]);

Now head over to Postman, open new tab and set it up to send a post to your app’s url and the /login route.

You should see it returning result is true. Easy right? Now let’s make this log a user in.

Step 6: Creating an Authenticator

Symfony has a concept called Authenticators. They are classes that handle user authentication. And of course, there is a command line tool that helps you get started with creating one. So to begin, type this command into your terminal:

php bin/console make:auth

This command comes from the Security package we installed at the beginning of this tutorial.

The first question the prompt will ask is which style of authentication do you want, with two options Empty or Login form. You would use a Login form authenticator if you were building a traditional app where the server renders the views. For an API, we need to create our own so choose Empty authenticator.

For class name, we’ll call it LoginAuthenticator. When you press enter, the authenticator will be created. Open the src/Security folder and you’ll see the LoginAuthenticator.php file. Open it up to find a class with a bunch of methods that all have a todo comment in them. We have some coding to do.

The first method we’ll work on is supports. This method should return either true or false. When it returns true, that tells Symfony that this authenticator should be used. So… when does this authenticator get used? It will be used when the user posts to the /login endpoint! To get which route is being accessed type:

$request->get("_route");

This will return the route name. So for us, we want to check that $request->get(“_route”) equals api_login. We also want to check that the request method is POST. To do that you would type:

$request->isMethod("POST");

So putting it all together your supports method should look like this:

public function supports(Request $request)
{
   return $request->get("_route") === "api_login" && $request->isMethod("POST");
}

Next is the getCredentials method. Here we need to return the information that will get us the user that’s trying to log in. So for that we need the email and password that was posted to /login. We learned when we created the register function that the Request object has a request property and we used the get method to get the POST data. Let’s do the same here and return an array with the email and password:

public function getCredentials(Request $request)
{
   return [
	   'email' => $request->request->get("email"),
	   'password' => $request->request->get("password")
   ];
}

Next up is the getUser function. This function should simply return the user that is trying to log in. If you notice it has two parameters, one is $credentials and the other is $userProvider.

The $credentials parameter is going to be whatever we return from the getCredentials function. To show you, type the following inside the getUser function:

var_dump($credentials);die;

Then go over to Postman and add an email and password in the Body tab, similar to what we did when testing out the register route. Then press send and you should see the $credentials array being output. And if you notice, the reason it’s being output is because we have posted to the /login route which was determined to be true in the supports function of our authenticator! If you try adding another user using the register route in our previous tab, it will work as expected.

So with that, we know that we can get the posted email from $credentials[’email’] inside the getUser function. But how can we get the user? Using the second parameter: $userProvider!

The UserProviderInterface has a method called loadUserByUsername. You might ask, but we use email, not username. Yes, you are right. Head over to your User entity class. If you scroll down a little you should find a method called getUsername and it returns $this->email. In Symfony’s Security package, they use “Username” as the universal name of whatever a person uses as their name when logging in. We chose to use email when we were creating the User entity so Symfony knew to create our User entity with that in mind.

So onto using the $userProvider to get our User entity. Like I said above, we will the loadUserByUsername method to get our user. Here is the code for that:

public function getUser($credentials, UserProviderInterface $userProvider)
{
   return $userProvider->loadUserByUsername($credentials['email']);
}

After getUser it’s checkCredentials. And if it wasn’t obvious, this method should check the user’s credentials 😀 checkCredentials receives the same $credentials parameter from the getUser method as well as a UserIterface object. UserInterface is the interface used with our User entity. UserInterface is used here instead of User because we could have really called our User entity anything, we could have called it Member, Person, Admin, etc. Having this be type hinted with UserInterface allows for us to use any type of class that implements UserInterface for this authenticator.

To make sure everything is working lets var_dump the $user parameter and see what happens. Inside checkCredentials type:

var_dump($user);die;

Go over to Postman, and on the login tab, use one of the emails that you used when testing out the register route. Then press send. It should come back with a User entity object.

So now that we know its still working correctly, we need to check the credentials. We have one problem though. The password we saved in our database is encoded and the password that’s passed from the credentials parameter is in plaintext. We need a UserPasswordEncoderInterface like in the register route! How can we do this?

In Symfony, with controllers, you can use dependency injection for any method you want. With other classes, you can only use dependency injection in the constructor.

So to continue, lets add a constructor that has a parameter that’s type hinted with UserPasswordEncoderInterface. We’ll also create a private property on this authenticator class that will hold the password encoder. Try setting that up yourself, if you need help take a look at my code below:

private $passwordEncoder;

public function __construct(UserPasswordEncoderInterface $passwordEncoder)
{
   $this->passwordEncoder = $passwordEncoder;
}

Be sure to add this use statement to the top of your file as well:

use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;

Now we can use this passwordEncoder inside our checkCredentials method! The passwordEncoder has a method called isPasswordValid. It takes two parameters, the first being a UserInterface object and the second the plaintext password. If you can, try writing this out from what I wrote above and if you need help, here is the code:

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

Next up is the onAuthenticationFailure method. This function will be called any time there is an error. If in the getUser method, the loadUserByUsername can’t find a user with the email we gave it, this onAuthenticationFailure method will be called. If the credentials are not correct this method will also be called.

The way you can get the error that occurred is from the AuthenticationException parameter. That exception has a method named getExceptionKey which holds an ok to display error message. So with this being an API, we’ll return json with that error message. Unlike in the controller however, we don’t have access to $this->json. Symfony has a JsonResponse object that we can use though to do this. We will first need to import it at the top of the file with a use statement. Here is that code:

use Symfony\Component\HttpFoundation\JsonResponse;

Now we need to return a new JsonResponse with the error message. Similar to when we used $this->json, we give the JsonResponse object an array for the first parameter and status code for the second like so:

public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
   return new JsonResponse([
	   'error' => $exception->getMessageKey()
   ], 400);
}

You can check that it’s working by using an incorrect password and sending a request in Postman.

Next is onAuthenticationSuccess. This method gets called when the checkCredentials function returns true. For that we don’t need to do anything special. We’ll just return a JsonResponse object with a result of true like so:

public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
   return new JsonResponse([
	   'result' => true
   ]);
}

The next method is named start. This method gets called whenever an endpoint is hit that requires authentication. If we were making a traditional web app where the server rendered the views, we’d redirect to a login form page. Since we have no login form yet and we are just building an API, we will return an error message in json like in the onAuthenticationFailure method. Except this time we will say: “Access Denied” as the message. Here is the code for that:

public function start(Request $request, AuthenticationException $authException = null)
{
   return new JsonResponse([
	   'error' => 'Access Denied'
   ]);
}

We can’t see how that works just yet, but you will soon.

Next up is supportsRememberMe. For this tutorial, we aren’t going to be using remember me. But essentially this method should return true if you want to use remember me, otherwise return false.

Here is the final code for our LoginAuthenticator class:

<?php

namespace App\Security;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;

class LoginAuthenticator extends AbstractGuardAuthenticator
{

   private $passwordEncoder;
   public function __construct(UserPasswordEncoderInterface $passwordEncoder)
   {
       $this->passwordEncoder = $passwordEncoder;
   }

   public function supports(Request $request)
   {
       return $request->get("_route") === "api_login" && $request->isMethod("POST");
   }

   public function getCredentials(Request $request)
   {
       return [
           'email' => $request->request->get("email"),
           'password' => $request->request->get("password")
       ];
   }

   public function getUser($credentials, UserProviderInterface $userProvider)
   {
       return $userProvider->loadUserByUsername($credentials['email']);
   }

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

   public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
   {
       return new JsonResponse([
           'error' => $exception->getMessageKey()
       ], 400);
   }

   public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
   {
       return new JsonResponse([
           'result' => true
       ]);
   }

   public function start(Request $request, AuthenticationException $authException = null)
   {
       return new JsonResponse([
           'error' => 'Access Denied'
       ]);
   }

   public function supportsRememberMe()
   {
       return false;
   }
}

Step 7: Creating an authenticated route

We have a way for our users to register and login, but now we need to make an endpoint that requires a user to be logged in to see.

Let’s go back to our UserController and create a function called profile. The route will be /profile and the name will be api_profile. Inside we just want to return a user entity in json. In Symfony, controllers that extend AbstractController, like our UserController does, have a method called getUser. This will return the user that is logged in. If you think you can, try to write that out yourself, otherwise here is the code for that:

/**
 * @Route("/profile", name="api_profile")
 */
public function profile()
{
   return $this->json([
	   'user' => $this->getUser()
   ]);
}

Now in Postman, open a new tab, and put in your app url along with the /profile route and hit send. If you didn’t test the /login with a correct login, you should see user: null. Before we try logging in and seeing if this worked, we want to make this user object only visible if the user is logged in. Symfony gives us a bunch of different ways to check if a user is logged in, but my favorite is to use annotations. So underneath our @Route annotation for the profile function, add this:

@IsGranted("ROLE_USER")

And at the top of your file add this use statement:

use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;

Now try to send the request in Postman again. You should see the Access Denied message that we set in our LoginAuthenticator!

Ok, lets try logging in and coming back to this /profile endpoint. Open the /login tab, use one of the email and password combinations you used to test out the registration. You should see result: true, like we set in our authenticator. Hop back over to the /profile tab and hit send. Boom! We have our user entity.

Let me explain some of the IsGranted stuff. So the IsGranted annotation checks to see if the user has a specific role. In this case we are checking for the role called ROLE_USER. If you go to the User entity class, and scroll down to the getRoles method, you’ll see the ROLE_USER role is always included in the return value. This was done for us by using the command line tool.

And lastly, our User entity is being output with the encoded password. While there isn’t much you can do with that, it’s not something we should really be making public. So we should hide it. The serializer package that we installed earlier in the tutorial gives us a way of doing that. In the User entity above the id, email and roles properties, add this annotation:

@Groups("api")

The full code for that part of the class should look similar to this:

/**
 * @ORM\Id()
 * @ORM\GeneratedValue()
 * @ORM\Column(type="integer")
 * @Groups("api")
 */
private $id;

/**
 * @ORM\Column(type="string", length=180, unique=true)
 * @Groups("api")
 */
private $email;

/**
 * @ORM\Column(type="json")
 * @Groups("api")
 */
private $roles = [];

Also, at the top of our file add the following use statement:

use Symfony\Component\Serializer\Annotation\Groups;

Groups is a way of identifying a set of properties that should be serialized. You can call your group anything. In our case, we are calling it api.

So now, we can tell the $this->json method which group to use when serializing the User entity and it will leave out the $password property since we didn’t include it in the group.

Head back over to the UserController and we will need to modify the return $this->json line. We know from outputting errors that the second parameter is an HTTP status code. So for that use 200. The 3rd parameter is an array of additional headers to use when outputting. For us, we don’t need any additional headers so we can use an empty array. The 4th parameter is called context. In this parameter we can pass an array with a groups key. The groups key should have an array of groups. So let’s put api in the array of groups so we only use properties in that group.

Here is what that code looks like:

return $this->json([
      'user' => $this->getUser()
   ], 
   200, 
   [], 
   [
      'groups' => ['api']
   ]
);

Now if you go back to Postman and click send on the /profile tab, you should see the user entity with everything except for the password!

Step 8: Logging Out

To give a user the ability to logout you don’t need to write any code. You just need to modify some configuration files. First, we need to set a route. For this it can’t be a controller method, it just needs to be a route. To do that, we need to modify routes.yaml in the config folder. You can actually set all of your routes here if you don’t want to use annotations. To create the logout route you’d add the following:

api_logout:
    path: /logout

Yaml is spacing sensitive so make sure there are 4 spaces in front of the path attribute.

api_logout is our route name and /logout is how someone would access this route from the web.

Next we need to modify the security.yaml file in the config/packages folder. Under the firewalls > main section add a new section called logout. Underneath logout add a property called path and set it equal to api_logout.

Here’s what that section under firewalls should look like:

firewalls:
    dev:
        pattern: ^/(_(profiler|wdt)|css|images|js)/
        security: false
    main:
        anonymous: true
        guard:
            authenticators:
                - App\Security\LoginAuthenticator
        logout:
            path: api_logout

Logout will redirect to the index page. So we need to make one final route for / so that it returns json. Currently if we were to load our app’s url without any of the routes we made, it shows the Welcome to Symfony page. So back in our UserController, let’s make a simple function similar to the login function. It should just return json that has a result equal to true and change it so any method can access it. Name it api_home and the route is /.

Here’s the code for that:

/**
 * @Route("/", name="api_home")
 */
public function home()
{
   return $this->json(['result' => true]);
}

Let’s try it out. Open a new tab and put in your app’s url with the /logout route and click send. You should see the result is true json.

And that’s it! You’ve got a basic API that allows for user registration and logging in! If you have any questions or are stuck at any of the parts in this tutorial let me know in the comments and I can help you out. Click Here to see all of the code for this project so far. Hopefully you learned some stuff and are able to build off of what you learned in the tutorial. In the next tutorial, I’m going to continue with this project and we’ll learn how to implement a json web token system into our API that we’ll use after the user has initially authenticated.

If you want to be notified when the next post in this series comes out, feel free to sign up to the newsletter below!