Implementar JWT en Laravel 8
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
Para probar, se debe iniciar sesión, para esto, tomamos alguno de los usuarios de prueba creado, y realizamos la petición
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
Para actualizar el token, llamamos al endpoint /api/auth/refresh, el cual entregará un 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
Finalmente, una prueba que es necesario realizar, es el logout, una vez hecho esto, nuestro token que estaba funcionando debería dejar de autenticar.
Ahora, al probar el token, ya no podemos realizar ningún llamado a los endpoints