Login Redirect Loop on Laravel in Production
Introduction
Some days ago I finished developing some first working features of a web application in Laravel. Working fine locally, authentication, some authorization roles, all fine and dandy.
When I deployed it to the live, production server, the public area could be accessed just fine, but the admin area was impossible to reach: an infinite redirect loop.
Why would login work locally but not on the production server‽ Well, let’s see.
Debugging
I started a simple debugging process which was painful because I had to change something locally and upload to production to see what would happen, since the local app was OK and did not present the same login infinite redirect loop problem.
I disabled the auth
middleware for the admin routes. No change same problem. Then I disabled a middleware I had created myself to handle roles. It was called admin
and it is like this:
public function handle($request, Closure $next)
{
if (Auth::check() && (Auth::user()->isEditor() || Auth::user()->isAdmin())) {
return $next($request);
}
return redirect('/login');
}
My User
model has those isAdmin
and isEditor
methods defined like so:
public function isAdmin()
{
return $this->has_role === 1;
}
public function isEditor()
{
return $this->has_role === 2;
}
Simple enough, right‽ But do note the triple equal sign comparison operator I am using.
Let’s Tinker for a Moment…
I used tinker
on my local and production environments. Take a look:
I did immediately spot the difference. The local development returns has_role
as numbers, integers, while the production side says they are strings! How come‽ Moreover, recall I am using triple equal sign comparison operators in my Admin
middleware? Yes, those two things combined resulted in the infinite redirect loop problem for the admin area.
I then changed ===
with ==
on the Admin middleware methods, and indeed, the problem on the production side was gone. Everything working fine!
The question is why has_role
values were integers locally, and strings on the production environment.
Why Different Data types?
When I deployed the application, I didn’t have seeds for the admin users, and I thought it would be just fine if I exported the local users from the DB and imported them on the production database, therefore, I did this (I am using MariaDB):
mysqldump -h localhost -u devel_user -ps3cr37 my_devel_db users | \
mysql -h server.db.net -u prod_user -ps3cr37 -D my_prod_db
I thought, “of course, the data traveled through the network (mysql client/server communication happens through TCP) and became strings!” Then I tried doing this on the server:
// Attempt to update has_role to 1, a number.
\App\User::find(1)->update(['has_role' => 1]);
\App\User::all()->pluck('has_role');
// → "1"
// → "2"
Nope, the numeric strings were still there… Then I tried through the mysql cli itself:
UPDATE users SET has_role = 1 WHERE id = 1;
But the problem persisted. PHP, Tinker and the Laravel application still thinking that 1
in the database is "1"
. No, the problem doesn’t seem to be because of the way I populated the users
table on the server.
PDO + mysql vs PDO + mysqlnd
Researching a little more, I found some people saying that PHP + PDO with the mysql extension poses that problem, while mysqlnd extension does not.
Indeed, doing php --info
showed this on my local, development box (running Arch Linux):
PDO Driver for MySQL => enabled
Client API version => mysqlnd 5.0.12-dev - 20150407 - $Id: b396954eeb2d1d9ed7902b8bae237b287f21ad9e
And this on the server:
PDO Driver for MySQL => enabled
Client API version => 5.6.36-82.0
The “Solution”
I decided the solution for now was just to use ==
instead of ===
in my Admin middleware. I have other applications on this server and would probably be unwise to attempt installing the mysqlnd
extension.
The lingering question is: what would have been the best approach in the first place that could have avoided the problem and saved me some hours of debugging and experimenting?
I am not perfect and still seeking enlightenment…