5

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?

Cy Rossignol
  • 16,216
  • 4
  • 57
  • 83
Lance
  • 3,193
  • 2
  • 32
  • 49

3 Answers3

3

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.

Cy Rossignol
  • 16,216
  • 4
  • 57
  • 83
0

try to name your route and do like this here:

Route::get('/login/{loginToken}', 'LoginController@login')->name('login.route');

if (isset($loginToken) && Auth::guest()) {
   return redirect()->route('login.route', [
      'token' => $loginToken
   ])      
}

Martin
  • 316
  • 1
  • 5
  • Since any of the routes/pages of the site could have the loginToken I thought middleware would do a better job of catching it. I am unsure of how this suggestion would solve my issue? – Lance Sep 20 '21 at 22:58
  • I understood that the user is not forwarded to the login screen. That would be because you call the controller directly. – Martin Sep 20 '21 at 23:08
  • Otherwise you can solve it via the ServiceProvider. – Martin Sep 20 '21 at 23:13
  • Right now the user IS going to the login form. If they have the token I do not want them sent to the login form I then want their login processes "behind the scenes" and allow them to go to their intended page. I was trying to solve with middleware but I will take a look and see if the ServiceProvider is a better way to do it, thanks for the idea. – Lance Sep 20 '21 at 23:15
0

Actually, for Laravel to know if the user is logged in or not, the request needs to passe the auth middleware. So your custom middleware would need to be triggered after the auth one.

So if your middleware need to know if the user is logged in or not, the auth middleware have to be passed first (also meaning it'll only work for routes or route groups under the auth middleware - as I see the auth middleware is not in your default app middleware stack).

And if the auth middleware redirects the user, then yours is never called.

override the Auth middleware

as your business is "auth related", you could easily override the authenticate middleware you are using. Copy or extend the vendor class, replace the auth middleware class with yours in the Kernel, and add your custom business after the default middleware business identified or not the user as logged in.

I guess you could also go by creating a custom authentication Guard and use it instead of the default one, but I think the custom auth middleware is faster.

I don't know much about your custom auth business, but be careful about tokens in URL (that can be sent by mail, or saved elsewhere) that authenticate a user: those tokens should expire after a delay and after use. Also, you should prevent brute-forcing of this parameter.

Mtxz
  • 3,749
  • 15
  • 29