I am using a middleware to check for the existence of a login token from another site. If the login token is present and the user is not already logged in, I would like to use the token to log the user in and send them to their intended page. If they are already logged in, I would like it to do nothing.
As suggested, should this be a ServiceProvider
instead?
Here is my middleware:
<?php
namespace App\Http\Middleware;
use Session;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\Auth\LoginController;
class CheckRepLoginToken
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next){
$loginToken = $request->repLoginToken;
$goto = '/'.$request->path();
if(isset($loginToken) && Auth::guest()){
(new LoginController)->login($loginToken,$goto);
}
return $next($request);
}
}
The problem is that I need this to all run prior to the $middlewareGroups
and $routeMiddleware
so the user IS NOT sent to the login screen if Auth::guest()
is true but the token is present.
I currently have the middleware in the protected $middleware
section of the Kernel
and everyone seems to be a "guest" whether or not they are logged in.
This is the kernel file:
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Fruitcake\Cors\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\CheckRepLoginToken::class,
// 'checkStatus' => \App\Http\Middleware\CheckStatus::class,
];
/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
}
How can I achieve my desired result without messing with the current authentication?
At first, middleware seems like the right tool for a one-time login token feature, but the implementation can be tricky without an understanding of Laravel's request pipeline. At the end of this answer, we'll look at a simple alternative that uses custom authentication functionality instead.
For traditional web requests from a browser, Laravel's authentication services depend on the existence of a session identified by a cookie. The question's CheckRepLoginToken
middleware is declared in the global middleware array in Kernel.php, and these handlers execute before route middleware which include the StartSession
middleware in the 'web'
group.
Since the StartSession
middleware initializes session state for a request, authentication context for a global CheckRepLoginToken
middleware is not available yet. Calling Auth::guest()
from the global middleware will always return true
for the configuration shown in the question. I'm not sure what the LoginController::login()
method does in your particular project, but I imagine that the auth state set-up by attempting to invoke that method from global middleware may disappear when the standard session and auth middleware run afterward.
Depending on what your LoginController::login()
method does, it may be enough to move the declaration for the CheckRepLoginToken
middleware below StartSession
in the 'web'
group. As an aside, some might consider it bad practice to instantiate the controller to call the method directly. We can achieve a similar outcome without much code:
public function handle(Request $request, Closure $next)
{
if ($request->has('repLoginToken') && Auth::guest()) {
$user = // ...try to fetch a user with $request->repLoginToken...
if ($user !== null) {
Auth::login($user);
}
}
return $next($request);
}
A more complete solution takes advantage of Laravel's pluggable authentication system. We can wrap Laravel's standard authentication guard with a custom implementation that handles the token.
First, we'll update config/auth.php to switch the default 'web'
guard to use a custom driver that we'll implement below. We rename the original 'web'
guard to 'session'
so that we can refer to it later.
'guards' => [
'web' => [
'driver' => 'rep-token',
],
'session' => [
'driver' => 'session',
'provider' => 'users',
]
],
Laravel's AuthManager
includes a helper method—viaRequest()
—that simplifies the creation of a Guard
that authenticates a user with data from the request context without the need to fully-implement Illuminate\Contracts\Auth\Guard
. We bind our custom guard in the boot()
method in AuthServiceProvider.php:
public function boot()
{
Auth::viaRequest('rep-token', function ($request) {
$baseGuard = Auth::guard('session');
if ($request->has('repLoginToken') && $baseGuard->guest()) {
$user = // ...try to fetch a user with $request->repLoginToken...
if ($user !== null) {
$baseGuard->login($user);
}
}
return $baseGuard->user();
});
}
As we can see, this wrapper reuses the functionality of Laravel's standard session-based authentication and handles the special case for the presence of repLoginToken
. It does not need any additional middleware.
Since this post is a public, I feel obligated to emphasize a point from Mtxz's answer. Exercise great caution in the design and implementation of a third-party authentication scheme. In general, anyone who obtains a valid token—including the third-party—has complete access to a user's account. The simplicity of the authentication flow described in this question suggests vulnerabilities that may not be acceptable for many applications.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With