dutchcodingcompany/filament-socialite
Filament Socialite adds OAuth social login buttons to Filament panels, letting users sign in with providers like Google, GitHub, and Facebook via Laravel Socialite. It includes configurable providers, redirect handling, and simple integration with Filament authentication.
1. Install and publish
composer require dutchcodingcompany/filament-socialite
php artisan vendor:publish --tag=filament-socialite-config
php artisan vendor:publish --tag=filament-socialite-migrations
php artisan migrate
The migration creates a socialite_users table that links (provider, provider_id) to your users table (plus name/email/avatar snapshots). Do this migration early — every later customization (linking accounts, storing token metadata, deleting users) builds on that table.
2. Add OAuth credentials
config/services.php stays vanilla Socialite config — the package adds nothing here:
'github' => [
'client_id' => env('GITHUB_CLIENT_ID'),
'client_secret' => env('GITHUB_CLIENT_SECRET'),
'redirect' => env('APP_URL').'/oauth/github/callback',
],
Note the callback path: it is registered by the package at the application root, not under your panel path. https://app.test/admin/oauth/github/callback will 404.
3. Register the plugin on the panel
use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin;
use DutchCodingCompany\FilamentSocialite\Provider;
public function panel(Panel $panel): Panel
{
return $panel
->plugin(
FilamentSocialitePlugin::make()
->providers([
Provider::make()
->slug('github') // config/services.php key + Socialite driver
->label('GitHub')
->icon('fab-github')
->color('gray')
->scopes(['user:email'])
->login()
->registration(),
])
->registration(true)
);
}
Older releases keep the same provider list in config/filament-socialite.php under providers; newer ones read it from the plugin (which also accepts ->enabled(), ->authGuard(), ->socialiteGuard(), ->userModel(), ->userResolver()). Pin a major version and read the shipped Provider class for exact setter names — the fluent API has changed between v2 and v3. On the 3.x line, ^3.3 is the current floor (see Gotchas for the behaviour change it introduces).
4. First use case
Log in to /admin with GitHub as a super-admin, then verify the row landed in socialite_users and that the linked users record is the one you expect. That single loop — button → provider → callback → resolver → login — exercises 90% of the package.
Then exercise the failure loop once: hit the callback with a stale/absent state param (/oauth/github/callback?code=bogus) and confirm you land back on the login page rather than a 500. On 3.3+ that is the expected behaviour; knowing what it looks like saves you an hour later.
Where to look first
| Question | File |
|---|---|
| What's configurable, and defaults | config/filament-socialite.php |
| Button placement, guard wiring | src/FilamentSocialitePlugin.php |
| Per-provider options (scopes, icon, login/registration) | src/Provider.php |
| How users are matched/created | src/FilamentSocialiteUserResolver.php, src/UserResolver.php (contract) |
| Route names/URLs | src/FilamentSocialiteServiceProvider.php |
| Failure handling (3.3+) | the package's provider-request controller / exception handling in src/ |
Two-tier provider toggling (env → panel). Keep client IDs in .env, but let the panel provider list be code. This is the pattern that scales: staging panels can offer only the corporate IdP, production offers everything, and nothing is gated behind env() calls inside config files you can't cache.
$providers = [Provider::make()->slug('github')->login()->registration()];
if (app()->environment('production')) {
$providers[] = Provider::make()
->slug('azure')
->label('Microsoft')
->icon('fab-microsoft')
->scopes(['openid', 'profile', 'email']);
}
FilamentSocialitePlugin::make()->providers($providers)
Login-only vs registration-enabled per provider. ->login() and ->registration() are independent. A very common split: public signup via Google/GitHub, but an internal IdP (Azure/Okta) that only logs in — combined with a custom resolver that refuses to create users from it. Registration being globally enabled but per-provider disabled is the cheapest "invite-only" gate you can ship.
Custom user resolver — the main extension point. The default resolver looks up an existing SocialiteUser by (provider, provider_id), and creates a user (or attaches to an existing email) when registration is on. Override it for anything real:
namespace App\Support;
use DutchCodingCompany\FilamentSocialite\FilamentSocialiteUserResolver;
use Illuminate\Contracts\Auth\Authenticatable;
class AppUserResolver extends FilamentSocialiteUserResolver
{
// Copy the parent signatures verbatim — they differ between releases.
public function createUser($socialiteUser): ?Authenticatable
{
if (! str_ends_with($socialiteUser->email, '@yourcompany.com')) {
return null; // aborts registration with the package's error flow
}
return User::create([
'name' => $socialiteUser->name,
'email' => $socialiteUser->email,
'email_verified_at' => now(), // the IdP already verified it
'password' => null, // see gotchas
]);
}
}
Wire it with ->userResolver(AppUserResolver::class). Returning null from createUser is the idiomatic "block this signup"; returning null from the resolve step is the "no such account, show registration" path.
Link accounts instead of duplicating them. Most real deployments have pre-existing users (seeded admins, imported staff). Let the resolver match on email and attach a SocialiteUser to the existing account rather than creating a second one. This is also what you want when a user signs in with GitHub first and Google second: same email → same users row, two socialite_users rows.
Reuse the buttons outside the login page. The package renders its button group through a Filament render hook on the auth pages. To surface it on a custom /login Livewire page or a marketing page, register the same hook yourself (->renderHook(PanelsRenderHook::AUTH_LOGIN_FORM_AFTER, fn () => view('filament-socialite::buttons'))) — or simply call socialiteLoginUrl($provider) style route helpers to build links (filament-socialite.login.redirect) and style them yourself.
Testing without hitting the IdP. Mock the Socialite factory; don't network:
Socialite::shouldReceive('driver->user')->andReturn(
(new \Laravel\Socialite\Two\User)
->map(['id' => '123', 'name' => 'Test', 'email' => '[email protected]', 'avatar' => null])
);
$this->get(route('filament-socialite.login.redirect', ['provider' => 'github']))
->assertRedirect();
$this->get(route('filament-socialite.login.callback', ['provider' => 'github']))
->assertAuthenticated();
Assert on the socialite_users row, not just the redirect — the interesting bugs are in the mapping. Also cover the negative path, which is now a first-class behaviour rather than an exception dump:
// 3.3+: a failing provider request sends the user back to login
$this->get(route('filament-socialite.login.callback', ['provider' => 'github', 'error' => 'access_denied']))
->assertRedirect(); // login page, not a 500
$this->assertGuest();
Multi-panel, multi-guard. ->authGuard('web') / ->socialiteGuard('web') must match the panel's guard. For a second panel on a different guard (e.g. customers), register a separate plugin instance on that panel with its own user model and resolver. Sharing one plugin across guards is the number-one cause of login loops.
Failed provider requests redirect instead of throwing (3.3+). As of 3.3.0 the package catches a failing provider request (user cancels at the IdP, access_denied, invalid state, driver errors) and redirects back to the login page rather than surfacing a 500. Good UX, but it changes your debugging posture:
storage/logs/laravel.log before assuming the redirect is a routing bug.try/catch around the callback, or a custom exception Handler render callback, may no longer fire for these cases. Anything you relied on for audit logging of failed logins should hook the redirect/flash path instead.UserResolver code that throws is inside the callback; verify whether your thrown exceptions are still propagated or get swallowed into the same redirect. Throw-and-log deliberately if you need alerting.^3.3.Callback URLs live at the root. Default route names are filament-socialite.login.redirect / .callback, resolving to /oauth/{provider}/redirect and /oauth/{provider}/callback. Copy that exact string into GitHub/Google/Azure consoles — including scheme, port and trailing-slash correctness. If you put the panel behind a different domain or path prefix, set APP_URL correctly and check php artisan route:list | grep oauth.
InvalidStateException / "state is invalid". Socialite uses a session-stored state token. Causes, in order of frequency: cached config after changing providers, SESSION_DOMAIN not matching the callback host, the user sitting on the login page long enough to lose the session, multiple login tabs, or a proxy/load balancer terminating HTTPS without TrustProxies configured. Fix the environment before you go hunting in the package. On 3.3+ this no longer blows up the request — you get redirected back to login and a log entry, so don't mistake the graceful landing for "it worked".
Password columns must tolerate null. OAuth-provisioned users have no password. Either make users.password nullable or generate a random hash in createUser. If you leave it non-nullable you'll get an opaque SQL error inside the callback, not a validation message — and on 3.3+ an exception inside the callback may end as that same login redirect, hiding the SQL error from the user entirely. Same for name — some providers return null.
Email collisions do not resolve themselves. The unique index is on (provider, provider_id), not email. Two providers with the same email, or an OAuth signup with an email that already exists, will happily create a second account unless your resolver handles the match-and-link case. Decide the rule explicitly and write a test for it.
mustVerifyEmail panels. If your panel implements FilamentUser/MustVerifyEmail, freshly created OAuth users with email_verified_at = null get bounced to the verification screen forever (they have no password to re-send anything meaningful). Set email_verified_at in createUser when the provider is trusted — GitHub with user:email, Google, and any OIDC IdP qualify.
Config cache will bite you. providers, enabled, guards and resolver class all live in config or the plugin. After editing, php artisan config:clear (and route:clear if you changed route prefixes). In production, config:cache at deploy time, and never read env() from inside the config array post-cache.
Icons need a set installed. ->icon('fab-github') requires blade-ui-kit/blade-icons plus blade-fontawesome/blade-simpleicons; otherwise you get a blank or an exception. Heroicons (heroicon-o-*) always work.
Local development needs a real hostname. GitHub and Google accept localhost, Facebook and most enterprise IdPs do not. Run php artisan serve --host=0.0.0.0 behind a tunnel (expose, ngrok, valet share) and register the tunnel host in APP_URL — otherwise you'll debug a "redirect_uri_mismatch" that isn't a package problem. Note that a mismatched redirect URI is an IdP-side error the user sees at the IdP, not a callback failure, so 3.3's redirect handling won't cover it.
Version drift is real. The package moved from config-array providers (v2) to fluent Provider::make() objects configured on the plugin (v3/v4, tracking Filament 3/4). The resolver contract signatures and even some setter names (slug() vs provider()) have shifted. Since the repo is small and lightly versioned, read src/Provider.php in the installed tag rather than following a blog post. Lock a version in composer.json if you're building on the resolver internals — and if you write tests that assert on failure behaviour, a ^3.2 constraint can silently change what "failed login" looks like when 3.3 lands.
Extension points worth knowing, in order of usefulness: custom UserResolver (provisioning rules) → per-provider Provider subclass (custom driver, redirect handling, dynamic scopes) → published SocialiteUser model/migration (extra columns like access_token or tenant_id) → events/render hooks (post-login redirects, audit logging). Start at the top; most "the package doesn't support X" problems are a resolver override.
Don't forget teardown. Deleting a users row leaves orphaned socialite_users unless your migration's foreign key cascades. Add ->cascadeOnDelete() when you publish the migration — retrofitting it later on a live table is annoying.
How can I help you explore Laravel packages today?