#Fortify redirecting after login
47 messages · Page 1 of 1 (latest)
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
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)?
Not sure what you mean, the default setup would already account for that; https://github.com/laravel/fortify/blob/1.x/src/Http/Responses/LoginResponse.php#L18-L19
Meaning, it will return a JSON response if you ask for a JSON response
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.
https://laravel.com/docs/11.x/fortify#customizing-authentication-redirects
Suggests a way to override the redirect, and it "works", but the redirect is always relative to the API domain, not the front-end domain
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
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
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.
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
So, did you overwrite the LoginResponse? Or do you have some other customizations?
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)
That doesn't make much sense tho, as you could see, it would account for that header and respond appropriately; https://github.com/laravel/fortify/blob/1.x/src/Http/Responses/LoginResponse.php#L18-L19
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
There doesn't seem to be anything that would trigger such a behavior
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
<?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)
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
Yeah, same reason as to why I'm so confused. I'll keep trying different things and report back if I find anything relevant
First step would be just dumping values, see if $request->wantsJson() returns true, see what the default LoginResponse does and returns as a response
yeah, It's the first thing I did, wouldn't have asked here otherwise 🤣
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
You never know around here, plenty of people come with questions like "I have 500 response" 😂
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
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
this is still an issue
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.phpandroutes/api.phpfiles
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?