Escenario
Estoy en el proceso de separar la parte API de una web de la del frontend. Para ello he creado un nuevo proyecto que solo atenderá peticiones del API. Ambos proyectos comparten la base de datos; en el proyecto API (PA) instalé Laravel Passport para gestionar la autenticación. El proyecto web (PW) necesita por tanto autenticarse en al API para mostrar los datos en sus páginas.Problema
PA solo admite autenticación por OAuth, gestionada por Passport; como es obvio no vamos a pedir al usuario que se identifique en dos sitios. Necesito un sistema que permita a PW invocar PA sin necesidad de enviar el usuario y contraseña.Solución
# Configuración del servidor del API
Para empezar damos de alta un nuevo cliente de Passport con el siguiente comando:
1 |
php artisan passport:client --client |
1 2 3 |
New client created successfully. Client ID: 11 Client secret: kjLGLFlMyFeW5ScN5RYkqDn86XV6ITmrC7YD1k4z |
Vamos a necesitar añadir un nuevo middleware que gestione las peticiones con un token de cliente, y la ruta que nos devolverá las credenciales del usuario:
EnHttp/Kernel.php en la lista $routeMiddleware:
1 |
'client_credentials' => \Laravel\Passport\Http\Middleware\CheckClientCredentials::class, |
1 2 3 |
Route::middleware("client_credentials")->group(function () { Route::resource("user", "UserController"); }); |
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 App\Http\Controllers; use App\Http\Resources\User as UserResource; use App\Services\UserService; /** * Description of UserController * * @author marcos */ class UserController extends Controller { private $userService; public function __construct(UserService $userService) { $this->userService = $userService; } public function show(int $userId) { $user = $this->userService->findById($userId); if($user !== null) { return new UserResource($user); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class User extends JsonResource { /** * Transform the resource into an array. * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request) { return [ 'id' => $this->id, 'name' => $this->first_name, 'username' => $this->username, 'email' => $this->email ]; } } |
# Configuración del servidor web
He instalado Socialite, una biblioteca de Laravel que usa OAuth para autenticarse con proveedores externos.
1 |
composer require laravel/socialite |
1 2 3 4 |
'passport' => [ 'client_id' => env('PASSPORT_CLIENT_ID'), 'client_secret' => env('PASSPORT_CLIENT_SECRET'), ], |
1 2 |
PASSPORT_CLIENT_ID=11 PASSPORT_CLIENT_SECRET=kjLGLFlMyFeW5ScN5RYkqDn86XV6ITmrC7YD1k4z |
1 2 3 4 5 6 7 |
'providers' => [ /* * Laravel Framework Service Providers... */ ..... Laravel\Socialite\SocialiteServiceProvider::class, |
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
<?php namespace Predatum\Providers; use Laravel\Socialite\Two\AbstractProvider; use Laravel\Socialite\Two\ProviderInterface; use Laravel\Socialite\Two\User; use Illuminate\Support\Arr; use Cartalyst\Sentinel\Laravel\Facades\Sentinel; class PassportProvider extends AbstractProvider implements ProviderInterface { private const API_HOST = "https://api.acme.com"; // Url de la aplicación del API /** * {@inheritdoc} */ protected function getAuthUrl($state) { return $this->buildAuthUrlFromBase(self::API_HOST . '/oauth/authorize', $state); } /** * {@inheritdoc} */ protected function getTokenUrl() { return self::API_HOST . '/oauth/token'; } /** * {@inheritdoc} */ protected function getUserByToken($token) { $response = $this->getHttpClient()->get( self::API_HOST . '/api/user/' . Sentinel::getUser()->id, $this->getRequestOptions($token) ); return json_decode($response->getBody(), true); } /** * {@inheritdoc} */ protected function mapUserToObject(array $user) { return (new User)->setRaw($user)->map([ 'id' => $user['data']['id'], 'name' => Arr::get($user, 'name'), 'email' => Arr::get($user, 'email'), ]); } /** * Get the default options for an HTTP request. * * @param string $token * @return array */ protected function getRequestOptions($token) { return [ 'headers' => [ 'Accept' => 'application/json', 'Authorization' => 'Bearer ' . $token, ], ]; } /** * Get the POST fields for the token request. * * @param string $code * @return array */ protected function getTokenFields($code) { $fields = [ 'grant_type' => 'client_credentials', 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'scope' => '*' ]; if ($this->usesPKCE()) { $fields['code_verifier'] = $this->request->session()->pull('code_verifier'); } return $fields; } } |
Nota: uso Sentinel para la autenticación/autorización en la aplicación, en caso de usar la herramienta incluida por defecto en Laravel habría que remplazar Sentinel::getUser()->id por auth()->user()->id.Extendemos Socialite para que incluya el driver passport
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 |
<?php namespace Acme\Providers; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Facades\URL; use Laravel\Socialite\Facades\Socialite; class AppServiceProvider extends ServiceProvider { /** * Bootstrap any application services. * * @return void */ public function boot() { Socialite::extend('passport', function ($app) { $config = $app['config']['services.passport']; return new PassportProvider( $app['request'], $config['client_id'], $config['client_secret'], URL::to("http:// locahost") // este argumento lo necesita Laravel\Socialite\Two\AbstractProvider, pero en realidad no se usa en nuestra implementación ); }); } |
1 2 |
/* SOCIALITE */ Route::get('user/api-oath-callback', 'Frontend\SecurityController@apiProviderCallback'); |
1 2 3 4 5 6 7 8 9 10 11 12 |
/** * Obtain the user information from the API. * * @return \Illuminate\Http\Response */ public function apiProviderCallback() { $user = Socialite::driver('passport')->stateless()->user(); return response()->json($user); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
{ "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIxMCIsImp0aSI6ImU2OGM3ZDAwODgwZTI4YTE2YjlkYzI5MGQ3M2ZkZTAyMTc1OTVmZTU3ZjdmYmFhMzRjOTI5ZTQ1ZmQ1MWZjNzUxNjVhYjE5NmNmYTRmNzM3IiwiaWF0IjoxNjYxMjg1MjMwLCJuYmYiOjE2NjEyODUyMzAsImV4cCI6MTY5MjgyMTIzMCwic3ViIjoiIiwic2NvcGVzIjpbIioiXX0.3_XT6LjDRhD8Gy0iogScpyz1Y7Km3pK-WOy-bIymMgaeXjtStlfTh0_YFOOCOTDEORK4TgOqB3Cs_u3QK-sbNJ-zqt5IL6v_4eLkRXZZnFEOFLL7TrMSV44kHm0kqqAU4_nJ02tE2TCg4PREAASv_YSDBAUgKfVps94bvmFEIy0yvLKVL2o-c_fJ3NOLLuhifkuH7qP44gAi2G2dGejRW3oZHsAkHvoaiJnjVKH0j00OV-1Unf2daJIrwkPfJNpFeA9b_6z-vqdIA5BJ484Bh03SxY1vjBpNp7ScmK3Gg9AN6xi8XyFCCgnY844dNk51Dvq36vaXcSwegY9oTzyEsdg_oTNBs9ol0ez16Jjfeq6oSVp3MlEZXR9k9RL5aDIC-wORn_0gJAbkq1gArXSvPamZ061LGVenWFyJaXwAcQulaKYFkr-rol__-B6BB0BGybgF8h1IAXB2z6GZ2xiQDXOhFTUKu6QOzMZ1gI16JsHBiTIHtGkn1Mo6wQrAZrfxXiX1rtgT_Nr5MgWZ2cEQ7kf-U6AsauYZdO8aUrO7KZ3jkaYXcVoOiCxd1URkILCr6_Qw_nzhm2IH88OJ_OTNh0C9adCIiJtruPH_b8MJSUd56k-WW1CTjtaMn8_A4TYxwcpW5jmRWt4K_VJbNmlQ3eBkQ2BadSyo2fdLxNi82Dk", "refreshToken": null, "expiresIn": 31536000, "approvedScopes": [""], "id": 1, "nickname": null, "name": null, "email": null, "avatar": null, "user": { "data": { "id": 1, "name": "Marcos", "username": "montana_max", } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<script> document.addEventListener("DOMContentLoaded", function () { fetch('https://web.acme.com/user/api-oath-callback') .then((response) => response.json()) .then((data) => { const token = data.token; fetch('https://api.acme.com/comment', { headers: { Authorization: `Bearer ${token}`, Accept: "application/json", } }) .then((response) => response.json()) .then((data) => console.log('comments', data)) }); }); </script> |
Nota: en caso de topar con errores relacionados con ‘Access-Control-Allow-Origin’, prueba a instalar el paquete laravel-cors.