socialiteproviders/streamelements
StreamElements OAuth provider for Laravel Socialite, letting apps add StreamElements login and fetch authenticated user profiles with a standard Socialite driver. Simple setup for Laravel authentication flows.
Minimal path from zero to a working "Sign in with StreamElements" flow on a package version in the 5.1.x line:
composer require socialiteproviders/streamelements
SocialiteProviders\Manager pattern: the driver is not registered by the provider itself, but by the manager's SocialiteWasCalled event.
app/Providers/EventServiceProvider.php add to $listen:
\SocialiteProviders\Manager\SocialiteWasCalled::class => [
\SocialiteProviders\StreamElements\StreamElementsExtendSocialite::class.'@handle',
],
EventServiceProvider): register the same listener in AppServiceProvider::boot() or bootstrap/app.php:
Event::listen(
\SocialiteProviders\Manager\SocialiteWasCalled::class,
\SocialiteProviders\StreamElements\StreamElementsExtendSocialite::class.'@handle'
);
SocialiteWasCalled is already registered; just add the StreamElementsExtendSocialite entry alongside it.config/services.php, reading from .env:
'streamelements' => [
'client_id' => env('STREAMELEMENTS_CLIENT_ID'),
'client_secret' => env('STREAMELEMENTS_CLIENT_SECRET'),
'redirect' => env('STREAMELEMENTS_REDIRECT_URI'),
],
STREAMELEMENTS_REDIRECT_URI. Nothing in this package can work around a mismatch here.Route::get('/auth/streamelements', fn () => Socialite::driver('streamelements')->redirect());
Route::get('/auth/streamelements/callback', function () {
$user = Socialite::driver('streamelements')->user();
// $user->getId() -> stable StreamElements account id
// $user->getNickname() -> display/username
// $user->getEmail() -> may be null depending on scopes
return $user->getRaw();
});
Dump getRaw() once on your first successful callback before writing any persistence logic — it is the fastest way to confirm which fields your account actually returns.Where to look first when something is off: the SocialiteProviders\StreamElements provider class (endpoints, scope defaults, and the user-mapping method getUserByToken/mapUserToObject), then config/services.php, then the registered event listener.
Standard web-login controller. Keep a thin controller and push persistence into an action class so you can reuse it from an API context:
public function callback(): RedirectResponse
{
$seUser = Socialite::driver('streamelements')->user();
$account = StreamerAccount::updateOrCreate(
['streamelements_id' => $seUser->getId()],
[
'username' => $seUser->getNickname(),
'email' => $seUser->getEmail(),
'access_token' => encrypt($seUser->token),
'refresh_token'=> $seUser->refreshToken ? encrypt($seUser->refreshToken) : null,
'expires_at' => $seUser->expiresIn ? now()->addSeconds($seUser->expiresIn) : null,
],
);
Auth::login($account->user);
return redirect()->intended('/dashboard');
}
Stateless mode for API/first-party flows. If you drive the callback from an API, a mobile app, or a popup window where the session cookie may not round-trip, use ->stateless(). Otherwise Socialite's state check will throw InvalidStateException even though StreamElements authenticated the user correctly.
Scope handling. StreamElements uses granular scopes (channel:read, chat:write, tips:read, points:write, overlays:read, bot:write, activities:read, and similar), and they are per-capability rather than per-resource. Two patterns:
Socialite::driver('streamelements')
->setScopes(['channel:read', 'tips:read', 'points:read'])
->redirect();
Confirm the exact scope strings in the StreamElements developer dashboard at the time you build — unknown scopes fail with invalid_scope, not a warning.Decouple "who they are" from "what we can do." Store the StreamElements id as the primary external key, keep the username as a mutable display field, and keep tokens in a separate table/column set with an expires_at. A StreamElements id is the only field stable enough to key on; usernames are renamable.
Refresh as a job, not inline. Because the Socialite user object gives you refreshToken and expiresIn, wrap the refresh call in a queued job that runs ahead of expiry:
Http::asForm()->post('https://api.streamelements.com/oauth2/token', [
'grant_type' => 'refresh_token',
'refresh_token' => decrypt($account->refresh_token),
'client_id' => config('services.streamelements.client_id'),
'client_secret' => config('services.streamelements.client_secret'),
]);
Testing. There is no Socialite::fake() for arbitrary drivers; mock the driver facade instead:
Socialite::shouldReceive('driver->user')->andReturn(
(new \Laravel\Socialite\Two\User)->map([
'id' => 'abc-123', 'nickname' => 'streamer', 'email' => null,
])
);
Extension point. If StreamElements changes its /me payload or you need extra fields (partner status, account type), the cleanest approach is to subclass the provided provider, override the user-mapping method, and register your class as the driver via Socialite::extend() rather than editing vendor code.
Driver [streamelements] is not supported — almost always the SocialiteWasCalled listener is missing or the handle method isn't registered under the manager package. This is the single most common setup failure. Check it before debugging credentials.http vs https, a trailing slash, www vs apex, or a port difference will all break it. Copy-paste, don't retype.config/services.php in a deployed environment, php artisan config:cache must be re-run (or config:clear locally). Stale config reads as "credentials are wrong" when they aren't.getEmail() can be null. StreamElements does not guarantee an email in the profile payload for every account type; make the column nullable and never rely on email for identity or account linking.user() throws, it doesn't return null. Wrap the callback in a try/catch for Laravel\Socialite\Two\InvalidStateException and the generic \Throwable from the token exchange, and redirect to a friendly error page rather than a 500. The exception message contains the OAuth error code (invalid_grant, invalid_scope) — log it verbatim; it's usually sufficient to diagnose without a debugger.expires_at check and a refresh.stateless() in the wrong place. Adding it to fix an InvalidStateException in a normal server-rendered login flow removes CSRF protection from the OAuth round-trip. Only use it where you genuinely have no session.$user->getRaw() is your friend; there's no need to enable HTTP logging to see what the provider parsed. If you need the actual HTTP exchange, drop a temporary Log::debug in your subclassed provider's request method rather than at the HTTP client level.^5.1 in composer.json, and when you next run composer update, watch the resolved laravel/socialite and socialiteproviders/manager versions — breakage in these packages almost always comes from a Socialite/Manager major bump rather than from the provider itself.How can I help you explore Laravel packages today?