Instalar Laravel y configurar base de datos
Puedes seguir los pasos aquí: https://laravel.com/docs/6.x/installation.
En mi caso utilizaré Laravel 6.
laravel new spa-auth
cd spa-auth
Instalar Passport
Puedes seguir los pasos aquí: https://laravel.com/docs/6.x/passport
composer require laravel/passport
php artisan migrate
php artisan passport:install
Una vez realicemos la migración e instalamos, passport
creará inicialmente 2 clientes. Para autenticarnos a nuestra API utilizaremos el cliente password grant
. El cliente con id 2
es del tipo esperado. Lo puedes ver en el siguiente fragmento.
Encryption keys generated successfully.
Personal access client created successfully.
Client ID: 1
Client secret: tGMXW17y3FDx3NhmPDyH4pVQyhMNxAcBlY3E5nuV
Password grant client created successfully.
Client ID: 2
Client secret: Kt2zlijS7PrqNWGINhbAmEb5YmUpzGRfkgqhJMEZ
Agregaremos el trait Laravel\Passport\HasApiTokens
a nuestro modelo de usuario App\User
<?php
namespace App;
use Laravel\Passport\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use HasApiTokens, Notifiable;
}
El proximo paso será llamar al método Passport::routes
dentro del método boot
del AuthServiceProvider
. Recuerda importar Laravel\Passport\Passport;
<?php
namespace App\Providers;
use Laravel\Passport\Passport;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* @var array
*/
protected $policies = [
'App\Model' => 'App\Policies\ModelPolicy',
];
/**
* Register any authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Passport::routes();
}
}
Haremos una pequeña modificación en lo que acabamos de insertar para que passport
no genere todas las rutas, ya que vamos a utilizar solo el cliente password grant
. En vez de agregar Passport::routes();
agregaremos:
Passport::routes(function($router){
$router->forAccessTokens();
});
Para finalizar en el archivo config/auth.php
modificaremos el driver que utilizará la API
para autenticar a passport
.
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'passport',
'provider' => 'users',
],
],
¿NECESITAS AYUDA CON TU WEB?
Prueba de la API
Utilizaremos un cliente como Postman
o Insomnia
.
Postman: https://www.getpostman.com
Insomnia: https://insomnia.rest
En mi caso estoy utilizando Laravel Valet, por lo que mi URL sería http://spa-auth.test
.
Si quieres saber como configurar Laravel Valet puedes hacerlo aquí: https://laravel.com/docs/6.x/valet
Crear usuario de prueba
Antes que nada debemos agregar algún usuario para realizar las pruebas simulando usuarios reales en el futuro. Lo haremos utilizando tinker
.
php artisan tinker
factory('App\User')->create()
El resultado será algo randómico. El password predeterminado es password
.
=> App\User {#3052
name: "Nickclaus Koch",
email: "[email protected]",
email_verified_at: "2019-10-07 19:18:24",
updated_at: "2019-10-07 19:18:24",
created_at: "2019-10-07 19:18:24",
id: 1,
}
Obtener el access_token
Abriremos Postman
y crearemos un nuevo POST request a http://spa-auth.test/oauth/token
En la pestaña body
, agregaremos campos como form-data
:
grant_type: password
client_id: 2
client_secret: Kt2zlijS7PrqNWGINhbAmEb5YmUpzGRfkgqhJMEZ
username: [email protected]
password: password
grant_type
, será password, ya que es el cliente seleccionado anteriormente.client_id
, será 2 (si miramos más arriba el cliente con el id 2 es del tipoPassword grant
)client_secret
, ingresaremos el hash del cliente 2 de passport (esto lo puedes encontrar en la tablaoauth_clients
)username
, será el usuario con el que queremos autenticarnos a la APIpassword
, será la contraseña del usuario que estamos autenticando, en este caso el predeterminado al crear un cliente desde unfactory
:password
.
La respuesta si lo hemos configurado todo tal cual, será:
En lo que estamos interesados es, él access_token
ya que para seguir realizando requests a la API de manera autenticada, debemos pasar ese valor en cada request.
Realizar un request autenticado con el access_token
Haremos la prueba de realizar un request de manera autenticada con el access_token
obtenido. En el archivo routes/api.php
encontraremos que existe de manera predeterminada esta ruta:
Route::middleware('auth:api')->get('/user', function (Request $request) {
return $request->user();
});
Como indica el middleware aplicado, http://spa-auth.test/api/user
es una ruta protegida por el guard api
(que lo hemos configurado para utilizar Passport
).
Crearemos un nuevo request en Postman a esa ruta enviando las siguientes cabeceras:
Accept: application/json
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6IjAyMjgwZDEzY2E3ZmNhOGJkZDcxZWUxNDZmMjM5MTU1YzQ4MjRmMTA2ZjM4MmRhZWYwZjI5MGM1YTBiZTI4NDExYTY5ZjhhMDVkZDllNWI0In0.eyJhdWQiOiIyIiwianRpIjoiMDIyODBkMTNjYTdmY2E4YmRkNzFlZTE0NmYyMzkxNTVjNDgyNGYxMDZmMzgyZGFlZjBmMjkwYzVhMGJlMjg0MTFhNjlmOGEwNWRkOWU1YjQiLCJpYXQiOjE1NzA0NzY1MTMsIm5iZiI6MTU3MDQ3NjUxMywiZXhwIjoxNjAyMDk4OTEyLCJzdWIiOiIxIiwic2NvcGVzIjpbXX0.ohXg5nKHmZMEHADYttHeshNaTad4wKSUD1ByT4NPhfRSTR0b5ZUPj13aNv2e6qNv06dUQimOiYn3C0yUzudcXVbww1lN2jz_UeYX7N-s1lsrQL04JgzFM48i9VzyF6Ujjz3YWrVTIqh12FFschboiqZsEY7ZvAXxXvLKyOin8mI4XIh47sbA_ABTZSMpjuC9ZiwxRlq3byXhzw7Khj5rwHS1SHsg7dPP32w3M4gmA8WqnjsThIwAI84pyFpX1EOtedOSydRLrvTAy-OpivNlCmvOaWq-DcseVZgmwCldvaWIMe_8AQDmIHX0E94ZUuqrLeDZfjdbAayXwRzL8mzVs4hNBSfLPNHk3ot885NjrklBtEhaKliwpdkcoGEjobdKvjgd8AVWJgzgRe-z15dEbNAZQiPtcfc5CGG8BTbwSvF__hIgUrZNN_JUiWdtlx5RnpcDC4WfyecEhW4GplB0nbnmFV9QymFCeSukgNFdezeqCL1Lm9reLrVU8ovM6qyvOasn4bX0C4Z2wA2zbKP_y-7RDZN7X9lQwU1XWdvyyUAVArfm3uKlFa2bre3kcDpSVYdtXzjadybA1cROEzYwTQ7u-cJG90N1VNYsw5ni4PK7jqPYdeSYf2atvT_-PkcA4uA-g5PU7Sdz5ypONcKuOeR8v9n6SiE5q-IikwQFw54
Accept
, será el tipo de respuesta que esperamos recibir en el cliente, en este casoapplication/json
.Authorization
, aquí enviaremos nuestroaccess_token
agregándole antes la palabra Bearer seguida de un espacio.
La respuesta será la información del usuario:
{
"id": 1,
"name": "Nicklaus Koch",
"email": "[email protected]",
"email_verified_at": "2019-10-07 19:18:24",
"created_at": "2019-10-07 19:18:24",
"updated_at": "2019-10-07 19:18:24"
}
Si por alguna razón no enviamos la cabecera Authorization o modificamos el access_token, no podremos acceder a los datos de nuestra API de manera autenticada, y el error será:
{
"message": "Unauthenticated."
}
Capa de seguridad
Si volvemos a nuestro request inicial para obtener el access_token
vemos que estamos pasando datos que no deberían ser públicos, como grant_type
, client_id
y client_secret
.
Vamos a crear una ruta y lógica específica en Laravel que se encargue de resolver esta cuestión y de proveernos estos datos sin que el usuario en el cliente deba enviarlos. El cliente -en este caso nuestra SPA hecha en Vue- solo enviará su usuario y password.
Crear el controlador e instalar Guzzle:
php artisan make:controller AuthController
Instalaremos un cliente http para PHP, Guzzle. Puedes obtener más información aquí: http://docs.guzzlephp.org/en/stable/.
composer require guzzlehttp/guzzle
Volveremos a AuthController.php
y crearemos un método login
y otro método logout
, al que le pasaremos el request del cliente.
El método login
realiza un request http a nuestra API
enviando los datos de username
y password
que vendrán del cliente $request
y agregando los datos que queríamos ocultar, grant_type
, client_id
y client_secret
.
Estos datos sensibles los ocultaremos en variables en el archivo .env
.
Antes agregaremos al archivo config/services.php
un nuevo array. Este array cargará los datos de nuestras variables.
'passport' => [
'login_endpoint' => env('PASSPORT_LOGIN_ENDPOINT'),
'client_id' => env('PASSPORT_CLIENT_ID'),
'client_secret' => env('PASSPORT_CLIENT_SECRET'),
],
Ahora agregaremos las variables al final de nuestro archivo .env
.
Recuerda cambiar esto por tus credenciales.
PASSPORT_LOGIN_ENDPOINT=http://spa-auth.test/oauth/token
PASSPORT_CLIENT_ID=2
PASSPORT_CLIENT_SECRET=Kt2zlijS7PrqNWGINhbAmEb5YmUpzGRfkgqhJMEZ
Volviendo al método login
de AuthController.php
, hemos envuelto el request http en un bloque try-catch para poder obtener información sobre si la consulta se realizó con éxito o si ha fallado.
Si todo sale bien, la respuesta que obtendremos será el access_token
.
En el caso de que algo salga mal, enmascararemos los errores con mensajes personalizados, esto lo haremos para no mostrar información relevante sobre un fallo a usuarios/clientes sin autorización para acceder a nuestra API.
Evaluaremos tres posibilidades, el error 400 ,el error 401 o cualquier otro error. El error 400 Bad Request
será de carácter general por ejemplo por falta de un parámetro en el request, mientras que el error 401 Unauthorized
será de credenciales erróneas. Si ocurre algún otro error que no sea 400 o 401, también lanzaremos un mensaje de error más genérico.
public function login(Request $request)
{
$http = new \GuzzleHttp\Client;
try {
$response = $http->post(config('services.passport.login_endpoint'), [
'form_params' => [
'grant_type' => 'password',
'client_id' => config('services.passport.client_id'),
'client_secret' => config('services.passport.client_secret'),
'username' => $request->username,
'password' => $request->password,
]
]);
return $response->getBody();
} catch (\GuzzleHttp\Exception\BadResponseException $e) {
if ($e->getCode() === 400) {
return response()->json('Invalid Request. Please enter a username or a password.', $e->getCode());
} else if ($e->getCode() === 401) {
return response()->json('Your credentials are incorrect. Please try again', $e->getCode());
}
return response()->json('Something went wrong on the server.', $e->getCode());
}
}
public function logout()
{
auth()->user()->tokens->each(function ($token, $key) {
$token->delete();
});
return response()->json('Logged out successfully', 200);
}
El método logout
será encargado de borrar todos los access_token
relacionados con nuestro usuario y devolver una respuesta de confirmación.
Crear las rutas en routes/api.php
Route::post('/login', 'AuthController@login');
Route::middleware('auth:api')->post('/logout', 'AuthController@logout');
Prueba de nuestra ruta
Hasta ahora nuestra API tiene 3 endpoints:
http://spa-auth.test/api/login (pública)
http://spa-auth.test/api/logout (protegida con auth:api)
http://spa-auth.test/api/user (protegida con auth:api)
Si quieres ver que rutas tienes configuradas en Laravel usa este comando:
php artisan route:list
Utilizaremos nuevamente Postman para realizar la prueba, esta vez realizaremos un POST
request a http://spa-auth.test/api/login
solo enviando los campos username
y password
del usuario que queremos autenticar.
username: [email protected]
password: password
La respuesta esperada será el access_token
para incluir en cualquier endpoint que queramos visitar de manera autenticada.
Quien se encarga de los datos sensibles será el backend, nunca pasaran por el cliente.
Por ultimo nos queda probar nuestra ruta de logout
, y lo debemos hacer de manera autenticada. Por lo tanto realizaremos un POST
request a http://spa-auth.test/api/logout
enviando el access_token
obtenido.
Antes de realizar el request para desconectarnos sería bueno revisar la tabla de la base de datos oauth_access_tokens
, en la misma veremos varias entradas para nuestro usuario, son los tokens que ha creado cada vez que se conecta.
Si nuestro request para desconectarnos es correcto, se borraran de la base de datos todos los token solicitados para ese usuario.
Conclusión
Hemos creado una pequeña API con autenticación con Laravel y Passport.
Esto lo hemos hecho para poder conectarnos a esta API desde una SPA (Single Page Application) realizada con Vue y acceder a los datos de manera segura.
En la parte 2 veremos como realizar una SPA básica y realizar consultas autenticadas a nuestra API.
Este trabajo más la parte 2, la hemos incluido en un repo para que puedas ver y experimentar: https://github.com/danielcharrua/laravelvue-boilerplate
Si te he ayudado, comparte! 🤓
HAGAMOS DESPEGAR TU NEGOCIO ONLINE 🚀
Te ayudo a potenciar tu página web o tienda online con estos servicios.
Como proteger ante ataque de fuerza bruta y CSRF?
Hola Juan, para proteger ante un ataque de fuerza bruta deberías implementar alguna manera de limitar los intentos de autenticación fallidos y bloquear el acceso por un determinado tiempo. Puedes inspirarte en el paquete que usa Laravel
Illuminate\Foundation\Auth\ThrottlesLogins
.En el caso de CSRF, eso ya lo trae implementado Laravel, lo puedes ver en el archivo
laravel/resources/js/bootstrap.js
.Si tienes algo que aportar puedes agregarlo aquí, así ayudas a otros lectores.
Hola Daniel!
Gran guía, me ha ayudado mucho.
Solo me ha confundido un poco el título del punto 4.2. Imagino que quieres decir «Crear las rutas en routes/api.php», no? Parece una errata.
Hola Daniel, antes que nada comentarte que me alegra que te sea de ayuda el artículo. Gracias por tomarte el tiempo de comentar para solucionar este tipo de detalles. Un saludo!
Hola Buenas
Seguí todos los pasos, todo bien hasta que llegue al paso 4,1, donde intente hacer el login a mi url que sería localhost:8000/api/login con postman y al momento de presionar send, enviando los datos de username y password, se queda todo el rato cargando, no muestra absolutamente nada, ni siquiera un error, queda cargando eternamente, alguna idea de que podría estar ocurriendo?
Hola Hugo, ¿te salieron bien las pruebas del paso 3?
Si es así, quizá te faltó hacer el paso 4.2 donde se crean las rutas de la aplicación de Laravel.
Si las pruebas del paso 3 salieron bien, y las rutas las tengo agregadas en el api.php
Yo estuve un buen rato rengando y lo logre siguiendo esto https://github.com/guzzle/guzzle/issues/1857#issuecomment-312617494 no se si tenga que ver que esta todo configurado con homestead en windows 10.
Hola Hugo, me ha pasado exáctamente lo mismo que a ti. Estaba usando el servidor de angular (php artisan serve) y por si diera problemas con Guzzle, lo monté en xampp con un vhost. Así me funcionaba.
Hola Daniel, te refieres a que estas usando el servidor de PHP? (has escrito Angular y luego mencionas «php artisan serve» y este último lo que ejecuta es el servidor de PHP)
Hola Daniel! Me parece genial tu articulo. Después de un montón de tiempo buscando tutoriales sobre como hacer un login en condiciones con Laravel Passport, ¡eres mi salvador!
Solo un par de preguntas: Consideras que este login está listo para ser subido a producción, o necesita algún detalle más? (Como lo que comentaban de la protección de ataque por fuerza bruta). Después, ¿hay alguna manera de diferenciar donde han sido generados los tokens? De esa manera el usuario podría controlar donde ha iniciado sesión y cerrarla selectivamente.
Muchas gracias crack!
Perdón, quiero decir, el servidor de Laravel ejecutando php artisan serve. Como decía en el siguiente comentario, tanto tutorial ya me ha vuelto loco jaja.
Nada hombre, suele suceder, solo lo quise aclarar por si otros lectores lo ven 😉
Hola Daniel, muchas gracias por el post pero me surge una duda sobre otras maneras de hacerlo. He visto otros sitios donde directamente hacen una consulta con el usuario y contraseña(sin enviar client_id y client_secret) y ya les devuelve el Bearer token, sin tener que utilizar una llamada con Guzzle al mismo servidor para pasarle el client_id y el client_secret.
Utilizan un Auth::attempt($credentials) y creo que por defecto Laravel ya coge el client_id y el client_secret de la password key que se genera al utilizar el comando php artisan passport:install
No es más facil hacerlo así y no tener que utilizar Guzzle?
Muchas gracias y un saludo, te dejo un link al post que me refiero a ver que tal lo ves https://medium.com/@cvallejo/sistema-de-autenticaci%C3%B3n-api-rest-con-laravel-5-6-240be1f3fc7d
Hola Sergio, estoy investigando tu pregunta, en cuanto tenga una respuesta documentada, la publico aquí mismo.
Daniel Excelente post muchas gracias tengo el mismo problema que Daniel y que Hugo que al mandar el request al /oauth se queda cargando y nunca responde
Hola Angel, ¿qué servidor o configuración utilizas en local? ¿php artisan serve? ¿Laravel Valet? ¿Laravel Homestead?
Lo hice desde el localhost, creo que es porque la acción en el POST del login coincide con la otra acción que se hace para generar el access_token y como es en el mismo puerto se queda cargando. No se supongo que es eso , la solución fue la que comento mas arriba Florencia . De cualquier forma vuelvo a felicitarte por este gran trabajo.
Gracias Angel! Me alegro que vaya bien!
Hola, vi que muchos tuvieron un problema con las respuestas de guzzle, el detalle es que «php artisan serve» es un servidor que al parecer solo puede ejecutar un hilo, esto quiere decir que o ejecuta la petición que se le esta enviando que en este caso es «http://localhost:8000/api/login» o ejecuta la que le indicas por medio de guzzle, entonces para que esto no suceda pueden configurar un vhost en su archivo de host de su sistema correspondiente, y configurar un vhost en el servidor de apache indicando donde se encuentra la carpeta public de su proyecto.
Aquí hay uno para linux y mac (puede variar un poco):
https://styde.net/como-crear-virtual-hosts-con-apache-para-linux-y-mac/
Aquí hay un articulo para hacerlo en windows
https://medium.com/@jhordydelaguila/creando-y-configurando-virtual-host-con-apache-para-xampp-windows-f90c2b0527ac
Happy coding & Enjoy! ~ 𝕳𝖆𝖓𝕲𝖔𝖚𝖍
Gracias por este POST ha servido de nuevas experiencias, en mi caso me he quedado,
4.3- Prueba de nuestra ruta
respuestas de guzzle configure, un vhost en el servidor pero no encontre respuesta cuando accedo a la Ruta, me trae el siguiente Error.
«Something went wrong on the server.»
Postman me trae: 405Method Not Allowed.
Listo Logrado el comentario de Florencia P. me ayudo mucho.
Hola Alfredo ¡me alegro que lo hayas conseguido! Un saludo.
Hola Buenas,
muchas gracias por tu post,
realmente era lo que necesitava i nos lo has puesto en bandeja :D.
Queria preguntar-te sobre la pregunta numero 8 de Sergio. Has podido investigar alguna manera «correcta» de conseguir el token sin la necessidad de instalar una libreria de php http para connectar con tu aplicacion.
Muchas Gracias !
Hola Daniel Excelente post muchas gracias, solo tengo una duda, que tan seguro es una ruta publica como tú ejemplo lo indica http://spa-auth.test/api/login (pública), si realizan una petición a la ruta ¿Pueden obtener el client_secret, client_id? por el config(‘services.passport.client_id’) y config(‘services.passport.client_secret’), que se guarda en el archivo config/services.php por medio de las variables PASSPORT_CLIENT_SECRET y PASSPORT_CLIENT_ID que están guardados en el .env si estamos utilizando un administrador de servidor como ejemplo Forge Laravel
Gracias por el POST
Buenas Daniel! Gracias por el post, me ayudó mucho.
Tengo un problema, quise probar con postman los codigos de error y resulta que si pongo el mail o la password, me lo toma como una bad request y me devuelve el codigo 400 y no el 401, no tengo muy claro cómo es que se hace (o en qué momento) la comprobacion de las credenciales de la request contra las de la db. Tenes alguna idea de por qué podria ser?
Saludos!
Este Article fue mencionado en charrua.es