Laravel 8: Using Fortify in APIs - laravel

Can Laravel Fortify be used in the context of API? From what I understand, Fortify (although being headless, i.e. doesn't include a UI layer) allows us to customize Login and Register pages, but it automatically redirects to HOME page upon successful authentication. And although the HOME page is customizable, that is not how API logins normally work. It should simply return a success token in JSON format without redirects of any kind.
There is an authenticateUsing function in Fortify, but even that simply allows us to customize authentication logic and not the returned data. Redirection is still performed by Fortify.
How can I use Fortify in the context of REST API?
Note: I'm going to use it from my Vue front-end application. I'm also going to get Sanctum into the game, but before that I just wanted to see if I can do regular token-based authentication using Fortify without having to write my own login, register and logout routes and controller functions.

Authentication can either be Session-based or Token-based.
Laravel Fortify only provides the backend logic nessecery for session-based authentication and therefore is not intended for token-based API authentication.
If you need token-based API authentication, you can use either Sanctum or Passport depending on your needs. But You'll have to write a bit of code, in either case.
If you decide to go with Laravel Passport, I have a boilerplate project that might be of use: https://github.com/pktharindu/laravel-api-boilerplate-passport

Just set 'Accept' header with 'application/json' or 'application/javascript' then fortify will response json formatted body not redirection.
by the way, use Sanctum instead of Passport for SPA is easier to keep token securely. google about where to store API token for SPA then you will find out why.

The redirects reason in my case was the default laravel RedirectIfAuthenticated provider.
By default in laravel 8 that provider looks like
<?php
namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Support\Facades\Auth;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #param string[]|null ...$guards
* #return mixed
*/
public function handle($request, Closure $next, ...$guards)
{
$guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
return redirect(RouteServiceProvider::HOME);
}
}
return $next($request);
}
}
just replace default foreach by the following code with that:
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
if ($request->expectsJson()) {
return response()->json([
'error' => 'Already authenticated.'
], 406);
}
return redirect(RouteServiceProvider::HOME);
}
}
Don't forget that for $request->expectsJson() to work fine you should include Accept: application/json in the request headers.

Related

Laravel api bypass auth api for specific ip address

Below is my laravel 8 api route and middleware that I use
Route::group(['middleware'=>['auth:api', 'StripScript'],'prefix' => 'v1'], function(){
Route::get('/list', [ListController::class, 'list']);
});
I this I want to bypass middleware 'auth:api' if I get request from specific ip address so user doesn't require to pass token and it should auto login as one specific user.
Note that I want to bypass auth and not to do 'Ip address Whitelist' as I do have whitelist for this I want to totally by pass for one IP address.
It's not good idea, But any way if you want try this...
Go app/Http/Middleware/Authenticate.php
Add && override handle function like below
public function handle($request, \Closure $next, ...$guards)
{
// check $request is coming from where and set a statement ture/false for a $isComingFromDesiredIp;
if (! $isComingFromDesiredIp) {
$this->authenticate($request, $guards);
}
return $next($request);
}
This should work:
Route::group(['middleware' => in_array(request()->ip(), $arrayOfTrustedIps) ? ['StripScript'] : ['auth:api', 'StripScript'],'prefix' => 'v1'], function(){
Route::get('/list', [ListController::class, 'list']);
});
You should also handle the references to the authed user, if there are any (e.g. auth()->user()->id will throw an error).
IMHO you should authenticate the trusted party via a token sent in header, if you have access to the api call code.
Managing these kind of exceptions in your code can quickly become cumbersome and will be challenging to understand for everyone (including you).

Laravel Guest Policies always return user as null unless using authentication middleware with Sanctum

I'm using Laravel Sanctum for authentication for an API and taking advantage of Policies for authorization.
The API has an endpoint to view a Post. Posts can either be draft or published. Anyone (including guests) can view published posts, but only admins can view draft posts.
The view policy looks like this:
/**
* Determine whether the user can view the model.
*
* #param \App\Models\User $user
* #param \App\Models\Post $post
* #return mixed
*/
public function view(?User $user, Post $post)
{
if($user && $user->isAdmin()){
return true;
}else{
return $post->published;
}
}
However, $user is always null, meaning the admins cannot view draft posts. But, if I run the auth:sanctum middleware on this route, it works fine for admins. However, that means then guest users get blocked from this endpoint because they are not authenticated.
My workaround currently is to use the authorizeForUser(auth('sanctum')->user(), 'view', $post) method to explicitly define the use. However, this forces me to break from the pattern used for other endpoints and doesn't seem like it should be necessary.
Any solutions for this problem?
I have also noticed if I want to access the user on these routes, I need to explicitly specify to use the sanctum guard, i.e. auth('sanctum')->user(). Is there a config setting required always to use Sanctum as default?
You can change your config file like this:
// config/auth.php
'defaults' => [
'guard' => 'sanctum',
],

Laravel 8 - Can't set cookies in middleware

I'm building a shopping cart SPA using Laravel and Vue.
Vue communicates with Laravel via the API, which is not a stateless as I'm using Sanctum for a cookie based session authentication services.
For guest users I want to store the cart info in a cookie. So I'm trying to set a cookie via a middleware I created with the following handle() method:
public function handle(Request $request, Closure $next)
{
if(!$request->cookie('cart_id')){
$cart_id = "some value";
return $next($request)->cookie(cookie()->forever('cart_id', $cart_id));
}
return $next($request);
}
I added this middleware to both 'api' and 'web' groups, but it doesn't seem to add the cookie no matter how many page requests I make.
Would appreciate some help with this Thanks

