Implementar JWT en Laravel 8

Nicolás F. Ormeño Rojas
5 min readFeb 20, 2021

--

Photo by Sangga Rima Roman Selia on Unsplash

Creación del proyecto

Para crear el proyecto, en esta ocasión se realizará con el tutorial de Primeros pasos con Laravel Sail.

Una vez con el contenedor levantado, ingresamos a él

./vendor/bin/sail shell

Estando dentro del contenedor, se debe instalar la dependencia requerida

composer require tymon/jwt-auth:dev-develop --prefer-source

“Al día en que se creó este tutorial, es necesario instalar la versión dev-develop de esta dependencia, ya que por un tema de versiones, no es posible instalar jwt-auth con Laravel 8, dependiendo de cuando leas este tutorial, puede ser que ya no sea necesario”

Publicar el archivo de configuración

php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

Generar la llave secreta

php artisan jwt:secret

Con todo lo anterior, se finaliza la configuración de la dependencia de JWT. Ahora toca comenzar a modificar el proyecto Laravel, el cual comienza modificando el modelo de usuario, implementando el contrato de Tymon\JWTAuth\Contracts\JWTSubject, lo cual significa que debemos implementar 2 métodos (getJWTIdentifier() y getJWTCustomClaims()). Quedando como el siguiente módulo

<?phpnamespace App\Models;use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Tymon\JWTAuth\Contracts\JWTSubject;
class User extends Authenticatable implements JWTSubject
{
use HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
/**
* 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 [];
}

}

Luego, se debe cambiar la configuración de los guards en config/auth.php, para que el API en lugar de usar token, utilice JWT.

...
'defaults' => [
'guard' => 'api',
'passwords' => 'users',
],
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'jwt',
'provider' => 'users',
'hash' => false,
],
],
...

Rutas

Generar rutas básicas en routes/api.php

Route::group([
'middleware' => 'api',
'prefix' => 'auth'
], function ($router) {
Route::post('login', 'AuthController@login');
Route::post('logout', 'AuthController@logout');
Route::post('refresh', 'AuthController@refresh');
Route::get('me', 'AuthController@me');
});

AuthController

Crear AuthController

php artisan make:controller AuthController

El cual debe quedar con algo como lo siguiente

<?phpnamespace App\Http\Controllers;use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use JWTAuth;
use Symfony\Component\HttpFoundation\Response;
class AuthController extends Controller
{
/**
* Create a new AuthController instance.
*
* @return void
*/
public function __construct()
{
$this->middleware(
'auth:api', ['except' => ['login']]
);
}
/**
* Get a JWT via given credentials.
*
* @param Request $request
* @return JsonResponse
*/
public function login(Request $request): JsonResponse
{
$validator = Validator::make($request->all(), [
'email' => ['required', 'email'],
'password' => ['required', 'string'],
]);
if ($validator->fails()) {
return response()->json($validator->errors(), Response::HTTP_BAD_REQUEST);
}
$credentials = $request->only(['email', 'password']); if (!$token = auth()->attempt($credentials)) {
return response()->json(['error' => 'Unauthorized'], Response::HTTP_UNAUTHORIZED );
}
return $this->respondWithToken($token);
}
/**
* Get the authenticated User.
*
* @return JsonResponse
*/
public function me(): JsonResponse
{
return response()->json(auth()->user());
}
/**
* Log the user out (Invalidate the token).
*
* @return JsonResponse
*/
public function logout(): JsonResponse
{
auth()->logout();
return response()->json(['message' => 'Successfully logged out']);
}
/**
* Refresh a token.
*
* @return JsonResponse
*/
public function refresh(): JsonResponse
{
return $this->respondWithToken(auth()->refresh());
}
/**
* Get the token array structure.
*
* @param string $token
*
* @return JsonResponse
*/
protected function respondWithToken(string $token): JsonResponse
{
return response()->json([
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => auth()->factory()->getTTL() * 60,
]);
}
}

Preparar la base de datos

Correr las migraciones default del proyecto

php artisan migrate

Crear Factory y Seeder para generar registros falsos en nuestra tabla de usuarios.

Para el Factory de los usuarios, no debemos generar ninguna acción, ya que este ya está creado, si queremos revisarlo, podemos revisar database/factories/UserFactory.php.

En cuanto al seeder, lo único que se debe hacer es ir a database/seeders/DatabaseSeeder.php y quitar los comentarios de la línea que llama al factory del usuario, quedando así

public function run()
{
\App\Models\User::factory(10)->create();
}

Correr el seeder

php artisan db:seed

Pruebas

Consideración: Todo llamado debe tener el header Accept: application/json, o de lo contrario, el API no responderá correctamente

Lo primero es probar el endpoint /api/auth/me para validar si tenemos o no autorización de su uso, y si todo es correcto, nos debería retornar 401 Unauthorized

Figura 1 No autorizado

Para probar, se debe iniciar sesión, para esto, tomamos alguno de los usuarios de prueba creado, y realizamos la petición

Figura 2 Login exitoso

De la respuesta, es importante aclarar:

  • access_token: El token con el que podemos realizar las llamadas
  • token_type: Indica que debemos llamarlo a través de bearer
  • expires_in: Indica en cuánto tiempo expirará nuestro token (en segundos)

Con el access_token obtenido, volvemos a llamar al endpoint “me”, agregándolo como bearer token

Figura 3 Solicitud de endpoint

Para actualizar el token, llamamos al endpoint /api/auth/refresh, el cual entregará un nuevo token

Figura 4 Nuevo token

Ahora, para estar seguros de que funcionó correctamente, debemos hacer 2 llamados, el primero, con el token que ya veníamos usando, en donde nos debe retornar un error de autenticación, y otro llamado con el token obtenido desde el refresh, con el cual si podemos realizar el llamado

Figura 5 Error por token actualizado
Figura 6 Éxito al utilizar el token actualizado

Finalmente, una prueba que es necesario realizar, es el logout, una vez hecho esto, nuestro token que estaba funcionando debería dejar de autenticar.

Figura 7 Logout exitoso

Ahora, al probar el token, ya no podemos realizar ningún llamado a los endpoints

Figura 8 Autenticación fallida luego de logout

--

--