Add scopes to already connected account in Socialstream
You have a project that uses Laravel Socialite via Socialstream (Socialite for Jetstream) and after that some of your users logged into their account, you realized that you were missing rights. This tutorial allows you to ask your user to give you additional rights!
This tutorial has been written for Socialstream 5.x
In my example, my users are logged in to Github but I need their permission to list their private repository. I knew I would eventually need this right, but I prefer to ask for more later when needed, so the user has more granular control over the right they grant to my application.
Updating Socialstream a bit
By default with SocialStream one of the actions is not published on your application, namely AuthenticateOAuthCallback.php
We don't need to rewrite the full action but we are gonna extend one of its functions. Here is the Action with alreadyAuthenticated()
replaced with our own version. I highlighted and commented the changes. If you are working with a different or updated version, you may be able to replicate this tutorial.
<?php namespace App\Actions\Socialstream; use App\Providers\RouteServiceProvider;
use Illuminate\Contracts\Auth\Authenticatable;use Illuminate\Http\RedirectResponse;use Illuminate\Support\Facades\Route;use Illuminate\Support\MessageBag;use JoelButcher\Socialstream\ConnectedAccount;use JoelButcher\Socialstream\Providers;use Laravel\Jetstream\Jetstream;use Laravel\Socialite\Contracts\User as ProviderUser; class AuthenticateOAuthCallback extends \JoelButcher\Socialstream\Actions\AuthenticateOAuthCallback{ protected function alreadyAuthenticated(Authenticatable $user, ?ConnectedAccount $account, string $provider, ProviderUser $providerAccount): RedirectResponse { // Get the route // While you are here, personalise this behaviour $route = match (true) { str(session('socialstream.previous_url', ''))->endsWith('github_private') => route('projects.create.github_private'), Route::has('profile.show') => route('profile.show'), Route::has('dashboard') => route('dashboard'), Route::has('home') => route('home'), default => RouteServiceProvider::HOME }; // Connect the account to the user, same as before if (! $account) { $this->createsConnectedAccounts->create($user, $provider, $providerAccount); $status = __('You have successfully connected :provider to your account.', ['provider' => Providers::name($provider)]); return class_exists(Jetstream::class) ? redirect()->to($route)->banner($status) : redirect()->to($route)->with('status', $status); } // Extract condition. If it's not the same user, return an error as before. $error = $account->user_id !== $user->id ? __('This :Provider sign in account is already associated with another user. Please log in with that user or connect a different :Provider account.', ['provider' => Providers::name($provider)]) : __('This :Provider sign in account is already associated with your user.', ['provider' => Providers::name($provider)]); if ($account->user_id !== $user->id) { $error = __('This :provider sign in account is already associated with another user. Please log in with that user or connect a different :provider account.', ['provider' => Providers::name($provider)]); return class_exists(Jetstream::class) ? redirect()->to($route)->dangerBanner($error) : redirect()->to($route)->withErrors((new MessageBag)->add('socialstream', $error)); } // If its the same user, update the connected account with the new updated token $this->updatesConnectedAccounts->update($user, $account, $provider, $providerAccount); $success = __('This :provider sign in account has been refreshed!', ['provider' => Providers::name($provider)]); return class_exists(Jetstream::class) ? redirect()->to($route)->banner($success) : redirect()->to($route); }}
Go to SocialstreamServiceProvider.php
and add our own AuthenticateOAuthCallback
<?phpnamespace App\Providers; use App\Actions\Socialstream\AuthenticateOAuthCallback; use App\Actions\Socialstream\CreateConnectedAccount;
use App\Actions\Socialstream\CreateUserFromProvider;use App\Actions\Socialstream\GenerateRedirectForProvider;use App\Actions\Socialstream\HandleInvalidState;use App\Actions\Socialstream\ResolveSocialiteUser;use App\Actions\Socialstream\UpdateConnectedAccount;use Illuminate\Support\ServiceProvider;use JoelButcher\Socialstream\Socialstream; class SocialstreamServiceProvider extends ServiceProvider{ public function boot(): void { Socialstream::resolvesSocialiteUsersUsing(ResolveSocialiteUser::class); Socialstream::createUsersFromProviderUsing(CreateUserFromProvider::class); Socialstream::createConnectedAccountsUsing(CreateConnectedAccount::class); Socialstream::updateConnectedAccountsUsing(UpdateConnectedAccount::class); Socialstream::handlesInvalidStateUsing(HandleInvalidState::class); Socialstream::generatesProvidersRedirectsUsing(GenerateRedirectForProvider::class); Socialstream::authenticatesOAuthCallbackUsing(AuthenticateOAuthCallback::class); }}
Checking the scopes
This part depends entirely on how you want to handle this in your application. You can add a new json column on connected_accounts
to remember which scopes you already have. Let me know if you want a tutorial on this. Or you can check which scopes are available at runtime.
Here I only need to verify once if the Github Account have the scope I asked for. Here is a little function to achieve that:
function checkGithubPrivateRepoScope(ConnectedAccount $github_connection): bool{ $client = new \Github\Client(); $client->authenticate(config('services.github.client_id'), config('services.github.client_secret'), AuthMethod::CLIENT_ID); $authorizations = $client ->authorizations() ->checkToken(config('services.github.client_id'), $github_connection->token); if (! in_array('repo', $authorizations['scopes'])) { return false; } return true;}
Asking for new scopes
Now that we know we need more rights. We're gonna ask politely to the user if he want's to gives us more. For that you just have to make a link to the existing oauth.redirect
route but with a new parameter that we will in GenerateRedirectForProvider
<x-action-link href="{{ route('oauth.redirect', ['provider' => 'github', 'need_private_access' => true]) }}" class="flex gap-3" > <x-socialstream-icons.github class="h-4 w-4"/> {{ __('Authorize access to private repo') }}</x-action-link>
Now to intercept this parameter, update GenerateRedirectForProvider.php
to add the scope to the socialite request.
<?phpnamespace App\Actions\Socialstream; +use Illuminate\Support\Facades\Request;use JoelButcher\Socialstream\Contracts\GeneratesProviderRedirect;use Laravel\Socialite\Facades\Socialite;use Symfony\Component\HttpFoundation\RedirectResponse; class GenerateRedirectForProvider implements GeneratesProviderRedirect{ public function generate(string $provider): RedirectResponse { $scopes = []; if ($provider == 'github') { $scopes = array_merge($scopes, [ 'read:user', 'user:email', ]); // If the user clicked a button to have additional scope, add them to the array if (boolval(Request::get('need_private_access', false))) { $scopes[] = 'repo'; } } return Socialite::driver($provider) ->scopes($scopes) ->redirect(); }}
And voilĂ !
When the user comes back, the updated token will be saved in the database and you will be able to list private repositories.
Syntax highlighting provided by torchlight.dev