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:

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:

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:

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

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:

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

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:

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:

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:

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.