DEV Community

Cover image for Adding Social Logins to Your Laravel Apps: Twitter and GitHub
Ash Allen
Ash Allen

Posted on • Originally published at ashallendesign.co.uk

Adding Social Logins to Your Laravel Apps: Twitter and GitHub

Introduction

In your Laravel applications, you would typically provide the functionality for your users to register and sign in using traditional email and password forms. But, there may be times when you want to allow users to sign in to your apps using third-party services such as Twitter, GitHub, and Google.

In this guide, we're going to look at the basics of how you can use Laravel Socialite to allow your users to sign in to your Laravel app using Twitter.

What is OAuth and Socialite?

Before we get started, it's worthwhile taking a step back and understanding what Laravel Socialite is and how it works. Socialite is a first-party package provided by the Laravel team that allows you to authenticate with OAuth providers, such as: Twitter, GitHub, GitLab, BitBucket, Facebook, LinkedIn, and Google.

There's also a community-driven site called Socialite Providers which provide support for even more OAuth providers such Apple, Instagram, and Dribbble.

If you haven't heard of OAuth before, you should still be able to follow this guide thanks to Socialite doing the majority of the heavy lifting for us. Essentially, according to Wikipedia, OAuth (Open Authorization) is an "open standard for access delegation, commonly used as a way for internet users to grant websites or applications access to their information on other websites but without giving them the passwords". If you've ever seen any sites that say "Sign in with Google", "Sign in with Twitter", etc, then you'll have likely followed an OAuth workflow.

In this particular guide, we're going to be using the newer OAuth 2.0 implementation rather than the older OAuth 1.0 implementation. If you're interested in finding out what the differences are between the two version, you can check out the Differences Between OAuth 1 and 2 article.

Signing in Using Twitter

Creating the App in Twitter

Before we touch any code in our Laravel project, we'll first need to set up a new Twitter app over at https://developer.twitter.com.

If you haven't already registered, you'll need to register and then head to the dashboard to create a new project.

After you've created your new project, you'll then need to create a new Twitter app and enable OAuth 2.0 for it. When enabling OAuth for your app, you will likely want to set your "Type of App" as "Web App". When adding your "Callback URI / Redirect URL", you will want to enter the exact URL that your users should be redirected to after allowing access to Twitter (we will cover this in more depth further down). In this particular tutorial, we will be using http://localhost/auth/callback/twitter as our callback URI. However, you'll need to make sure that you add your live server's URL here too, otherwise it will only work on your local development site. For example, if your site is hosted at https://my-awesome-app.com, you'll want to add the localhost URL and also add https://my-awesome-app.com/auth/callback/twitter.

For a more in-depth guide of how to set up the project and app in Twitter, you can check out the Projects documentation on Twitter.

It's also worth noting that if you want access to the user's email address (which you likely will want), you'll need to apply for "Elevated Access" for your project. Without the extra permission, you won't be able to view your user's email address.

Installing Socialite

To get started with using Socialite, you'll need to install the laravel/socialite package using the following command:

composer require laravel/socialite
Enter fullscreen mode Exit fullscreen mode

You'll then want to add your Twitter project's credentials and our callback URL to your config/services.php config file like using the twitter-oauth-2 field like so:

return [

    // ...

    'twitter' => [
        'client_id' => env('TWITTER_CLIENT_ID'),
        'client_secret' => env('TWITTER_CLIENT_SECRET'),
        'redirect' => env('OAUTH_CALLBACK_URL'),
    ],

    // ...

];
Enter fullscreen mode Exit fullscreen mode

In your .env file, you'll then be able to add the fields:

TWITTER_CLIENT_ID=client-id-goes-here
TWITTER_CLIENT_SECRET=client-secret-goes-here
OAUTH_CALLBACK_URL=http://localhost/auth/callback/twitter
Enter fullscreen mode Exit fullscreen mode

It's important to remember that your OAUTH_CALLBACK_URL field must be an absolute URL. For example, you would need to use http://localhost/auth/callback/twitter rathen than just /auth/callback/twitter.

Preparing the Database

Now that we have the app set up on Twitter and have Socialite configured, we can set up our database to handle Socialite. We'll want to keep track of whether a user was registered using Socialite and may also want to keep track of their tokens if we want to make API requests on the user's behalf.

For the purpose of this guide, I'm going to assume that you don't have a users table in your database, or a migration to create the table yet. So, we'll start by making a new database migration to create this table by using the following command:

php artisan make:migration create_users_table --create=users
Enter fullscreen mode Exit fullscreen mode

