revolution/socialite-mastodon
A Laravel Socialite provider for Mastodon OAuth2 authentication. Add Mastodon login to your Laravel app using Socialite's familiar API, with simple configuration and support for any Mastodon instance. Lightweight driver integrates seamlessly with existing Socialite setups for federated social sign-i...
1. Install the driver
composer require revolution/socialite-mastodon
The package ships a service provider that auto-registers (Laravel package discovery) and hooks a mastodon driver into Socialite. There is typically nothing to publish — confirm by reading src/ after install; the whole package is a ServiceProvider plus a Provider class, so it is a 10-minute read and worth doing before you build on it.
2. Register an OAuth app on the instance you'll authenticate against
Mastodon is federated, so credentials are per instance, not global. On e.g. https://mastodon.social go to Preferences → Development → New application, set the redirect URI to your callback URL (must match exactly, including scheme and trailing-slash behaviour), and request scopes read:accounts (add write/follow only if you actually post or follow).
3. Configure credentials in config/services.php:
'mastodon' => [
'client_id' => env('MASTODON_CLIENT_ID'),
'client_secret' => env('MASTODON_CLIENT_SECRET'),
'redirect' => env('MASTODON_REDIRECT_URI'), // https://yourapp.test/auth/mastodon/callback
],
4. Routes
Route::get('/auth/mastodon/redirect', fn () => Socialite::driver('mastodon')
->setInstance(session('mastodon_instance'))
->redirect());
Route::get('/auth/mastodon/callback', function () {
$user = Socialite::driver('mastodon')
->setInstance(session('mastodon_instance'))
->user();
// $user->getId(), ->getNickname(), ->getName(), ->getAvatar()
});
setInstance() is the one API you must get right — it accepts the instance host (with or without scheme); everything else is standard Socialite. First use case: a "Sign in with Mastodon" button where the user first types their instance domain. If you only ever support one instance (your own community server, or mastodon.social), hard-code it in config and skip the picker entirely — that removes most of the complexity below.
Instance-aware login flow. The instance is part of the auth request, so persist it before redirecting:
public function redirect(Request $request)
{
$instance = Str::of($request->input('instance'))
->lower()->trim()->after('@')->trim('/')
->start('https://')->toString();
$request->session()->put('mastodon_instance', $instance);
return Socialite::driver('mastodon')->setInstance($instance)->redirect();
}
Validate the host first (filter_var + a DNS//api/v1/instance HEAD check) — otherwise you're making server-side requests to attacker-supplied URLs.
Persist users on a composite key. Mastodon account IDs are only unique within an instance. Your users table needs mastodon_instance + mastodon_id as a unique pair; a bare provider_id column will collide across servers.
User::updateOrCreate(
['mastodon_instance' => $instance, 'mastodon_id' => $social->getId()],
['name' => $social->getName() ?? $social->getNickname(),
'handle' => $social->getNickname(), // acct, e.g. "alice" or "[email protected]"
'avatar' => $social->getAvatar()]
);
Credential strategy — three tiers. Pick one deliberately:
client_id/client_secret in services.php. Simplest, ship this first.POST {instance}/api/v1/apps with client_name, redirect_uris, scopes, then cache the returned client_id/client_secret per host. This is the only path that truly works for arbitrary instances, and it is not something you should assume the package does — implement it in your own service class and feed the credentials to the driver.Scopes. Mastodon has no email scope because the API never returns an email address. Request read:accounts for the profile and be explicit — don't rely on the package default.
API access after login. Store the returned access_token (Mastodon tokens don't expire by default but are revocable). For server-to-server calls use Laravel's HTTP client with withToken() against /api/v1/accounts/verify_credentials rather than round-tripping through Socialite; userFromToken() may be supported but is worth testing before you depend on it.
Testing. Never hit a real instance in CI. Fake the Socialite driver:
Socialite::shouldReceive('driver->setInstance->user')->andReturn(
(new \Laravel\Socialite\Two\User)->map(['id' => 1, 'nickname' => 'alice', 'name' => 'Alice'])
);
For your own dynamic-registration code, Http::fake(['*/api/v1/apps' => Http::response([...])]).
getEmail() returns null. This breaks Breeze/Jetstream out of the box, and email is NOT NULL UNIQUE in the default migration. Decide up front: prompt for an email after OAuth, or generate a placeholder like {instance}+{id}@users.noreply.invalid. Do not trust a fake-looking address to be deliverable.401 Unauthorized from /oauth/token almost always means instance/credential mismatch — you registered the app on mastodon.social but the user typed fosstodon.org. Log both the instance and the client_id in the failure path; otherwise this is a miserable bug to chase.@[email protected], mastodon.social/, https://Mastodon.Social. Strip the leading @user@, trim slashes, lowercase the host, force https. Store the normalized form and compare on it.http://localhost:8000/... (dev) and your production URI on the same app.InvalidStateException after the callback usually means an expired session, a cached config, or a proxy stripping cookies. Catch it and redirect to the login page rather than 500ing.setInstance state leaks between users — build the driver fresh inside each request.composer.json, and know that the fallback is cheap: a Mastodon Socialite provider is roughly getAuthUrl(), getTokenUrl(), getUserByToken() against /api/v1/accounts/verify_credentials, plus mapUserToObject(). Budget half a day to vendor the class into app/Socialite/ if the package ever stalls — you'll likely need to subclass it anyway for custom user mapping, which is the cleanest extension point (override mapUserToObject() or getUserByToken() rather than editing vendor code).verify_credentials on every page load.ArrayAccess payload before mapping — Mastodon's verify_credentials returns fields Socialite's default mapper drops (note, followers_count, bot, created_at) that you may want for a richer profile.How can I help you explore Laravel packages today?