Skip to content

Instantly share code, notes, and snippets.

@mirkorap
Last active February 22, 2024 14:51
Show Gist options
  • Save mirkorap/cd9f2933304eb868e489e7b6b573074c to your computer and use it in GitHub Desktop.
Save mirkorap/cd9f2933304eb868e489e7b6b573074c to your computer and use it in GitHub Desktop.
Basic examples how to implement a REST API with Symfony 4 + FOSRestBundle + FOSUserBundle + FOSOauthServerBundle with all oauth2 code flow

How to implement a REST APIs with Symfony 4 + FOSRestBundle + FOSUserBundle + FOSOauthServerBundle using main Oauth2 code flows

Table of contents

  1. Introduction
  2. Install bundles
  3. Configuration
    1. FOSRestBundle
    2. JMSSerializerBundle
    3. NelmioApiDocBundle
    4. FOSUserBundle
    5. FOSOAuthServerBundle
  4. Security
  5. Make migrations
  6. Create an ApiController
  7. Authorization code
    1. Refresh token
  8. Implicit
  9. Password credentials

Introduction

In this gist I will explain you how to create a basic REST APIs system with Symfony 4 and FOSOauthServerBundle using main Oauth2 code flows. The flows that we implement will be:

  1. Authorization code
  2. Implicit
  3. Password credentials

I will separate the gist into two configuration parts. The first part is the common configuration that is equal for all Oauth2 code flows above. In the second part I will explain, for each flow, the basic configuration type that you need. So let's coding!

Install bundles

The first step is to download Symfony and the related bundles.

composer create-project symfony/website-skeleton oauth2-server
cd oauth2-server
composer require friendsofsymfony/rest-bundle
composer require jms/serializer-bundle
composer require nelmio/api-doc-bundle
composer require friendsofsymfony/user-bundle "~2.0@dev"
composer require friendsofsymfony/oauth-server-bundle

Below I will explain for each bundle what they do and how to configure them.

Configuration

FOSRestBundle

FOSRestBundle allow us to configure our APIs and serve resources in different format in an easy way. There are different configuration options that you can use, but in our example we will serve resources in json/xml format for all routes starting with /api, instead for all others we will serve simple html pages.

In the config/packages/fos_rest.yaml file insert these configuration options:

fos_rest:
    routing_loader:
        default_format: html
        include_format: true

    format_listener:
        enabled: true
        rules:
             - { path: '^/api', priorities: ['json', 'xml'], fallback_format: json, prefer_extension: false }
             - { path: '^/', priorities: ['html'], fallback_format: html, prefer_extension: false }

    view:
        view_response_listener: true

Documentation: https://symfony.com/doc/master/bundles/FOSRestBundle/index.html

JMSSerializerBundle

JMSSerializerBundle allows you to serialize your data into a requested output format such as JSON, XML, or YAML.

In the config/packages/jms_serializer.yaml file insert these configuration options:

jms_serializer:
    visitors:
        xml:
            format_output: '%kernel.debug%'

Documentation: https://jmsyst.com/bundles/JMSSerializerBundle

NelmioApiDocBundle

NelmioApiDocBundle is a Symfony's bundle that allows us to generate documentation for our APIs.

In the config/packages/nelmio_api_doc.yaml file insert these configuration options:

nelmio_api_doc:
    documentation:
        info:
            title: Oauth2 Server App
            description: This is my oauth2 server app
            version: 1.0.0

        securityDefinitions:
            Bearer:
                type: apiKey
                description: 'Value: Bearer {access_token}'
                name: Authorization
                in: header
    areas:
        path_patterns:
            - ^/api(?!/doc$)

In the config/routes.yaml file insert this route option:

NelmioApiDocBundle:
    resource: "@NelmioApiDocBundle/Resources/config/routing/swaggerui.xml"
    prefix:   /api/doc

Documentation: https://symfony.com/doc/current/bundles/NelmioApiDocBundle/index.html

