#Fortify redirecting after login

47 messages · Page 1 of 1 (latest)

deep belfry
#

Fortify seems to always try to redirect me upon successful login, is there any way to disable this? or redirect so something that doesn't point to the api itself?

vocal night
#

If you're consuming it as an API, you'd need to request a JSON response, otherwise it would use redirects. So you'd send the Accept: application/json header

deep belfry
# vocal night If you're consuming it as an API, you'd need to request a JSON response, otherwi...

Thank you for the quick response;
I don't know if this is what you meant. But even by sending the Accept header I get the 302 response with the Location redirect.
Preferably, I'd want this behavior to be disable as I could handle the redirect myself in the front-end. If this is not possible, is there any way to instead send a Location which points to a page requested from the front-end itself (in this case something like Location: http://localhost:3000/dashboard)?

vocal night
deep belfry
#

Right, I didn't point out that my current Laravel configuration does not have any views and all requests are already treated as requesting a JSON response using a middleware.
The front-end is a separate application.

vocal night
#

Still not sure what you're trying to do here. When you're consuming an API, a redirect isn't going to work anyway, as those are XHR requests, it wouldn't redirect the browser, instead you'll then get a redirect with an HTML response, which you can't do anything with when you're consuming it as an API

deep belfry
#

I'm trying to disable this behavior altogether (or change the Location header being returned from the API)
The current behavior is documented here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location.

The flow goes as such:

Front-end Sends: @login -> Laravel[Fortify] /login Receives: {email: ..., password: ...} and Sends -> Front-end Receives: 200 Body: {two_factor: false}

On the next @login

Front-end Sends: @login -> Laravel[Fortify] /login Receives: {email: ..., password: ...} and Sends -> Front-end Receives: 302 Body: {two_factor: false} - Headers: {..., Location: http://[LaravelAPI]/, ...} -> Front-end attempts so make a call according to the spec above to http://[LaravelAPI]/ and fails because the route doesn't exist

MDN Web Docs

The HTTP Location response header indicates the URL to redirect a page to.
It only provides a meaning when served with a 3XX redirection response or a 201 Created status response.

vocal night
#

Yes, and that's done with the login response, as I already showed, it would return a JSON response if you make a request asking for a JSON response. If you overwrite the login response, then yeah, you'd need to account for that. The location header doesn't have much to do with it, if you're consuming it as an API then you wouldn't even want a 3xx response, as there's not much you can do with such a response. So sending the correct headers would give instructions on what type of response you're asking for, ie. a JSON response.

deep belfry
#

Yes, I understand, I'm already requesting a JSON response, that's the issue

#

it's still trying to redirect me, although I'm requesting a JSON response

vocal night
#

So, did you overwrite the LoginResponse? Or do you have some other customizations?

deep belfry
#

I only tried overwriting it. And also the default configuration for fortify doesn't seem to fix the issue

#

(I had set home to null)

vocal night
#

So it really sounds like you've made some modifications somewhere

#

Perhaps even on your server? So outside of PHP

#

Or maybe some other middleware that's performing a redirect

deep belfry
#

There doesn't seem to be anything that would trigger such a behavior

vocal night
#

And what does you FortifyServiceProvider look like? Please use code-snippets, as these screenshots are barely readable, plus it's a lot more work to copy/paste code

deep belfry
#
<?php

namespace App\Providers;

use App\Actions\Fortify\UpdateUserProfileInformation;
use App\Actions\Fortify\UpdateUserPassword;
use Illuminate\Support\Facades\RateLimiter;
use App\Actions\Fortify\ResetUserPassword;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\ServiceProvider;
use App\Actions\Fortify\CreateNewUser;
use Laravel\Fortify\Fortify;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Laravel\Fortify\Contracts\LoginResponse;

class FortifyServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        //
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        Fortify::createUsersUsing(CreateNewUser::class);
        Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
        Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
        Fortify::resetUserPasswordsUsing(ResetUserPassword::class);

        RateLimiter::for('login', function (Request $request) {
            $throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())) . '|' . $request->ip());

            return Limit::perMinute(5)->by($throttleKey);
        });

        RateLimiter::for('two-factor', function (Request $request) {
            return Limit::perMinute(5)->by($request->session()->get('login.id'));
        });
    }
}

It's the default one

#

(with some use artifacts from when I tried to overwrite the toResponse for /login)

vocal night
#

I guess try to debug a bit? Really doesn't make sense if you're using the default setup, as it has this built-in

deep belfry
#

Yeah, same reason as to why I'm so confused. I'll keep trying different things and report back if I find anything relevant

vocal night
#

First step would be just dumping values, see if $request->wantsJson() returns true, see what the default LoginResponse does and returns as a response

deep belfry
#

yeah, It's the first thing I did, wouldn't have asked here otherwise 🤣

deep belfry
#

I've found that when the client is authenticated (has already received a token and is valid?) it doesn't even pass through the Fortify Service Provider anymore, it seems like theres something else in the way

#

probably Sanctum

vocal night
deep belfry
#

yeah, I feel that 💀

#

This is my current testing ground:

// FortifyServiceProvider.php

<?php

namespace App\Providers;