This should create a new migration for us in our project's database/migrations folder. We'll then update this migration to look sopmething like this:

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email');
            $table->string('password');
            $table->string('avatar_url');
            $table->string('twitter_id')->nullable();
            $table->string('twitter_token')->nullable();
            $table->string('twitter_refresh_token')->nullable();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
};
Enter fullscreen mode Exit fullscreen mode

We can then run this migration and add the users table to our database using the following command:

php artisan migrate
Enter fullscreen mode Exit fullscreen mode

Preparing the Model

Now that the datbase is migrated, we can create our User model. We'll do this by running the following command:

php artisan make:model User
Enter fullscreen mode Exit fullscreen mode

We can then update our model to look like so:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    protected $fillable = [
        'name',
        'email',
        'password',
        'avatar_url',
        'twitter_id',
        'twitter_token',
        'twitter_refresh_token',
    ];

    protected $hidden = [
        'password',
        'remember_token',
        'twitter_token',
        'twitter_refresh_token',
    ];

    protected $casts = [
        'twitter_token' => 'encrypted',
        'twitter_refresh_token' => 'encrypted',
    ];
}
Enter fullscreen mode Exit fullscreen mode

Notice how we've defined that the twitter_token and twitter_refresh_token fields should be encrypted. We'll look at the reasoning for this further down.

Setting Up the Controller and Routes

Now that we have the database and our model set up correctly, we'll need to add two new routes and a controller to handle the routes.

The routes will be responsible for two actions:

  1. A route to direct the user away from our Laravel app to Twitter. This is where the user will allow permission to authenticate in our app via Twitter.
  2. A route that the user will be redirected to in our application after allowing permission in Twitter.

First, let's create these routes in our routes/web.php file like so:

Route::controller(OAuthController::class)->group(function () {
    Route::get('/auth/redirect/twitter', 'redirect')->name('oauth.redirect');
    Route::get('/auth/callback/twitter', 'callback')->name('oauth.callback');
});
Enter fullscreen mode Exit fullscreen mode

We can then create our OAuthController that we are using in our routes. To start off, we'll add our redirect method to the controller that will redirect the user away to Twitter:

namespace App\Http\Controllers;

use Laravel\Socialite\Facades\Socialite;
use Symfony\Component\HttpFoundation\RedirectResponse;

class OAuthController extends Controller
{
    public function redirect(): RedirectResponse
    {
        return Socialite::driver('twitter-oauth-2')->redirect();
    }

    // ...
}
Enter fullscreen mode Exit fullscreen mode

As you can see, Socialite is doing the heavy lifting for us, so the method is really simple to write. It's also worth noting that we need to pass twitter-oauth-2 here rather than just twitter because we want to use the OAuth 2.0 implementation rather than the OAuth 1.0 implementation.

Now that we've added the route and controller method to redirect the user to Twitter, we need to create a new controller method that will handle when the user returns to the site. For the purpose of this example and to keep the code all in one place to be readable, I'm going to place all of the code in the controller method. But, feel free to split up the code (similar to how it is shown in my Cleaning Up Laravel Controllers article) in your own projects to fit your own preferences.

So our new callback method might look something like this:

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Laravel\Socialite\Facades\Socialite;
use Symfony\Component\HttpFoundation\RedirectResponse;

class OAuthController extends Controller
{
    // ...

