Sanctum SPA Authentication: How to log out from other devices? - laravel

Is this feature included in the SPA Authentication? By reading the docs on Laravel Sanctum, it doesn't look like it, but at the same time, this is a common feature (used if user reset password or if you want only one login instance from a user) so I thought that it must be included...
After searching, I found this:
use Illuminate\Support\Facades\Auth;
Auth::logoutOtherDevices($currentPassword);
but you'll need to uncomment this middleware in the web before you can use this.
'web' => [
// ...
\Illuminate\Session\Middleware\AuthenticateSession::class,
// ...
],
However this only works on web and not on api. So I googled again how to have the same functionality with api and I found this youttube video. Here's what the guy did:
Create his own middleware (ex. AuthenticateSessionSPA.php).
Copy everything in AuthenticateSession and paste it in the new middleware. Change namespace and class name.
Create private variable with value of 'sanctum' (ex. private $driver
= 'sanctum')
Replace all $this->auth->getDefaultDriver() with $this->driver. So
the code will use the sanctum driver instead.
The file looks like this:
<?php
namespace App\Http\Middleware\Custom;
use Closure;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Contracts\Auth\Factory as AuthFactory;
class AuthenticateSessionSPA
{
private $driver = 'sanctum';
/**
* The authentication factory implementation.
*
* #var \Illuminate\Contracts\Auth\Factory
*/
protected $auth;
/**
* Create a new middleware instance.
*
* #param \Illuminate\Contracts\Auth\Factory $auth
* #return void
*/
public function __construct(AuthFactory $auth)
{
$this->auth = $auth;
}
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if (! $request->hasSession() || ! $request->user()) {
return $next($request);
}
if ($this->guard()->viaRemember()) {
$passwordHash = explode('|', $request->cookies->get($this->auth->getRecallerName()))[2] ?? null;
if (! $passwordHash || $passwordHash != $request->user()->getAuthPassword()) {
$this->logout($request);
}
}
if (! $request->session()->has('password_hash_'.$this->driver)) {
$this->storePasswordHashInSession($request);
}
if ($request->session()->get('password_hash_'.$this->driver) !== $request->user()->getAuthPassword()) {
$this->logout($request);
}
return tap($next($request), function () use ($request) {
if (! is_null($this->guard()->user())) {
$this->storePasswordHashInSession($request);
}
});
}
/**
* Store the user's current password hash in the session.
*
* #param \Illuminate\Http\Request $request
* #return void
*/
protected function storePasswordHashInSession($request)
{
if (! $request->user()) {
return;
}
$request->session()->put([
'password_hash_'.$this->driver => $request->user()->getAuthPassword(),
]);
}
/**
* Log the user out of the application.
*
* #param \Illuminate\Http\Request $request
* #return void
*
* #throws \Illuminate\Auth\AuthenticationException
*/
protected function logout($request)
{
$this->guard()->logoutCurrentDevice();
$request->session()->flush();
throw new AuthenticationException('Unauthenticated.', [$this->driver]);
}
/**
* Get the guard instance that should be used by the middleware.
*
* #return \Illuminate\Contracts\Auth\Factory|\Illuminate\Contracts\Auth\Guard
*/
protected function guard()
{
return $this->auth;
}
}
And this works wonderfully, I can now use Auth::logoutOtherDevices($currentPassword) in my api.
So my question is if this is safe? I'm just a jr. dev (6 months) and I'm not that confident in this solution since I found it from someone on the net and not from the docs. I'm wondering maybe the laravel devs didn't implement it because they have reasons like security?
What are your thoughts on this method? If you don't agree with this, how would you implement logging out from other devices in a project that has a SPA frontend and Laravel api?

Doesn't look like this is included in Laravel Sanctum SPA Authentication. So I just did my original answer found on the youtube video

Related

Optimize a method that checks url segments issue

