smorken/social-auth
smorken/social-auth is a Laravel/PHP package that adds social authentication to your app, letting users log in through third-party OAuth providers. It integrates with Laravel's authentication system to simplify social login setup and account handling.
This package is not on Packagist — it lives in a Bitbucket VCS repository, so the first job is making Composer aware of it before you can even require it.
1. Register the VCS repository and pin it
{
"repositories": [
{
"type": "vcs",
"url": "https://bitbucket.org/scott_morken/social-auth.git"
}
]
}
composer require smorken/social-auth:"dev-master#<commit-sha>"
If the repo is private, every developer machine and CI runner needs a Bitbucket app password or SSH key configured. That is the most common reason the install works on your laptop and fails in the pipeline. Commit composer.lock immediately — an unpinned VCS dependency can resolve to a different commit between builds.
2. Discover what actually shipped
There is no published documentation, so treat vendor/smorken/social-auth as the spec. Read these four things before writing any application code:
php artisan vendor:publish --provider="Smorken\SocialAuth\SocialAuthServiceProvider"
# if tagged publishing exists, prefer explicit tags:
php artisan vendor:publish --tag=social-auth-config
php artisan vendor:publish --tag=social-auth-migrations
Then look at, in order: config/social-auth.php (which providers and which routes), src/Http/Controllers/* (the callback logic and, critically, the user-linking decision), routes/* (what URLs are registered and behind which middleware), and the published migration (the provider / provider_id schema).
3. Wire up credentials and the callback URL
Provider secrets belong in config/services.php, referenced from config/social-auth.php via env(). In the provider's console (Google Cloud, GitHub OAuth Apps, etc.) register the exact callback URL the package generated — trailing slash and localhost vs 127.0.0.1 matter. Register both a local and a production callback.
4. Migrate and try one provider end-to-end
php artisan migrate
<a href="{{ route('social.redirect', 'github') }}">Sign in with GitHub</a>
Verify the route name from the package's routes file first; it may be social-auth.redirect or similar. The first real use case is almost always: click button → provider consent → callback → Auth::user() is populated and the user lands on your post-login route.
Start with the built-in flow, then wrap it. The package gives you redirect/callback routes and a controller that resolves a provider user into a local User. Ship that on day one behind your own thin service so the dependency stays swappable:
// app/Services/SocialLoginService.php
namespace App\Services;
use Laravel\Socialite\Contracts\User as SocialiteUser;
class SocialLoginService
{
public function resolveLocalUser(string $provider, SocialiteUser $remote): \App\Models\User
{
// your linking policy lives here, not in the package
}
}
Bind it in a service provider and, if the package allows overriding the user resolver/controller via config, point it there. If it does not, publish the controller and extend it.
Make the linking policy explicit. The default behaviour of packages in this space is usually "match by email, create if missing." That is convenient and dangerous. Prefer an accounts table so one user can hold multiple providers:
Schema::create('social_accounts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('provider', 32);
$table->string('provider_id', 191);
$table->string('token', 2048)->nullable();
$table->string('refresh_token', 2048)->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamps();
$table->unique(['provider', 'provider_id']);
});
Link only when provider_id matches, or when the provider asserts email_verified. If an email matches an existing local account but the provider email is unverified, force a password confirmation step instead of silently logging in.
Customize post-login redirect per context. Rather than editing the controller, listen for the framework's Login / Registered events or add a small redirectTo() override so you can honour session('url.intended') and send first-time social users to an onboarding or profile-completion screen.
Feature-flag providers per environment. Keep a driver list in config and iterate in Blade so you can enable Google in production while leaving GitHub in staging only:
@foreach (config('social-auth.providers', []) as $provider => $enabled)
@if ($enabled)
<a href="{{ route('social.redirect', $provider) }}" class="btn btn-outline-dark">
Continue with {{ ucfirst($provider) }}
</a>
@endif
@endforeach
Test with a mocked driver, not a real provider. Socialite resolves drivers through the Socialite facade; mock the driver in the container so you never hit the network:
public function test_github_callback_creates_user()
{
$remote = Mockery::mock(\Laravel\Socialite\Contracts\User::class);
$remote->shouldReceive('getId')->andReturn('12345');
$remote->shouldReceive('getEmail')->andReturn('[email protected]');
$remote->shouldReceive('getName')->andReturn('Dev');
$driver = Mockery::mock(\Laravel\Socialite\Contracts\Provider::class);
$driver->shouldReceive('user')->andReturn($remote);
Socialite::shouldReceive('driver')->with('github')->andReturn($driver);
$this->get(route('social.callback', 'github'))->assertRedirect();
$this->assertDatabaseHas('users', ['email' => '[email protected]']);
}
Add a second test asserting that an existing local account with a matching but unverified email is not auto-linked. That test is the one that matters.
Respect existing auth routes. If you already have Auth::routes() or Breeze installed, check for collisions on /login, /auth/*, or /register before enabling the package's routes. Register them in a distinct prefix (e.g. /oauth/{provider}) if the package's routes are configurable — it avoids a confusing boot-time "route already defined" failure or, worse, silently shadowing your password login.
Never put a provider secret in a published config file. Published configs land in config/ and get committed. Keep only env() calls there; php artisan config:cache will bake whatever is in the file into bootstrap/cache/config.php, which is where accidental secret exposure happens.
Account takeover via email auto-linking is the headline risk. A provider that returns an unverified email plus a naive "find or create by email" produces a one-request account takeover. Read the package's user-resolution code before your first production login and be prepared to override it. This is the single most important 30 minutes you will spend with this package.
Pin to a commit, add a license check. Unlicensed code has unclear legal status in a commercial product. Check for a LICENSE file; if absent, get a written answer before exposure. Also note that a VCS-only dependency receives no composer audit advisories — you are the security feed.
Session state breaks behind a load balancer. OAuth state is stored in the session. If any app instance uses file sessions, users will intermittently get "invalid state" on the callback under multi-instance deployment. Use Redis or database sessions before you scale horizontally; this failure is intermittent and expensive to diagnose in production.
Provider user data is inconsistent. GitHub often returns a null or private email; Facebook may not return an email without the email scope; Google returns email_verified but not all providers do. Make users.email nullable or synthesise a placeholder, and require a profile-completion step when email is missing. Add email to the requested scopes explicitly in the provider config.
Column sizes and types bite on migration. provider_id must be a string, not an integer — some providers return very long numeric IDs and others return opaque strings. Token columns need room for JWT-style access tokens (string(2048) or text) and should be encrypted at rest via an Eloquent cast if you persist refresh tokens.
Provider-pinned Socialite constraints are the most likely upgrade blocker. Check composer.json in the vendor directory for laravel/socialite and illuminate/* constraints now, against both your current Laravel version and your intended next major. A wrapper pinning Socialite to an old major will block your framework upgrade, and the breakage will appear at the callback layer — the worst place for a user-visible failure.
Debugging the OAuth loop. Symptom-to-cause map: redirect_uri_mismatch → the callback URL registered at the provider differs from what Laravel generated (check APP_URL, proxy headers, and http vs https behind a TLS-terminating load balancer); invalid_state → session lost, cookie domain/same-site misconfiguration, or a new tab started the flow; 403 on callback → state consumed twice, usually a double-submitted redirect or a browser prefetch. Log the outgoing redirect URL and the callback query string at debug level, then turn it off.
Behind a proxy, force HTTPS scheme. If your load balancer terminates TLS, add URL::forceScheme('https') in a service provider (or trust the proxy headers) or every generated redirect URI will be http:// and the provider will reject it in production only.
Extension points to check for. Look for a bindable user-resolver interface, publishable views for the login buttons, and a config key for the post-callback redirect. If they exist, use them — they keep you out of a forked controller. If they do not, publish the controller, extend it, and keep your diff to a single class so a later swap to plain Socialite touches one file.
How can I help you explore Laravel packages today?