Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Discord Laravel Package

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.

Deep Wiki
Context7

Getting Started

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

  • The real source lives in the SocialiteProviders/Providers monorepo under src/Discord/. This package is a read-only subtree split — file issues and PRs against the monorepo, not here.
  • Discord's own docs: OAuth2 (scopes, token endpoint) and the GET /users/@me resource.
  • The DiscordExtendSocialite class here is trivial — almost all behaviour (scopes, mapping, user class) comes from SocialiteProviders\Discord\Provider in the monorepo.

Implementation Patterns

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.

Gotchas and Tips

  • 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.
  • The ID is a snowflake string. A Discord user ID is a 64-bit integer. Store it as 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.
  • Avatar hash is not a URL. 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.
  • Redirect URI must match byte-for-byte. Discord rejects near-misses (http vs https, trailing slash, localhost vs 127.0.0.1). Register one for local, one for prod.
  • Registering the listener is the #1 "driver not supported" cause. If 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.
  • State/CSRF errors. 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.
  • Scope format. Scopes are space-delimited in the query string. Pass them as an array to ->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.
  • Maintenance mode. Last release was 2023-07; this is a thin, stable wrapper. If Discord bumps API versions (currently v10), expect the version to be pinned in the provider. If you need a newer API endpoint, call it directly with Http::withToken() rather than waiting for a package update.
  • Subtree split. 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.
  • Version alignment. 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.
  • Rate limits. User-token endpoints are limited per-token; bot endpoints per-guild. Cache /users/@me/guilds results rather than calling on every page load, and respect Retry-After on 429.
  • Debugging the raw payload. When mapping looks wrong, dump $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.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package