FOSUserBundle

FOSUserBundle allows us to manage users (store and load users, authenticate users, manage roles etc.)

In the config/packages/fos_user.yaml file insert these configuration options (if the file doesn't exist, create it!):

fos_user:
    db_driver: orm
    user_class: App\Entity\User
    firewall_name: main

    from_email:
        address: john@example.com
        sender_name: john@example.com

In the config/routes.yaml file insert this route option:

fos_user:
    resource: "@FOSUserBundle/Resources/config/routing/all.xml"

In the config/packages/framework.yaml file insert this line at the end of file:

templating:
    engines: twig

Now let's create our entity User class:

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Model\User as BaseUser;

/**
 * @ORM\Table("users")
 * @ORM\Entity(repositoryClass="App\Repository\UserRepository")
 */
class User extends BaseUser
{

    /**
     * @ORM\Id()
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(type="integer")
     */
     protected $id;

     public function getId(): ?int
     {
        return $this->id;
     }
}

Documentation: https://symfony.com/doc/current/bundles/FOSUserBundle/index.html

FOSOAuthServerBundle

FOSOAuthServerBundle is a Symfony's bundle that allows us to create our oauth2 server app. In this gist I will explain you how to use it with the main oauth2 code flows (Authorization code, Implicit, Password credentials). Let's take a look to the configuration options:

In the config/packages/fos_oauth_server.yaml file insert these configuration options (if the file doesn't exist, create it!):

fos_oauth_server:
    db_driver:           orm
    client_class:        App\Entity\Client
    access_token_class:  App\Entity\AccessToken
    refresh_token_class: App\Entity\RefreshToken
    auth_code_class:     App\Entity\AuthCode
    service:
        user_provider: fos_user.user_provider.username

In the config/routes.yaml file insert these route options:

fos_oauth_server_token:
    resource: "@FOSOAuthServerBundle/Resources/config/routing/token.xml"

# Add this route option only if you are going to use the Authorization code flow
fos_oauth_server_authorize:
    resource: "@FOSOAuthServerBundle/Resources/config/routing/authorize.xml"

Now we need to create the entities that we have just specified in the fos_oauth_server.yaml file:

App\Entity\Client

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use FOS\OAuthServerBundle\Entity\Client as BaseClient;

/**
 * @ORM\Table("oauth2_clients")
 * @ORM\Entity(repositoryClass="App\Repository\ClientRepository")
 */
class Client extends BaseClient
{

    /**
     * @ORM\Id()
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(type="integer")
     */
    protected $id;

    public function getId(): ?int
    {
        return $this->id;
    }
}

App\Entity\AccessToken

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use FOS\OAuthServerBundle\Entity\AccessToken as BaseAccessToken;

/**
 * @ORM\Table("oauth2_access_tokens")
 * @ORM\Entity(repositoryClass="App\Repository\AccessTokenRepository")
 * @ORM\AttributeOverrides({
 *     @ORM\AttributeOverride(name="token",
 *         column=@ORM\Column(
 *             name   = "token",
 *             type   = "string",
 *             length = 128
 *         )
 *     )
 * })
 */
class AccessToken extends BaseAccessToken
{

    /**
     * @ORM\Id()
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(type="integer")
     */
    protected $id;

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\Client")
     * @ORM\JoinColumn(nullable=false)
     */
    protected $client;

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\User")
     */
    protected $user;

    public function getId(): ?int
    {
        return $this->id;
    }
}

App\Entity\RefreshToken

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use FOS\OAuthServerBundle\Entity\RefreshToken as BaseRefreshToken;

/**
 * @ORM\Table("oauth2_refresh_tokens")
 * @ORM\Entity(repositoryClass="App\Repository\RefreshTokenRepository")
 * @ORM\AttributeOverrides({
 *     @ORM\AttributeOverride(name="token",
 *         column=@ORM\Column(
 *             name   = "token",
 *             type   = "string",
 *             length = 128
 *         )
 *     )
 * })
 */
class RefreshToken extends BaseRefreshToken
{

    /**
     * @ORM\Id()
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(type="integer")
     */
    protected $id;

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\Client")
     * @ORM\JoinColumn(nullable=false)
     */
    protected $client;

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\User")
     */
    protected $user;

    public function getId(): ?int
    {
        return $this->id;
    }
}

App\Entity\AuthCode

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use FOS\OAuthServerBundle\Entity\AuthCode as BaseAuthCode;

/**
 * @ORM\Table("oauth2_auth_codes")
 * @ORM\Entity(repositoryClass="App\Repository\AuthCodeRepository")
 * @ORM\AttributeOverrides({
 *     @ORM\AttributeOverride(name="token",
 *         column=@ORM\Column(
 *             name   = "token",
 *             type   = "string",
 *             length = 128
 *         )
 *     )
 * })
 */
class AuthCode extends BaseAuthCode
{

    /**
     * @ORM\Id()
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(type="integer")
     */
    protected $id;

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\Client")
     * @ORM\JoinColumn(nullable=false)
     */
    protected $client;

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\User")
     */
    protected $user;

    public function getId(): ?int
    {
        return $this->id;
    }
}

Above you can see the annotation @ORM\AttributeOverrides. This is required to solve the error: SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes during migrations.

Documentation: https://github.com/FriendsOfSymfony/FOSOAuthServerBundle/blob/master/Resources/doc/index.md

Security

In the config/packages/security.yaml file insert these configuration options:

security:
    encoders:
        FOS\UserBundle\Model\UserInterface: bcrypt

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: ROLE_ADMIN

    providers:
        fos_userbundle:
            id: fos_user.user_provider.username

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

        oauth_token:
            pattern: ^/oauth/v2/token
            security: false

        # Add this firewall only in the Authorization code flow
        oauth_authorize:
            pattern: ^/oauth/v2/auth
            form_login:
                provider: fos_userbundle
                check_path: /oauth/v2/auth_login_check
                login_path: /oauth/v2/auth_login
            anonymous: true

        api_doc:
            pattern: ^/api/doc
            fos_oauth: false
            stateless: true
            anonymous: true

        api:
            pattern: ^/api
            fos_oauth: true
            stateless: true
            anonymous: false

        main:
            pattern: ^/
            form_login:
                provider: fos_userbundle
                csrf_token_generator: security.csrf.token_manager
            logout: true
            fos_oauth: false
            anonymous: true

    access_control:
        - { path: ^/oauth/v2/auth_login$, role: IS_AUTHENTICATED_ANONYMOUSLY } # Add this only in the Authorization code flow
        - { path: ^/api/doc, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
        - { path: ^/login, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }

Make migrations

We have finally finished to configure our bundles. Now we can start to see oauth2 code flows, but first we need to create migrations!

So let's open your terminal and run:

php bin/console make:migration
php bin/console doctrine:migration:migrate

Then we need to create a user, so again open your terminal and run:

php bin/console fos:user:create
Please choose a username:admin
Please choose an email:admin@example.com
Please choose a password:admin
Created user admin

Create an ApiController

Create an ApiController that will serve our resources. In this example we will create a simple API that returns all users.

<?php

namespace App\Controller;

use App\Entity\User;
use App\Repository\UserRepository;
use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\View\View;
use Nelmio\ApiDocBundle\Annotation\Model;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use FOS\RestBundle\Controller\FOSRestController;
use Swagger\Annotations as SWG;

class ApiController extends FOSRestController
{

    /**
     * List all users
     * @SWG\Parameter(
     *     in="query",
     *     type="number",
     *     minimum="0",
     *     name="offset",
     *     description="Offset from which to start listing users.",
     *     default="0"
     * )
     * @SWG\Parameter(
     *     in="query",
     *     type="integer",
     *     name="limit",
     *     description="How many users to return.",
     *     default="10"
     * )
     * @SWG\Response(
     *     response="200",
     *     description="Success",
     *     @SWG\Schema(
     *         type="array",
     *         @Model(type=User::class)
     *     )
     * )
     * @SWG\Response(
     *     response="401",
     *     description="Unauthorized"
     * )
     * @Rest\Get("/api/users")
     *
     * @param Request $request
     * @return View
     */
    public function getAllUsersAction(Request $request)
    {
        $offset = $request->query->get('offset', 0);
        $limit = $request->query->get('limit', 10);

        /* @var UserRepository $userRepository */
        $userRepository = $this->getDoctrine()->getRepository(User::class);
        $users = $userRepository->getAllUsers($offset, $limit);

        return $this->view($users);
    }
}

In the src/Repository/UserRepository let's add the getAllUsers function:

<?php

namespace App\Repository;

use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Symfony\Bridge\Doctrine\RegistryInterface;

/**
 * @method User|null find($id, $lockMode = null, $lockVersion = null)
 * @method User|null findOneBy(array $criteria, array $orderBy = null)
 * @method User[]    findAll()
 * @method User[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
 */
class UserRepository extends ServiceEntityRepository
{
    public function __construct(RegistryInterface $registry)
    {
        parent::__construct($registry, User::class);
    }

    /**
     * @param int $offset
     * @param int $limit
     * @return mixed
     */
    public function getAllUsers(int $offset = 0, int $limit = 10)
    {
        return $this->createQueryBuilder('u')
            ->setMaxResults($limit)
            ->setFirstResult($offset)
            ->getQuery()
            ->getResult();
    }

}

Authorization code

Finally (after a lot of configuration) we can start to see the integration of oauth2 code flows with Symfony! Let's start with the Authorization code flow.

First of all we need to create a client, so open your terminal and run:

php bin/console fos:oauth-server:create-client --redirect-uri="http://localhost:8000/redirect-uri-example" --grant-type="authorization_code"

In my case the client created by the bundle has these data:

+------------+---------------------------------------------------------+------------------------------------------------------+
| id         | random_id                                               | secret                                               |
+------------+---------------------------------------------------------+------------------------------------------------------+
| 1          | 3pwul4sg6ge8s444og4wk4g88480cosowosg0k080owo0o0wck      | 4t1922al57ack8wckcgoc84ko4oogogcoso40kwwkc4csw4cwk   |
+------------+---------------------------------------------------------+------------------------------------------------------+

client_id: 1_3pwul4sg6ge8s444og4wk4g88480cosowosg0k080owo0o0wck (id + random_id)
secret: 4t1922al57ack8wckcgoc84ko4oogogcoso40kwwkc4csw4cwk

Then we need to create a SecurityController with two routes: /oauth/v2/auth_login and /oauth/v2/auth_login_check. These are the routes that we have specified above in the oauth_authorize firewall inside the security.yaml file.

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;

class SecurityController extends AbstractController
{

    private $tokenManager;

    public function __construct(CsrfTokenManagerInterface $tokenManager = null)
    {
        $this->tokenManager = $tokenManager;
    }

    /**
     * @Route("/oauth/v2/auth_login", name="auth_login")
     * @param Request $request
     * @return Response
     */
    public function loginAction(Request $request)
    {
        /** @var $session Session */
        $session = $request->getSession();

        $authErrorKey = Security::AUTHENTICATION_ERROR;
        $lastUsernameKey = Security::LAST_USERNAME;

        if ($request->attributes->has($authErrorKey)) {
            $error = $request->attributes->get($authErrorKey);
        } elseif (null !== $session && $session->has($authErrorKey)) {
            $error = $session->get($authErrorKey);
            $session->remove($authErrorKey);
        } else {
            $error = null;
        }

        if (!$error instanceof AuthenticationException) {
            $error = null;
        }

        $lastUsername = (null === $session) ? '' : $session->get($lastUsernameKey);

        $csrfToken = $this->tokenManager ? $this->tokenManager->getToken('authenticate')->getValue() : null;

        $securityCheckPath = $this->generateUrl('auth_login_check');

        return $this->render('@FOSUser/Security/login.html.twig', array(
            'last_username' => $lastUsername,
            'error' => $error,
            'csrf_token' => $csrfToken,
            'security_check_path' => $securityCheckPath,
        ));
    }

    /**
     * @Route("/oauth/v2/auth_login_check", name="auth_login_check")
     * @param Request $request
     */
    public function loginCheckAction(Request $request)
    {

    }
}

To complete the SecurityController we need to have a login page where the user will be redirect if he isn't already logged in. For simplicity let's copy the FOSUserBundle template files. You can find them inside the directory vendor/friendsofsymfony/user-bundle/Resources/views. From that directory you should copy the directory Security and the file layout.html.twig and put them inside the directory templates/bundles/FOSUserBundle.

More information about the overriding of templates you can find here: Symfony's bunde override.
Now open the file templates/bundles/FOSUserBundle/Security/login_content.html.twig and in the action of form replace the path fos_user_security_check with auth_login_check.

To verify that all works properly, we should call this route: http://localhost:8000/oauth/v2/auth?client_id=1_3pwul4sg6ge8s444og4wk4g88480cosowosg0k080owo0o0wck&redirect_uri=http://localhost:8000/redirect-uri-example&response_type=code

The client_id query parameter should be replaced by the client_id created previously. The response should be a page with two buttons: "allow" and "deny". After clicking in the "allow" button you will redirect to the redirect_uri. In the url you can notice a code query parameter, that is our authorization code! Now we need to exchange that code with an access_token. So let's create a POST request to /oauth/v2/token:

http POST http://localhost:8000/oauth/v2/token \
    grant_type=authorization_code \
    client_id=1_3pwul4sg6ge8s444og4wk4g88480cosowosg0k080owo0o0wck \
    client_secret=4t1922al57ack8wckcgoc84ko4oogogcoso40kwwkc4csw4cwk \
    code=NWVmMDY4NjE3OWUwOTI4MmZkN2FiNmI4NTgwMGNhNzg3ZTRjYmMzZmI5MmUzMWFiYzk0YWJjMmZiM2MxNmExZQ \
    redirect_uri=http://localhost:8000/redirect-uri-example \
HTTP/1.1 200 OK
Cache-Control: no-store, private
Connection: close
Content-Type: application/json

The response should looks like this:

{
    "access_token": "ZTFiZWIwMzI4ZTg4MmYzNTExNjkxMzYwODBlMzQ3NTgxNzM1ODMxYTQzNGZkZDI0ZjQ4MmRkN2ZjMDRhMWU2YQ",
    "expires_in": 3600,
    "token_type": "bearer",
    "scope": null,
    "refresh_token": "NDFiNjY3MWU3YmZhMDkxNjI5NjBjZDhhNmYzNmJkYjI1NTIzMGJiZTQzYTA0NzlkYjU2OTgzYmM5MzZlODg2NQ"
}

Now we can finally call our APIs! Create a request to localhost:8000/api/users and pass in the header the access_token:

http GET localhost:8000/api/users \
    "Authorization:Bearer ZTFiZWIwMzI4ZTg4MmYzNTExNjkxMzYwODBlMzQ3NTgxNzM1ODMxYTQzNGZkZDI0ZjQ4MmRkN2ZjMDRhMWU2YQ"
HTTP/1.1 200 OK
Cache-Control: no-cache
Connection: close
Content-Type: application/json

The response should looks like this:

[
    {
        "id": 1,
        "username": "admin",
        "username_canonical": "admin",
        "email": "admin@example.com",
        "email_canonical": "admin@example.com",
        "enabled": true,
        "password": "$2y$13$HMApz6BLBHXbsLx2GZWJcOTxx3bqv2yKWg7vst5c7C10SXpNgVlE.",
        "last_login": "2018-11-11T20:40:54+01:00",
        "groups": [],
        "roles": []
    }
]

Refresh token

In the authorization code flow (and in the password credentials flow too) you gain another important token, the refresh_token! This token help you to refresh the access_token when expires, without having to ask the user to re-grant access to his data to the application he is using. To use the refresh_token you need to add it as grant_type during the creation of client:

php bin/console fos:oauth-server:create-client --redirect-uri="http://localhost:8000/redirect-uri-example" --grant-type="authorization_code" --grant-type="refresh_token"

Then to get a new access_token you need to call the route: /oauth/v2/token with the refresh_token in the body of request:

http POST http://localhost:8000/oauth/v2/token \
    grant_type=refresh_token \
    client_id=1_3pwul4sg6ge8s444og4wk4g88480cosowosg0k080owo0o0wck \
    client_secret=4t1922al57ack8wckcgoc84ko4oogogcoso40kwwkc4csw4cwk \
    refresh_token=YzFlMjQ3ZjRkZTVmZTA5ODZhZmI0YzA4Y2Y5YzY4ZmEwYjNhNTNhNWY0YTVkYmRiZjUxZjBlZTQ1NWNlZjY4OQ \
    redirect_uri=http://localhost:8000/redirect-uri-example \
HTTP/1.1 200 OK
Cache-Control: no-store, private
Connection: close
Content-Type: application/json

Implicit

This flow is principally used in SPA. A difference from the previous flow is that we will receive an access_token immediately, so we don't need to exchange an authorization code for an access_token. You may think "Why I should use the Authorization code flow if the Implicit one is more simple?". The answer is easy, you MUST use the authorization code flow if you need to call APIs even when the user is offline. In fact with the Implicit flow you will not receive a refresh_token (for security reason), so when the access_token expires the user must authorize access again. For this reason this flow is more suitable in the SPA. Let's see how it works:

Again we need to create a client, so open your terminal and run:

php bin/console fos:oauth-server:create-client --redirect-uri="http://localhost:8000/redirect-uri-example" --grant-type="implicit"

In my case the client created by the bundle has these data:

+------------+---------------------------------------------------------+------------------------------------------------------+
| id         | random_id                                               | secret                                               |
+------------+---------------------------------------------------------+------------------------------------------------------+
| 2          | 2shspbzhca2ogw44kckw00s4s800g0s484004ccokcw8gogwks      | 3fcdpac3iw6cw8k8gs48kwc8ows4s4wkcg8gwgsgck84g48k0s   |
+------------+---------------------------------------------------------+------------------------------------------------------+

client_id: 2_2shspbzhca2ogw44kckw00s4s800g0s484004ccokcw8gogwks (id + random_id)
secret: 3fcdpac3iw6cw8k8gs48kwc8ows4s4wkcg8gwgsgck84g48k0s

With these data we can call the route http://localhost:8000/oauth/v2/auth?client_id=2_2shspbzhca2ogw44kckw00s4s800g0s484004ccokcw8gogwks&redirect_uri=http://localhost:8000/redirect-uri-example&response_type=token

The client_id query parameter should be replaced by the client_id created previously. The response should be a page with two buttons: "allow" and "deny". After clicking in the "allow" button you will redirect to the redirect_uri. In the url you can notice an hash with the access_token. Now we can call our API to get all users (remember to pass a Bearer header with the access_token):

http GET localhost:8000/api/users \
    "Authorization:Bearer NDYxMmUwZGI5OTQ4Y2UzNDQ5M2JhY2YwYmU3YmM2NDlkMDUyNTk4MjE2N2ZhNDU2OWQ2MTc4NjM1MmYxMDBmOQ"
HTTP/1.1 200 OK
Cache-Control: no-cache
Connection: close
Content-Type: application/json

The response should looks like this:

[
    {
        "id": 1,
        "username": "admin",
        "username_canonical": "admin",
        "email": "admin@example.com",
        "email_canonical": "admin@example.com",
        "enabled": true,
        "password": "$2y$13$HMApz6BLBHXbsLx2GZWJcOTxx3bqv2yKWg7vst5c7C10SXpNgVlE.",
        "last_login": "2018-11-11T20:40:54+01:00",
        "groups": [],
        "roles": []
    }
]

Password credentials

This flow is used when we need to serve resources to a trusted application. Normally is used when the application that will use our APIs is our application itself. During the request to retrieve the access_token, the external application must send the username and password too. Let's start to see how it works:

Again we need to create a client, so open your terminal and run:

php bin/console fos:oauth-server:create-client --redirect-uri="http://localhost:8000/redirect-uri-example" --grant-type="password"

In my case the client created by the bundle has these data:

+------------+---------------------------------------------------------+------------------------------------------------------+
| id         | random_id                                               | secret                                               |
+------------+---------------------------------------------------------+------------------------------------------------------+
| 3          | 492ohmxbo3k0gggwg08wso4cs0k4sswcwg8wo8so0cgo4cwo0s      | 64jack8z62888k4sgcgssow080wcgsk04408wogkc4owgkgokc   |
+------------+---------------------------------------------------------+------------------------------------------------------+

client_id: 3_492ohmxbo3k0gggwg08wso4cs0k4sswcwg8wo8so0cgo4cwo0s (id + random_id)
secret: 64jack8z62888k4sgcgssow080wcgsk04408wogkc4owgkgokc

With these data we can call the route to get the access_token, but this time we should send also username and password!

http POST http://localhost:8000/oauth/v2/token \
    grant_type=password \
    client_id=3_492ohmxbo3k0gggwg08wso4cs0k4sswcwg8wo8so0cgo4cwo0s \
    client_secret=64jack8z62888k4sgcgssow080wcgsk04408wogkc4owgkgokc \
    username=admin \
    password=admin
HTTP/1.1 200 OK
Cache-Control: no-store, private
Connection: close
Content-Type: application/json

The username and password that you see above come from the user created by FOSUserBundle's command php bin/console fos:user:create. Now we can call our API

http GET localhost:8000/api/users \
    "Authorization:Bearer MmQ5MzRlOTRhNWUzMzY0ZGU1ZjMzMjkwY2I0YjlhN2NlOTBhZjg5NGJjMGM5Yzk2ZGFiYWI5YTBkYjBiMWIyNw"
HTTP/1.1 200 OK
Cache-Control: no-cache
Connection: close
Content-Type: application/json

The response should looks like this:

[
    {
        "id": 1,
        "username": "admin",
        "username_canonical": "admin",
        "email": "admin@example.com",
        "email_canonical": "admin@example.com",
        "enabled": true,
        "password": "$2y$13$HMApz6BLBHXbsLx2GZWJcOTxx3bqv2yKWg7vst5c7C10SXpNgVlE.",
        "last_login": "2018-11-11T20:40:54+01:00",
        "groups": [],
        "roles": []
    }
]
@KosolapovR
Copy link

KosolapovR commented Feb 3, 2020

I do it step by step as you have, but when i try GET localhost:8000/api/users response is:
{"error": "access_denied", "error_description": "OAuth2 authentication required"}
Could you help me?
Note: previous respone was {"access_token":"ZmFkZjQ1NzdjYTY0ZGMwNDYyZDkyNWM1NjU3MmUwZjNiNjIwYTgxYWNmOWZjZDIwZmUyODIwNDFlOWNhZDRmYQ","expires_in":3600,"token_type":"bearer","scope":null,"refresh_token":"Yjk2MmYzMzA1MWU4YTc1YWU2ZTVlMWQyYTI4N2I3NjA5YWE4ZGM4OGMwZDFjYTc1MzJjMDJiZTA2ODE0ZTFiOQ"}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment