En este tutorial te voy a mostrar cómo crear una API REST en Laravel usando Laravel Passport. Gracias a esto podrás exponer tu API y consumirla con JavaScript (para una app web del lado del cliente) o con una app móvil y cualquier lenguaje que hable HTTP con JSON.
Lo interesante es que además de configurar una API JSON en Laravel, vas a aprender cómo agregar autenticación; de este modo los usuarios podrán iniciar sesión, recibir un token y luego usarlo para realizar las otras operaciones.
Verás que es muy sencillo. Al final del post colocaré unas pruebas de consumo de la API con Postman.
Requisitos
Necesitamos obviamente una app de Laravel existente y funcional, además de composer. No importa si está recién creada o ya tiene un avance.
Si quieres que tu aplicación tenga autenticación, debes usar la autenticación que Laravel trae por defecto. En caso de que no quieras autenticación o no hayas seguido los métodos de Laravel, tendrás que hacer los ajustes pertinentes en cada lugar.
Instalar passport
Comenzamos instalando el paquete laravel/passport pues no viene incluido por defecto en el framework. Ejecuta:
composer require laravel/passport
Más tarde migra tu base de datos pues se necesita hacer unas cosas de oauth:
php artisan migrate
Después, generar claves para passport:
php artisan passport:install
La salida debe ser algo así:
Encryption keys generated successfully.
Personal access client created successfully.
Client ID: 1
Client secret: blablablablabla
Password grant client created successfully.
Client ID: 2
Client secret: blablablabla
Modificar el modelo de User
Dentro de User (el modelo) debemos agregar el trait de HasApiTokens. Para ello miramos el encabezado del archivo y vemos que dice algo como use Notifiable
, agregamos HasApiTokens
.
Además, también agregamos el use Laravel\Passport\HasApiTokens;
y al final el archivo User.php debe verse parecido a lo siguiente:
<?php
namespace App;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\HasApiTokens;
class User extends Authenticatable
{
use Notifiable, HasApiTokens;
/**
* 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',
];
}
Modificando AuthServiceProvider para las rutas
Ahora en AuthServiceProvider.php (dentro de app/Providers)en el encabezado agregamos un use Laravel\Passport\Passport;
y dentro del método boot
invocamos a Passport::routes();
El archivo debe ser parecido a lo siguiente:
<?php
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Laravel\Passport\Passport;
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();
}
}
Passport para guard de autenticación
En config/auth.php
dentro de guards
> api
en driver
colocamos passport
. Mi archivo quedó así:
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option controls the default authentication "guard" and password
| reset options for your application. You may change these defaults
| as required, but they're a perfect start for most applications.
|
*/
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| here which uses session storage and the Eloquent user provider.
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| Supported: "session", "token"
|
*/
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'passport',
'provider' => 'users',
'hash' => false,
],
],
/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| If you have multiple user tables or models you may configure multiple
| sources which represent each model / table. These sources may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
/*
|--------------------------------------------------------------------------
| Resetting Passwords
|--------------------------------------------------------------------------
|
| You may specify multiple password reset configurations if you have more
| than one user table or model in the application and you want to have
| separate password reset settings based on the specific user types.
|
| The expire time is the number of minutes that the reset token should be
| considered valid. This security feature keeps tokens short-lived so
| they have less time to be guessed. You may change this as needed.
|
*/
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_resets',
'expire' => 60,
'throttle' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Password Confirmation Timeout
|--------------------------------------------------------------------------
|
| Here you may define the amount of seconds before a password confirmation
| times out and the user is prompted to re-enter their password via the
| confirmation screen. By default, the timeout lasts for three hours.
|
*/
'password_timeout' => 10800,
];
Presta atención a la línea 45 pues es la única que se cambia.
Controlador Auth
Creamos un controlador, en mi caso lo llamaré AuthController
, y como vamos a usar el namespace de Auth dentro de Facades al inicio agregamos un use Illuminate\Support\Facades\Auth;
En este archivo es en donde manejamos toda la autenticación de la API, iniciamos sesión devolviendo un token, cerramos sesión, etcétera.
Debo admitir que este archivo lo he copiado (pero no por ello no lo he analizado) y le he hecho ligeras modificaciones. Agradecimientos a quien lo haya creado originalmente
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
class AuthController extends Controller
{
public function signup(Request $request)
{
$request->validate([
'name' => 'required|string',
'email' => 'required|string|email|unique:users',
'password' => 'required|string|confirmed',
]);
$user = new User([
'name' => $request->name,
'email' => $request->email,
'password' => bcrypt($request->password),
]);
$user->save();
return response()->json([
'message' => 'Successfully created user!'], 201);
}
public function login(Request $request)
{
$request->validate([
'email' => 'required|string|email',
'password' => 'required|string',
'remember_me' => 'boolean',
]);
$credentials = request(['email', 'password']);
if (!Auth::attempt($credentials)) {
return response()->json([
'message' => 'Unauthorized'], 401);
}
$user = $request->user();
$tokenResult = $user->createToken('Personal Access Token');
$token = $tokenResult->token;
if ($request->remember_me) {
$token->expires_at = Carbon::now()->addWeeks(1);
}
$token->save();
return response()->json([
'access_token' => $tokenResult->accessToken,
'token_type' => 'Bearer',
'expires_at' => Carbon::parse(
$tokenResult->token->expires_at)
->toDateTimeString(),
]);
}
public function logout(Request $request)
{
$request->user()->token()->revoke();
return response()->json(['message' =>
'Successfully logged out']);
}
public function user(Request $request)
{
return response()->json($request->user());
}
}
Básicamente estamos creando un controlador de Laravel en donde registramos, logueamos y mostramos al usuario logueado, pero usando JSON en lugar de plantillas de blade o similares; ya que la API de Laravel va a usar JSON para la transmisión de datos.
Para esto usamos todas las medidas que se siguen al registrar usuarios: se hacen las validaciones pertinentes, la contraseña es hasheada, etcétera.
Resumen hasta el momento
Hasta ahora mis comandos con su salida han sido:
root@parzibyte:/# composer require laravel/passport
Using version ^8.4 for laravel/passport
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 13 installs, 0 updates, 0 removals
- Installing symfony/psr-http-message-bridge (v2.0.0): Downloading (100%)
- Installing phpseclib/phpseclib (2.0.25): Downloading (100%)
- Installing psr/http-factory (1.0.1): Downloading (100%)
- Installing php-http/message-factory (v1.0.2): Downloading (100%)
- Installing nyholm/psr7 (1.2.1): Downloading (100%)
- Installing defuse/php-encryption (v2.2.1): Downloading (100%)
- Installing lcobucci/jwt (3.3.1): Downloading (100%)
- Installing league/event (2.2.0): Downloading (100%)
- Installing league/oauth2-server (8.0.0): Downloading (100%)
- Installing laminas/laminas-zendframework-bridge (1.0.1): Downloading (100%)
- Installing laminas/laminas-diactoros (2.2.2): Downloading (100%)
- Installing firebase/php-jwt (v5.1.0): Downloading (100%)
- Installing laravel/passport (v8.4.1): Downloading (100%)
phpseclib/phpseclib suggests installing ext-libsodium (SSH2/SFTP can make use of some algorithms provided by the libsodium-php ext
ension.)
phpseclib/phpseclib suggests installing ext-mcrypt (Install the Mcrypt extension in order to speed up a few other cryptographic op
erations.)
phpseclib/phpseclib suggests installing ext-gmp (Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary
precision integer arithmetic operations.)
Writing lock file
Generating optimized autoload files
> Illuminate\Foundation\ComposerScripts::postAutoloadDump
> @php artisan package:discover --ansi
Discovered Package: facade/ignition
Discovered Package: fideloper/proxy
Discovered Package: fruitcake/laravel-cors
Discovered Package: laravel/passport
Discovered Package: laravel/tinker
Discovered Package: laravel/ui
Discovered Package: nesbot/carbon
Discovered Package: nunomaduro/collision
Package manifest generated successfully.
root@parzibyte:/# cls
root@parzibyte:/# php artisan migrate
Migrating: 2016_06_01_000001_create_oauth_auth_codes_table
Migrated: 2016_06_01_000001_create_oauth_auth_codes_table (1.75 seconds)
Migrating: 2016_06_01_000002_create_oauth_access_tokens_table
Migrated: 2016_06_01_000002_create_oauth_access_tokens_table (1.34 seconds)
Migrating: 2016_06_01_000003_create_oauth_refresh_tokens_table
Migrated: 2016_06_01_000003_create_oauth_refresh_tokens_table (3.62 seconds)
Migrating: 2016_06_01_000004_create_oauth_clients_table
Migrated: 2016_06_01_000004_create_oauth_clients_table (1.26 seconds)
Migrating: 2016_06_01_000005_create_oauth_personal_access_clients_table
Migrated: 2016_06_01_000005_create_oauth_personal_access_clients_table (0.9 seconds)
root@parzibyte:/# php artisan passport:install
Encryption keys generated successfully.
Personal access client created successfully.
Client ID: 1
Client secret: blabla
Password grant client created successfully.
Client ID: 2
Client secret: blabla
root@parzibyte:/# php artisan make:controller AuthController
Controller created successfully.
Configurando y protegiendo rutas
Ahora vamos a configurar las rutas dentro de routes/api.php; vamos a prefijarlas con auth, así no interfieren con otras e indicamos claramente que pertenecen a la API.
<?php
// ...
Route::group(['prefix' => 'auth'], function () {
Route::post('login', 'AuthController@login');
Route::post('signup', 'AuthController@signup');
// Las siguientes rutas además del prefijo requieren que el usuario tenga un token válido
Route::group(['middleware' => 'auth:api'], function() {
Route::get('logout', 'AuthController@logout');
Route::get('user', 'AuthController@user');
// Aquí agrega tus rutas de la API. En mi caso (EN MI CASO, EL TUYO PUEDE VARIAR) he agregado una de productos
Route::get("productos", function () {
return response()->json(\App\Producto::all());
});
});
});
Dentro de la configuración de esas rutas también agregamos un grupo de rutas que tendrán el middleware auth:api
; de este modo hacemos que solo estén disponibles si quien consume la API tiene un token válido.
Como lo indico en los comentarios, es justo dentro de ese grupo de rutas que debes agregar tus otros métodos; es decir, armar toda tu API.
Probando API de Laravel
Ahora vamos a probar la API REST con JSON que acabamos de crear con Laravel. Como lo dije, puedes consumirla desde cualquier lugar que hable JSON y HTTP (la web del lado del cliente con JS, apps móviles e incluso lenguajes de servidor).
Para probar de manera simple voy a usar Postman. Te repito que puedes agregar más rutas de acuerdo a tus necesidades; no hay límite en esto.
La ruta de la API
Normalmente, si usas Apache con Laravel, visitas la ruta public
. Ahora sería la public/api/auth/
y en caso de que no uses Apache entonces sería la ruta raíz agregando /api/auth/
.
Obviamente tienes que agregar la ruta a la ruta de la API. Por ejemplo, para hacer login sería /api/auth/login
.
Nota: recuerda que los datos deben ser enviados codificados como JSON. También debes especificar el encabezado Content-Type
en application/json
y opcionalmente el X-Requested-With
en XMLHttpRequest
.
Iniciar sesión y obtener token de API: login
Lo primero que necesitamos es obtener un token al iniciar sesión. Este token será de tipo bearer y será devuelto en caso de que el usuario y la contraseña sean correctos.
Debemos hacer una petición post a /login
con dos datos JSON: email y password. Algo así:
{
"email": "parzibyte@gmail.com",
"password": "1234567s8"
}
Si son incorrectos, la respuesta será:
{
"message": "Unauthorized"
}
En caso de que los datos sean correctos, la respuesta será:
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIxIiwianRpIjoiYTQwNWExOWEwNDFkMTI0ZGNjNzk1NDE3MjZjZmMzMDRlNDMxMzQ2NjA3ZWQyZWYyNzhhOTZlNjU4NzE3OTUxYzQ2NDQ1MjcyYWM0M2I3ZGUiLCJpYXQiOjE1ODQ4NjA5MDEsIm5iZiI6MTU4NDg2MDkwMSwiZXhwIjoxNjE2Mzk2OTAxLCJzdWIiOiIxMCIsInNjb3BlcyI6W119.d8UV79s5Utf8nrVa-oiySCNkXz_5KoyoD69V2UGpGgbqjbEmrk-_8MzaDzOEexhhCKoR4hhZlH7BlqxeqwaQetRBvnPbzp0PDdOQhenHcEy_BYwJvz_SAMyR4f-8aG-M9o5K5-Eh1yzjVai5Dx1qjiIHvPg_wAJwRW0tWRQNXysUnEaxqLTBrPX5KDWdWYpmQjvOYgitY2nTVpmxZ-Jvcx5kxVxaymkh3GI1em2qDdZcdKr88fR3eiMNjCMADecIhHMnLW_5YXZFSm0_CEHL7UgQMDxnes2Mj6idr4AkPmvyaS_WyAUUh18iQnv_UPH1ZmeVCTJxAXXqEyZ1rE3CmuJcKsscq_eYkg_NViyWvqrKio7OKQnPdcxhrDRyMgxKvvlXajvQRgNwPDcks6FiGfewKUidfTGtsXtdcqDBqV_KRzPb50qPnpQCT4ciEec74tLkwd66XIZGCrrtQPucEDZOr_QfLzJkTUlsDyscjHXCbcq1Kesyq7Ql0JuCxr36maOh3FgevbGrCXvU15SBRxwOYHPB5WIAoyrwi2gPa5pA7ZxeQrxCnEwSz_pxXVC-blablablablablavtyDzm079BeeyL1qcKOPvcwAjF5zz46o5LdT5VMB-Z694o9TS3Ja7JHwil63Q9ULSmXhJyVvyYblablablabla",
"token_type": "Bearer",
"expires_at": "2021-03-22 01:08:21"
}
Ahora es tu responsabilidad guardar el token, la fecha de expiración, etcétera. La petición dentro de Postman se ve así:
Obtener usuario logueado
Aunque probablemente estés pensando “y para qué quiero conocer el usuario logueado” este ejemplo muestra cómo consumir determinada ruta que necesite que el usuario esté logueado.
La petición debe ser de tipo get a la ruta /user
. Debes incluir el token en los encabezados. El nombre del encabezado es Authorization
y el valor del encabezado debe ser Bearer [aquí el token recibido al iniciar sesión]
Te explico bien lo de Bearer pues a mí me costó comprenderlo: debes concatenar la cadena "Bearer"
, un espacio y el token que habías recibido. Suponiendo que el token es 123 entonces el valor del encabezado sería "Bearer 123"
.
En caso de que el token sea inválido, la respuesta será:
{
"message": "Unauthenticated."
}
Y en caso de que sea válido, la respuesta será algo como:
{
"id": 10,
"name": "Luis Cabrera Benito",
"email": "parzibyte@gmail.com",
"email_verified_at": null,
"created_at": "2020-03-10T07:42:56.000000Z",
"updated_at": "2020-03-10T07:42:56.000000Z"
}
Adjunto una captura de postman por si te quedan dudas:
En este caso, por obvias razones, devuelve el usuario. Pero bien podría devolver otros datos según lo programes; al final será una API que puede interactuar con todo el ecosistema de Laravel.
Al clonar el repositorio
Si llevas un control de versiones con, por ejemplo, git; cada que clones tu repositorio recuerda generar las claves con:
php artisan passport:install
Conclusión
Recuerda que este es un ejemplo y una guía; no es que tengas que seguirla paso a paso, puedes hacerle tus modificaciones (como yo lo hice), agregarle más o menos métodos, configurar otras rutas, etcétera.
Con esto puedes extender tu app de Laravel más allá del lenguaje del servidor.
Si te sirve de algo puedes ver el commit exacto en donde agregué la API con autenticación en Laravel a mi sistema de ventas. También te invito a explorar más sobre Laravel en mi blog.
Buenísimo todo lo que haces.
Gracias por sus comentarios. Saludos!
¿Cómo lo implementarías en el frontend con laravel Http client?