Salta el contingut

JWT (JSON Web Token)

Els mecanismes d'autenticació tradicional en aplicacions web estan basats en sessions: l'usuari envia les seues credencials a través d'algun formulari, el servidor ho valida i emmagatzema en la sessió les dades de l'usuari logueado, perquè, mentre no caduque la sessió o la tanque l'usuari, puga seguir accedint sense haver de tornar a autenticar-se.

No obstant açò, aquest tipus d'autenticació té la limitació de ser exclusiva per a aplicacions web, és a dir, per a clients web que es connecten a servidors web. Si volguérem adaptar l'aplicació a mòbil, o a una versió d'escriptori, no podríem seguir emprant aquest mecanisme.

Per a superar aquest escull, podem utilitzar l'autenticació basada en tokens. Aquesta és una autenticació "sense estat" (stateless), la qual cosa significa que no s'emmagatzema gens entre client i servidor per a seguir accedint autenticats. El que es fa és el següent:

  1. El client envia al servidor les seues credencials (usuari i password)
  2. El servidor les valguda, i si són correctes, genera una cadena xifrada anomenada token, que conté la validació de l'usuari, a més de certa informació addicional que puguem voler afegir (com el login de l'usuari, per exemple). Aquest token s'envia de tornada a l'usuari com a resposta a la seua autenticació.
  3. A partir d'aquest punt, cada vegada que el client vulga autenticar-se contra el servidor per a sol·licitar un recurs, n'hi ha prou que envie el token que el servidor li va proporcionar. El servidor s'encarregarà de verificar-ho per a comprovar que és correcte, i donar-li accés o denegar-li-ho.

Igual que les sessions, els tokens també poden tenir una caducitat, que s'indica dins del propi token. Si, passat aqueix temps, el servidor rep el token, ho descartarà com a invàlid (caducat), i el client tornarà a no estar autenticat.

JSON Web Token és un estàndard obert (RFC 7519) que defineix un mode compacte i autònom de transmetre de forma segura la informació entre dues parts com un objecte JSON. Aquesta informació pot ser verificada i és fiable perquè està signada digitalment.

Els JWT es poden signar usant un secret (amb l'algoritme HMAC) o utilitzant un parell de claus públiques/privades usant RSA i contenen la informació de l'usuari autenticat (el nom d'usuari, per exemple)

Estructura del token

Estructura

Exemple de token extret de jwt.io

Cicle de vida del token JWT

Seqüència

Intercanvi de missatges

Implementació en Symfony

Per a poder treballar amb JWT en Symfony, podem emprar (entre uns altres) el bundle lexik/LexikJWTAuthenticationBundle, que s'instal·la d'aquesta manera:

composer require jwt-auth
A més, necessitarem afegir el bundle de seguretat de Symfony:

composer require security

Entitat User

Caldrà disposar d'una entitat que implemente la interfície UserInterface que ja tenim del projecte anterior.

Endpoints

Usarem dos endpoints:

  • /api/login serà la ruta on es troba el formulari.
  • /api/login_check serà la ruta que valida les credencials.

Generació de certificats

Per a poder codificar els tokens, és necessari generar uns parell de certificats.

Generarem un de privat per a generar el token quan l'usuari es valide, i un públic per a poder-lo validar quan l'usuari ho envie.

Per a açò, executem aquestes ordre des del directori arrel del projecte. Quan ens ho demane, triarem com passphrase la paraula movies (és només una paraula o frase que utilitzar per a xifrar el contingut, triem aqueixa per exemple):

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

Amb aquestes ordres hem creat (1) una clau privada basada en l'algorisme AES i (2) una pública a partir de la privada.

Configuració del fitxer .env

Ara hem d'editar també el fitxer .env o .env.local (millor aquest) i afegir aquestes línies:

JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
JWT_PASSPHRASE=movies
JWT_TOKENTTL=3600 

L'atribut JWT_PASSPHRASE haurà de coincidir amb el que hem indicat en generar els certificats en el pas anterior (movies, en el nostre cas).

L'atribut JWT_TOKENTTL és el temps de vida o caducitat del token, en segons.

En aquest cas, li donem un temps de validesa d'una hora.

Configuració de config/packages/lexik_authentication.yaml

Aquest fitxer quedarà d'aquesta manera, en el qual indiquem on estan generades la clau privada i pública, la paraula de xifrat i el temps de vida:

lexik_jwt_authentication:
    secret_key: '%env(resolve:JWT_SECRET_KEY)%' # required for token creation
    public_key: '%env(resolve:JWT_PUBLIC_KEY)%' # required for token verification
    pass_phrase: '%env(JWT_PASSPHRASE)%' # required for token creation
    token_ttl: %env(JWT_TOKENTTL)% # in seconds, default is 3600

Configuració de config/packages/security.yaml

L'arxiu principal de seguretat config/packages/security.yaml haurà de contenir aquests atributs per a l'autenticació per token:

# Symfony 5.3 and higher
security:
    enable_authenticator_manager: true
    # ...
    #  user provider, password hasher and so on...
    #  
    # ...

    firewalls:
        login:
            pattern: ^/api/login
            stateless: true
            json_login:
                check_path: /api/login_check
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure

        api:
            pattern:   ^/api
            stateless: true
            jwt: ~

    access_control:
        - { path: ^/api/login, roles: PUBLIC_ACCESS }
        - { path: ^/api,       roles: IS_AUTHENTICATED_FULLY }

Caldrà afegir també la següent ruta en config/routes.yaml:

api_login_check:
    path: /api/login_check

El que hem definit en aquest fitxer és:

  • El firewall login que s'activarà en accedir a la ruta api/login. Ruta que serà pública, com s'observa en access_control.
  • json_login indica que s'espera una sol·licitud via JSON.
  • El firewall api s'activarà en la resta de rutes de la API, on s'indicarà que cap aplicar l'autenticació jwt.
  • jwt: ~ activa l'autenticador JWT.

Provant l'autenticació

Per a provar que l'autenticació funciona, crearem una nova petició POST en Postman a la URI /api/login_check, i li passem en el cos de la petició l'usuari (username) i la contrasenya (password). En aquest exemple, suposem que l'usuari és user i la contrasenya (sense encriptar) és user. Haurem d'afegir també una capçalera (Header) amb l'atribut Content­Type establit a application/json.

Si tot va correctament, rebrem com a resposta un token:

Provant l'autenticació amb Postman

Si analitzem el token obtindrem:

Anàlisi del token

La signatura és invàlida ja que no s'ha pogut verificar amb les claus pública i privada.

Provant l'autorització

Ara, provarem a obtenir un llistat de pel·lícules. Si llancem la petició en Postman sense cap tipus d'autorització, rebrem aquest missatge de tornada:

{
    "code": 401,
    "message": "JWT Token not found"
}

Hem d'afegir una capçalera Authorization el valor de la qual siga el prefix "Bearer " (incloent l'espai final) seguit del token que ens ha enviat el servidor en autenticar-nos:

Sol·licitud amb el token inclòs

D'aquesta forma sí que obtindrem el llistat de pel·lícules. Haurem de procedir de la mateixa forma (enviant el token en la capçalera Authorization) per a poder emprar la resta de peticions.

Recursos