Ash Allen
Posted on June 22, 2022
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
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'),
],
// ...
];
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
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
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');
}
};
We can then run this migration and add the users
table to our database using the following command:
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:
php artisan make:model User
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',
];
}
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:
- 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.
- 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');
});
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();
}
// ...
}
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');
}
}
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();
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;
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');
}
}
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;
}
}
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',
};
}
}
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');
});
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();
}
// ...
}
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');
}
}
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',
];
}
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! ๐
Posted on June 22, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 28, 2024