# Laravel Passport JWT/Password Grant Authentication This is the backend implementation of the following blogpost from Hasura - [The Ultimate Guide to handling JWTs on frontend clients](https://blog.hasura.io/best-practices-of-using-jwt-with-graphql/). - **Laravel Version:** v6.10.1 - **Passport Version:** v8.2.0 --- **Assumed knowledge:** - [Laravel](https://laravel.com/docs/6.x) - [Passport](https://laravel.com/docs/6.x/passport) - Basic JWT authentication knowledge (read the [blog post](https://blog.hasura.io/best-practices-of-using-jwt-with-graphql/) above) - Basic [Http middleware](https://laravel.com/docs/6.x/middleware) knowledge ## Table of Content [ToC] ## Basic MVP ### Register api routes ```php // routes/api.php Route::post('/login', 'Api\LoginController'); Route::post('/logout', 'Api\LogoutController'); Route::post('/keep-login', 'Api\KeepLoginController'); ``` ### Add cookie middlewares to api routes ```php // app/Http/Kernel.php class Kernel extends HttpKernel { // ... protected $middlewareGroups = [ // ... 'api' => [ 'throttle:60,1', // Add this \App\Http\Middleware\EncryptCookies::class, // and this. Not sure if this is required. \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, 'bindings', ], ]; // ... } ``` ### Configure Passport's token and refresh token lifetime ```php // app/Providers/AuthServiceProvider.php class AuthServiceProvider extends ServiceProvider { // ... public function boot() { $this->registerPolicies(); // Add these 2 lines Passport::tokensExpireIn(now()->addMinutes(15)); Passport::refreshTokensExpireIn(now()->addMinutes(15)); } } ``` ### Create Api\LoginController This is our wrapper around Passport's AccessTokenController ```php // app/Http/Controllers/API/LoginController.php namespace App\Http\Controllers\API; use Laravel\Passport\Http\Controllers\AccessTokenController; use Psr\Http\Message\ServerRequestInterface; use App\Http\Controllers\Controller; use Illuminate\Http\Request; class LoginController extends Controller { public function __invoke( ServerRequestInterface $accessTokenRequest, AccessTokenController $accessTokenIssuer, Request $request ) { // Inject password grant_type. // This can be refactor to middleware. $request->request->add([ 'grant_type' => 'password', ]); // Inject client id and secret. // This can be refactor to middleware. $request->request->add([ 'client_id' => 'YOUR_CLIENT_ID', 'client_secret' => 'YOUR_CLIENT_SECRET', 'scope' => '*', ]); // Main logic $response = $accessTokenIssuer->issueToken( $accessTokenRequest->withParsedBody( $request->validate([ 'grant_type' => 'required|string', 'client_id' => 'required|numeric', 'client_secret' => 'required|string', 'username' => 'required|email|max:255', 'password' => 'required|string', 'scope' => 'required|string', ]) ) ); // Add refresh token cookie to response. // This can be refactor to midleware. $refreshToken = json_decode($response->content())->refresh_token ?? ''; if (!empty($refreshToken)) { $response->cookie('refresh_token', $refreshToken, 15); } return $response; } } ``` ### Create Api\KeepLoginController ```php // app/Http/Controllers/API/KeepLoginController.php namespace App\Http\Controllers\API; use Laravel\Passport\Http\Controllers\AccessTokenController; use Psr\Http\Message\ServerRequestInterface; use App\Http\Controllers\Controller; use Illuminate\Http\Request; class KeepLoginController extends Controller { public function __invoke( ServerRequestInterface $accessTokenRequest, AccessTokenController $accessTokenIssuer, Request $request ) { // Inject refresh token grant_type. // This can be refactor to middleware. $request->request->add([ 'grant_type' => 'refresh_token', ]); // Inject refresh token from cookie. // This can be refactor to middleware. $request->request->add([ 'refresh_token' => $request->cookie('refresh_token'), ]); // Inject client id and secret. // This can be refactor to middleware. $request->request->add([ 'client_id' => 'YOUR_CLIENT_ID', 'client_secret' => 'YOUR_CLIENT_SECRET', ]); // Main logic $response = $accessTokenIssuer->issueToken( $accessTokenRequest->withParsedBody( $request->validate([ 'grant_type' => 'required|string', 'client_id' => 'required|numeric', 'client_secret' => 'required|string', 'refresh_token' => 'nullable|string', 'scope' => 'required|string', ]) ) ); // Add refresh token cookie to response. // This can be refactor to midleware. $refreshToken = json_decode($response->content())->refresh_token ?? ''; if (!empty($refreshToken)) { $response->cookie('refresh_token', $refreshToken, 15); } return $response; } } ``` ### Create Api\LogoutController ```php // app/Http/Controllers/API/LogoutController.php namespace App\Http\Controllers\API; use App\Http\Controllers\Controller; use Illuminate\Http\Request; class LogoutController extends Controller { public function __contruct() { $this->middleware([ 'auth:api', ]); } public function __invoke(Request $request) { $request->user()->token()->revoke(); return response() // Remove refresh token cookie from response. // This can be refactor to middleware. ->cookie('refresh_token', null, -262800) ->noContent(); } } ``` ## Refactor with Middlewares ### Create InjectPasswordGrantType middleware ```php // app/Http/Middleware/InjectPasswordGrantType.php namespace App\Http\Middleware; use Closure; class InjectPasswordGrantType { public function handle($request, Closure $next) { $request->request->add([ 'grant_type' => 'password', ]); return $next($request); } } ``` ### Create InjectClientIdAndSecret middleware ```php // app/Http/Middleware/InjectClientIdAndSecret.php namespace App\Http\Middleware; use Closure; class InjectClientIdAndSecret { public function handle($request, Closure $next) { $request->request->add([ 'client_id' => 'YOUR_CLIENT_ID', 'client_secret' => 'YOUR_CLIENT_SECRET', 'scope' => '*', ]); return $next($request); } } ``` ### Create AddRefreshTokenCookieToResponse middleware ```php // app/Http/Middleware/AddRefreshTokenCookieToResponse.php namespace App\Http\Middleware; use Closure; class AddRefreshTokenCookieToResponse { public function handle($request, Closure $next) { $response = $next($request); refreshToken = json_decode($response->content())->refresh_token ?? ''; if (!empty($refreshToken)) { $response->cookie('refresh_token', $refreshToken, 15); } return $response; } } ``` ### Refactor Api\LoginController with middlewares ```php // app/Http/Controllers/API/LoginController.php namespace App\Http\Controllers\API; use Laravel\Passport\Http\Controllers\AccessTokenController; use App\Http\Middleware\AddRefreshTokenCookieToResponse; use App\Http\Middleware\InjectClientIdAndSecret; use App\Http\Middleware\InjectPasswordGrantType; use Psr\Http\Message\ServerRequestInterface; use App\Http\Controllers\Controller; use Illuminate\Http\Request; class LoginController extends Controller { public function __construct() { $this->middleware([ // Request middlewares InjectPasswordGrantType::class, InjectClientIdAndSecret::class, // Response middlewares AddRefreshTokenCookieToResponse::class, ]); } public function __invoke( ServerRequestInterface $accessTokenRequest, AccessTokenController $accessTokenIssuer, Request $request ) { return $accessTokenIssuer->issueToken( $accessTokenRequest->withParsedBody( $request->validate([ 'grant_type' => 'required|string', 'client_id' => 'required|numeric', 'client_secret' => 'required|string', 'username' => 'required|email|max:255', 'password' => 'required|string', 'scope' => 'required|string', ]) ) ); } } ``` ### Create InjectRefreshTokenGrantType middleware ```php // app/Http/Middleware/InjectRefreshTokenGrantType.php namespace App\Http\Middleware; use Closure; class InjectRefreshTokenGrantType { public function handle($request, Closure $next) { $request->request->add([ 'grant_type' => 'refresh_token', ]); return $next($request); } } ``` ### Create InjectRefreshTokenFromCookie middleware ```php // app/Http/Middleware/InjectRefreshTokenFromCookie.php namespace App\Http\Middleware; use Closure; class InjectRefreshTokenFromCookie { public function handle($request, Closure $next) { $request->request->add([ 'refresh_token' => $request->cookie('refresh_token'), ]); return $next($request); } } ``` ### Refactor Api\KeepLoginController with middlewares ```php // app/Http/Controllers/API/KeepLoginController.php namespace App\Http\Controllers\API; use Laravel\Passport\Http\Controllers\AccessTokenController; use App\Http\Middleware\AddRefreshTokenCookieToResponse; use App\Http\Middleware\InjectRefreshTokenFromCookie; use App\Http\Middleware\InjectRefreshTokenGrantType; use App\Http\Middleware\InjectClientIdAndSecret; use Psr\Http\Message\ServerRequestInterface; use App\Http\Controllers\Controller; use Illuminate\Http\Request; class KeepLoginController extends Controller { public function __construct() { $this->middleware([ // Request middlewares InjectRefreshTokenGrantType::class, InjectRefreshTokenFromCookie::class, InjectClientIdAndSecret::class, // Response middlewares AddRefreshTokenCookieToResponse::class, ]); } public function __invoke( ServerRequestInterface $accessTokenRequest, AccessTokenController $accessTokenIssuer, Request $request ) { return $accessTokenIssuer->issueToken( $accessTokenRequest->withParsedBody( $request->validate([ 'grant_type' => 'required|string', 'client_id' => 'required|numeric', 'client_secret' => 'required|string', 'refresh_token' => 'nullable|string', 'scope' => 'required|string', ]) ) ); } } ``` ### Create RemoveRefreshTokenCookieFromResponse middleware ```php // app/Http/Middleware/RemoveRefreshTokenCookieFromResponse.php namespace App\Http\Middleware; use Closure; class RemoveRefreshTokenCookieFromResponse { public function handle($request, Closure $next) { $response = $next($request); $response->cookie('refresh_token', null, -262800); return $response; } } ``` ### Refactor Api\LogoutController with middlewares ```php // app/Http/Controllers/API/LogoutController.php namespace App\Http\Controllers\API; use App\Http\Middleware\RemoveRefreshTokenCookieFromResponse; use App\Http\Controllers\Controller; use Illuminate\Http\Request; class LogoutController extends Controller { public function __contruct() { $this->middleware([ 'auth:api', // Response middlewares RemoveRefreshTokenCookieFromResponse::class, ]); } public function __invoke(Request $request) { $request->user()->token()->revoke(); return response()->noContent(); } } ```