    public function callback(): RedirectResponse
    {
        $oAuthUser = Socialite::driver('twitter-oauth-2')->user();

        $user = User::updateOrCreate([
            'twitter_id' => $oAuthUser->getId(),
        ], [
            'name' => $oAuthUser->getName(),
            'email' => $oAuthUser->getEmail(),
            'password' => Hash::make(Str::random(50)),
            'avatar_url' => $oAuthUser->getAvatar(),
            'twitter_token' => $oAuthUser->token,
            'twitter_refresh_token' => $oAuthUser->refreshToken,
        ]);

        Auth::login($user);

        return redirect()->route('dashboard');
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, Socialite has done a lot of the heavy lifting again. But, let's take a step through what the code is doing to understand the workflow.

We started by calling Socialite::driver('twitter-oauth-2')->user(). This is using the parameters that were passed back in the URL from Twitter to resolve the user's Twitter details. We can call many different methods on the $oAuthUser field after we've successfully resolved a user, such as:

$oAuthUser->getId();
$oAuthUser->getNickname();
$oAuthUser->getName();
$oAuthUser->getEmail();
$oAuthUser->getAvatar();
Enter fullscreen mode Exit fullscreen mode

Because we are also using an OAuth 2.0 provider, we're also able to access the following fields:

$oAuthUser->token;
$oAuthUser->refreshToken;
$oAuthUser->expiresIn;
Enter fullscreen mode Exit fullscreen mode

If a user can't be resolved using the user() method, a Laravel\Socialite\Two\InvalidStateException exception will be thrown. This might be thrown for multiple reasons, such as:

  • The request is replayed (you can only access the URL once).
  • The user presses the 'cancel' button and doesn't allow permission to sign in via Twitter.
  • Some (or all) of the query parameters are incorrect. This could potentially be down to malicious trying to find a vulnerability with the registration and sign in process.

To keep this guide simple, I've not added handling for any of these situations. But, it might be something that you'll want to add in your projects rather than just displaying a 500 error page.

It's worth noting, in this particular tutorial, we're only covering how to sign in to your Laravel application using Twitter as an alternative to using a traditional registration form. However, if you'd like your Laravel application to make API calls on behalf of the authenticated user, you'll be able to use token and refreshToken fields to make those requests. As an example, you might want to do this if you're building a Twitter analytics or scheduling application (such as ilo.so) and want to post tweets on the user's behalf. For security reasons, it's really important to remember that you shouldn't store these tokens unless you have to and are actually going to use them. You'll likely also want to encrypt them before storing for extra security at a bare minimum. Although encrypting these tokens with your Laravel app's APP_KEY wouldn't protect the keys from being compromised and decrypted if your app server is compromised, they will at least provide a small amount of protection if only your database is compromised. Storing the keys securely is something that you would need to decide on a project-by-project basis to come up with a strategy that suits you (and your users) the best.

In our controller, after we've resolved our Twitter user, we use User::updateOrCreate(). This is done so that we can check whether the user has already signed in to our app in the past using Twitter. If they have, we'll update their details to make sure that we have the most up to date information about them (such as their name, avatar, and tokens). If the user doesn't exist, we'll create them in our database. After that, we'll then authenticate the user and redirect them to our application's dashboard.

With the approach that we've used here, it's possible that you may have multiple users in your database with the same email address. For example, if you allow traditional sign ups, or signing in with Twitter and GitHub, this could result in you potentially having 3 users with the same email address (one for each registration method). So, to cover this situation, I sometimes like to add extra checks to my controller's callback method and to my traditional registration form to prevent users from registering with the same email address using multiple methods. Likewise, you might also want to update your traditional login form to prevent OAuth users from trying to sign in. In this example, we've created a random 50 character password, so it's unlikely that the user will be able to sign in. But, it would still be recommended to add an explicit check to prevent an OAuth-registered user from signing in. However, this is something that you might want to allow in your projects, so this is something that you may want to change on a project-by-project basis.

Just as a recap, by the time that we've finished creating our controller, it should look something like this:

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Laravel\Socialite\Facades\Socialite;
use Symfony\Component\HttpFoundation\RedirectResponse;

class OAuthController extends Controller
{
    public function redirect(): RedirectResponse
    {
        return Socialite::driver('twitter-oauth-2')->redirect();
}

    public function callback(): RedirectResponse
    {
        $oAuthUser = Socialite::driver('twitter-oauth-2')->user();

        $user = User::updateOrCreate([
            'twitter_id' => $oAuthUser->getId(),
        ], [
            'name' => $oAuthUser->getName(),
            'email' => $oAuthUser->getEmail(),
            'password' => Hash::make(Str::random(50)),
            'avatar_url' => $oAuthUser->getAvatar(),
            'twitter_token' => $oAuthUser->token,
            'twitter_refresh_token' => $oAuthUser->refreshToken,
        ]);

        Auth::login($user);

        return redirect()->route('dashboard');
    }
}
Enter fullscreen mode Exit fullscreen mode

Taking it Further

Model Helper Methods

If your project allows signing in using a traditional form and mutliple OAuth providers, you might want to add some helpful methods or accessors to your User model to make your code more readable. This can be useful for if you want to perform different types of business logic depending on where the user registered from. For example, let's say that your project supports signing in using Twitter and GitHub. You could add the following methods to your models:

class User extends Model
{
    // ...

    public function isOAuthUser(): bool
    {
        return ! $this->isTwitterUser()
            && ! $this->isGithubUser();
    }

    public function isTwitterUser(): bool
    {
        return $this->twitter_id !== null;
    }

    public function isGithubUser(): bool
    {
        return $this->github_id !== null;
    }
}
Enter fullscreen mode Exit fullscreen mode

This means that in your code, you'd now be able to use these methods like so: $user->isOAuthUser(), $user->isTwitterUser(), $user->isGithubUser().

Error Reporting

If you're using a third-party error reporting system (such as Flare, Bugsnag, Honeybadger, etc), you'll want to ensure that none of the OAuth related keys or credentials are submitted during a bug report. For example, you'll want to make sure that the user's twitter_token and twitter_refresh_token aren't submitted. The majority of error reporting systems provide some sort of functionality to redact specific fields or data from the data submitted to them, so you'll need to make sure that you read the necessary documentation to make sure you have them configured correctly.

Multiple Providers

Depending on your project, you might want to provide the functionality for other OAuth providers to be used to sign in. For example, you might want to allow users to sign in using Twitter or GitHub. If this is the case, we can make some small changes to our existing code from above to do this. For this part, we'll make the assumption that you have read the set up guide for creating a GitHub app and added the necessary fields to the config/services.php file.

We could start by making use of PHP 8.1s enums, and creating one like so:

namespace App\Enums;

enum OAuthProvider: string
{
    case Twitter = 'twitter';

    case GitHub = 'github';

    public function driver(): string
    {
        return match ($this) {
            self::Twitter => 'twitter-oauth-2',
            self::GitHub => 'github',
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

We can then change our routes to accept a {provider} rather than being hardcoded as twitter:

Route::controller(OAuthController::class)->group(function () {
    Route::get('/auth/redirect/{provider}', 'redirect')->name('oauth.redirect');
    Route::get('/auth/callback/{provider}', 'callback')->name('oauth.callback');
});
Enter fullscreen mode Exit fullscreen mode

We could then update our controller's redirect method to make use of the enum route binding that Laravel provides:

namespace App\Http\Controllers;

use App\Enums\OAuthProvider;
use Laravel\Socialite\Facades\Socialite;
use Symfony\Component\HttpFoundation\RedirectResponse;

class OAuthController extends Controller
{
    public function redirect(OAuthProvider $provider): RedirectResponse
    {
        return Socialite::driver($provider->driver())->redirect();
    }

    // ...
}
Enter fullscreen mode Exit fullscreen mode

Now, if the user navigates to /auth/redirect/twitter or /auth/redirect/github, the $provider->driver() call will return the necessary driver name (twitter-oauth-2 and github respectively). Whereas, if the user navigates to the route and passes a provider that we don't have listed in our OAuthProvider enum, the user will receive a 404 response.

We can then update our controller's callback method like so:

namespace App\Http\Controllers;

use App\Enums\OAuthProvider;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Laravel\Socialite\Facades\Socialite;
use Symfony\Component\HttpFoundation\RedirectResponse;

class OAuthController extends Controller
{
    // ...

    public function callback(OAuthProvider $provider): RedirectResponse
    {
        $oAuthUser = Socialite::driver($provider->driver())->user();

        $user = User::updateOrCreate([
            'oauth_id' => $oAuthUser->getId(),
            'oauth_provider' => $provider,
        ], [
            'name' => $oAuthUser->getName(),
            'email' => $oAuthUser->getEmail(),
            'password' => Hash::make(Str::random(50)),
            'avatar_url' => $oAuthUser->getAvatar(),
            'oauth_token' => $oAuthUser->token,
            'oauth_refresh_token' => $oAuthUser->refreshToken,
        ]);

        Auth::login($user);

        return redirect()->route('dashboard');
    }
}
Enter fullscreen mode Exit fullscreen mode

In this method, we've removed all mention here of Twitter. Instead, we are using four new fields: oauth_id, oauth_provider, oauth_token, and oauth_refresh_token. These changes are made using the assumption that the project will only ever allow a user to sign in using a single provider.

To get this working, you'll then want to update your model to cast the oauth_provider to an OAuthProvider enum instance:

namespace App\Models;

use App\Enums\OAuthProvider;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    protected $fillable = [
        'name',
        'email',
        'password',
        'avatar_url',
        'oauth_id',
        'oauth_provider',
        'oauth_token',
        'oauth_refresh_token',
    ];

    protected $hidden = [
        'password',
        'remember_token',
        'oauth_token',
        'oauth_refresh_token',
    ];

    protected $casts = [
        'oauth_provider' => OAuthProvider::class,
        'oauth_token' => 'encrypted',
        'oauth_refresh_token' => 'encrypted',
    ];
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Hopefully, this post should have given you an insight into how you can use Socialite in your Laravel applications to allow users to sign in using Twitter. It should have also given you a few ideas about how you can improve and extend this workflow to work with multiple OAuth 2.0 providers.

If you enjoyed reading this post, I'd love to hear about it. Likewise, if you have any feedback to improve the future ones, I'd also love to hear that too.

If you're interested in getting updated each time I publish a new post, feel free to sign up for my newsletter.

Keep on building awesome stuff! 🚀

Discussion (1)

Collapse
bobbyiliev profile image
Bobby Iliev

Great post! Well done!