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

Socialite Slack Laravel Package

mpociot/socialite-slack

Slack OAuth provider for Laravel Socialite. Adds a Slack driver so you can authenticate users with their Slack workspace account through Socialite's familiar redirect and callback API, alongside the built-in Google, GitHub, and Facebook providers.

View on GitHub
Deep Wiki
Context7

Getting Started

mpociot/socialite-slack is a single-class Socialite OAuth2 driver for Slack. Day-to-day, you use it exactly like any other Socialite driver: Socialite::driver('slack')->redirect() out, Socialite::driver('slack')->user() back. Treat it as ~150 lines of OAuth glue you own, not a dependency you composer require and forget.

1. Get the source into your app (don't pin the package)

The last release (2017) targets Socialite 3.0; on Laravel 9+ / Socialite 5.x Composer will likely refuse to resolve it. Read it once, then vendor it:

composer require mpociot/socialite-slack --dev   # scratch branch only, to read the source

Copy src/SlackProvider.php into app/Socialite/SlackProvider.php, namespace it App\Socialite, and remove the Composer entry. MIT license makes this clean. This is the whole "adoption" decision.

2. Configure credentials

// config/services.php
'slack' => [
    'client_id'     => env('SLACK_CLIENT_ID'),
    'client_secret' => env('SLACK_CLIENT_SECRET'),
    'redirect'      => env('SLACK_REDIRECT_URI'), // must match the Slack app exactly
],

3. Register the driver

// app/Providers/AppServiceProvider.php  (boot)
use Laravel\Socialite\Facades\Socialite;
use App\Socialite\SlackProvider;

Socialite::extend('slack', function ($app) {
    $config = $app['config']['services.slack'];

    return Socialite::buildProvider(SlackProvider::class, $config);
});

4. Two routes, one controller

// routes/web.php
Route::get('/auth/slack/redirect', [SlackAuthController::class, 'redirect'])->name('slack.redirect');
Route::get('/auth/slack/callback', [SlackAuthController::class, 'callback'])->name('slack.callback');
public function redirect()
{
    return Socialite::driver('slack')
        ->scopes(['identity.basic', 'identity.email', 'identity.avatar', 'identity.team'])
        ->redirect();
}

public function callback()
{
    $slackUser = Socialite::driver('slack')->user();

    // $slackUser->getId(), ->getName(), ->getEmail(), ->getAvatar(), ->getRaw()
    // Team identity lives in $slackUser->getRaw()['team']
}

5. Set up the Slack app first

Before you write a line of controller code, create the Slack app and set the redirect URL and user scopes in the dashboard. The most common first-run failure is a scope or redirect URI that doesn't match what the driver sends — Slack answers with invalid_scope or redirect_uri_mismatch, not a helpful stack trace.

First use case to wire up

Start with account linking, not login: an authenticated user clicks "Connect Slack," you store the token and (team_id, user_id) on their record. It's reversible, it doesn't change your login path, and it's the cheapest way to prove the flow works end-to-end before you make Slack a primary auth option.


Implementation Patterns

Wrap the driver in an internal adapter

Domain code should never touch the provider class. One interface, one implementation, one file to swap when you migrate to socialiteproviders/slack or Slack OIDC:

interface SlackIdentity
{
    public function redirectUrl(array $scopes = []): string;
    public function resolveCallback(): SlackAccount;
}

final class SlackAccount
{
    public function __construct(
        public string $teamId,
        public string $userId,
        public ?string $email,
        public ?string $name,
        public ?string $avatar,
        public string $accessToken,
        public array $raw,
    ) {}
}
final class SocialiteSlackIdentity implements SlackIdentity
{
    public function resolveCallback(): SlackAccount
    {
        $user = Socialite::driver('slack')->user();
        $raw  = $user->getRaw();

        return new SlackAccount(
            teamId:      $raw['team']['id'] ?? '',
            userId:      $raw['user']['id'] ?? $user->getId(),
            email:       $user->getEmail(),
            name:        $user->getName(),
            avatar:      $user->getAvatar(),
            accessToken: $user->token,
            raw:         $raw,
        );
    }
}

Everything downstream (controllers, jobs, tests) depends on SlackIdentity, never on the package.

Persist with a composite key, not an email

One human can be in five workspaces with the same email. Slack identity is the pair (team_id, user_id):

Schema::create('slack_identities', function (Blueprint $t) {
    $t->id();
    $t->foreignId('user_id')->constrained()->cascadeOnDelete();
    $t->string('team_id');
    $t->string('slack_user_id');
    $t->string('access_token');       // cast to 'encrypted' on the model
    $t->string('team_name')->nullable();
    $t->string('slack_email')->nullable();
    $t->timestamps();
    $t->unique(['team_id', 'slack_user_id']);
    $t->unique(['user_id', 'team_id']);
});

Cast access_token to encrypted on the Eloquent model. Never log the raw callback payload without redacting access_token.

Use stateless() for Anything Non-Browser

Socialite's default flow stores state in the session. For mobile handoffs, API-driven installs, or anything that starts outside the same browser session, use:

return Socialite::driver('slack')->stateless()->redirect();

…and ->stateless()->user() on the callback. If you stay stateful, you get CSRF protection for free — don't disable it in the browser flow "because it works."

Scope handling: pass it per call, not in config

The driver reads scopes from config, but per-flow overrides are clearer and more auditable:

// identity-only sign-in
Socialite::driver('slack')->scopes(['identity.basic', 'identity.email'])->redirect();

// install-flavoured (still user scopes — this package does NOT do bot scopes)
Socialite::driver('slack')->scopes(['identity.basic', 'identity.team', 'identity.avatar'])->redirect();

The scopes you send must be a subset of what's configured on the Slack app's User Token Scopes. Adding a scope in code without adding it in the dashboard is the single most common invalid_scope cause.

Feature-flag the flow

if (! config('features.slack_auth')) {
    return redirect()->route('login')->with('status', 'Slack sign-in is not available.');
}

Slack-side failures surface on your callback route. A flag plus a fallback (redirect()->route('login')) keeps a broken Slack flow from becoming a broken login page.

Testing: fake the two HTTP calls, not Socialite

The driver only makes two outbound requests: the token exchange and users.identity. Fake both:

Http::fake([
    'slack.com/api/oauth.access'  => Http::response(['ok' => true, 'access_token' => 'xoxp-test']),
    'slack.com/api/users.identity' => Http::response([
        'ok'   => true,
        'user' => ['id' => 'U123', 'name' => 'Ada', 'email' => '[email protected]', 'image_72' => '...'],
        'team' => ['id' => 'T123', 'name' => 'Acme', 'domain' => 'acme'],
    ]),
]);

$this->get('/auth/slack/callback')->assertRedirect('/dashboard');
$this->assertDatabaseHas('slack_identities', ['team_id' => 'T123', 'slack_user_id' => 'U123']);

Add a contract test that asserts the driver's authorize and token URLs — it's the cheapest guard against Slack-side endpoint changes, and it fails loudly the day the legacy endpoints stop accepting traffic.


Gotchas and Tips

The endpoints are legacy. The driver targets slack.com/oauth/authorize and slack.com/api/oauth.access with the users.identity response shape. Slack has since moved to oauth/v2/authorize + oauth.v2.access, which returns authed_user rather than a top-level user, and to granular scopes. Check the current Slack OAuth docs before your spike, and expect to edit getAuthUrl(), getTokenUrl(), and getUserByToken() — this is a fork-and-fix, not a configure-and-go.

Sign in with Slack is now OIDC-flavoured. Newer Slack apps sign users in with openid / profile / email scopes and fetch identity from the OIDC userinfo endpoint. If your Slack app is configured that way, the legacy identity.* flow is not just deprecated — it's a different getUserByToken() implementation entirely. Decide which flow you're on before forking.

Slack returns HTTP 200 on failure. {"ok": false, "error": "invalid_scope"} comes back with a 200 status. The driver's getUserByToken() will happily json_decode that and hand you a broken user object. Add an explicit check in your fork:

$payload = json_decode($response->getBody(), true);

if (! ($payload['ok'] ?? false)) {
    throw new InvalidStateException('Slack identity failed: '.($payload['error'] ?? 'unknown'));
}

Do this at every Slack call site, forever. It's Slack's most reliably repeated footgun.

getEmail() can be null. Slack users can hide their email, and if identity.email isn't granted you get null back — not an exception. Never use the Slack email as a required unique identifier or as a NOT NULL column. Map to an existing account by (team_id, slack_user_id) first, then by verified email if you must, and handle the null case as "ask the user to confirm."

Redirect URI is exact-match. http vs https, trailing slash, www, port — all must match the Slack app config character-for-character. Local dev via ngrok means updating the Slack app every time the tunnel URL changes; keep a .env var (SLACK_REDIRECT_URI) so you change it in one place.

Don't fight Composer. If the install fails on Socialite 5.x, that's the expected outcome, not a solvable problem. Copy the class, move on. If you do keep it in composer.json, composer why-not laravel/socialite will explain the block in one line — worth running before you spend an afternoon on constraints.

Extension points, if you keep it close to upstream:

  • Override getAuthUrl(), getTokenUrl(), getUserByToken(), and mapUserToObject() in a subclass — that's the whole surface.
  • protected $scopeSeparator = ' ' — Slack uses space-separated scopes, unlike comma-separated providers. Copying a Google/Facebook provider and forgetting this breaks scopes silently.
  • If you add fields (team name, avatar size), put them in mapUserToObject() and read the rest from getRaw() — don't rely on undocumented User properties.

Token lifecycle is entirely yours. The legacy flow gives you a user token with no refresh token and no rotation. You own: encryption at rest, expiry handling, revocation on uninstall (app_uninstalled webhook is not this package), and multi-workspace credential management. Budget for it explicitly — the OAuth handshake is the easy half.

Debugging checklist, in order:

  1. Log the raw callback payload once (redact the token). Does it have ok: true, user.id, team.id?
  2. redirect_uri_mismatch → compare the string in config('services.slack.redirect') to the Slack dashboard, byte for byte.
  3. invalid_scope → dashboard scopes vs ->scopes([...]), and user-token vs bot-token scope sections.
  4. invalid_code → codes are single-use and expire in minutes; you're re-hitting the callback (double-submit, prefetch, retry middleware).
  5. Session errors on user() → you called stateless() on redirect but not on callback, or vice versa.

Where to extend next. Once the flow is green and flagged, the natural follow-ons are: a job that validates the token against auth.test, a team_id → tenant mapping so multi-workspace works, and only then the Web API (which is a different package or the official Slack SDK — don't try to grow this driver into it).

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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect