How to Add Sign in With GitHub to Wave

How to Add Sign in With GitHub to Wave

Written by Kim Hallberg on Nov 12th, 2021 Views Report Post

Introduction

In this tutorial, we will cover how to add social login to Wave. For this example, we will use GitHub as our OAuth provider.

We will start by adding Laravel's Socialite package. This package is designed for quickly and conveniently authenticating with an OAuth provider.

composer require laravel/socialite

Boostrapping Socialite

Next, we will follow the steps given to us via the Socialite documentation. Namely, setting up our environment variables. Linking those together with our services config. Creating our controllers and lastly our routes.

Let's start by updating our .env and .env.example by adding these three variable keys.

GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GITHUB_REDIRECT_URL=

Note that we're not adding the secret keys to the .env.example file and only empty placeholders.

Moving on to our config/services.php file, let us add the correct configuration so that Socialite gets the GitHub OAuth keys.

'github' => [
    'client_id'     => env('GITHUB_CLIENT_ID'),
    'client_secret' => env('GITHUB_CLIENT_SECRET'),
    'redirect'      => env('GITHUB_REDIRECT_URL'),
]

Before we go over to GitHub and get our secret keys, let us generate a new empty controller and add some routes.

php artisan make:controller Auth\\GitHubSocialiteController

This command will generate an empty controller in our existing Controller\Auth folder. We will quickly add two empty methods called redirect and callback to it. There will be the ones our routes will call when hit.

class GitHubSocialiteController extends Controller
{
    public function redirect() {}

    public function callback() {}
}

Now in our routes/web.php, we can add our two routes. Adding a name to our redirect endpoint makes it easier to generate a URL for it later.

Route::middleware('guest')->group(function () {
    Route::get('auth/github/redirect', [GitHubSocialiteController::class, 'redirect'])->name('github.login');
    Route::get('auth/github/callback', [GitHubSocialiteController::class, 'callback']);    
});

The reason for adding the guest middleware is that we don't want authenticated users to try and logged-in again when they're already logged in.

Registering a GitHub OAuth App

Now that we have the boilerplate for Socialie done, we can head over to GitHub and register our new OAuth app.

Screenhot of GitHub's register a new OAuth application page

Fill out the information with your application's name and appropriate URLs. The callback URL should match the URL you gave to the callback route.

After registering the new app with GitHub, we get redirected to the applications' general settings. Here we can grab our applications' client ID and generate a new client secret.

Keep in mind that you'll only see the applications' client secret ones. So make sure you copy it after generating it. Otherwise, you'll have to delete the old secret and generate a new one.

Screenshot of generating a client secret key

With these keys generated, we can now update our .env variables with those keys. Of course, your keys and callback URL will be different.

GITHUB_CLIENT_ID=4f636859a54b7410a51f
GITHUB_CLIENT_SECRET=26ba921db4bb0a8dca6c733de5705266ed28346d
GITHUB_REDIRECT_URL='https://wave.test/auth/github/callback'

With these keys set, Laravel Socialite can now create the correct payload to send to GitHub when a user's trying to authenticate via our application.

Extending our User Model

Now that we've done the Socialite boilerplate and registered our new OAuth application. Our next step is to prepare our user model to store our id provided by GitHub.

We do this by generating a new migration where we add our new column.

php artisan make:migration AddProviderIdToUsersTable

This command will generate a new migration named Y_m_d_His_add_provider_id_to_users_table.php. Modify the new file and add the new varchar column named provider_id.

The column should be nullable since not all users will use the feature. Add unique since GitHub will not return the same id for different users so we will reflect that in our column.

class AddProviderIdToUsersTable extends Migration
{
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('provider_id')->nullable()->unique();
        });
    }

    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('provider_id');
        });
    }
}

Last but not least, add the provider_id to the App\Users' fillable property. That way, we can easily add it when creating our user later.

/**
 * @var array
 */
protected $fillable = [
    ...,
    'verified',
    'trial_ends_at',
    'provider_id',
];

Authenticating with GitHub

The time has finally come to connect these pieces together and start authenticating against GitHub when a user hits the auth/redirect endpoint.

We will start with the simplest of additions to our code, a single line in our GitHubSocialiteController controller.

Update your redirect method and add the following line of code.

public function redirect()
{
    return Socialite::driver('github')->redirect();
}

Lets' not forget to also use the Socialite facade, if your IDE or editor has auto-complete you can skip this since it's already there.

use Laravel\Socialite\Facades\Socialite;

With this Socialite will generate the necessary payload so when a user travels to the auth/redirect endpoint.

It will then redirect to GitHub, ask for the user permission to authenticate the application, and grant them access to the users' identity.

Screenhot of GitHub's OAuth authentication process

Handling the OAuth Callback

Our last section will be the one with the most code. That is because we have things to consider in this callback state.

We first need to get the user identity provided by GitHub, we also need to consider Wave's trial functionality and give the user our default role.

Luckily, that isn't so difficult. Since the code for that has already been written by the maintainers of Wave we can simply copy that to match the functionality.

Update the callback method in your GitHubSocialiteController controller and add the following code.

public function callback()
{
    $github = Socialite::driver('github')->user();
    $role   = Role::where('name', '=', config('voyager.user.default_role'))->first();

    $trial_days    = setting('billing.trial_days', 14);
    $trial_ends_at = null;

    if (intval($trial_days) > 0) {
        $trial_ends_at = now()->addDays(setting('billing.trial_days', 14));
    }
}

Stepping through the code a bit. Firstly using the Socialite facade to get the identity provided by GitHub. Secondly, we use Voyager's Role model, and using the config we can find the default role, we'll need that soon.

Last but not least we get the trial days defined in our site settings. We will also set the trial_ends_at variable to null as a default. We will only overwrite that if our trial days are greater than 0.

Meaning if we've set our trial days to -1, effectively negating them we don't set an end date and leave it null.

We're almost done with this tutorial. We are finally going to tackle the last real hard part. Consider this scenario, we've already registered several users before we add this feature.

Now, what happens when an already existing user tries to sign in with GitHub on our application? Well, their email is probably the same, maybe their username is also the same? So adding them again will create a duplicate entry error.

Maybe we only add the provider_id to their record? That will only work for existing users though. For new, users signing up for the first time via GitHub, we need to add their email, username, etc.

We will have three states to contend with, existing users, new users, and returning users. Lets' handle all three scenarios step-by-step, starting with returning users.

First, we need to check if any user in our records already has the id added, if that's the case we can simply return that user and sign them in.

Update your callback method with the following code.

$user = User::query()
    ->where('provider_id', $github->getId())
    ->first();

if ($user !== null) {
    return $this->login($user, 'Successfully logged in.');
}

For now, you don't have to worry about the login method, I will show that last since it's a method we will call in more than one place.

Next, we will tackle existing users. This is for the most part a carbon copy of the last scenario concerning returning users. The difference is we're checking for a matching email address, and if found we add the provider id and log the user in.

Add the snippet below the last one in your callback method.

$user = User::query()
        ->where('email', $github->getEmail())
        ->first();

if ($user !== null) {
    $user->update([
        'provider_id' => $github->getId(),
    ]);

    return $this->login($user, 'Successfully logged in.');
}

Two scenarios have not been handled. we'll now handle the last and final scenario, the new user. This scenario is where we'll use the $role and $trial_ends_at variables we defined before.

Add this final code snippet to your callback method.

$user = User::query()->create([
    'provider_id'   => $github->getId(),
    'email'         => $github->getEmail(),
    'name'          => $github->getName(),
    'password'      => bcrypt(str_random()),
    'username'      => $github->getNickname(),
    'verified'      => 1,
    'trial_ends_at' => $trial_ends_at,
    'role'          => $role->id,
]);

event(new Registered($user));

return $this->login($user, 'Thanks for signing up!');

Taking full advantage of the identity provided for us by GitHub, we get an email for the user that's already verified by GitHub, a name, and a username. We only need to add the default role using the id and add the calculated trial end date.

A caveat here though, new users will get a randomly generated password. For safety reasons we won't email it to them since it's never a good idea to send plain text passwords over the wire.

So new users added via GitHub will have to reset their password via the forgot your password feature.

Before logging the new user in we send off Laravel's registered event.

Now before I end this post off. I will give you the last piece of this puzzle.

The login method, it's simply a DRY private method used to log the user in and redirect to the wave dashboard with a success toast message.

private function login($user, $message) {
    auth()->guard()->login($user, false);

    return redirect()
        ->route('wave.dashboard')
        ->with([
            'message'      => $message,
            'message_type' => 'success',
        ]);
}

Conclusion

And there you have it, now all you need to do is add some sign-in or sign-up with GitHub buttons on your website, here's a quick example that works with Wave out-of-the-box.

<a href="{{ route('github.login') }}" class="flex w-full items-center justify-center space-x-4 mt-4 py-4 px-8 text-white bg-gray-800 shadow-lg rounded-md font-medium text-lg">
    <svg class="w-8 h-8 fill-current" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
        <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
    </svg>
    <span>
        Sign in with GitHub
    </span>
</a>

As before, thank you for reading and the full source code is available on my GitHub under the wave-with-socialite repository.

Have a great day. 👋

Comments (0)