Symfony backend rest api, oauth authentikáció

Symfony (3.4) rest api-t építünk a következő leírásban. A leírás az oldal alján levő linkekből készült. Friendofsymfony csomagot használjuk rest szervernek, szükség van az oauth-server csomagra is az authentikációhoz és a serializer csomagra is. JSON Web Token (JWT) OAuth 2.0 Bearer token-es authentikációt használunk. Csak érvényes access_token-en rendelkező hívást fogadunk el. Ha lejár akkor kérni kell újat a refresh_token küldésével.

Írjuk be a composer.json-be a következő csomagokat és nyomjunk composer update-et:
    "beberlei/DoctrineExtensions": "^1.1",
    "doctrine/doctrine-bundle": "^1.6",
    "doctrine/orm": "^2.5",
    "friendsofsymfony/oauth-server-bundle": "1.6.1",
    "friendsofsymfony/rest-bundle": "2.3.*",
    "friendsofsymfony/user-bundle": "2.1.2",
    "incenteev/composer-parameter-handler": "^2.0",
    "jms/serializer-bundle": "2.3.*",

A config.yml-be a következőket állítsuk be:
fos_rest:
    routing_loader:
        default_format: json
        include_format: false
    body_listener: true
    format_listener:
        rules:
            prefer_extension: false
            fallback_format: json
    param_fetcher_listener: true
    access_denied_listener:
        json: true
    # Enable serializer for the REST API
    serializer:
        serialize_null: true
    view:
        view_response_listener: 'force'
        formats:
            json: true
    # Disable CSRF protection
    disable_csrf_role: ROLE_API

fos_user:
    db_driver: orm
    firewall_name: api                                  # Seems to be used when registering user/reseting password,
                                                        # but since there is no "login", as so it seems to be useless in
                                                        # our particular context, but still required by "FOSUserBundle"
    user_class: UsersBundle\Entity\User
    from_email:
      address: "test@app.eu"
      sender_name: "Test App"

fos_oauth_server:
    db_driver:           orm
    client_class:        UsersBundle\Entity\Client
    access_token_class:  UsersBundle\Entity\AccessToken
    refresh_token_class: UsersBundle\Entity\RefreshToken
    auth_code_class:     UsersBundle\Entity\AuthCode
    service:
        user_provider: fos_user.user_provider.username_email            # This property will be used when valid credentials are given to load the user upon access token creation

jms_serializer:
    metadata:
        auto_detection: true
    handlers:
        datetime:
            default_format: c

security.yml:
security:
    encoders:
        FOS\UserBundle\Model\UserInterface:
            algorithm: bcrypt
            cost:      15

    providers:
        fos_userbundle:
            id: fos_user.user_provider.username # fos_user.user_provider.username_email does not seem to work (OAuth-spec related ("username + password") ?)

    firewalls:

        oauth_token:                                   # Everyone can access the access token URL.
              pattern: ^/oauth/v2/token
              security: false
        api:
              pattern: ^/                                # All URLs are protected
              fos_oauth: true                            # OAuth2 protected resource
              stateless: true                            # Do no set session cookies
              anonymous: false                           # Anonymous access is not allowed

routing.yml
# app/config/routing.yml
web_test:
    resource: "@TestBundle/Controller/Rest/TestController.php"
    type:   annotation

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

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

Itt ezen a ponton kövessük a következő linken a User Entity részt az entity-k és táblák létrehozásához: User Entity

Generáljunk client_id-t (itt a kliens a frontend-et jelenti, nem userenként kell generálni) a következő paranccsal:
# php bin/console fos:oauth-server:create-client --grant-type="password" --grant-type="refresh_token"
 
Amit generáltunk client_id és secret_id-t írjuk be a következő insert sql-be:
INSERT INTO `oauth2_clients` (`id`, `random_id`, `redirect_uris`, `secret`, `allowed_grant_types`) VALUES
(1,	'3bcbxd9e24g0gk4swg0kwgcwg4o8k8g4g888kwc44gcc0gwwk4',	'a:0:{}',	'4ok2x70rlfokc8g0wws8c8kwcokw80k44sg48goc0ok4w0so0k',	'a:2:{i:0;s:8:\"password\";i:1;s:13:\"refresh_token\";}');

