socialiteproviders/twitch
Twitch OAuth2 provider for Laravel Socialite. Adds Twitch login to your Laravel app through the Socialite Providers ecosystem, handling the Twitch authorization flow and user profile retrieval with the familiar, simple Socialite API.
1. Install both pieces. The provider is a thin extension, not a standalone OAuth client:
composer require laravel/socialite socialiteproviders/twitch
The socialiteproviders/twitch package depends on socialiteproviders/manager and laravel/socialite. Check composer show socialiteproviders/twitch to confirm the Socialite major version it was split against (the 2024-04-01 release targets Socialite 5.x).
2. Register a Twitch application at https://dev.twitch.tv/console/apps. Note the Client ID, Client Secret, and set the OAuth Redirect URL exactly (e.g. https://yourapp.test/auth/twitch/callback). Twitch is strict about exact string match — including trailing slashes.
3. Add credentials to config/services.php:
'twitch' => [
'client_id' => env('TWITCH_CLIENT_ID'),
'client_secret' => env('TWITCH_CLIENT_SECRET'),
'redirect' => env('TWITCH_REDIRECT_URI'),
],
4. Register the Socialite event listener. This is the step people forget. Socialite doesn't know about twitch until the package's TwitchExtendSocialite listener runs.
Laravel ≤ 10 (app/Providers/EventServiceProvider.php):
protected $listen = [
\SocialiteProviders\Manager\SocialiteWasCalled::class => [
\SocialiteProviders\Twitch\TwitchExtendSocialite::class . '@handle',
],
];
Laravel 11+ (app/Providers/AppServiceProvider.php boot()):
Event::listen(
\SocialiteProviders\Manager\SocialiteWasCalled::class,
\SocialiteProviders\Twitch\TwitchExtendSocialite::class . '@handle',
);
5. First use case — "Log in with Twitch":
Route::get('/auth/twitch/redirect', fn () =>
Socialite::driver('twitch')->scopes(['user:read:email'])->redirect()
);
Route::get('/auth/twitch/callback', function () {
$twitchUser = Socialite::driver('twitch')->user();
// $twitchUser->getId(), ->getNickname(), ->getName(),
// ->getEmail(), ->getAvatar(), ->token, ->refreshToken
});
Where to look first: the monorepo at SocialiteProviders/Providers (the package you installed is a read-only subtree split — bugs and PRs go upstream). The provider README and https://socialiteproviders.com/Twitch/ document the exact listener class name and config keys.
Standard redirect/callback with persistence. Twitch returns a Helix data[0] object; the provider flattens it, so you get a normal Socialite User. Persist by provider_user_id (the Twitch numeric ID as a string) rather than email — many Twitch accounts have no verified email.
$twitchUser = Socialite::driver('twitch')->user();
$user = User::updateOrCreate(
['twitch_id' => $twitchUser->getId()],
[
'name' => $twitchUser->getName(), // display_name
'nickname' => $twitchUser->getNickname(), // login handle
'email' => $twitchUser->getEmail(),
'avatar' => $twitchUser->getAvatar(),
]
);
// Store tokens if you plan to call the Helix API on the user's behalf
$user->forceFill([
'twitch_token' => $twitchUser->token,
'twitch_refresh_token' => $twitchUser->refreshToken,
'twitch_expires_at' => now()->addSeconds($twitchUser->expiresIn ?? 0),
])->save();
Auth::login($user);
Request the right scopes. The default scope set is empty, so getEmail() returns null unless you ask for user:read:email. Add any Helix scopes you'll need for follow/sub/bits features:
Socialite::driver('twitch')
->scopes(['user:read:email', 'user:read:follows'])
->redirect();
Scopes are space-separated on the wire; the provider handles that, but you must pass them as an array.
Stateless / API mode. For a mobile or SPA client that POSTs an access token:
$user = Socialite::driver('twitch')->stateless()->userFromToken($accessToken);
userFromToken skips the redirect dance and hits the Helix /helix/users endpoint directly — handy for token refresh flows.
Refresh handling. Twitch access tokens expire (~60 days by default, shorter for some grants). Since Socialite's built-in refresh helper isn't guaranteed on older Socialite versions, store refresh_token and expires_in, and refresh against https://id.twitch.tv/oauth2/token yourself when expired. Check $user->expiresIn is non-null before trusting it.
Testing. Mock Socialite rather than hitting Twitch:
Socialite::shouldReceive('driver->user')->andReturn(
(new \Laravel\Socialite\Two\User)->map([
'id' => '12345', 'nickname' => 'streamer',
'name' => 'Streamer', 'email' => '[email protected]',
'avatar' => 'https://...',
])
);
Or use Socialite::fake('twitch') on newer Socialite. This lets you exercise the callback controller without network calls or secrets.
Multi-provider login. Because the listener registers a twitch driver inside Socialite's manager, you can coexist with google, github, etc. Keep the config key lowercase twitch — Socialite lowercases driver names when resolving, and a mismatch here is a common silent failure.
The listener is mandatory. Without registering TwitchExtendSocialite, Socialite::driver('twitch') throws Driver [twitch] not supported. This is the #1 support question. On Laravel 11+, confirm the listener runs in boot(), not register().
getEmail() is often null. Twitch only returns email when user:read:email is in the scopes and the user has a verified email. Always handle null; don't put a NOT NULL constraint on the email column when it comes from Twitch.
Exact redirect URI matching. Twitch's OAuth will reject with redirect_mismatch if the callback URL differs from the console entry by even a trailing slash. Localhost vs 127.0.0.1 are distinct. Put the exact value in TWITCH_REDIRECT_URI and use route()/url() consistently.
InvalidStateException. Socialite's state check fails across load-balanced servers without sticky sessions, or if the callback route runs in stateless() mode by accident. Either share the session store or move to stateless() end-to-end.
Helix requires a Client-Id header. The provider adds the Twitch Client-Id for you; if you later call Helix yourself with a raw token, you must send both Authorization: Bearer ... and Client-Id: ... or get 401 Unauthorized.
expiresIn may be null. Twitch returns expires_in in the token response, but Socialite historically didn't always propagate it. Guard against null when computing expiry, and treat the stored token as refreshable rather than assuming a fixed lifetime.
Subtree split — don't patch it. socialiteproviders/twitch is generated from SocialiteProviders/Providers. Any PR opened here is ignored; file it against the monorepo. Also, version pins here mirror the monorepo release cadence, not per-provider fixes.
Config key casing. Use 'twitch' everywhere: config key, driver('twitch'), listener name. Driver name resolution is case-sensitive in some Socialite paths, and Twitch in config will silently yield a null config.
Debugging Twitch-side errors. A 400 Bad Request with invalid_client means bad client ID/secret; invalid_grant/invalid_scope means your requested scope is wrong or the app isn't approved for it (some scopes, like channel:read:subscriptions, require Twitch app review and are silently dropped). Log $e->getResponse()->getBody() from Socialite's GuzzleException for the real message.
Extension points. For a custom Helix endpoint on the same driver, subclass SocialiteProviders\Twitch\Provider and override getUserByToken()/mapUserToObject(), then register a second listener that instantiates your subclass. Don't try to monkey-patch the shipped provider — re-installing will overwrite it.
Avatar URLs. Twitch profile_image_url changes independently of the user ID. Refresh it on each login instead of relying on the cached copy, or the user's avatar will go stale.
How can I help you explore Laravel packages today?