14 Novembre, 2018 | Di

Come integrare API Platform e JWT Authentication in un'applicazione

Come integrare API Platform e JWT Authentication in un'applicazione

In questo articolo introduciamo l’integrazione tra API Platform e JWT Authentication in un progetto web. Descriveremo ogni singolo passo dell’installazione, dell’implementazione e dell’integrazione tra API Platform e JWT Authentication.

La nostra Stack

  • PHP 7.2

  • Symfony 4.1

  • MariaDB 10.2

  • Apache 2.4

Cos’è API Platform

API Platform è un framework full stack potente ma semplice da usare, dedicato specificatamente a progetti basati sulle API (Application Programming Interface).

Il framework include una libreria PHP per creare API che supportano gli standard più moderni (JSON-LD, GraphQL, OpenAPI).

Perché abbiamo scelto API Platform?

API Platform presenta un gran numero di vantaggi perfetti per le nostre esigenze, tra i quali:

  • Supporta nativamente e completamente gli standard open web, content negotiation e Linked Data (Swagger, JSON-LD, JSON API, GraphQL, Hydra, HAL, HTTP...);

  • Permette alle nostre API di esporre automaticamente i dati strutturati in schema.org/JSON-LD, in questo modo vedremo un miglioramento anche lato SEO in quanto Google premia questi formati;

  • È compatibile con Symfony Framework e in particolare Doctrine ORM Bridge, oltre a supportare la maggior parte dei bundle Symfony;

  • Ha una sua recipe Symfony Flex ufficiale;

  • Permette di creare, recuperare, aggiornare ed eliminare risorse (CRUD);

  • Possiede un admin JavaScript dinamico, sviluppato sopra a React e React Admin;

  • Presenta una bella UI e una documentazione leggibile dalle macchine (Swagger/OpenAPI);

  • Ha diversi metodi di autenticazione (Basic HTTP, cookies, ma anche JWT e OAuth attraverso estensioni).

Installazione di API Platform

Sebbene API Platform sia fornita con Symfony, avevamo già Symfony (con Flex) e diamo per assunto che tu abbia un progetto Symfony in corso. Dal momento che vogliamo avere il pieno controllo sulla struttura delle directory e sulle dipendenze, l'abbiamo installato tramite Symfony Flex e Composer nella root del nostro progetto, come indicato nella documentazione di API Platform:

$ composer req api

Poi, abbiamo creato il database e il suo schema:

$ bin/console doctrine:database:create
$ bin/console doctrine:schema:create

Una volta installato con questo metodo, l’API sarà visibile con il path /api. Per vedere la documentazione dell’API dovremo aprire http://your_server_address/api e voilà! Facile, vero? Ora implementiamo JWT Authentication nella nostra applicazione.

Implementazione di JWT Authentication

API Platform permette di aggiungere facilmente l’autenticazione basata su JWT (JSON Web Token) alla nostra API, usando LexikJWTAuthenticationBundle.

API Platform afferma che “LexikJWTAuthenticationBundle requires your application to have a properly configured user provider. You can either use the Doctrine user provider provided by Symfony (recommended), create a custom user provider or use API Platform's FOSUserBundle integration.”

Visto che abbiamo msgphp/eav-bundle e msgphp/user-bundle installati, utilizzeremo il suo user provider di sicurezza per la nostra applicazione.

Installare LexikJWTAuthenticationBundle

Installiamo LexikJWTAuthenticationBundle aggiungendolo al nostro composer.json:

$ composer req "lexik/jwt-authentication-bundle"

Generare le chiavi SSH

Generiamo le chiavi SSH:

$ mkdir config/jwt
$ openssl genrsa -out config/jwt/private.pem -aes256 4096
$ openssl rsa -pubout -in config/jwt/private.pem -out
config/jwt/public.pem

Creare Routes, Controller, l’entità User e il User Repository

Abbiamo bisogno di gestire la registrazione, il controllo e gli endpoint del login creando il nostro controller, le routes, l'entità user e il user repository.

Ecco la nostra entità User in src/Entity/User/User.php:

<?php
 
namespace App\Entity\User;
 
use Doctrine\ORM\Mapping as ORM;
use MsgPhp\User\Entity\User as BaseUser;
use MsgPhp\User\UserIdInterface;
use MsgPhp\Domain\Event\DomainEventHandlerInterface;
use MsgPhp\Domain\Event\DomainEventHandlerTrait;
use MsgPhp\User\Entity\Credential\EmailPassword;
use MsgPhp\User\Entity\Features\EmailPasswordCredential;
use MsgPhp\User\Entity\Features\ResettablePassword;
use MsgPhp\User\Entity\Fields\RolesField;
 
/**
* @ORM\Entity(repositoryClass="App\Repository\User\UserRepository")
*/
class User extends BaseUser implements DomainEventHandlerInterface
{
   use DomainEventHandlerTrait;
   use EmailPasswordCredential;
   use ResettablePassword;
   use RolesField;
 
   /**
    * @ORM\Id()
    * @ORM\GeneratedValue()
    * @ORM\Column(type="msgphp_user_id")
    */
   private $id;
 
   public function __construct(UserIdInterface $id, string $email, string $password)
   {
       $this->id = $id;
       $this->credential = new EmailPassword($email, $password);
   }
 
   public function getId(): UserIdInterface
   {
       return $this->id;
   }
}

Nota: La nostra entità User esisteva già nel progetto (che viene fornito con msgphp/user-bundle, come abbiamo già accennato). Nel caso in cui avessimo appena creato una nuova entità User, utilizzeremmo il comando qui sotto per aggiornare il nostro database prima di andare avanti:

$ bin/console doctrine:schema:update --force

Ora, inseriamo nel file di configurazione delle routes config/routes.yaml:

  • il riferimento all'endpoint del JSON login check:

api_login_check:
   path:     /auth/login_check
   methods:  [POST]

  • il controllore del login src/Controller/Api/AuthController.php:
<?php
 
namespace App\Controller\Api;
 
use App\Entity\User\User;
use App\Repository\User\UserRepository;
use MsgPhp\User\UserId;
use MsgPhp\User\UserIdInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
 
/**
* Class AuthController
* @Route("/auth")
*/
class AuthController extends AbstractController
{
   /**
    * @Route("/registration",
    *     name="api_user_registration",
    *     methods={"POST"}
    * )
    *
    * @param Request $request
    * @return Response
    */
   public function register(Request $request)
   {
       $em = $this->getDoctrine()->getManager();
 
       $email = $request->request->get('_username');
       $password = password_hash($request->request->get('_password'), PASSWORD_BCRYPT);
 
       $user = new User(new UserId(), $email, $password);
 
       $em->persist($user);
       $em->flush();
 
       return new Response(sprintf('User %s successfully created', $user->getEmail()));
   }
 
   /**
    * @Route("/loggedin", name="api_user_loggedin")
    * @param UserRepository $userRepository
    * @return Response
    */
   public function loggedin(UserRepository $userRepository)
   {
       /** @var UserIdInterface $userId */
       $userId = $this->getUser()->getUserId();
 
       $credentials = $userRepository->getUsernameAndPasswordByUserId($userId);;
 
       return new Response(sprintf('Logged in as %s', $credentials['username']));
   }
}

  • il repository del nostro User src/Repository/User/UserRepository.php:
<?php
 
namespace App\Repository\User;
 
use App\Entity\User\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use MsgPhp\User\UserIdInterface;
use Symfony\Bridge\Doctrine\RegistryInterface;
 
class UserRepository extends ServiceEntityRepository
{
   const TABLE_ALIAS = 'user';
 
   public function __construct(RegistryInterface $registry)
   {
       parent::__construct($registry, User::class);
   }
 
   public function getOneByUserIdQueryBuilder(UserIdInterface $userId)
   {
       return $this->createQueryBuilder(self::TABLE_ALIAS)
           ->andWhere(self::TABLE_ALIAS.'.id = :userId')
           ->setParameter('userId', $userId)
           ;
   }
 
   public function getUsernameAndPasswordByUserId(UserIdInterface $userId)
   {
       $user = $this->getOneByUserIdQueryBuilder($userId)
                    ->getQuery()
                    ->getSingleResult();
 
       return [
           'username' => $user->getEmail(),
           'password' => $user->getPassword()
       ];
   }
}

Configurare i sistemi di sicurezza di Symfony

Ora abbiamo bisogno che Symfony sappia del nostro user provider di sicurezza, del nostro encoder, del JSON login e del JWT Authenticator. Lo facciamo configurando il file config/packages/security.yaml:

security:
   encoders:
       MsgPhp\User\Infra\Security\SecurityUser: bcrypt
 
   providers:
       msgphp_user:
           id: MsgPhp\User\Infra\Security\Jwt\SecurityUserProvider
 
   firewalls:
       api_login:
           provider: msgphp_user
           pattern:  ^/auth/login
           stateless: true
           anonymous: true
           json_login:
               check_path: /auth/login_check
               success_handler: lexik_jwt_authentication.handler.authentication_success
               failure_handler: lexik_jwt_authentication.handler.authentication_failure
 
       api_registration:
           pattern:  ^/auth/registration
           stateless: true
           anonymous: true
 
       api_loggedin:
           pattern:  ^/auth/loggedin
           provider: msgphp_user
           stateless: true
           anonymous: false
           guard:
               authenticators:
                   - lexik_jwt_authentication.jwt_token_authenticator
 
       dev:
           pattern: ^/(_(profiler|wdt)|css|images|js)/
           security: false
 
       main:
           anonymous: true
           form_login:
               provider: msgphp_user
               login_path: /admin/login
               check_path: /admin/login
               default_target_path: /admin/profile
               username_parameter: email
               password_parameter: password
 
   # Easy way to control access for large sections of your site
   # Note: Only the *first* access control that matches will be used
   access_control:
       - { path: ^/admin/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
       - { path: ^/admin/reset-password, roles: IS_AUTHENTICATED_ANONYMOUSLY }
       - { path: ^/admin/forgot-password, roles: IS_AUTHENTICATED_ANONYMOUSLY }
       - { path: ^/admin, roles: ROLE_ADMIN }
       - { path: ^/profile, roles: IS_AUTHENTICATED_FULLY }
       - { path: ^/auth/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
       - { path: ^/auth/registration, roles: IS_AUTHENTICATED_ANONYMOUSLY }
       - { path: ^/auth/loggedin, roles: IS_AUTHENTICATED_FULLY }

Preparare la configurazione e la documentazione delle API resources

Configurare le API resources

Per configurare le API resources, modifichiamo il file config/packages/api_platform.yaml:

api_platform:
   title: NEWWEB
   mapping:
       paths:
           - '%kernel.project_dir%/src/Entity'
           - '%kernel.project_dir%/config/api_platform/resources.yaml'

Documentazione delle API resources:

Come abbiamo già accennato, API Platform supporta la specifica OpenApi/Swagger che consente sia a umani che a macchine di comprendere le funzionalità dei nostri servizi/operazioni API. Per utilizzarlo, creiamo un file resources.yaml nella directory config/api_platform:

resources:
 App\Entity\User\User:
   collectionOperations:

Di default, API Platform ci fornirà le operazioni di base come GET, POST, PUT e DELETE con la specifica che abbiamo visto sopra. Se visitiamo http://your_server_address/api nel nostro browser utilizzando solo quella specifica, vedremo qualcosa di questo genere:

Come integrare API Platform e JWT Authentication in un'applicazione

Bello, vero? Ma per il bene della nostra applicazione, editiamo il file resources.yaml per aggiungere operazioni custom:

resources:
 App\Entity\User\User:
   collectionOperations:
     create_user:
       route_name: api_user_registration
       method: post
       controller: App\Controller\Api\AuthController
       swagger_context:
         consumes:
           - application/x-www-form-urlencoded
         parameters:
           - in: formData
             name: _username
             required: true
             type: string
             description: "User's username or email address"
 
           - in: formData
             name: _password
             required: true
             type: string
             description: "User's password"
 
         example:
           name: User
           description: Api User registration
 
         responses:
           200:
             description: "User successfully registered"
             schema:
               type: object
               required:
                 - _username
                 - _password
               properties:
                 _username:
                   type: string
 
                 _password:
                   type: string
 
     get_jwt_token:
       route_name: api_login_check
       method: post
       swagger_context:
         summary: Performs a login attempt, returning  a valid JWT token on success
         description: >
           Login check to get a valid JWT token
         consumes:
           - application/json
         parameters:
           - in: body
             name: credentials
             type: string
             description: "User's username or email address"
             schema:
               type: object
               required:
                 - username
                 - password
               properties:
                 username:
                   type: string
                 password:
                   type: string
 
         example:
           name:
             {
               "username":"test",
               "password":"fantozzi"
             }
           description: User login attempt
 
         responses:
           200:
             description: "Successful login attempt, returning a new JWT token"
             schema:
               type: object
               required:
                 - username
                 - password
               properties:
                 username:
                   type: string
 
                 password:
                   type: string
 
     api_loggedin:
       route_name: api_user_loggedin
       method: post
       swagger_context:
         summary: Performs a succesful login with a JWT token
         description: >
           Returns a loggedin response
         security:
           - APIKeyHeader: []
         parameters:
           - in: header
             name: Authorization
             type: string
 
         securityDefinitions:
           APIKeyHeader:
             type: apiKey
             name: Authorization
             in: header

Come possiamo vedere dal file resources.yaml, possiamo specificare nella documentazione di un operazione:

  • cosa fa;

  • di quali parametri e tipologie di parametri ha bisogno;

  • quale schema ha;

  • quali risposte e tipologie di risposte restituisce;

  • un esempio di struttura e del suo utilizzo.

Qui è possibile inserire tutti i tipi di definizioni e contesti delle operazioni.

Una volta che abbiamo aggiornato il file resources.yaml con la configurazione vista sopra, visitando http://your_server_address/api dovremmo vedere questo:

Come integrare API Platform e JWT Authentication in un'applicazione

Ora inizia il divertimento: vediamo come implementare il tutto.

Si entra in azione

Registrazione di un utente

Prima di tutto, dobbiamo registrare un utente nel nostro database. Apriamo l’interfaccia utente Swagger di API Platform in un browser. Visitiamo http://your_server_address/api, apriamo l’operazione /auth/registration POST e clicchiamo il bottone Try it out. Forniamo un nome utente (o un indirizzo email) e una password, quindi clicchiamo sul pulsante Execute:

Come integrare API Platform e JWT Authentication in un'applicazione

Se l’operazione di salvataggio del nuovo utente è riuscita, riceveremo un messaggio di successo con tutti i dettagli della nostra richiesta POST:

Come integrare API Platform e JWT Authentication in un'applicazione

Ottenere un token di accesso

Il nostro utente è stato registrato correttamente ed è ora di eseguire un controllo sul login e ottenere un token JWT. Per farlo, apriamo l'operazione /auth/login_check POST e facciamo clic sul pulsante Try it out. Qui dobbiamo fornire un body JSON contenente il nome utente e la password che abbiamo fornito prima per la registrazione, e poi fare clic sul pulsante Execute:

Come integrare API Platform e JWT Authentication in un'applicazione

Se tutto ha funzionato correttamente, allora riceveremo una risposta con il nostro JSON Web Token:

Come integrare API Platform e JWT Authentication in un'applicazione

Adesso abbiamo il nostro token di accesso, che è valido per un'ora per impostazione predefinita. È tempo di fare il login.

Fare login utilizzando il token di accesso

Visto che abbiamo creato un token di accesso JWT valido, adesso possiamo accedere. Copiamo il token dal campo Response Body dello step precedente:

Come integrare API Platform e JWT Authentication in un'applicazione

Ora, apriamo l'operazione /auth/loggedin POST e facciamo clic sul pulsante Try it out. In questa sezione faremo passare il nostro token di accesso come un header in authentication, quindi inseriamolo nel campo di input in questo modo: Bearer [TOKEN].

Poi, clicchiamo sul pulsante Execute:

Come integrare API Platform e JWT Authentication in un'applicazione

Verrà visualizzata un’icona a forma di lucchetto alla destra del nome dell’operazione, il che significa che la nostra route è protetta. Come fa API Platform a sapere che è protetta? Magia? No, non è magia, deriva tutto dal security context della definizione della nostra operazione api_loggedin che abbiamo documentato prima in resources.yaml:

...
api_loggedin:
 ...
 swagger_context:
   ...
   security:
     - APIKeyHeader: []
   ...
   securityDefinitions:
     APIKeyHeader:
       type: apiKey
       name: Authorization
       in: header

Se tutto ha funzionato, riceveremo una risposta come questa:

Come integrare API Platform e JWT Authentication in un'applicazione

Abbiamo finito. Ecco che API Platform e la nostra JWT Authentication sono integrati.

Conclusione

Quindi, abbiamo completato il nostro breve viaggio con API Platform e JWT Authentication. Come abbiamo potuto constatare, è abbastanza semplice creare progetti API-first con API Platform e addirittura integrarli con concetti complessi come la JWT Authentication. Se hai bisogno di aiuto o vuoi parlarci del tuo progetto, clicca sul bottone e contattaci.