How to implement a REST APIs with Symfony 4 + FOSRestBundle + FOSUserBundle + FOSOauthServerBundle using main Oauth2 code flows
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:
- Authorization code
- Implicit
- 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!
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.
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 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 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 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 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:
<?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;
}
}
<?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;
}
}
<?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;
}
}
<?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.
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
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 }
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"}