use App\Actions\Fortify\UpdateUserProfileInformation;
use App\Actions\Fortify\UpdateUserPassword;
use Illuminate\Support\Facades\RateLimiter;
use App\Actions\Fortify\ResetUserPassword;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\ServiceProvider;
use App\Actions\Fortify\CreateNewUser;
use Laravel\Fortify\Fortify;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Laravel\Fortify\Contracts\LoginResponse;

class FortifyServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        $this->app->instance(
            LoginResponse::class,
            new class implements LoginResponse
            {
                public function toResponse($request)
                {
                    dd($request->wantsJson());
                    return $request->wantsJson()
                        ? response()->json(['two_factor' => false])
                        : redirect()->intended(Fortify::redirects('login'));
                }
            }
        );
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        Fortify::createUsersUsing(CreateNewUser::class);
        Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
        Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
        Fortify::resetUserPasswordsUsing(ResetUserPassword::class);

        RateLimiter::for('login', function (Request $request) {
            $throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())) . '|' . $request->ip());

            return Limit::perMinute(5)->by($throttleKey);
        });

        RateLimiter::for('two-factor', function (Request $request) {
            return Limit::perMinute(5)->by($request->session()->get('login.id'));
        });
    }
}
#
// JSONResponse Middleware
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class JSONResponse
{
    public function handle(Request $request, Closure $next)
    {
        $request->headers->set('Accept', 'application/json');
        dd($request);

        return $next($request);
    }
}
#

The dd from the middleware gets triggered before Fortify Service Provider, everything checks out, Accept: application/json is correct

#

however, when I comment the dd out from the middleware, the FortifyServiceProvider doesn't get it's dd triggered

#

which would otherwise show that the $request does indeed wantsJson()

#

the only other thing that is interacting with authentication is Sanctum let me add that to the tags, but the Sanctum config is also untouched

deep belfry
#

Interesting

curl 'http://localhost:8081/login' -X POST -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:135.0) Gecko/20100101 Firefox/135.0' -H 'Accept: application/json' -H 'Accept-Language: en-US,en;q=0.5' -H 'Accept-Encoding: gzip, deflate, br, zstd' -H 'Referer: http://localhost:5173/' -H 'content-type: application/json' -H 'Origin: http://localhost:5173' -H 'Connection: keep-alive' -H 'Cookie: theme=dark; XSRF-TOKEN=eyJpd...nIjoiIn0%3D; nebula_backend_session=eyJpdiI...In0%3D' -H 'Sec-Fetch-Dest: empty' -H 'Sec-Fetch-Mode: cors' -H 'Sec-Fetch-Site: same-site' -H 'Priority: u=0' --data-raw '{"email":"test@example.com","password":"asdfasdf"}'
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="refresh" content="0;url='http://localhost:8081'" />

        <title>Redirecting to http://localhost:8081</title>
    </head>
    <body>
        Redirecting to <a href="http://localhost:8081">http://localhost:8081</a>.
    </body>
</html>%  
#

This leads me to src/vendor/symfony/http-foundation/RedirectResponse.php, which is required by fruitcake/php-cors, which is a laravel/framework requirement

#

If I remove the session token it goes through without redirect

#

That session token name is generated from config/session.php

#

and that config is used in the SessionManager from the laravel/framework (in /vendor/laravel/framework/src/Illuminate/Session/SessionManager.php)

#

My specific configuration uses the a database driver for the session handling

#
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=localhost
deep belfry
#

this is still an issue

deep belfry
#

After some more investigation I've found this method in the laravel framework:

// vendor/laravel/framework/src/Illuminate/Foundation/Configuration/Middleware.php
public function getMiddlewareGroups()
    {
        $middleware = [
            'web' => array_values(array_filter([
                \Illuminate\Cookie\Middleware\EncryptCookies::class,
                \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
                \Illuminate\Session\Middleware\StartSession::class,
                \Illuminate\View\Middleware\ShareErrorsFromSession::class,
                \Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
                \Illuminate\Routing\Middleware\SubstituteBindings::class,
                $this->authenticatedSessions ? 'auth.session' : null,
            ])),

            'api' => array_values(array_filter([
                $this->statefulApi ? \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class : null,
                $this->apiLimiter ? 'throttle:'.$this->apiLimiter : null,
                \Illuminate\Routing\Middleware\SubstituteBindings::class,
            ])),
        ];

        $middleware = array_merge($middleware, $this->groups);

...

the api and web got me thinking...

I then spent some time searching for this functionality in the docs and sure enough:
https://laravel.com/docs/11.x/middleware#laravels-default-middleware-groups

However this section states:

Remember, Laravel automatically applies these middleware groups to the corresponding routes/web.php and routes/api.php files

Given that this is my routes/api.php file:

<?php

use Illuminate\Support\Facades\Route;


Route::resource('user', \App\Http\Controllers\UserController::class);

There must've been something else going on...

#

After some playing around I found that the issue had to do with how fortify was configured in my current setup, this was the fix:

diff --git a/src/config/fortify.php b/src/config/fortify.php
index 6218edc..d7e93a1 100644
--- a/src/config/fortify.php
+++ b/src/config/fortify.php
@@ -101,7 +101,7 @@
     |
     */
 
-    'middleware' => ['web'],
+    'middleware' => ['api'],

This begs the question: "Is this intended behavior?"

I have yet to find any documentation on why this change is needed and my search through the source code was inconclusive

#

@vocal night Sorry for the ping, I think these findings might interest you.
is there an explanation as to why this functions this way? despite the documentation saying otherwise?