Készítsünk controllert a rest api hívásoknak (példa egy random controllerből):
<?php

namespace TestBundle\Controller\Rest;

use ScheduleBundle\Entity\Test;
use FOS\RestBundle\Controller\FOSRestController;
use Symfony\Component\HttpFoundation\Request;
use FOS\RestBundle\Controller\Annotations as Rest;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\View\View;
use FOS\RestBundle\Controller\Annotations\Route;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;

/**
 *
 */
class TestController extends FOSRestController
{
    /**
     * @Rest\Get("/api/test")
     *
     *
     * @return Response
     */
    public function getAction()
    {
        $em = $this->getDoctrine()->getManager();
        $restresult = $em->getRepository('TestBundle:Test')->getAllTest();

        if ($restresult === null) {
            return new View("there are no test exist", Response::HTTP_NOT_FOUND);
        }

        $encoders = array(new XmlEncoder(), new JsonEncoder());
        $normalizers = array(new ObjectNormalizer());

        $serializer = new Serializer($normalizers, $encoders);
        $jsonContent = $serializer->serialize($restresult, 'json');

        return new Response($jsonContent);
    }

    /**
     * @Rest\Get("/api/test/show/{id}")
     */
    public function idAction($id)
    {
        $singleresult = $this->getDoctrine()->getRepository('TestBundle:Test')->find($id);
        if ($singleresult === null) {
            return new View("test not found", Response::HTTP_NOT_FOUND);
        }

        $encoders = array(new XmlEncoder(), new JsonEncoder());
        $normalizers = array(new ObjectNormalizer());

        $serializer = new Serializer($normalizers, $encoders);
        $jsonContent = $serializer->serialize($singleresult, 'json');

        return new Response($jsonContent);
    }

    /**
     * @Rest\Post("/api/test/add")
     *
     */
    public function addAction(Request $request)
    {
        $data = $request->get('data');
        $data = json_decode(base64_decode($data), true);
        $test = new Test();

        $testNumber = isset($data['testNumber']) ? $data['testNumber'] : NULL;
        $timeFrom = isset($data['timeFrom']) ? $data['timeFrom'] : NULL;
        $timeTo = isset($data['timeTo']) ? $data['timeTo'] : NULL;
        $name = isset($data['name']) ? $data['name'] : NULL;
        $color = isset($data['color']) ? $data['color'] : NULL;

        if (empty($testNumber)) {
            return new View("NULL VALUES ARE NOT ALLOWED" . '-' . $request, Response::HTTP_NOT_ACCEPTABLE);
        }

        $test->setTestNumber($testNumber);
        $test->setTimeFrom($timeFrom);
        $test->setTimeTo($timeTo);
        $test->setName($name);
        $test->setColor($color);

        $em = $this->getDoctrine()->getManager();
        $em->persist($test);
        $em->flush();

        return new View("Test Added Successfully", Response::HTTP_OK);
    }

    /**
     * @Rest\Put("/api/test/edit/{id}")
     */
    public function updateAction($id, Request $request)
    {
        $data = $request->get('data');
        $sn = $this->getDoctrine()->getManager();
        $test = $this->getDoctrine()->getRepository('TestBundle:Test')->find($id);

        $data = json_decode(base64_decode($data), true);

        $testNumber = isset($data['testNumber']) ? $data['testNumber'] : NULL;
        $timeFrom = isset($data['timeFrom']) ? $data['timeFrom'] : NULL;
        $timeTo = isset($data['timeTo']) ? $data['timeTo'] : NULL;
        $name = isset($data['name']) ? $data['name'] : NULL;
        $color = isset($data['color']) ? $data['color'] : NULL;

        if (empty($test)) {
            $response = new Response("test not found");
            $response->headers->set('Content-Type', 'application/json');
            $response->headers->set('Access-Control-Allow-Origin', 'http://192.168.1.2:4200');
            $response->headers->set('Access-Control-Allow-Methods', 'PUT');
            return new View("test not found", Response::HTTP_NOT_FOUND);

        } elseif (!empty($data['testNumber']) && !empty($data['timeFrom']) && !empty($data['timeTo'])) {
            $test->setTestNumber($testNumber);
            $test->setTimeFrom($timeFrom);
            $test->setTimeTo($timeTo);
            $test->setName($name);
            $test->setColor($color);

            $sn->persist($test);
            $sn->flush();

            $response = new Response("Test Updated Successfully");
            $response->headers->set('Content-Type', 'application/json');
            $response->headers->set('Access-Control-Allow-Origin', 'http://192.168.1.2:4200');
            $response->headers->set('Access-Control-Allow-Methods', 'PUT');
            return new View("Test Updated Successfully", Response::HTTP_OK);

        } else {
            $response = new Response("Test name or value cannot be empty");
            $response->headers->set('Content-Type', 'application/json');
            $response->headers->set('Access-Control-Allow-Origin', 'http://192.168.1.2:4200');
            $response->headers->set('Access-Control-Allow-Methods', 'PUT');
            return new View("Test name or active field cannot be empty", Response::HTTP_NOT_ACCEPTABLE);
        }
    }

