Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Laravel test for URL param in middleware BEFORE group Middleware

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?

like image 438
Lance Avatar asked Oct 14 '22 19:10

Lance


1 Answers

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.

like image 138
Cy Rossignol Avatar answered Oct 19 '22 07:10

Cy Rossignol