I've been using a method in Laravel Middleware that checks for strings in any URL segment to block the IP if it matches the "blacklisted" strings.
In the beginning, I had just a few strings to check, but now, the list is growing, and when I tried to optimize it to use a blacklist array, I ended up in a complete mess in the code and in my mind.
I believe this can be done but can't figure out the best way to optimize this middleware. Below is a sample of the Middleware code with notes where I'm having trouble.
In the handle($request, Closure $next) method is calling the $this->inUrl() method for all the blacklisted strings.
I've tried to add a protected $blacklisted array, to be used in the $this->inUrl() but can't make it work.
Thank you in advance for any suggestions that would be much appreciated and welcome. I am also thinking of providing the code as a gist on GitHub when optimized.
namespace App\Http\Middleware;
/**
* Class VerifyBlacklistedRequests
*
* #package App\Http\Middleware
*/
class VerifyBlacklistedRequests
{
/**
* The array of blacklisted request string segments
*
* #access protected
* #var array|string[]
*/
protected array $blacklisted = [
'.env', '.ftpconfig', '.vscode', ',git', '.git/HEAD'
// etc...
];
/**
* Handle an incoming request.
*
* #access public
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
*
* #return mixed
*/
public function handle($request, Closure $next)
{
if($this->inUrl('.env')
|| $this->inUrl('.ftpconfig')
|| $this->inUrl('.vscode')
|| $this->inUrl('.git')
|| $this->inUrl('.git/HEAD')
// many more checks below the above ones
) {
// logic that blocks the IP goes here and working fine
}
return $next($request);
}
/**
* Check if the string is in any URL segment or at the one specified.
*
* #access protected
*
* #param string|mixed $value Segment value/content.
* #param integer $segment Segment position.
*
* #return bool
*/
protected function inUrl(string $value, $segment = -1)
{
if($segment !== -1 && request()->segment($segment) === $value) {
return true;
}
collect(request()->segments())->each(function ($segment) use ($value) {
if($segment === $value) {
return true;
}
});
return false;
}
}
After all the suggestions, kindly posted here, I ended up with a solution that uses some of the suggested methods.
The result ended up by reducing the pages' loading time by more than 1 second.
My final implementation:
Created a config file security.php which contains the blacklisted request strings, and a shortlist of whitelisted IPs.
The security.php config file
<?php
return [
/*
|--------------------------------------------------------------------------
| Whitelisted IPs configuration
|--------------------------------------------------------------------------
|
| These are the settings for the whitelisted IPs. The array contains
| the IPs that should not trigger the IP block.
|
*/
'whitelisted_ips' => [
// whitelisted IPs array
],
/*
|--------------------------------------------------------------------------
| Blacklisted request strings configuration
|--------------------------------------------------------------------------
|
| These are the settings for the blacklisted request strings. The array contains
| the strings that should trigger the IP to be blocked.
|
*/
'blacklisted_requests' => [
'.env',
'.ftpconfig',
'.vscode',
'.git',
'.git/HEAD',
'_profiler',
'__media__',
'administrator',
//...
];
];
Optimized the middleware removing the loops on the inUrl() method
The VerifyBlacklistedRequests middleware
<?php
namespace App\Http\Middleware;
use Closure;
/**
* Class VerifyHackingAttemptsRequests
*
* #property \Illuminate\Config\Repository|\Illuminate\Contracts\Foundation\Application|mixed white_listed_ips
* #property \Illuminate\Config\Repository|\Illuminate\Contracts\Foundation\Application|mixed blacklist
* #package App\Http\Middleware
*/
class VerifyHackingAttemptsRequests
{
/**
* #access protected
* #var \Illuminate\Config\Repository|\Illuminate\Contracts\Foundation\Application|mixed
*/
protected $blacklist;
/**
* #access protected
* #var \Illuminate\Config\Repository|\Illuminate\Contracts\Foundation\Application|mixed
*/
protected $white_listed_ips;
/**
* VerifyHackingAttemptsRequests constructor
*
* #access public
*/
public function __construct()
{
$this->blacklist = config('security.blacklisted_requests');
$this->white_listed_ips = config('security.whitelisted_ips');
}
/**
* Handle an incoming request.
*
* #access public
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
*
* #return mixed
* #since 2.8.1
*/
public function handle($request, Closure $next)
{
$exists = false;
foreach(request()->segments() as $segment) {
if(in_array($segment, $this->blacklist)) {
$exists = true;
}
}
if($exists) {
$this->blockIp($request)
}
return $next($request);
}
/**
* Method to save an IP in the Blocked IP database table
*
* #access protected
*
* #param \Illuminate\Http\Request $request
*
* #return \App\Models\BlockedIp
*/
protected function blockIp(Request $request, $notes = null)
{
// the logic to persist the data through the BlockedIp model
}
}
In summary, the inUrl() method was removed, removing all the loops and method calls and, as mentioned above, the pages' loading time was sliced by more than 50%.
Thanks to all for the suggested methods which contributed to helping me solve the problem.
I recommend you to create literal routes, so it is easier to maintain. Go to RouteServiceProvider and create a new reading similar to web or api, so any route that is in that new file, it will ban/block the IP.
I don't know if doing this will optimize the code but the code is much more readable I think.
namespace App\Http\Middleware;
/**
* Class VerifyBlacklistedRequests
*
* #package App\Http\Middleware
*/
class VerifyBlacklistedRequests
{
/**
* The array of blacklisted request string segments
*
* #access protected
* #var array|string[]
*/
protected array $blacklisted = [
'.env', '.ftpconfig', '.vscode', ',git', '.git/HEAD'
// etc...
];
/**
* Handle an incoming request.
*
* #access public
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
*
* #return mixed
*/
public function handle($request, Closure $next)
{
//loop over the list instead of that long conditions
foreach($this->blacklisted as $blacklistedItem) {
if($this->inUrl($blacklistedItem))
{
// logic that blocks the IP goes here and working fine
}
}
}
/**
* Check if the string is in any URL segment or at the one specified.
*
* #access protected
*
* #param string|mixed $value Segment value/content.
* #param integer $segment Segment position.
*
* #return bool
*/
protected function inUrl(string $value, $segment = -1)
{
if($segment !== -1 && request()->segment($segment) === $value) {
return true;
}
foreach(request()->segments() as $segment) {
if($segment === $value) {
return true;
}
}
return false;
}
}
I believe you can use DB and make the blacklist as indexed in DB,
Database would handle and search for this itself with its inner Engine.
protected function urlWasInBlackList($segment = -1){
$segmentStrings= $segment != -1 ? [request()->segment($segment)] : request()->segments();
return DB::table('blacklisted')->select('*')->whereIn('pattern',$segmantStrings)->exists()
}
and using some function like this instead of the inUrl(). it will check against the forbidden words Against the DB.
Update : Avoid DB
however DB handles this situation with its InnoDB engine quickly,
maybe you want to avoid DB, because of connection overhead or simply not dependent your project just because one functionality,
if you want to a avoid DB,
you have to check url with each black listed word with a loop and it would be O(n),
and if you multiply sections count, it would be O(n*m). The
Optimal way is to avoid loop, and make a hash table like what DB does.
so I go for hash_tables in php and found in the php array doc
that php associative arrays are hash map.
and the function array_key_exists() is looking into hash table for keys, you can see in the php source for the array_key_exists() codes which addressing the ht as hash map here:
array.c line #6071
zen_hash.h line #529
so I suggest to use something like this:
protected array $blacklisted = [
'.env' => null, '.ftpconfig' => null, '.vscode' => null, ',git' => null, '.git/HEAD' => null
];
for blacklist definitions.
and
$segmentStrings= $segment != -1 ? [request()->segment($segment)] : request()->segments();
foreach(segmentStrings as $segmentString){
return array_key_exists($segmentString, $blacklisted);
}