    /**
     * @Rest\Delete("/api/test/delete/{id}")
     */
    public function deleteAction($id)
    {
        $sn = $this->getDoctrine()->getManager();
        $test = $this->getDoctrine()->getRepository('TestBundle:Test')->find($id);
        if (empty($test)) {
            return new View("test not found", Response::HTTP_NOT_FOUND);
        } else {
            $sn->remove($test);
            $sn->flush();
        }
        return new View("deleted successfully", Response::HTTP_OK);
    }
}

Két beállításra még szükségünk lesz.
/src/UsersBundle/Resources/config/routing/authorize.xml:
<?xml version="1.0" encoding="UTF-8" ?>

<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">

    <route id="fos_oauth_server_authorize" path="/oauth/v2/auth" methods="GET POST OPTIONS">
        <default key="_controller">fos_oauth_server.controller.authorize:authorizeAction</default>
    </route>

</routes>

/src/UsersBundle/Resources/config/routing/token.xml:
<?xml version="1.0" encoding="UTF-8" ?>

<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">

    <route id="fos_oauth_server_token" path="/oauth/v2/token" methods="GET POST OPTIONS">
        <default key="_controller">fos_oauth_server.controller.token:tokenAction</default>
    </route>

</routes>

Indítsuk el a rest apit:
# php bin/console server:start 0.0.0.0:8000

Ha szükséges üríthetjük a cache-t:
# php app/console fos:oauth-server:clean

Bejelentkezés példa:
POST http://localhost:8000/app_dev.php/oauth/v2/token
grant_type=password
client_id=1_3bcbxd9e24g0gk4swg0kwgcwg4o8k8g4g888kwc44gcc0gwwk4
client_secret=4ok2x70rlfokc8g0wws8c8kwcokw80k44sg48goc0ok4w0so0k
username=admin
password=admin

Új token kérés példa:
POST http://localhost:8000/app_dev.php/oauth/v2/token
grant_type=refresh_token
client_id=1_3bcbxd9e24g0gk4swg0kwgcwg4o8k8g4g888kwc44gcc0gwwk4
client_secret=4ok2x70rlfokc8g0wws8c8kwcokw80k44sg48goc0ok4w0so0k
refresh_token=ZDg4NDg4NjI1MjBlOWQ3MjYyN2Q0MTlkNTc5ZjNkNWI0MmIxYWUwMTg1YmQ5OTAyNDI0ZjRkYjU2NWViYjhlNg

Egy rest api get hívás példa:
GET http://localhost:8000/app_dev.php/api/test Authorization=Bearer ZDg4NDg4NjI1MjBlOWQ3MjYyN2Q0MTlkNTc5ZjNkNWI0MmIxYWUwMTg1YmQ5OTAyNDI0ZjRkYjU2NWViYjhlNg

Olvasnivaló:
https://gist.github.com/diegonobre/341eb7b793fc841c0bba3f2b865b8d66
https://stackoverflow.com/questions/46362122/custom-authentication-in-a-symfony-3-using-external-rest-api
https://www.cloudways.com/blog/rest-api-in-symfony-3-1/
https://phpbuilder.com/creating-a-rest-api-in-symfony-3/
https://codereviewvideos.com/course/symfony-3-rest-tutorial
https://www.cloudways.com/blog/rest-api-in-symfony-3-1/ 
https://stackoverflow.com/questions/40386573/symfony-fos-oauth-with-custom-user
2018.11.17.