Skip to content

Instantly share code, notes, and snippets.

@mirkorap
Last active February 22, 2024 14:51
Show Gist options
  • Select an option

  • Save mirkorap/cd9f2933304eb868e489e7b6b573074c to your computer and use it in GitHub Desktop.

Select an option

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

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 code flows above, 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/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 a different configuration options that you can use, but in our example we need to 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 allow 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 allow us to manage users (store and load users, register and authenticate users etc.)

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

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 to 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 bundle that allow us to create our oauth2 server app. In this gist I will exaplain 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:

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 only if you are using 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 configuration 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.

Make migrations

Now that we have finally finished to configure our bundles 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:migrations
php bin/console doctrine:migration:migrate

Now we need to create our 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

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 }
@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