Skip to content

Commit 74cd344

Browse files
Added case-sensitivity option for usernames (#485)
* Added case sensitivity option for usernames. Introduced a new configuration option 'case_sensitive_usernames'. This feature allows to decide whether usernames should be treated as case-sensitive. The default configuration value is true to prevent backwards-compatibility breaks. If it is set to false, the system will convert all usernames to lowercase before storing or comparing them. This prevents issues with case-sensitive database collations, where users would not enter their email address in the exact variation used at registration. Accompanying tests and implementation have been updated or added accordingly. * Removed legal disclaimer * Removed superfluous function imports * formatting --------- Co-authored-by: Taylor Otwell <[email protected]>
1 parent 14d3df0 commit 74cd344

9 files changed

+124
-0
lines changed

config/fortify.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
'home' => '/home',
1414
'prefix' => '',
1515
'domain' => null,
16+
'lowercase_usernames' => false,
1617
'limiters' => [
1718
'login' => null,
1819
],

src/Actions/CanonicalizeUsername.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace Laravel\Fortify\Actions;
4+
5+
use Illuminate\Support\Str;
6+
use Laravel\Fortify\Fortify;
7+
8+
class CanonicalizeUsername
9+
{
10+
/**
11+
* Handle the incoming request.
12+
*
13+
* @param \Illuminate\Http\Request $request
14+
* @param callable $next
15+
* @return mixed
16+
*/
17+
public function handle($request, $next)
18+
{
19+
$request->merge([
20+
Fortify::username() => Str::lower($request->{Fortify::username()}),
21+
]);
22+
23+
return $next($request);
24+
}
25+
}

src/Http/Controllers/AuthenticatedSessionController.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Illuminate\Routing\Controller;
88
use Illuminate\Routing\Pipeline;
99
use Laravel\Fortify\Actions\AttemptToAuthenticate;
10+
use Laravel\Fortify\Actions\CanonicalizeUsername;
1011
use Laravel\Fortify\Actions\EnsureLoginIsNotThrottled;
1112
use Laravel\Fortify\Actions\PrepareAuthenticatedSession;
1213
use Laravel\Fortify\Actions\RedirectIfTwoFactorAuthenticatable;
@@ -83,6 +84,7 @@ protected function loginPipeline(LoginRequest $request)
8384

8485
return (new Pipeline(app()))->send($request)->through(array_filter([
8586
config('fortify.limiters.login') ? null : EnsureLoginIsNotThrottled::class,
87+
config('fortify.lowercase_usernames') ? CanonicalizeUsername::class : null,
8688
Features::enabled(Features::twoFactorAuthentication()) ? RedirectIfTwoFactorAuthenticatable::class : null,
8789
AttemptToAuthenticate::class,
8890
PrepareAuthenticatedSession::class,

src/Http/Controllers/ProfileInformationController.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
use Illuminate\Http\Request;
66
use Illuminate\Routing\Controller;
7+
use Illuminate\Support\Str;
78
use Laravel\Fortify\Contracts\ProfileInformationUpdatedResponse;
89
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
10+
use Laravel\Fortify\Fortify;
911

1012
class ProfileInformationController extends Controller
1113
{
@@ -19,6 +21,12 @@ class ProfileInformationController extends Controller
1921
public function update(Request $request,
2022
UpdatesUserProfileInformation $updater)
2123
{
24+
if (config('fortify.lowercase_usernames')) {
25+
$request->merge([
26+
Fortify::username() => Str::lower($request->{Fortify::username()}),
27+
]);
28+
}
29+
2230
$updater->update($request->user(), $request->all());
2331

2432
return app(ProfileInformationUpdatedResponse::class);

src/Http/Controllers/RegisteredUserController.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
use Illuminate\Contracts\Auth\StatefulGuard;
77
use Illuminate\Http\Request;
88
use Illuminate\Routing\Controller;
9+
use Illuminate\Support\Str;
910
use Laravel\Fortify\Contracts\CreatesNewUsers;
1011
use Laravel\Fortify\Contracts\RegisterResponse;
1112
use Laravel\Fortify\Contracts\RegisterViewResponse;
13+
use Laravel\Fortify\Fortify;
1214

1315
class RegisteredUserController extends Controller
1416
{
@@ -51,6 +53,12 @@ public function create(Request $request): RegisterViewResponse
5153
public function store(Request $request,
5254
CreatesNewUsers $creator): RegisterResponse
5355
{
56+
if (config('fortify.lowercase_usernames')) {
57+
$request->merge([
58+
Fortify::username() => Str::lower($request->{Fortify::username()}),
59+
]);
60+
}
61+
5462
event(new Registered($user = $creator->create($request->all())));
5563

5664
$this->guard->login($user);

stubs/fortify.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,19 @@
5050

5151
'email' => 'email',
5252

53+
/*
54+
|--------------------------------------------------------------------------
55+
| Lowercase Usernames
56+
|--------------------------------------------------------------------------
57+
|
58+
| This value defines whether usernames should be lowercased before saving
59+
| them in the database, as some database system string fields are case
60+
| sensitive. You may disable this for your application if necessary.
61+
|
62+
*/
63+
64+
'lowercase_usernames' => true,
65+
5366
/*
5467
|--------------------------------------------------------------------------
5568
| Home Path

tests/AuthenticatedSessionControllerTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,26 @@ public function test_two_factor_challenge_requires_a_challenged_user()
428428
$this->assertNull(Auth::getUser());
429429
}
430430

431+
public function test_case_insensitive_usernames_can_be_used()
432+
{
433+
app('config')->set('fortify.lowercase_usernames', true);
434+
435+
$this->loadLaravelMigrations(['--database' => 'testbench']);
436+
437+
TestAuthenticationSessionUser::forceCreate([
438+
'name' => 'Taylor Otwell',
439+
'email' => '[email protected]',
440+
'password' => bcrypt('secret'),
441+
]);
442+
443+
$response = $this->withoutExceptionHandling()->post('/login', [
444+
'email' => '[email protected]',
445+
'password' => 'secret',
446+
]);
447+
448+
$response->assertRedirect('/home');
449+
}
450+
431451
protected function getPackageProviders($app)
432452
{
433453
return [FortifyServiceProvider::class];

tests/ProfileInformationControllerTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,26 @@ public function test_contact_information_can_be_updated()
2323

2424
$response->assertStatus(200);
2525
}
26+
27+
public function test_email_address_will_be_updated_case_insensitive()
28+
{
29+
app('config')->set('fortify.lowercase_usernames', true);
30+
31+
$user = Mockery::mock(Authenticatable::class);
32+
33+
$this->mock(UpdatesUserProfileInformation::class)
34+
->shouldReceive('update')
35+
->with($user, [
36+
'name' => 'Taylor Otwell',
37+
'email' => '[email protected]',
38+
])
39+
->once();
40+
41+
$response = $this->withoutExceptionHandling()->actingAs($user)->putJson('/user/profile-information', [
42+
'name' => 'Taylor Otwell',
43+
'email' => '[email protected]',
44+
]);
45+
46+
$response->assertStatus(200);
47+
}
2648
}

tests/RegisteredUserControllerTest.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,29 @@ public function test_users_can_be_created_and_redirected_to_intended_url()
5252

5353
$response->assertRedirect('http://foo.com/bar');
5454
}
55+
56+
public function test_usernames_will_be_stored_case_insensitive()
57+
{
58+
app('config')->set('fortify.lowercase_usernames', true);
59+
60+
$this->mock(CreatesNewUsers::class)
61+
->shouldReceive('create')
62+
->with([
63+
'email' => '[email protected]',
64+
'password' => 'password',
65+
])
66+
->once()
67+
->andReturn(Mockery::mock(Authenticatable::class));
68+
69+
$this->mock(StatefulGuard::class)
70+
->shouldReceive('login')
71+
->once();
72+
73+
$response = $this->post('/register', [
74+
'email' => '[email protected]',
75+
'password' => 'password',
76+
]);
77+
78+
$response->assertRedirect('/home');
79+
}
5580
}

0 commit comments

Comments
 (0)