Laravel passport / allow api routes for guests / bypass Laravel api middleware

This is the only one solution for passport authentication that I have found for a week of struggles. Enjoy!
Paste code bellow in file app/Http/Middleware/Authenticate.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Contracts\Auth\Factory as Auth;
class Authenticate extends Middleware
{
/**
* The authentication factory instance.
*
* #var \Illuminate\Contracts\Auth\Factory
*/
protected $auth;
/**
* Create a new middleware instance.
*
* #param \Illuminate\Contracts\Auth\Factory $auth
* #return void
*/
public function __construct(Auth $auth)
{
$this->auth = $auth;
}
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #param string[] ...$guards
* #return mixed
*
* #throws \Illuminate\Auth\AuthenticationException
*/
public function handle($request, Closure $next, ...$guards)
{
$this->authenticate($guards);
return $next($request);
}
/**
* Determine if the user is logged in to any of the given guards.
*
* #param array $guards
* #return void
*
* #throws \Illuminate\Auth\AuthenticationException
*/
protected function authenticate(array $guards)
{
if (empty($guards)) {
return $this->auth->authenticate();
}
foreach ($guards as $guard) {
if ($this->auth->guard($guard)->check()) {
return $this->auth->shouldUse($guard);
}else{
return null;
}
}
throw new AuthenticationException('Unauthenticated.', $guards);
}
}
After this, all routes will be available for guests.
Create new middleware DenyIfNotAuthenticated. Add there code from default app/Http/Middleware/Authenticate.php. It will be like this:
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
class DenyIfNotAuthenticated extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* #param \Illuminate\Http\Request $request
* #return string|null
*/
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
return route('login');
}
}
}
Add to Kernel.php file line: 'auth.deny' => \App\Http\Middleware\DenyIfNotAuthenticated::class, bellow 'auth' => \App\Http\Middleware\Authenticate::class,
Routs in routes/api.php looks like this:
Route::apiResource('recipes', 'RecipesController'); // accessible for guests
Route::group(['middleware' => ['auth.deny:api']], function () {
Route::get('ingredients', 'IngredientsController#index');
}); // accessible only for authorized users

Middleware and user - laravel 5

