Expose a REST API to different kinds of users with Api-Platform [Part 1/4]

Benoit POLASZEK
5 min readJun 3, 2019

--

The purpose of this guide is to help you better understand how Symfony and Api-Platform can work with different types of users. Imagine the scenario of an online shop where:

  • An Admin (Employee) can view all orders
  • A Customer can only view his own orders
  • Some order informations can only be visible to employees

How can we deal with this with a single endpoint like /api/orders?
In this tutorial, you will learn to:

  • Set up an application with multiple user classes
  • Set up a JWT authentication with multiple user types
  • Set up a user-filtered REST API and leverage Symfony Serializer to obfuscate sensitive information into the API, depending on the user type
  • Create an authenticated API Client to make Ajax requests.

Requirements

I assume you’re not a complete newcomer on Symfony, Api-Platform and front-end coding.
At the time of writing, the versions used are:

  • Symfony 4.3
  • Api-Platform 2.4
  • Webpack Encore 0.27.

Get started

Let’s start on a fresh project (I assume you have Composer installed globally).

composer create-project symfony/skeleton sample-api-project && cd sample-api-project

We’ll need the following packages for the moment (because our project uses Symfony Flex, we can use short aliases for packages):

composer require orm security annotations twig

And some helpful tools to get started quicker:

composer require --dev maker debug-pack server

Okay, now we have an empty project to work on.

Create sample controllers

Let’s assume that our shop will be accessible on /shop and our back-office on /admin. Let’s create a dummy homepage for each of these endpoints:

# src/Controller/Shop/HomeController.php

namespace App\Controller\Shop;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\Routing\Annotation\Route;

final class HomeController
{
/**
* @Route("/", name="shop_home")
* @Template("home.html.twig")
*
* @return array
*/
public function __invoke()
{
return [
'title' => 'Customer home',
];
}

}

And for /admin:

# src/Controller/Admin/HomeController.php

namespace App\Controller\Admin;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\Routing\Annotation\Route;

final class HomeController
{
/**
* @Route("/", name="admin_home")
* @Template("home.html.twig")
*
* @return array
*/
public function __invoke()
{
return [
'title' => 'Employee home',
];
}

}

Then, create the corresponding template:

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

{% block body %}

<div>
<h3>{{ title }}</h3>
</div>

{% endblock %}

Finally, mount /admin and /shop in your router configuration:

# config/routes/annotations.yaml
admin:
resource: ../../src/Controller/Admin/
type: annotation
prefix: /admin

shop:
resource: ../../src/Controller/Shop/
type: annotation
prefix: /shop

Now, if you run the following command:

bin/console debug:router | grep home

You should see this:

admin_home ANY ANY ANY /admin/
shop_home ANY ANY ANY /shop/

Start the development server:

bin/console server:start

You should be able to reach /admin and /shop with the controllers you just created.

Create the user classes

The purpose of this tutorial is to work with 2 different kind of users: Employees and Customers. We could use a single User class and distinguish them with roles, but the idea of our approach is that Employee John could impersonate a Customer Alice on /shop while still being logged in as John on /admin (so as to help Alice in purchasing your star product, for example).

Symfony Maker to the rescue!

bin/console make:user Employee

Use the default values provided during the interaction.
Now edit your security.yaml, rename the provider app_user_provider to employees, and set password encoding to plaintext (I’ll tell you why later); it should look like this:

# config/packages/security.yaml
security:
encoders:
App\Entity\Employee:
algorithm: plaintext

providers:
employees:
entity:
class: App\Entity\Employee
property: email

Now, do the same with the Customer entity:

bin/console make:user Customer

Update your security.yaml as well:

# config/packages/security.yaml
security:
encoders:
App\Entity\Employee:
algorithm: plaintext
App\Entity\Customer:
algorithm: plaintext


providers:
employees:
entity:
class: App\Entity\Employee
property: email
customers:
entity:
class: App\Entity\Customer
property: email

Update your database schema (or create/run a migration — don’t forget to edit your database credentials in your .env.dev file)

bin/console doctrine:database:create; bin/console doctrine:schema:update --force

With the help of PHPMyAdmin or any other tool, insert those users by hand:

  • john@employee with password 123456 in table employee
  • bob@customer with password qwerty in table customer
  • alice@customer with password abc123 in table customer

Since we store passwords as plaintext, we’ll be able to log in with those users later without having to encrypt / decrypt passwords — this is just for the purpose of this tutorial; never do this in production!

Set up firewalls and authenticators

The goal of this part is to restrict the /admin area to Employee users, and the /shop area to Customer users.

Is your security.yaml still open? Add the corresponding areas under the firewalls section:

# config/packages/security.yaml
security:
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false

admin:
pattern: ^/admin
anonymous: true
provider: employees

shop:
pattern: ^/shop
anonymous: true
provider: customers


main:
anonymous: true

Now, let’s create an authenticator for employees. Run the following command:

bin/console make:auth

During the interaction, select “Login form authenticator”, EmployeeAuthenticator as the authenticator name, admin for the firewall, and App\Entity\Employee as the user class.

Since we’re going to create several authenticators, we’ve got to do these extra steps to avoid collisions:

  • Move the generated templates/security/login.html.twig to templates/admin/security/login.html.twig
  • Move the generated src/Controller/SecurityController.php to src/Controller/Admin/SecurityController.php (edit its namespace as well)
  • Open the file and target the admin/security/login.html.twig template
  • Replace the route name app_login by admin_login
  • In the generated file App\Security\EmployeeAuthenticator, replace the 2 occurences of app_login by admin_login.

Repeat the operation for customers:

bin/console make:auth

During the interaction, select “Login form authenticator”, CustomerAuthenticator as the authenticator name, shop for the firewall, and App\Entity\Customer as the user class.

  • Move the generated templates/security/login.html.twig to templates/shop/security/login.html.twig
  • Move the generated src/Controller/SecurityController.php to src/Controller/Shop/SecurityController.php (edit its namespace as well)
  • Open the file and target the shop/security/login.html.twig template
  • Replace the route name app_login by shop_login
  • In the generated file App\Security\CustomerAuthenticator, replace the 2 occurences of app_login by shop_login.

Your security.yaml must now look like this:

# config/packages/security.yaml
security:
encoders:
App\Entity\Employee:
algorithm: plaintext
App\Entity\Customer:
algorithm: plaintext


providers:
employees:
entity:
class: App\Entity\Employee
property: email
customers:
entity:
class: App\Entity\Customer
property: email

firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false

admin:
pattern: ^/admin
anonymous: true
provider: employees
guard:
authenticators:
- App\Security\EmployeeAuthenticator

shop:
pattern: ^/shop
anonymous: true
provider: customers
guard:
authenticators:
- App\Security\CustomerAuthenticator

main:
anonymous: true

Add the following lines to allow access to login pages:

# config/packages/security.yaml
access_control:
- { path: ^/admin/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin, roles: IS_AUTHENTICATED_FULLY }
- { path: ^/shop/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/shop, roles: IS_AUTHENTICATED_FULLY }

You should now see this when running bin/console debug:router:

admin_home ANY ANY ANY /admin/
admin_login ANY ANY ANY /admin/login
shop_home ANY ANY ANY /shop/
shop_login ANY ANY ANY /shop/login

In your browser, open 2 tabs and hit both/admin and /shop (both should redirect you to the appropriate login page):

  • You should be able to log in as john@employee with password 123456 on /admin
  • You should be able to log in as a bob@customer with password qwerty on /shop
  • Both sessions are completely independant from each other :-)

Next

--

--