Introduzione

Innanzitutto andiamo a definire il concetto di JWT, acronimo di Json Web Token.
Il JWT è un JSON-based open standard (RFC 7519 – JSON Web Token (JWT)) per creare access token, decisamente utili per architetture API-REST in cui le comunicazioni sono stateless cioè il server non conosce nulla del client e tutte le informazioni devono risedere nel client.
Per esempio, a seguito di una login eseguita correttamente, il server può generare e restituire al client un token che contiene l’informazione “logged as admin”. Il client, nelle chiamate successive, potrà utilizzare (cioè inviare) questo token per indicare al server di essere loggato con privilegi di admin.
I token sono firmati con la chiave del server che potrà quindi verificarne la legittimità e integrità.
I token sono particolarmente compatti quindi utilizzabili in particolare nei contesti di single sign-on (SSO).

Implementazione

Ormai siamo abituati che nell’ecosistema di Symfony si trova quasi sempre il bundle corretto per le proprie esigenze.
Anche questa volta le aspettative non vengono tradite: sembra che il bundle LexikJWTAuthenticationBundle sia proprio fatto apposta per le nostre esigenze.
Tralasciamo le solite indicazioni di installazione che sono ben specificate nella documentazione e che non presentano nulla di particolare o difficile e veniamo al sodo.
Implementiamo quindi il meccanismo base di autenticazione a dei servizi Web API REST/Json avvalendoci di questo bundle e del comodissimo firewall Guard di Symfony.
Per questa semplice guida andremo a soffermarci solo sulla parte server-side che è il motore del sistema.

  1. Creiamo un metodo che gestisce l’autenticazione dell’utente nel controller dedicato alle API.
...

/**
 * @Route("/api")
 */
class DefaultController extends Controller
{
    /**
     * @Route(path="/token-authentication", name="api_token_authentication")
     */
    public function tokenAuthentication(Request $request)
    {
        $username = $request->request->get('username');
        $password = $request->request->get('password');

        $user = $this->getDoctrine()->getRepository('FooUserBundle:User')
            ->findOneBy(['username' => $username]);

        if (!$user) {
            throw $this->createNotFoundException(sprintf('Utente %s non trovato.', $username));
        }

        $userChecker = new UserChecker();
        $userChecker->checkPreAuth($user);

        if (!$this->get('security.password_encoder')->isPasswordValid($user, $password)) {
            throw $this->createAccessDeniedException(sprintf('Dati di accesso per l\'utente %s non validi.', $username));
        }

        $userChecker->checkPostAuth($concorrente);

        // Use LexikJWTAuthenticationBundle to create JWT token that hold only information about user name
        $token = $this->get('lexik_jwt_authentication.jwt_encoder')->encode(['username' => $user->getUsername()]);

        // Return genereted token
        return new JsonResponse(['token' => $token]);
    }

Come vedete non è nulla di particolare, a fronte di uno username e password cerchiamo l’utente e se è valido, creiamo un token contenente lo username. Il token è creato dal servizio offerto dal bundle appena installato.

  1. Lato client, chiamiamo questo metodo…
curl -X POST http://localhost:8000/api/token-authentication -d _username=johndoe -d _password=test

… per ottenere in risposta il token.

{
   "token" : "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE0MzQ3Mjc1MzYsInVzZXJuYW1lIjoia29ybGVvbiIsImlhdCI6IjE0MzQ2NDExMzYifQ.nh0L_wuJy6ZKIQWh6OrW5hdLkviTs1_bau2GqYdDCB0Yqy_RplkFghsuqMpsFls8zKEErdX5TYCOR7muX0aQvQxGQ4mpBkvMDhJ4-pE4ct2obeMTr_s4X8nC00rBYPofrOONUOR4utbzvbd4d2xT_tj4TdR_0tsr91Y7VskCRFnoXAnNT-qQb7ci7HIBTbutb9zVStOFejrb4aLbr7Fl4byeIEYgp2Gd7gY"
}
  1. Creiamo quindi un firewall implementando l’interfaccia AbstractGuardAuthenticator
...
class JwtAuthenticator extends AbstractGuardAuthenticator
{
    private $em;
    private $jwtEncoder;

    public function __construct(EntityManager $em, JWTEncoder $jwtEncoder)
    {
        $this->em = $em;
        $this->jwtEncoder = $jwtEncoder;
    }

    public function start(Request $request, AuthenticationException $authException = null)
    {
        return new JsonResponse('Auth header required', 401);
    }

    public function getCredentials(Request $request)
    {
        if (!$request->headers->has('Authorization')) {
            return null;
        }

        $extractor = new AuthorizationHeaderTokenExtractor('Bearer');

        $token = $extractor->extract($request);

        if (!$token) {
            return null;
        }

        return $token;
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        $data = $this->jwtEncoder->decode($credentials);

        if (!$data){
            return null;
        }

        $username = $data['username'];

        $user = $this->em->getRepository('FooUserBundle:User')
            ->findOneBy(['username' => $username]);

        if (!$user){
            return null;
        }

        return $user;
    }

    public function checkCredentials($credentials, UserInterface $user)
    {
        return true;
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
        return new JsonResponse([
            'message' => $exception->getMessage()
        ], 401);
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        return;
    }

    public function supportsRememberMe()
    {
        return false;
    }
}

Da notare che questo firewall entra in funzione solo per chiamate che presentano l’header “Authorization” che deve contenere il token precedentemente generato.
Inoltre il firewall non verifica le credenziali, dà per scontato che il token sia già di per sé, nella sua interezza e validità, l’elemento che garantisce l’autenticazione.
Quindi, in estrema sintesi, se il token è valido, il firewall si preoccupa solo di estrarne lo username e, da qui, eventualmente, caricare l’utente o i servizi necessari per gestire la chiamata.
Ricordiamoci infine di registrare il firewall in security.yml:

security:
      firewalls:
          api:
            pattern: ^/api
            anonymous: ~
            guard:
                authenticators:
                    - api.jwt_token_authenticator
    access_control:
          - { path: ^/api/token-authentication, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/api, roles: [ROLE_USER] }

Riferimenti