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 les valida i emmagatzema en la sessió les dades de l'usuari, perquè, mentre no caduque la sessió o la tanque l'usuari, puga seguir accedint sense haver de tornar a autenticar-se.
No obstant això, 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 res entre client i servidor per a seguir accedint autenticats. El que es fa és el següent:
- El client envia al servidor les seues credencials (usuari i contrasenya)
- El servidor les valida, 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 nom d'usuari, per exemple). Aquest token s'envia de tornada a l'usuari com a resposta a la seua autenticació.
- A partir d'aquest punt, cada vegada que el client vullga autenticar-se contra el servidor per a sol·licitar un recurs, n'hi ha prou en que envie el token que el servidor li va proporcionar. El servidor s'encarregarà de verificar-lo per a comprovar que és correcte, i donar-li accés o denegar-li'l.
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.
Estructura del token
Cicle de vida del token JWT
Implementació en Symfony
Per a poder treballar amb JWT en Symfony, podem emprar (entre
altres) el bundle lexik/LexikJWTAuthenticationBundle
, que s'instal·la
d'aquesta manera:
composer require jwt-auth
composer require security
Entitat User
Caldrà disposar d'una entitat que implemente la interfície UserInterface
que ja tenim del projecte anterior.
Endpoints
Usarem un endpoint:
/login
serà la que validarà les credencials.
Generació de certificats
Per a poder codificar els tokens, és necessari generar un parell de certificats.
Generarem un privat per a generar el token quan l'usuari es valide, i un públic per a poder-lo validar quan l'usuari l'envie.
Per a açò, executem la següent ordre des del directori arrel del projecte.
php bin/console lexik:jwt:generate-keypair
Amb aquesta ordre es crearà una clau privada,
i una pública a partir de la privada. Aquestes claus requeriran una contrasenya
que es genera automàticament i es troba en el fitxer .env
, en la clau JWT_PASSPHRASE
.
En el fitxer .env
s'hauran creat estes línies:
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
JWT_PASSPHRASE=14ea51a657fac12644ab2653d8933644
I en el fitxer de configuració config/packages/lexik_authentication.yaml
estes:
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
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:
# config/packages/security.yaml
security:
# https://symfony.com/doc/current/security.html#c-hashing-passwords
password_hashers:
App\Entity\User: 'auto'
# https://symfony.com/doc/current/security/authenticator_manager.html
enable_authenticator_manager: true
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
# used to reload user from session & other features (e.g. switch_user)
users:
entity:
class: App\Entity\User
property: username
firewalls:
dev:
pattern: ^/_(profiler|wdt)
security: false
api:
pattern: ^/api/
stateless: true
provider: users
jwt: ~
main:
json_login:
check_path: login # The name in routes.yaml is enough for mapping
username_path: username
password_path: password
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
access_control:
- { path: ^/$, roles: PUBLIC_ACCESS } # Allows accessing the Swagger UI
- { path: ^/login, roles: PUBLIC_ACCESS } # Allows accessing login controller
- { path: ^/api$, roles: PUBLIC_ACCESS } # Allows accessing the doc
- { path: ^/, roles: IS_AUTHENTICATED_FULLY } # Forces authentication in all the requests
Caldrà afegir també la següent ruta en config/routes.yaml
:
login:
path: /login
El que hem definit en aquest fitxer és:
- El firewall
main
que s'activarà en accedir a la ruta/login
. Ruta que serà pública, com s'observa enaccess_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 /login
, 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 ContentType
establit a application/json
.
Tests
Seria interessant crear un test que a més ens permetrà provar el control d'accés.
Si tot va correctament, rebrem com a resposta un token:
Si analitzem el token obtindrem:
La signatura és invàlida ja que no s'ha pogut verificar amb les claus pública i privada.
Provant l'autorització
Ara, obtindrem un llistat de tweets. Si llancem la petició en Postman (o en el test) sense cap tipus d'autorització, rebrem aquest missatge:
{
"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:
D'aquesta forma sí que obtindrem el llistat de tweets. Haurem de procedir
de la mateixa forma (enviant el token en la capçalera Authorization
) per a
poder emprar la resta de sol·licituds.
Activar l'autorització des de la documentació
Primerament vincularem el sistema d'autenticació afegint:
# config/packages/api_platform.yaml
api_platform:
swagger:
api_keys:
JWT:
name: Authorization
type: header
Després, per poder provar l'autenticació, afegirem en lexik_jwt_authentication.yaml
:
api_platform:
check_path: /login
username_path: username
password_path: password
Una vegada obtingut el token caldrà afegir-lo a la capçalera des del botó [Authorize]
,
introduint el text: Bearer NEW_TOKEN
. Canviant NEW_TOKEN
pel token obtingut.
Recursos
- JWT Authentication en API Platform
- Security en API Platform
- https://jwt.io/
- Functional Testing with JWT Authentication Bundle
- LexikJWTAuthenticationBundle
- https://enmilocalfunciona.io/construyendo-una-web-api-rest-segura-con-json-web-token-en-net-parte-i/
- https://www.adictosaltrabajo.com/2017/09/25/securizar-un-api-rest-utilizando-json-web-tokens/
- https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS