🛠 Build and Secure a simple Symfony API server using Auth0
Tags: programming, webdev, php
Published: 2021-11-30 13:16:20

#### 📘 In this post, you will learn how to build a Symfony API server from scratch. You will also take it a step further to secure some of the endpoints. You'll use Auth0 to handle authentication and authorization. 

### <br>

## Prerequisites

To follow along with this tutorial, you should have reasonable knowledge of Object-Oriented Programming in PHP and basic knowledge of building applications with Symfony. You will also need the following:

- [Composer](https://getcomposer.org/) globally installed on your computer to manage dependencies
- [Symfony CLI](https://symfony.com/download) installed on your computer. Follow the [instructions here](https://symfony.com/download) to set it up for your operating system.
- An Auth0 account. You can <a href="https://auth0.com/signup" data-amp-replace="CLIENT_ID" data-amp-addparams="anonId=CLIENT_ID(cid-scope-cookie-fallback-name)">sign up for a free Auth0 account here</a>.
- [Angular CLI](https://angular.io/cli) globally installed on your computer. Please note that you don't have to be proficient in building applications with Angular. It is only required here to test the API that we will build.

## What You'll Build

You will build a simple Symfony API server with three different endpoints. Each endpoint will return different types of messages depending on the access the user has.

### Public endpoint

- `GET /api/messages/public`

This endpoint should be exposed to anyone unauthorized. It is expected to return the following message:

    {
      "message": "The API doesn't require an access token to share this message."
    }

### Protected endpoint

- `GET /api/messages/protected`

This endpoint will be protected against unauthorized access. Only **authorized users** with a valid access token in their HTTP request header will be able to see the following message:

    {
      "message": "The API successfully validated your access token."
    }

### Admin endpoint

- `GET /api/messages/admin`

Similar to the protected endpoint, this requires the access token to contain a `read:admin-messages` permission to access the admin data. This is often referred to as [Role-Based Access Control (RBAC)](https://auth0.com/docs/authorization/rbac/).

## Getting Started

Here you will start building the Symfony API by setting up and installing a new Symfony application and its required dependencies.

### Scaffolding the Symfony application

To begin, open your terminal, navigate to your preferred development directory, and issue the following command to scaffold a new project using [Composer](https://getcomposer.org/):

```bash
composer create-project symfony/website-skeleton api-symfony-server
```

Once the installation process is completed, switch to the new directory you just created:

````bash
cd api-symfony-server
```
```bash
cp .env .env.local
```

This file is ignored by Git as it matches an existing pattern in `.gitignore` (which Symfony generated). One of the benefits of this file is that it helps to store your credentials outside of code to keep them safe.

Next, update the `DATABASE_URL` parameter in `.env.local` so that the app uses an SQLite database instead of the PostgreSQL default. To do that, comment out the existing `DATABASE_URL` entry and uncomment the SQLite option so that it matches the example below.

```bash
DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
```

> NOTE: The database will be created in the var directory in the project's root directory and be named `data.db`.

### Running the application

Make sure you're in the main project directory and using the [Symfony CLI](https://symfony.com/download), start the application with the following command:

```bash
symfony serve
```

Navigate to [http://localhost:8000](http://localhost:8000) to view the default homepage of the new Symfony application:

![Symfony Homepage](https://images.ctfassets.net/23aumh6u8s0i/3ZJuxRs46wKcsyxHFWvk1a/1fdc14d3c974eb9d7b1402572cbc595e/01_symfony.jpg)

## Building the API

In this section, we will focus on creating controllers that will handle the logic for each endpoint mentioned earlier. We will start with the public endpoint and gradually proceed to handle other endpoints.

Stop the application from running using `CTRL + C` and then hit `Enter`

### Create controllers and configure each endpoint

Start by issuing the following command from the terminal within the root directory of your project to create a Controller:

```bash
php bin/console make:controller APIController
```

You will see the following output:

```
 created: src/Controller/APIController.php
 created: templates/api/index.html.twig

  Success!

 Next: Open your new controller class and add some pages!
```

Locate the newly created controller in `src/Controller/APIController.php` and update its content with the following:

```php
// src/Controller/APIController.php
<?php

namespace App\Controller;

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

/**
 * @Route("/api/messages")
 */
class APIController extends AbstractController
{
    /**
     * @Route("/public", name="public")
     */
    public function publicAction()
    : JsonResponse
    {
        return $this->json(["message" => "The API doesn't require an access token to share this message."], Response::HTTP_OK);
    }

    /**
     * @Route("/protected", name="protected")
     */
    public function protectedAction()
    : JsonResponse
    {
        return $this->json(["message" => "The API successfully validated your access token."], Response::HTTP_OK);
    }

    /**
     * @Route("/admin", name="admin")
     */
    public function adminAction(): JsonResponse
    {
        return $this->json(["message" => "The API successfully recognized you as an admin."], Response::HTTP_OK);
    }
}
```

From the snippet above, this controller contains three different methods:

- `publicAction()`
- `protectedAction()`
- `adminAction()`

Each is designed to handle `/public`, `/protected`, and `/admin` endpoints and returns the appropriate messages, respectively.

Back in the terminal, start the application again with `symfony serve` and open up an API testing tool such as [Postman](https://www.getpostman.com/) to test each endpoint.

Start with the public endpoint. Create a new GET request to this endpoint [http://localhost:8000/api/messages/public](http://localhost:8000/api/messages/public). You will get the message `The API doesn't require an access token to share this message`, as shown below:

![Public Endpoint Using Postman](https://images.ctfassets.net/23aumh6u8s0i/1EVW07UZTck5Ed6VuLwnCP/d8c9ef7f1e726cbe990eee619ff21b6a/02_symfony.png)

Next, try out the protected endpoint on [http://localhost:8000/api/messages/protected](http://localhost:8000/api/messages/protected).

![Protected Endpoint using Postman](https://images.ctfassets.net/23aumh6u8s0i/4rNwtFqz9vDFG208KDCEYL/9d0f71f61e1689397240487559531303/03_symfony.png)

And lastly, the admin endpoint on [http://localhost:8000/api/messages/admin] (http://localhost:8000/api/messages/admin) will give you the message `The API successfully recognized you as an admin.`:

![Admin Endpoint using Postman](https://images.ctfassets.net/23aumh6u8s0i/7ApYe4u0vxvxjAafSJB4XO/4d5a1fe9f1ed3187c7f076ff06b4638d/04_symfony.png)

At the moment, all the created endpoints can be accessed by anyone. Of course, this is not what we want. You need to ensure that `/protected` and `/admin` endpoints are exposed to authorized users only. You will start the configuration in the next section.

If you haven't yet, make sure you <a href="https://auth0.com/signup" data-amp-replace="CLIENT_ID" data-amp-addparams="anonId=CLIENT_ID(cid-scope-cookie-fallback-name)">create a free Auth0 account now</a>.

### Securing protected and admin endpoints

You will use Auth0 to secure the endpoints (protected and admin). To do that, you will need to head back to your [Auth0 dashboard](https://manage.auth0.com/dashboard) and configure an API.

To begin, navigate to [the API section of your Auth0 management dashboard](https://manage.auth0.com/#/apis) by clicking "Applications"> "APIs". If you have created any APIs before, this will show you the list of all APIs for your account, but for this tutorial, go ahead and click on the **"CREATE API"** button and set up a new one.

![Create New API](https://images.ctfassets.net/23aumh6u8s0i/3U1zGzKsMkPCD0BzsmsPwq/9ba551bcb9448c3dc5ea6e9118bd7bb7/05_symfony.png)

Provide a friendly name such as `Symfony API Server` for the API and set its identifier to `https://localhost:8000`. You are free to use any name and identifier, but if you want to follow this tutorial exactly, you should maintain the values above. Leave the signing algorithm as `RS256` and click on the **"Create"** button to proceed. You will need the values from here later in the tutorial.

### Install dependencies and configure authentication

To secure the `GET /api/messages/protected` and `GET /api/messages/admin` endpoints you will use the JWT authentication bundle for Symfony named [auth0/jwt-auth-bundle](https://github.com/auth0/jwt-auth-bundle).

Stop the application from running using `CTRL + C` and run the following command to install the bundle using composer:

```bash
composer require auth0/jwt-auth-bundle:"~4.0"
```

After installing the bundle in your project, you should find a new file located at `config/packages/jwt_auth.yaml`. If not, create the file and paste the following content in it:

```yaml
jwt_auth:
  domain: "%env(AUTH0_DOMAIN)%"
  client_id: "%env(AUTH0_CLIENT_ID)%"
  audience: "%env(AUTH0_AUDIENCE)%"
```

Earlier, when you created an API, Auth0 also automatically created a test application for your API. This will be the Auth0 application that will hold your users. You can find this by clicking `Applications` > `Applications`, then selecting the Test Application from the list that matches what you named your API. If you named it the same as in this tutorial, it will be "Symfony API Server (Test Application)". You can also select and use any other applications for your account. But for this tutorial, click on the test application, and you will see a page as shown here:

![Test Application](https://images.ctfassets.net/23aumh6u8s0i/3futjuuyRCj6GKwAcZDsCb/6f75f56df649b0c7798afc5444b944fc/06_symfony.jpg)

Open `.env.local` file and update the values of the environment variables below:

```
CLIENT_ORIGIN_URL=http://localhost:4040
AUTH0_AUDIENCE=http://localhost:8000
AUTH0_DOMAIN=YOUR_AUTH0_DOMAIN
AUTH0_CLIENT_ID=YOUR_AUTH0_ID
AUTH0_CLIENT_SECRET=YOUR_AUTH0_CLIENT_SECRET
```

Ensure to replace `YOUR_AUTH0_DOMAIN`, `YOUR_AUTH0_CLIENT_ID`, and `YOUR_AUTH0_CLIENT_SECRET` placeholders with the appropriate values as obtained from your Auth0 Dashboard.

### Setting up User and User provider

Handling authentication and authorization requires Auth0 to be aware of the currently authenticated user. This is the job of a [User provider](https://symfony.com/doc/current/security/user_provider.html) in Symfony, as it helps to reload a user from the session and load the user for other specific features like using username or email for authentication.

If users of our API were stored in the database, creating a custom user provider might not be necessary, but here, we will load users from a custom location (Auth0), hence the need to create one.

To begin with, navigate to the `src` folder and create a new folder named `Security` and within the newly created folder, create another one and call it `User`. Next, create the user class within the `User` folder and name it `WebServiceUser.php`. Open the newly created file and paste the following code into it:

```php
<?php

namespace App\Security\User;

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

class WebServiceUser implements
    UserInterface, EquatableInterface {

    private $roles;
    private $jwt;

    public function __construct($jwt, $roles) {

        $this->roles = $roles;
        $this->jwt = $jwt;
    }

    /**
     * @inheritDoc
     */
    public function getRoles()
    : array {

        return $this->roles;
    }

    /**
     * @inheritDoc
     */
    public function getPassword()
    : ?string {

        return null;
    }

    /**
     * @inheritDoc
     */
    public function getSalt()
    : ?string {

        return null;
    }

    public function isEqualTo(UserInterface $user)
    : bool {

        if (!$user instanceof WebServiceUser) {
            return false;
        }

        return $this->getUsername() === $user->getUsername();
    }

    /**
     * @inheritDoc
     */
    public function getUsername() {

        return $this->jwt["email"] ?? $this->jwt["sub"];
    }

    /**
     * @inheritDoc
     */
    public function eraseCredentials() {
    }

    public function getUserIdentifier() {
        return $this->jwt["email"] ?? $this->jwt["sub"];
    }
}
```

Here, the `WebServiceUser` class implements two different interfaces:

- [UserInterface](https://api.symfony.com/4.1/Symfony/Component/Security/Core/User/UserInterface.html) &mdash; represents the interface that all User classes must implement
- [EquatableInterface](https://api.symfony.com/4.1/Symfony/Component/Security/Core/User/EquatableInterface.html) &mdash; used to test if two objects are equal in security and re-authentication context

Next, create a file in the `User` folder and name it `WebServiceAnonymousUser.php`. This will return the anonymous user. Use the following content for it:

```php
<?php

namespace App\Security\User;

class WebServiceAnonymousUser extends WebServiceUser {

    public function __construct() {

        parent::__construct(null, ['IS_AUTHENTICATED_ANONYMOUSLY']);
    }

    public function getUsername() {

        return null;
    }
}
```

To wrap thing up, create another file within the `User` folder and name it `WebServiceUserProvider.php`. Once you are done, paste the following code in it:

```php
<?php

namespace App\Security\User;

use Auth0\JWTAuthBundle\Security\Core\JWTUserProviderInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Polyfill\Intl\Icu\Exception\NotImplementedException;


class WebServiceUserProvider implements JWTUserProviderInterface {

    public function loadUserByJWT($jwt)
    : WebServiceUser {
        $data = ['sub' => $jwt->sub];
        $roles = [];
        $roles[] = 'ROLE_OAUTH_AUTHENTICATED';

        return new WebServiceUser($data, $roles);
    }

    public function getAnonymousUser()
    : WebServiceAnonymousUser {

        return new WebServiceAnonymousUser();
    }

    public function loadUserByUsername($username) {

        throw new NotImplementedException('method not implemented');
    }

    public function refreshUser(UserInterface $user) {

        if (!$user instanceof WebServiceUser) {
            throw new UnsupportedUserException(
                sprintf('Instances of "%s" are not supported.', get_class($user))
            );
        }

        return $this->loadUserByUsername($user->getUsername());
    }

    public function supportsClass($class)
    : bool {

        return $class === 'App\Security\User\WebServiceUser';
    }

    public function loadUserByIdentifier(string $identifier)
    {
        throw new NotImplementedException('method not implemented');
    }
}
```

This class implements the `JWTUserProviderInterface` from the [Auth0 bundle](https://github.com/auth0/jwt-auth-bundle) installed earlier, which specifies the important methods that the `WebServiceUserProvider` class must implement. These methods are:

- `loadUserByJWT`: it receives the decoded JWT Access Token and returns a User.
- `getAnonymousUser`: returns an anonymous user that represents an unauthenticated one (usually represented by the role `IS_AUTHENTICATED_ANONYMOUSLY`)

[Read more...](https://auth0.com/blog/build-and-secure-a-simple-symfony-api-server-using-auth0/?utm_source=content_synd&utm_medium=sc&utm_campaign=symfony)