Allow Users to Login via Email or Username in Laravel - Kevin McKee
Published on

Allow Users to Login via Email or Username in Laravel

Author

Most people who use Laravel applications use the standard authentication system that ships along with the Laravel framework. It's an amazingly simple way to get up and running with authentication.

However, the default implementation has users register with their email address instead of a username. You may find yourself wanting to allow a user to login with either an email address or a username.

In fact, I prefer how WordPress handles this. In WordPress each user account has an email and a username, and when logging in you can enter either one along with your password to get authenticated.

Let's see how to do this in Laravel.

Before we go any further, I need to give basically all the credit here to Aaron Saray. He helped me greatly in finding this solution.

I am assuming you have a Laravel application already and have implemented authentication with:

php artisan make:auth

First, we will need to add a migration to add the username to the users table.

php artisan make:migration add_username_to_users_table

Then complete the migration with the following:

public function up()
{
    Schema::table('users', function (Blueprint $table) {
        $table->string('username')->after('email')->nullable()->unique();
        $table->string('email')->change()->nullable();
    });
}

In my case, I made the email nullable because a good portion of the users of my application do not have email addresses. If you believe this website then only about 90% of internet users actually use email at least monthly.

After we run this migration using

php artisan migrate

you can go into your database and add a username to any account. You will certainly want to update your UI to allow people to register with a username, but I'm not going to cover that in this post.

However, we do need to make sure if someone has a username in the database, they can use it to login. To do this, we need to update the AuthServiceProvider.

First, lets create a new service provider with

php artisan make:provider CustomAuthServiceProvider

Here we want to extend the EloquentUserProvider, so it will look like this:

class CustomAuthServiceProvider extends EloquentUserProvider

Now we are going to just make a change to one method: the retrieveByCredentials method. Go into your EloquentUserProvider, find the retrieveByCredentials method, copy it and paste it into your CustomAuthServiceProvider.

In fact, we just need to update a single line. Find the line that says this:

$query->where($key, $value);

And change it to this

$query->where($key, $value)->orWhere('username', $value);

Now when someone logs in, Laravel will take what the user entered as their email/username, and try to match it against either the email OR the username column.

The whole file should look like this:

<?php

namespace App\Providers;

use Illuminate\Auth\EloquentUserProvider;
use Illuminate\Contracts\Support\Arrayable;
use Str;

// make sure to extend EloquentUserProvider here
class CustomAuthServiceProvider extends EloquentUserProvider
{
    /**
     * Retrieve a user by the given credentials.
     *
     * @param  array  $credentials
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveByCredentials(array $credentials)
    {
        if (empty($credentials) ||
            (count($credentials) === 1 &&
                array_key_exists('password', $credentials))) {
            return;
        }

        // First we will add each credential element to the query as a where clause.
        // Then we can execute the query and, if we found a user, return it in a
        // Eloquent User "model" that will be utilized by the Guard instances.
        $query = $this->newModelQuery();
        foreach ($credentials as $key => $value) {
            if (Str::contains($key, 'password')) {
                continue;
            }
            if (is_array($value) || $value instanceof Arrayable) {
                $query->whereIn($key, $value);
            } else {
                $query->where($key, $value)->orWhere('username', $value); // here is the change
            }
        }

        return $query->first();
    }
}

Now the last thing to do is make sure Laravel actually uses this new provider. To do that, let's open up the AuthServiceProvider and add the following to the boot method.

Auth::provider('custom', function ($app) {
    return new CustomAuthServiceProvider($app->make(HasherContract::class), User::class);
});

You do need to pass in the HasherContract and User because the constructor of the EloquentUserProvider requires them. Here's the full file. Take note of the use statement with the HasherContract.

<?php

namespace App\Providers;

use App\User;
use Illuminate\Contracts\Hashing\Hasher as HasherContract; // make sure to add this
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        'App\Model' => 'App\Policies\ModelPolicy',
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        // here's the new stuff
        Auth::provider('custom', function ($app) {
            return new CustomAuthServiceProvider($app->make(HasherContract::class), User::class);
        });
    }
}

Now let's update the auth.php config file to point to this new provider. Find your 'providers' array and update the driver to custom.

// config/auth.php

'providers' => [
    'users' => [
        'driver' => 'custom', // updated from eloquent
        'model' => App\User::class,
    ],
],

At this point everything should be working. If you haven't made any changes to your login form, you will probably need to remove HTML email validation on the 'email' field. Now it should be able to accept a regular string value.

Now you can add a username to any user in your database and test. You should be able to enter the email OR the username and either one will work as long as you enter the correct password.

If you have any questions I can try to help, but again Aaron Saray is the true expert here!

Other Implications

If you do plan to take this approach, there are a few things to consider:

  • For any user account that does not have an email, you cannot send them notifications. Your app may have any number of notifications to consider, but even in this simple case they don't have any way to reset their password.
  • You should use your application logic to ensure you never have a user account with NULL in both the email and username fields.
  • Similarly, you will probably want to make sure entries in the username field do not overlap with emails from other accounts. For example, if there is a user account with their email as joe@test.com, you shouldn't allow a different user to signup with their username as joe@test.com.

Want to talk about this post? Connect with me on Twitter →