socialiteproviders/discord
Discord OAuth2 provider for Laravel Socialite. Adds Discord login to your Laravel app, letting users authenticate via Discord and retrieve their profile, guilds, and connections with minimal setup.
Install
composer require socialiteproviders/discord
This pulls in socialiteproviders/manager (which registers the SocialiteWasCalled event bridge) and expects laravel/socialite to already be installed.
Configure credentials in config/services.php:
'discord' => [
'client_id' => env('DISCORD_CLIENT_ID'),
'client_secret' => env('DISCORD_CLIENT_SECRET'),
'redirect' => env('DISCORD_REDIRECT_URI'),
],
Register the listener. Laravel ≤10 goes in app/Providers/EventServiceProvider.php:
protected $listen = [
\SocialiteProviders\Manager\SocialiteWasCalled::class => [
\SocialiteProviders\Discord\DiscordExtendSocialite::class.'@handle',
],
];
Laravel 11+ (no EventServiceProvider by default) — put it in AppServiceProvider::boot():
Event::listen(
\SocialiteProviders\Manager\SocialiteWasCalled::class,
[\SocialiteProviders\Discord\DiscordExtendSocialite::class, 'handle']
);
Create the Discord app at https://discord.com/developers/applications → OAuth2 → add your redirect URI exactly (scheme + host + path, no trailing slash).
First use case — the classic login flow:
// routes/web.php
Route::get('/auth/discord', fn () => Socialite::driver('discord')->redirect());
Route::get('/auth/discord/callback', function () {
$discord = Socialite::driver('discord')->user();
// $discord->getId(), ->getName(), ->getEmail(), ->getAvatar(), ->getRaw()
});
Where to look first
SocialiteProviders/Providers monorepo under src/Discord/. This package is a read-only subtree split — file issues and PRs against the monorepo, not here.GET /users/@me resource.DiscordExtendSocialite class here is trivial — almost all behaviour (scopes, mapping, user class) comes from SocialiteProviders\Discord\Provider in the monorepo.Custom scopes. Defaults usually include identify. Add email, guilds, connections, guilds.join as needed:
Socialite::driver('discord')
->scopes(['identify', 'email', 'guilds'])
->redirect();
Token-based / stateless flows (SPA, mobile, API). Discord is a good fit for "Sign in with Discord" from a JS client: do the OAuth dance client-side, POST the access token to your API, and resolve it server-side:
$discord = Socialite::driver('discord')->userFromToken($request->access_token);
userFromToken() bypasses the state check entirely — no stateless() needed for this path.
Account linking and registration. Discord gives you a stable snowflake ID; key your users table on that, not email:
$user = User::updateOrCreate(
['discord_id' => $discord->getId()],
[
'name' => $discord->getName(), // global_name if set, else username
'email' => $discord->getEmail(), // nullable — see Gotchas
'avatar' => $discord->getAvatar(), // raw hash, not a URL
]
);
Auth::login($user);
Building the avatar URL (Socialite returns only the hash, so do this yourself):
function discordAvatar(?string $id, ?string $hash, int $size = 128): string
{
if (! $hash) {
// account has no custom avatar — default embed avatar 0
return 'https://cdn.discordapp.com/embed/avatars/0.png';
}
$ext = str_starts_with($hash, 'a_') ? 'gif' : 'png';
return "https://cdn.discordapp.com/avatars/{$id}/{$hash}.{$ext}?size={$size}";
}
Using the token for further Discord calls. Grab the raw token via $discord->token (or getRaw()['...'] depending on version) and reuse it:
$guilds = Http::withToken($discord->token)
->get('https://discord.com/api/v10/users/@me/guilds')
->json();
Use this to check guild membership before allowing access to a "members only" area.
Guild join (guilds.join). This is the one Discord flow that needs a bot. Request the guilds.join scope, ensure your bot is already in the target guild, then:
Http::withToken(config('services.discord.bot_token'))
->put("https://discord.com/api/v10/guilds/{$guildId}/members/{$discord->getId()}", [
'access_token' => $discord->token,
]);
Refresh tokens. Discord returns a refresh_token in the token response. If you need long-lived access, store both and refresh via POST /oauth2/token with grant_type=refresh_token. Socialite won't do this automatically — you own the persistence.
Testing. Mock the driver so you don't hit Discord:
Socialite::shouldReceive('driver->user')->andReturn(
(new \Laravel\Socialite\Two\User)->map([
'id' => '123456789012345678',
'nickname' => 'someone',
'name' => 'Someone',
'email' => '[email protected]',
'avatar' => 'abc123',
])
);
Or use Http::fake() if you're exercising the real provider end-to-end.
getEmail() can be null. You only get an email if you requested the email scope and the account has a verified email. Always handle null; don't make email required in the users table unless you have a fallback.string/VARCHAR, never as a JS-facing int — JavaScript will silently truncate it.discriminator is dead. Discord finished migrating to global_name; migrated accounts return discriminator: "0". Prefer global_name → username and don't build UI around #1234. $discord->getName() usually maps this for you, but check getRaw() if you need both.getAvatar() returns the raw hash (or null). If it's null, use the embed avatar fallback — ${id}.png will 404. Animated avatars start with a_ and must be requested as .gif.http vs https, trailing slash, localhost vs 127.0.0.1). Register one for local, one for prod.Socialite::driver('discord') throws InvalidArgumentException: Driver [discord] is not supported, you forgot the SocialiteWasCalled listener or have a cached config. Run php artisan config:clear and confirm the listener is registered for your Laravel version.InvalidStateException means a lost session, a stale tab, or a proxy stripping cookies. Don't reach for stateless() in browser flows just to silence it — fix the session. Reserve stateless() / userFromToken() for API clients.->scopes([...]); don't assemble the string yourself. Unknown scopes are silently ignored by Discord — a missing email often means a typo, not a Discord bug.Http::withToken() rather than waiting for a package update.composer require socialiteproviders/discord gets you code that is generated from the monorepo. Local edits will be overwritten on update; PRs against this repo are ignored. Patch the monorepo or fork.socialiteproviders/discord and socialiteproviders/manager are released in lockstep from the same monorepo. Keep them on the same major version or you'll get signature/property mismatch errors.guilds.join requires bot cooperation. The scope alone does nothing; the bot must be in the guild, and your PUT request must use the bot token, not the user token. This is the most common "it returns 403" scenario./users/@me/guilds results rather than calling on every page load, and respect Retry-After on 429.$discord->getRaw() — the provider's User class only exposes Socialite's standard fields (id, nickname, name, email, avatar), and Discord's response carries more (verified, locale, mfa_enabled, premium_type) that you'll need to read from the raw array.How can I help you explore Laravel packages today?