revolution/socialite-discord
Discord OAuth2 provider for Laravel Socialite. Adds Discord login to your Laravel app with a simple driver: install the package, add your Discord client ID and secret, then use Socialite::driver('discord') to authenticate users and retrieve their Discord profile.
This package is a Laravel Socialite driver for Discord's OAuth2 endpoints. The first thing to do is check the README for the required registration method—some versions of revolution/socialite-discord ship a SocialiteDiscordServiceProvider you register in config/app.php, while others expect you to call Socialite::extend('discord', ...) yourself in a service provider's boot() method. Confirm which by opening src/ and looking for a ServiceProvider and a DiscordProvider class.
Create a Discord application at https://discord.com/developers/applications, add an OAuth2 redirect (e.g. https://yourapp.test/auth/discord/callback), and copy the Client ID and Client Secret. The redirect must match exactly—Discord does no normalization.
Configure credentials in config/services.php:
'discord' => [
'client_id' => env('DISCORD_CLIENT_ID'),
'client_secret' => env('DISCORD_CLIENT_SECRET'),
'redirect' => env('DISCORD_REDIRECT_URI'),
],
Wire up two routes:
Route::get('/auth/discord/redirect', fn () =>
Socialite::driver('discord')
->scopes(['identify', 'email'])
->redirect()
);
Route::get('/auth/discord/callback', function () {
$discord = Socialite::driver('discord')->user();
// $discord->getId(), ->getNickname(), ->getName(), ->getEmail(), ->getAvatar()
return redirect('/');
});
Your first end-to-end use case is almost always "log in with Discord" on a gaming/community app: request identify + email, persist the Discord snowflake ID as a unique column, and use discriminator + avatar to render a profile.
Standard login flow. Follow Socialite's normal pattern: redirect out, callback in, updateOrCreate on the Discord ID.
$discord = Socialite::driver('discord')->user();
$user = User::updateOrCreate(
['discord_id' => $discord->getId()],
[
'name' => $discord->getNickname() ?? $discord->getName(),
'email' => $discord->getEmail(),
'avatar_hash' => $discord->getAvatar(),
'discord_token' => $discord->token,
]
);
Auth::login($user);
Reaching into the raw payload. The Discord user object includes fields Socialite doesn't surface (discriminator, verified, banner, accent_color, locale, mfa_enabled). Access them via $discord->user (the raw array) or $discord->getRaw(), depending on which AbstractUser version the package extends. Wrap this in a small DTO so you're not scattering array access through the codebase.
Guild membership. If your app is a guild dashboard, request the guilds scope and then hit https://discord.com/api/users/@me/guilds with $discord->token in an Authorization: Bearer ... header to list servers the user can manage. For auto-joining new signups, use guilds.join (requires a bot in the target guild with CREATE_INVITE / membership permissions) and PUT /guilds/{guild.id}/members/{user.id}. This is not part of the driver—you'll do it with Http::withToken().
Storing the token for later API calls. The package follows Socialite's convention of exposing $user->token and $user->refreshToken. Persist both (encrypted cast) plus expiresIn if you plan to call the Discord API after login. Discord access tokens expire after 7 days; refresh via POST /api/oauth2/token with grant_type=refresh_token—Socialite will not do this for you.
Multiple account linking. For apps where users can link Discord after registering with email/password, split the redirect/callback from login: store the state or a session flag ("linking user X") and, in the callback, attach discord_id to the authenticated user rather than Auth::login().
Extending scopes per route. Socialite::driver('discord')->scopes([...]) is chainable and additive. Keep a default scope set in a small DiscordScopes constant class so scopes stay consistent between the redirect and any documentation.
discriminator to "0" for all migrated accounts and forces unique lowercase usernames. Do not build login keys or display strings like username#1234—treat username alone as unique and treat discriminator === '0' as "new-style account."getEmail() can return null. The email scope is optional on Discord, and some users have no verified email. Make the email column nullable or fall back to a synthetic {discord_id}@users.noreply.discord placeholder, and never trust it as a global identifier.getAvatar() returns a hash, not a URL. Build the CDN URL yourself: https://cdn.discordapp.com/avatars/{id}/{hash}.png?size=128. If the hash is null, fall back to Discord's default avatars (https://cdn.discordapp.com/embed/avatars/{index}.png, index = (snowflake >> 22) % 6). Also note the newer .webp and animated .gif (hash starts with a_) variants.discord.com/api/v6 or discordapp.com/api; both are deprecated/redirected. Grep the provider for the base URL and bump it to https://discord.com/api/v10 (or v8) if you hit 404s or deprecation warnings. Forking the provider is often easier than PRing a dormant 8-star repo.state is not automatic in some builds. Socialite validates state in AbstractProvider::user() only if you passed it—make sure you're using the standard redirect()/user() pair and, ideally, CSRF-protected callback routes with auth middleware where appropriate.access_token, refresh_token, expires_in, scope—the driver may map expiresIn differently than, say, Google or GitHub. Log the raw $user->user + token once during integration to see exactly what you're getting.scopes() joins with %20; passing an array of arrays or a comma string will silently fail with an invalid_scope error from Discord. Valid scopes: identify, email, connections, guilds, guilds.join, gdm.join, bot, webhook.incoming, applications.commands.invalid_request: Invalid "redirect_uri" in request—check that services.discord.redirect matches character-for-character, including trailing slashes, scheme, and port. Config caching (php artisan config:clear) is a common culprit during local dev.user() is a single call, but if you fan out to /users/@me/guilds and guild-join calls during login you'll hit the global 50 req/sec bucket quickly on a busy callback. Queue the guild-membership work rather than doing it inline.src/DiscordProvider.php before adopting it; expect to vendor or fork it if you need v10 endpoints, PKCE, or refresh-token helpers. The well-maintained alternative is socialiteproviders/discord—evaluating both side-by-side costs ten minutes and tells you which to standardize on.How can I help you explore Laravel packages today?