Laravel Passport, multiple connexions via password client

I am having troubles understanding how could I implement multiple connexions for same user via password client with Laravel Passport:
I have a mobile app, that needs to communicate with a Laravel based API. My users, at first launch of the app, will have to enter their login and password to get an access_token.
So I think that I need to put my password client secret in the code of my mobile app to be able to request tokens. But what if, my user has an iPhone and an iPad and he wants to login from both.
I'm asking because every time I make a request to POST /oauth/token, from the same password_client, every access_token of a certain user requested with my password_client gets revoked.
That would mean that, every time my user would use his iPad, he would be disconnected from his iPhone because the token wouldn't be valid anymore?
Am I missing something?
I believe that Passport changed the way it was dealing with the Access Token creation and the method #issueToken in AccessTokenController does no longer revoke old tokens (check Multiple Access Token).
I think that this change was introduced before #jesús-lozano-m answer and therefore there is no longer the need to customize controller.
However, if you want to revoke old tokens, now you can do that by setting a listener to the Passport Event AccessTokenCreated.
app/Providers/eventServiceProvider.php:
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Event;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider {
protected $listen = [
'Laravel\Passport\Events\AccessTokenCreated' => [
'App\Listeners\RevokeOldTokens'
]
];
public function boot() {
parent::boot();
}
}
app/Listeners/RevokeOldTokens.php:
<?php
namespace App\Listeners;
use Laravel\Passport\Events\AccessTokenCreated;
use Laravel\Passport\Client;
use Carbon\Carbon;
class RevokeOldTokens {
public function __construct() {
//
}
public function handle(AccessTokenCreated $event) {
$client = Client::find($event->clientId);
// delete this client tokens created before one day ago:
$client->tokens()
->where('user_id', $event->userId)
->where('created_at', '<', Carbon::now()->subDay())
->delete();
}
}
You can write your own Controller and Routes...
Passport has a defined "Laravel\Passport\Http\Controllers\AccessTokenController" and there is a method called "issueToken()".
If you see the method below it calls the function "revokeOtherAccessTokens()", and this deletes or revoke all "access_tokens" using the "Laravel\Passport\TokenRepository"
So what you can do is to write your own controller and prevent to call "revokeOtherAccessTokens()"
The fact that you must keep in mind is that the access tokens will never be pruned or revoked at least that the refresh token became issued or manually delete them.
Refresh tokens and access tokens are revoked when refresh token is issued, because the "League\OAuth2\Server\Grant\RefreshTokenGrant" in method "respondToAccessTokenRequest()", it already revoke old "access_token" and "refresh_token", so we don't have to worry about revoke or delete them in this case.
...
// Expire old tokens
$this->accessTokenRepository->revokeAccessToken($oldRefreshToken['access_token_id']);
$this->refreshTokenRepository->revokeRefreshToken($oldRefreshToken['refresh_token_id']);
...
Here is an sample implementation, hope it helps:
routes:
Route::post('oauth/access_token', 'Auth\OAuth2Controller#issueToken');
customized controller:
<?php
namespace App\Http\Controllers\Auth;
use Laravel\Passport\Http\Controllers\HandlesOAuthErrors;
use Zend\Diactoros\Response as Psr7Response;
use Psr\Http\Message\ServerRequestInterface;
use League\OAuth2\Server\AuthorizationServer;
use App\Http\Controllers\Controller;
class OAuth2Controller extends Controller
{
use HandlesOAuthErrors;
/**
* The authorization server.
*
* #var AuthorizationServer
*/
protected $server;
/**
* Create a new controller instance.
*
* #param AuthorizationServer $server
* #return void
*/
public function __construct(AuthorizationServer $server)
{
$this->server = $server;
}
/**
* Authorize a client to access the user's account.
*
* #param ServerRequestInterface $request
* #return Response
*/
public function issueToken(ServerRequestInterface $request)
{
return $this->withErrorHandling(function () use ($request) {
return $this->server->respondToAccessTokenRequest($request, new Psr7Response);
});
}
}
I wirte below mention code in my app/Providers/AuthServiceProvider and its working. It won't removke old token and allow me login from multiple devicess
use Dusterio\LumenPassport\LumenPassport;
public function boot()
{
$this->setPassportConfiguration();
}
private function setPassportConfiguration(): void
{
LumenPassport::allowMultipleTokens();
}

Laravel - Setting session variables when using Basic Auth

I'm implementing an API so that clients can access our system. I'm currently using Auth Basic for authentication. Is there a way for me to set additional session variables when they authenticate this way?
There are session variables we usually set when they log in from the login page, and so there is functionality that depends on those values. So I'll need a way to set those same variables, but when using the Auth Basic middleware.
You can write your own basic auth controllers and functions as described in the laravel documentation
Your authentication controller would look something like this:
<?php
namespace App\Http\Controllers;
use Auth;
use Illuminate\Routing\Controller;
class AuthController extends Controller
{
/**
* Handle an authentication attempt.
*
* #return Response
*/
public function authenticate()
{
if (Auth::attempt(['email' => $email, 'password' => $password])) {
// Authentication passed...
return redirect()->intended('dashboard');
}
}
}

Resources