How can i assign middleware to user? I just follow the guide on laravel 5.2 but i can't figure...
I'm able to create middleware ( i have admin middleware)
<?php
namespace App\Http\Middleware;
use Closure;
class Admin
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
return $next($request);
}
}
I'm able to assign middleware to route
Route::group(['middleware' => ['auth', 'admin']], function () {
Route::resource('admin/tasks', 'Admin\\TasksController');
});
but how can i check if user is admin or not? I just follow the docs on laravel 5.2 for authentication, but i dont know how to access the page only for "admin" middleware...
Question 1 How to check if user is admin
I think using session is a good solution. You can store the user status in the session. And in the Admin middleware, you can check if user is admin by if (session('statut') === 'admin').
Question 2 Page Access of users
If user is admin, we will pass the request by return $next($request);
If user is not admin, we will redirect to index page or other page
you want by return new RedirectResponse(url('/'));
The following code may help you.
<?php namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\RedirectResponse;
class Admin {
public function handle($request, Closure $next)
{
if (session('statut') === 'admin')
{
return $next($request);
}
return new RedirectResponse(url('/'));
}
}
I would recommend you to use ENTRUST Laravel package
Entrust is a succinct and flexible way to add Role-based Permissions
to Laravel 5.
I have a small example for you, it very simple
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Factory as Auth;
class Authenticate
{
/**
* The authentication guard factory instance.
*
* #var \Illuminate\Contracts\Auth\Factory
*/
protected $auth;
/**
* Create a new middleware instance.
*
* #param \Illuminate\Contracts\Auth\Factory $auth
* #return void
*/
public function __construct(Auth $auth)
{
$this->auth = $auth;
}
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #param string|null $guard
* #return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
if ($this->auth->guard($guard)->guest()) {
return response('Unauthorized.', 401);
}
return $next($request);
}
}
If you only have guest and admin(who is authenticated in your system) you should do like above. But if you have another roles you will have to attach ACL (for ex https://github.com/Zizaco/entrust)

laravel redirect to url after login

I have trouble with redirecting to an url after login.
The situation is that someone visits a blog post, and needs to login before adding a comment. So the user clicks on the login link and logs in on "auth/login", and is always redirected to "/home".
I want the user to be redirected to the blogpost when an url is set like "auth/login?redirect=url/to/blogpost"
I have the following Middleware:
app\Http\Middleware\RedirectIfAuthenticated
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Guard;
class RedirectIfAuthenticated
{
/**
* The Guard implementation.
*
* #var Guard
*/
protected $auth;
/**
* Create a new filter instance.
*
* #param Guard $auth
* #return void
*/
public function __construct(Guard $auth)
{
$this->auth = $auth;
}
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if ($this->auth->check()) {
return redirect('/home');
}
return $next($request);
}
}
app\Http\Middleware\Authenticate
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Guard;
class Authenticate
{
/**
* The Guard implementation.
*
* #var Guard
*/
protected $auth;
/**
* Create a new filter instance.
*
* #param Guard $auth
* #return void
*/
public function __construct(Guard $auth)
{
$this->auth = $auth;
}
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if ($this->auth->guest()) {
if ($request->ajax()) {
return response('Unauthorized.', 401);
} else {
return redirect()->guest('auth/login');
}
}
return $next($request);
}
}
Why don't you use the intended method on redirector? Read about this in docs
The intended method on the redirector will redirect the user to the URL they were attempting to access before being caught by the authentication filter. A fallback URI may be given to this method in case the intended destination is not available.
I've decided to copy and paste the getLogin function of the trait AuthenticatesUsers into my AuthController. I overwrite the function AND keep the trait as is.
I've just added
\Session::put('url.intended',\URL::previous());
If you're using standard authentication from Laravel 5, find a app/Http/Controllers/Auth/AuthController.php file and change $redirectPath to this:
protected $redirectPath = '/url/to/blogpost';

laravel 5 throws token error on facebook canvas

I try to build a facebook canvas app using laravel 5. I use a custom middleware class to disable crsf for specific routes. can be found here. I ended up with another error.
Class 'App\Http\Middleware\TokenMismatchException' not found
This is my middlewareclass:
<?php namespace App\Http\Middleware;
use Closure;
class VerifyCsrfMiddleware extends \Illuminate\Foundation\Http\Middleware\VerifyCsrfToken
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if ($this->isReading($request) || $this->excludedRoutes($request) || $this->tokensMatch($request))
{
return $this->addCookieToResponse($request, $next($request));
}
throw new TokenMismatchException;
}
/**
* Ignore CSRF on these routes.
*
* #param \Illuminate\Http\Request $request
* #return bool
*/
private function excludedRoutes($request)
{
$routes = [
'app/canvas'
// ... insert all your canvas endpoints here
];
foreach($routes as $route){
if ($request->is($route)) {
return true;
}
}
return false;
}
}

Resources