directorytree/bartender
Bartender gives you a ready-made controller, routes, and default implementation for handling user authentication with Laravel Socialite providers—with nearly everything customizable. Requires PHP 8, Laravel 9, and Socialite 5.
Bartender is a thin, opinionated layer over Laravel Socialite: you get a controller, two routes, and a default UserProviderRepository that creates/updates users. The whole day-to-day value is that adding a provider is a config entry, one line in a service provider, and a Blade link — not a controller, not a handleCallback() method, not a user-mapping class.
Minimal setup (≈15 minutes for the first provider):
composer require directorytree/bartender
php artisan vendor:publish --provider="DirectoryTree\Bartender\BartenderServiceProvider"
php artisan migrate
The published migrations add provider_id, provider_name, provider_access_token, provider_refresh_token to users. If you are not going to use provider tokens, delete the token migration before running migrate — it is a separate file specifically so you can skip it.
Where to look first, in order:
routes/web.php — register the routes once: Bartender::routes();config/services.php — each provider's redirect must be the path of the callback route (/auth/google/callback), not an absolute URL.app/Providers/AppServiceProvider::boot() — Bartender::serve('google'); for each driver. This is what binds a driver name to a handler; forgetting it is the #1 source of errors.app/Models/User.php — only touch it if you want token storage or a non-default namespace (Bartender::setUserModel(...)).ProviderHandler, ProviderRepository, ProviderRedirector) live, and it's the part you'll actually read repeatedly.First use case — "Login with Google" on an existing app:
<a href="{{ route('auth.driver.redirect', 'google') }}">Login with Google</a>
Clicking it hits /auth/google/redirect, Socialite redirects to Google, Google returns to /auth/google/callback, Bartender looks up or creates the user by provider ID (or email), logs them in, and redirects. You write zero auth code. That is the entire pitch, and it holds up.
One handler per provider, only when you need per-provider behavior. The default handler is fine for most providers. The moment you need custom scopes, subclass instead of replacing:
class MicrosoftUserHandler extends UserProviderHandler
{
public function redirect(Provider $provider, string $driver): RedirectResponse
{
$provider->scopes(['Mail.ReadWrite', 'offline_access']);
return parent::redirect($provider, $driver);
}
}
// AppServiceProvider::boot()
Bartender::serve('google'); // default handler
Bartender::serve('microsoft', MicrosoftUserHandler::class); // custom handler
This is the pattern I'd standardise on: Bartender::serve($driver) as the default, Bartender::serve($driver, Handler::class) as the exception. It keeps AppServiceProvider readable and makes it obvious which providers have special needs.
Swap the repository for domain rules, not for convenience. ProviderRepository has exactly two methods — exists(string $driver, SocialiteUser $user) and updateOrCreate(string $driver, SocialiteUser $user) — which is a nice, small contract to own. Bind it in AppServiceProvider::register():
$this->app->bind(ProviderRepository::class, UserProviderRepository::class);
Realistic reasons to do this:
exists() and throw, then let the redirector flash a message.tenant_id at creation time from a subdomain or session value.Swap the redirector for post-login flow and messaging. ProviderRedirector has four outcome hooks (unableToAuthenticateUser, userAlreadyExists, unableToCreateUser, userAuthenticated) and is the right place for:
Auth::login($user) + Session::regenerate() — the README explicitly recommends regenerating the session here to avoid session fixation. If you implement this interface yourself, do not skip that.Token storage, only when you have a consumer. Apply StoresProviderTokens on the model, add encrypted casts, and hide both columns:
class User extends Authenticatable implements StoresProviderTokens
{
protected $hidden = ['provider_access_token', 'provider_refresh_token'];
protected function casts(): array
{
return [
'provider_access_token' => 'encrypted',
'provider_refresh_token' => 'encrypted',
];
}
}
The typical consumer is a queued job that calls a provider API on the user's behalf (calendar sync, mailbox scanning, Drive import). Note the package stores tokens; it does not refresh them. You own the refresh loop — read provider_refresh_token, call the provider's token endpoint, write back.
Testing. Feature-test the routes, not the internals: hit route('auth.driver.redirect', 'google') and assert the redirect to the provider; hit the callback with the Socialite provider mocked (via the Socialite facade) and assert the user was created, logged in, and redirected. Because the controller lives in the package, your tests are effectively testing your ProviderRepository and ProviderRedirector bindings — which is exactly what you want to be asserting anyway.
Multi-surface apps. This package is session/web-oriented: it returns RedirectResponse and logs the user in via the session guard. For an SPA needing API tokens, use it for the redirect/callback dance but swap in a ProviderRedirector that issues a token and redirects back to your frontend callback URL. Do not try to make UserProviderHandler do double duty for stateless API auth.
redirect in config/services.php must match the route path. Setting it to a full URL that differs from /auth/{driver}/callback produces confusing provider-side errors (redirect_uri_mismatch) rather than anything Bartender can report.Driver [X] not supported means the Socialite provider driver isn't registered. That's a socialiteproviders.com install step (listener in EventServiceProvider, Socialite::extend), not a Bartender problem. Finish the provider's install guide fully.Routing requirement for "driver" cannot be empty means you forgot Bartender::serve('google'). It surfaces on the login link click, which makes it look like a routes problem — it isn't.serve() belongs in boot(), the bindings in register(). Mixing them up works until something resolves early (route caching, package discovery order) and then silently doesn't.services.php, run php artisan config:cache in any cached environment, or you'll debug a stale client_id for twenty minutes.ProviderRepository if deletion means "never return."Hash::make (fixed in 1.2.1 to respect your app's hasher). Those users cannot log in with a password — make password nullable or provide a "set password" flow if you support both auth modes.(provider_name, provider_id) in your own migration; the published migrations add columns but relying on application-level exists() alone leaves a race window on concurrent callbacks.APP_KEY. Rotating the key invalidates every stored access/refresh token — which is fine (users re-auth) but should be a documented part of your key-rotation runbook, not a surprise incident.ProviderHandler subclass; refresh is your job. Know this before promising "connect your Google account" features.Bartender::routes() claims /auth/{driver}/redirect and /auth/{driver}/callback. If you already have an /auth/* namespace, register Bartender's routes first and inspect the facade signature for prefix/middleware options (the README doesn't document them, so read the source rather than guessing).ProviderHandler per-driver, ProviderRepository, ProviderRedirector) are clean seams. If you find yourself wanting to fork the package, you've probably missed a binding — the only genuinely opinionated, hard-to-change part is that callbacks are RedirectResponse-shaped.How can I help you explore Laravel packages today?