Adding Social Logins to Your Laravel Apps: Twitter and GitHub
Tags: laravel, php, webdev
Published: 2022-06-22 21:30:57

## 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](https://laravel.com/docs/9.x/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](https://socialiteproviders.com/) 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](https://en.wikipedia.org/wiki/OAuth), OAuth (**O**pen **Auth**orization) 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](https://www.oauth.com/oauth2-servers/differences-between-oauth-1-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](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](https://developer.twitter.com/en/docs/projects/overview).

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:

```text
composer require laravel/socialite
```

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:

```php
return [

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

];
```

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

```text
TWITTER_CLIENT_ID=client-id-goes-here
TWITTER_CLIENT_SECRET=client-secret-goes-here
OAUTH_CALLBACK_URL=http://localhost/auth/callback/twitter
```

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:

```text
php artisan make:migration create_users_table --create=users
```

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:

```php
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');
    }
};
```

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

```text
php artisan migrate
```

### Preparing the Model

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

```text
php artisan make:model User
```

We can then update our model to look like so:

```php
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',
    ];
}
```

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:

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

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:

```php
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();
    }

    // ...
}
```

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](https://ashallendesign.co.uk/blog/cleaning-up-laravel-controllers) article) in your own projects to fit your own preferences.

So our new `callback` method might look something like this:

```php
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');
    }
}
```

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:

```php
$oAuthUser->getId();
$oAuthUser->getNickname();
$oAuthUser->getName();
$oAuthUser->getEmail();
$oAuthUser->getAvatar();
```

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

```php
$oAuthUser->token;
$oAuthUser->refreshToken;
$oAuthUser->expiresIn;
```

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](https://ilo.so/?via=AshAllenDesign)) 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:

```php
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');
    }
}
```


## 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:

```php
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;
    }
}
```

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](https://flareapp.io/), [Bugsnag](https://www.bugsnag.com/), [Honeybadger](https://www.honeybadger.io/), 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:

```php
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',
        };
    }
}
```

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

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

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

```php
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();
    }
    
    // ...
}
```

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:

```php
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');
    }
}
```

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:

```php
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',
    ];
}
```

## 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](https://ashallendesign.co.uk/blog).

Keep on building awesome stuff! 🚀