Una de las características que Laravel 5.x nos ha facilitado bastante es la autenticación. Con sólo un comando, habremos generado las rutas, vistas y controladores automáticamente (lo que se conoce como «scaffolding).
Pero aunque esto está muy bien, puede que no se ajuste totalmente a nuestras necesidades. Laravel gestiona la autenticación y sesión entre otras formas, por cookies. ¿Y que pasa si queremos usar tokens para nuestras API’s ? Laravel introduce en su versión 5.2 la autenticación por tokens en rutas «api» de nuestra aplicación.
Esto nos pone las cosas fáciles pero ¿qué pasa si queremos ir más allá y personalizar las respuestas en función del estado de ese token enviado en cada petición? o ¿ qué pasa si queremos usar distintos «guards» mediante paquetes de terceros? Pues bien, en este post, te mostraré como modificar lo que Laravel nos trae por defecto manteniendo las vistas tal y como están para realizar los ejemplos.
Estos son los pasos a seguir:
- Instalar el paquete JWT-Auth (para la autenticación y manejo de los tokens).
- Instalar el paquete JWT Guard (para usar jwt como driver de autenticación) aquí.
- Configurar los drivers de autenticación.
- Modificar el modelo de usuario «User».
- Sustituir el middleware de autenticación de Laravel por otro previamente modificado.
- Realizar las modificaciones necesarias en el paquete JWT-Auth para que funcione el middleware «auth:api» en las rutas.
- Crear un service provider para poder usar el «guard» que queramos, en nuestro caso con el driver «api», en grupos de rutas.
Empezamos instalando el paquete para usar JWT’s:
1 |
composer require tymon/jwt-auth:1.0.*@dev |
Ahora edita el siguiente archivo:
1 2 3 4 5 6 7 8 |
'providers' => [ .... Tymon\JWTAuth\Providers\LaravelServiceProvider::class, ], 'aliases' => [ .... 'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class, ], |
Al acabar, hay que publicar el archivo de configuración de ese paquete y luego establecer la «key» privada que se usará para descifrar los tokens (lo hace JWT-Auth automáticamente y de forma transparente a nosotros).
1 2 3 4 5 6 7 |
// Publica el archivo de configuracion php artisan vendor:publish // Genera la key que usará JWT-Auth para firmar los tokens php artisan jwt:secret |
Turno de JWT Guard:
Añadimos el paquete a nuestro proyecto:
1 |
composer require irazasyed/jwt-auth-guard |
Registramos el Service Provider:
1 |
Irazasyed\JwtAuthGuard\JwtAuthGuardServiceProvider::class |
Establecemos del driver de autenticación:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
'guards' => [ 'api' => [ 'driver' => 'jwt-auth', 'provider' => 'users' ], ], 'providers' => [ 'users' => [ 'driver' => 'eloquent', 'model' => App\User::class, ], ], |
Editamos el modelo User tal que:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
use Illuminate\Foundation\Auth\User as Authenticatable; use Tymon\JWTAuth\Contracts\JWTSubject as JWTSubject; class User extends Authenticatable implements JWTSubject { . . . . /** * Get the identifier that will be stored in the subject claim of the JWT * * @return mixed */ public function getJWTIdentifier(){ return $this->getKey(); } /** * Return a key value array, containing any custom claims to be added to the JWT * * @return array */ public function getJWTCustomClaims() { return [ 'user' => [ 'id' => $this->id, ] ]; } } |
Ya que la versión 1.0@dev de jwt-auth no incluye las respuestas JSON en caso de no proporcionar el token en las peticiones o que éste sea inválido o que haya expirado, tenemos 2 opciones:
- No modificamos ningún archivo y usamos el middleware «jwt.auth» para proteger las rutas que necesiten JWTokens.
- Modificamos una serie de archivos para poder usar el middleware «auth:api» que viene de serie con jwt guard.
Vamos a hacer un test, abre el archivo de rutas api:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Route::get('users/create/{id?}', function ($id=1) { $token = Auth::guard('api')->generateTokenById($id); echo $token; }); // Usando el middleware "jwt.auth" Route::get('users/show', function() { $user = \Auth::guard('api')->user(); return $user; })->middleware('jwt.auth'); // Usando el middleware "auth:api" Route::get('test', function () { return 'jwt protected route'; })->middleware('auth:api'); |
Para usar el middleware «auth:api» modificamos lo siguientes archivos:
- app\Http\Kernel.php
- vendor\tymon\jwt-auth\src\Http\Middleware\Authenticate.php
- vendor\tymon\jwt-auth\src\Http\Middleware\BaseMiddleware.php
- vendor\tymon\jwt-auth\src\Providers\JWT\Namshi.php
- vendor\tymon\jwt-auth\src\Exceptions\JWTException.php
- El resto de archivos en la carpeta de excepciones \vendor\tymon\jwt-auth\src\Exceptions les agregamos una propiedad, nada más.
Vamos uno a uno. Primero comentamos el middleware que trae Laravel y añadimos el nuevo:
1 2 3 4 5 |
protected $routeMiddleware = [ // 'auth' => \Illuminate\Auth\Middleware\Authenticate::class, 'auth' => \Tymon\JWTAuth\Http\Middleware\Authenticate::class, . . . . ]; |
Editamos Authenticate.php con el contenido de esta seccion:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
<?php namespace Tymon\JWTAuth\Http\Middleware; use Closure; use Illuminate\Http\Request; use Tymon\JWTAuth\Exceptions\JWTException; use Tymon\JWTAuth\Exceptions\TokenExpiredException; class Authenticate extends BaseMiddleware { public function handle(Request $request, Closure $next) { if (! $token = $this->auth->setRequest($request)->getToken()) { return $this->respond('tymon.jwt.absent', 'token_not_provided', 400); } try { $user = $this->auth->authenticate($token); } catch (TokenExpiredException $e) { return $this->respond('tymon.jwt.expired', 'token_expired', $e->getStatusCode(), [$e]); } catch (JWTException $e) { return $this->respond('tymon.jwt.invalid', 'token_invalid', $e->getStatusCode(), [$e]); } if (! $user) { return $this->respond('tymon.jwt.user_not_found', 'user_not_found', 404); } $this->events->fire('tymon.jwt.valid', $user); return $next($request); } } |
Editamos BaseMiddleware.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<?php namespace Tymon\JWTAuth\Http\Middleware; use Tymon\JWTAuth\JWTAuth; use Tymon\JWTAuth\Exceptions\JWTException; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Routing\ResponseFactory; abstract class BaseMiddleware { protected $response; protected $events; protected $auth; public function __construct(JWTAuth $auth, ResponseFactory $response, Dispatcher $events ) { $this->response = $response; $this->events = $events; $this->auth = $auth; } public function respond($event, $error, $status, $payload = []) { $response = $this->events->fire($event, $payload, true); return $response ?: $this->response->json(['error' => $error], $status); } } |
Editamos Namshi.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public function decode($token) { try { // Let's never allow insecure tokens $jws = $this->jws->load($token, false); } catch (InvalidArgumentException $e) { throw new TokenInvalidException('Could not decode token: '.$e->getMessage(), 400, $e); } if (! $jws->verify($this->getVerificationKey(), $this->getAlgo())) { throw new TokenInvalidException('Token Signature could not be verified.'); } return (array) $jws->getPayload(); } |
Editamos JWTException.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<?php namespace Tymon\JWTAuth\Exceptions; class JWTException extends \Exception { protected $statusCode = 500; public function __construct($message = 'An error occurred', $statusCode = null) { parent::__construct($message); if (! is_null($statusCode)) { $this->setStatusCode($statusCode); } } public function setStatusCode($statusCode) { $this->statusCode = $statusCode; } public function getStatusCode() { return $this->statusCode; } } |
Y ya por último, el resto de Exceptions añadiendoles una propiedad:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// InvalidClaimException.php protected $statusCode = 400; // PayloadException.php protected $statusCode = 500; // TokenBlacklistedException.php protected $statusCode = 401; // TokenExpiredException.php protected $statusCode = 401; // TokenInvalidException.php protected $statusCode = 400; |
Si navegamos a la ruta «/api/users/create» el resultado es un token, justo lo que necesitábamos.
Si con el token anterior navegamos a la ruta «/api/users/show?token=token_obtenido_anteriormente» obtendremos los datos del usuario con ID 1 de la base de datos. ¡¡Misión cumplida!!
Por último haremos un hack para que podamos elegir el Guard a aplicar en un grupo de rutas, cuya fuente fue extraida de aqui :
1 |
php artisan make:provider SetGuardOnRouteProvider |
Editamos el archivo generado, tal que:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class SetGuardOnRouteProvider extends ServiceProvider { public function boot() { $this->app['router']->matched(function (\Illuminate\Routing\Events\RouteMatched $event) { $route = $event->route; if (!array_has($route->getAction(), 'guard')) { return; } $routeGuard = array_get($route->getAction(), 'guard'); $this->app['auth']->resolveUsersUsing(function ($guard = null) use ($routeGuard) { return $this->app['auth']->guard($routeGuard)->user(); }); $this->app['auth']->setDefaultDriver($routeGuard); }); } . . . . } |
Y registramos el provider:
1 2 3 4 |
'providers' => [ . . . App\Providers\SetGuardOnRouteProvider::class, ], |
Ahora, las rutas de ejemplo quedarían así:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Route::group(['guard' => 'api'], function () { Route::get('users/create/{id?}', function ($id = 1) { $token = \Auth::generateTokenById($id); echo $token; }); Route::get('users/show', function () { $user = \Auth::user(); return $user; })->middleware('jwt.auth'); Route::get('test', function () { return 'jwt protected route'; })->middleware('auth:api'); }); |
Ahora, accediendo a las rutas que hemos definido, podemos comprobar las respuestas que nos devuelve el middleware de autenticación:
- Token not provided : cuando no se aporta el token en la url o en las cabeceras de la petición.
- Token invalid: cuando el token enviado no es válido.
- Token expired: cuando el tiempo de vida del token haya expirado.
Esta solución es MUY intrusiva ya que hemos modificado archivos que se encuentran en la carpeta «vendor» de Laravel, cosa que no se debe hacer. El motivo es porque cuando actualicemos las dependencias de nuestro proyecto, nos habremos cargado todo este trabajo.
Esto es sólo para salir del paso. Más adelante veremos una solución más elegante y sobre todo, correcta de gestionar la autenticación.
Actualizado:
Tienes la versión no intrusiva en este post.