socialiteproviders/zoho
Zoho OAuth 2.0 provider for Laravel Socialite. Adds Zoho single sign-on to your Laravel app through the SocialiteProviders manager, so users can log in with their Zoho account using the familiar Socialite::driver('zoho') API. Install via Composer and configure your client ID and secret.
composer require socialiteproviders/zoho — this pulls in the shared socialiteproviders/manager package. You don't write or register a service provider; the driver is attached via an event listener.SocialiteWasCalled:// app/Providers/EventServiceProvider.php
protected $listen = [
\SocialiteProviders\Manager\SocialiteWasCalled::class => [
\SocialiteProviders\Zoho\ZohoExtendSocialite::class.'@handle',
],
];
On Laravel 11+ (no EventServiceProvider by default), either Event::listen(...) in AppServiceProvider::boot() or drop a listener in app/Listeners with a handle(SocialiteWasCalled $event) type-hint — Laravel's event auto-discovery will find it.
config/services.php. Socialite reads services.zoho, so the key name matters:'zoho' => [
'client_id' => env('ZOHO_CLIENT_ID'),
'client_secret' => env('ZOHO_CLIENT_SECRET'),
'redirect' => env('ZOHO_REDIRECT_URI'),
// data-center TLD: com | eu | in | com.au | jp | ca
// confirm the exact key name and default in the provider source
'domain' => env('ZOHO_DOMAIN', 'com'),
],
vendor/socialiteproviders/zoho/src/Provider.php before writing any app code. It is short, and it is the only authoritative answer to: which config keys are read, which scopes are requested by default, which host serves the profile, and how Zoho's PascalCase payload is mapped onto Socialite's User. The docs for this package are thin; the source is not.Socialite::driver('zoho')->user() in tinker. Get that green before you think about token storage or calling Zoho APIs.return Socialite::driver('zoho')->redirect();, then in the callback $user = Socialite::driver('zoho')->user(); and look up/create your local user. Resolve by ZUID ($user->getId()), not by email — Zoho emails can change and can be unverified.ZohoCRM.modules.contacts.READ) via ->scopes([...]) or ->setScopes([...]), and register the same scopes on the client in the Zoho API console first. Zoho rejects unregistered scopes outright with invalid_scope.eu account cannot authenticate against accounts.zoho.com. If your user base is single-region, hardcode domain in config. If it isn't, resolve the driver per tenant/region and build it explicitly rather than relying on the singleton:$provider = Socialite::buildProvider(
\SocialiteProviders\Zoho\Provider::class,
config('services.zoho_eu') // same shape, domain => 'eu'
);
$user->accessTokenResponseBody (Socialite 5.x) gives you the raw token response if the provider doesn't expose refresh tokens directly.ZohoAuthenticator or similar) with redirect(): RedirectResponse and userFromCallback(): ZohoIdentity. This gives you one place to handle DC resolution, scope lists, and the eventual replacement of this package — which, given its release history, you should assume will happen.stateless() for API/mobile flows. Session-based state validation will fail on any client that doesn't maintain the cookie jar.Http::fake() at the driver boundary. Mock the Socialite facade (Socialite::shouldReceive('driver->user')) or, better, fake your own ZohoAuthenticator interface and keep a single integration test that exercises the real driver against a stubbed Guzzle handler.domain produces a failed token exchange or a profile with null fields, not a clear "wrong data center" error. Confirm which DC each test account lives in before debugging anything else.Zoho-oauthtoken <token>, not Bearer <token>. The provider handles this for you for the profile call — but the moment you write your own refresh or CRM client call, using Bearer gets you a 401 with no useful body.access_type=offline (and usually prompt=consent). Without them Zoho issues a short-lived access token only. Pass them on the redirect: Socialite::driver('zoho')->with(['access_type' => 'offline', 'prompt' => 'consent'])->redirect();. Zoho also rotates/limits refresh tokens per client — re-consenting on every login can invalidate earlier ones, so guard against re-issuing on every request.application/x-www-form-urlencoded with Accept: application/json; otherwise you can get an HTML redirect or an empty body. Also, Zoho sometimes returns HTTP 200 with an error key in the JSON — Socialite's getAccessToken will fail with a generic InvalidArgumentException, so log the raw body when the exchange fails.getId() may be null/empty if the profile scope wasn't granted. Check EmailVerified (Zoho returns it) before trusting an email match for account linking, and always fall back to ZUID.composer.lock, and if you hit return-type or ?param = null deprecations, vendor the ~150 lines into app/Socialite/ZohoProvider.php and register with Socialite::extend('zoho', ...) rather than fighting the dependency..env values is a common source of invalid_client / redirect-mismatch errors.php artisan config:clear after deploying credential or domain changes. Cached config from a previous data center is a genuinely confusing failure mode.domain, scope, and redirect_uri query params; log the raw token response body; then log the raw profile array before it reaches mapUserToObject. Those three dumps localize nearly every failure.Socialite::extend() for a custom driver, Socialite::buildProvider() for per-tenant instances, and ->with() for extra authorization parameters. Those three cover most of what "I need to change the package's behaviour" actually means in practice.How can I help you explore Laravel packages today?