socialiteproviders/zalo
Zalo OAuth provider for Laravel Socialite, letting you add Zalo login to your app with a simple driver. Install via Composer, configure your app ID and secret, then authenticate users through Zalo’s OAuth flow alongside other Socialite providers.
composer require socialiteproviders/zalo
This assumes laravel/socialite is already installed in your app. The 5.0.0 release notes are only a compare link (4.1.0...5.0.0), so before you commit, open the diff and read composer.json — a socialiteproviders major bump almost always means a Socialite major floor (and transitively a newer PHP/Laravel). If you're on an older Laravel, pin ^4.1 and stay there rather than fighting dependency resolution. Also sanity-check that socialiteproviders/manager is pulled in — that's what SocialiteWasCalled and extendWith rely on.
1. Configure config/services.php. Keep the secret out of the file; use env only.
'zalo' => [
'client_id' => env('ZALO_CLIENT_ID'),
'client_secret' => env('ZALO_CLIENT_SECRET'),
'redirect' => env('ZALO_REDIRECT_URI'),
],
2. Register the provider. On Laravel ≤10 this typically lives in EventServiceProvider::$listen. On Laravel 11+ (no default EventServiceProvider) either rely on the manager's auto-registration via the package discovery or add the listener explicitly in AppServiceProvider::boot():
Event::listen(function (SocialiteWasCalled $event) {
$event->extendWith(ZaloExtendSocialite::class);
});
3. Create the Zalo app in the developer console. Note three things before leaving that screen: the App ID, the App Secret, and the exact list of scopes your app has been approved for. The provider can only request scopes the app already has — asking for one that isn't approved fails at redirect time, not at code time.
Where to look first. Your two controller actions (redirect and callback), the services.php block, and the Zalo console scope list. In practice, ~90% of first-run failures trace back to one of those three — not to the package itself.
First use case (the 15-minute path): a ZaloController@redirect that returns Socialite::driver('zalo')->redirect(), and a callback action that exchanges the code, pulls the user, and finds-or-creates a local account. Get that round trip working against a tunnel URL in local dev before touching anything else.
The standard redirect/callback pair — this is 95% of what you'll write:
public function redirect()
{
return Socialite::driver('zalo')->scopes(['openid', 'profile'])->redirect();
}
public function callback(Request $request)
{
$zaloUser = Socialite::driver('zalo')->user();
$user = User::firstOrCreate(
['zalo_id' => $zaloUser->getId()],
['name' => $zaloUser->getName()],
);
Auth::login($user, remember: true);
return redirect()->intended(route('dashboard'));
}
Note the missing email above — see the nullable-email gotcha below.
Token persistence. Zalo access tokens are short-lived; the refresh token is the piece worth storing. Save access_token, refresh_token, and a computed expires_at (derive it yourself from the raw array rather than trusting whichever helper, since shape varies by provider). Check expiry before any Graph-style call. Do not assume the driver auto-refreshes — refresh support is a common gap in Socialite providers, so verify against the 5.0 source before building on it.
Account linking, not just login. Most real Zalo integrations attach Zalo to an existing user, not create a new one. Pattern:
if (Auth::check()) {
Auth::user()->update(['zalo_id' => $zaloUser->getId()]); // let DB unique constraint catch collisions
}
Guard against a zalo_id already belonging to someone else by putting a unique index on the column and catching UniqueConstraintViolationException, rather than a SELECT-then-INSERT which races under concurrent callbacks.
Testing. Socialite has no built-in fake, so Http::fake() the token endpoint and userinfo endpoint — or bind a fake driver into the container in a test case. Cover at minimum: happy path, user cancels (error=access_denied in the query string), and state mismatch. Assert on the mapped user object fields, not just that a user was created — that's how you catch a silent field rename on upgrade.
Multi-environment. Zalo issues per-app credentials, so maintain separate apps for local/staging/prod and always drive ZALO_REDIRECT_URI from env. The local tunnel URL must be re-registered in the console every time it changes — a fixed subdomain saves considerable pain.
Socialite::with() → driver(), changed getAuthUrl() semantics). Pin ^4.1 if you're not ready to move your framework.id, openid, and email keys moved. A silent rename breaks your firstOrCreate lookup and quietly spawns duplicate accounts. Cover the mapping with a test before deploying.http://localhost and the production URL; trailing slashes matter. The string in the console must match byte-for-byte.id across your staging and production apps. Never merge accounts on Zalo ID alone without scoping by app, and never expose it as a public identifier (URLs, API responses).InvalidArgumentException (Invalid state) or comes back with error in the query string. Wrap ->user() in try/catch and redirect to login with a flash message rather than surfacing a 500.stateless() is for backend flows only. Calling it on the browser redirect flow disables CSRF protection on the callback. If you need it for a mobile/API client, add your own nonce verification.users.email nullable or synthesize a fallback, otherwise registration fails silently for a subset of users.mapUserToObject() or getUserByToken(), then register your subclass in the SocialiteWasCalled listener instead of the shipped one. Cache the fetched profile in the session rather than calling Zalo on every request.How can I help you explore Laravel packages today?