socialiteproviders/wechat-web
WeChat Web OAuth provider for Laravel Socialite, enabling "Sign in with WeChat" for websites. Adds WeChat as a Socialite driver with minimal setup so you can authenticate users and fetch their profile via the WeChat Open Platform.
What it is. A Socialite driver for WeChat Official Account (公众号) H5 web authorization — the snsapi_base / snsapi_userinfo OAuth flow that runs inside the WeChat in-app browser. This is not the same as WeChat Open Platform login (socialiteproviders/wechat) or Mini Program login. Get the account type wrong and nothing works.
Install
composer require socialiteproviders/wechat-web
Register the listener. Laravel ≤10 in app/Providers/EventServiceProvider.php:
protected $listen = [
\SocialiteProviders\Manager\SocialiteWasCalled::class => [
'SocialiteProviders\\WeixinWeb\\WeixinWebExtendSocialite@handle',
],
];
Laravel 11+ has no EventServiceProvider by default — use a closure in AppServiceProvider::boot():
use Illuminate\Support\Facades\Event;
use SocialiteProviders\Manager\SocialiteWasCalled;
use SocialiteProviders\WeixinWeb\Provider;
Event::listen(function (SocialiteWasCalled $event) {
$event->extendSocialite('wechat-web', Provider::class);
});
Configure
// config/services.php
'wechat-web' => [
'client_id' => env('WECHAT_APP_ID'),
'client_secret' => env('WECHAT_APP_SECRET'),
'redirect' => env('WECHAT_REDIRECT_URI'), // https://example.com/auth/wechat/callback
],
The config key must equal the driver string registered by the listener. Confirm both by grepping the package:
grep -r "extendSocialite" vendor/socialiteproviders/wechat-web/. The namespace isSocialiteProviders\WeixinWebeven though the package is namedwechat-web— that mismatch is the #1 source of copy-paste errors.
Look here first. The whole provider is one file: vendor/socialiteproviders/wechat-web/Provider.php (~100 lines). Read it before writing any code — it tells you exactly which scopes, params, and user fields you actually get, which beats guessing from the WeChat docs.
First use case. A mobile landing page inside WeChat: identify the visitor, bind their openid/unionid to a users row, and skip the SMS/password login entirely.
// routes/web.php
Route::get('/auth/wechat', [WeChatController::class, 'redirect']);
Route::get('/auth/wechat/callback', [WeChatController::class, 'callback']);
public function redirect()
{
return Socialite::driver('wechat-web')
->with(['scope' => 'snsapi_userinfo'])
->redirect();
}
public function callback(Request $request)
{
try {
$wx = Socialite::driver('wechat-web')->user();
} catch (\Throwable $e) {
return redirect('/')->withErrors('微信授权失败,请重试');
}
$user = User::updateOrCreate(
['wechat_openid' => $wx->getId()],
[
'wechat_unionid' => $wx->getRaw()['unionid'] ?? null,
'name' => $wx->getNickname(),
'avatar' => $wx->getAvatar(),
]
);
Auth::login($user, true);
return redirect()->intended('/dashboard');
}
Choose the scope deliberately — it changes the UX, not just the payload.
| Scope | User sees | You get | Use for |
|---|---|---|---|
snsapi_base |
Nothing (silent redirect) | openid only |
Repeat visitors, silent re-identification |
snsapi_userinfo |
Consent screen | openid, nickname, headimgurl, unionid, sex, city |
First-time registration |
A common production pattern is two-phase: snsapi_base on every page load to cheaply recover a known session, and snsapi_userinfo only on the explicit "登录/注册" button. Note that snsapi_base gives you no profile — $wx->getNickname() will be empty, so branch on scope before touching user fields.
Treat unionid as the canonical identity, openid as per-account. openid is scoped to a single 公众号 — if you ever add a second account, a Mini Program, or a mobile app, the same human gets a different openid. unionid is stable across all products under one 开放平台 account. Store both, index unionid, and only fall back to openid when unionid is absent (i.e. the 公众号 is not yet bound to an Open Platform account — a very common misconfiguration).
Abstract the driver behind a small service so tests and future provider swaps don't leak into controllers:
class WeChatAuthenticator
{
public function redirectUrl(string $scope = 'snsapi_userinfo'): string
{
return Socialite::driver('wechat-web')->with(['scope' => $scope])->redirect()->getTargetUrl();
}
}
Multiple Official Accounts. Socialite's config-driven driver only supports one credential set. For a tenant/multi-account setup, build providers on the fly instead of relying on services.php:
$provider = Socialite::buildProvider(
\SocialiteProviders\WeixinWeb\Provider::class,
['client_id' => $account->app_id, 'client_secret' => $account->app_secret, 'redirect' => $url]
);
Combine with JS-SDK, don't duplicate it. This package only does OAuth. If the page also needs wx.config, share the tokens from an Official Account SDK (EasyWeChat) and keep this package strictly for identity — two packages both fetching Official Account access tokens will fight over the IP-whitelisted token quota.
Testing. You cannot exercise the real flow in CI. Fake the driver in feature tests:
Socialite::shouldReceive('driver->user')->andReturn(
(new \Laravel\Socialite\Two\User)->map([
'id' => 'oXyz...', 'nickname' => '测试用户', 'avatar' => 'https://...',
'unionid' => 'uXyz...',
])
);
Keep the callback controller thin — everything that could break (user creation, unionid merging) should live in a service you can unit-test with a plain array.
The package is effectively frozen (last release 2020-12-01, 29 stars). It's a read-only subtree split; issues belong in SocialiteProviders/Providers, and no one is actively patching it. Budget for either pinning laravel/socialite to a compatible major or vendoring Provider.php into app/Socialite/ (it's 100 lines — often cheaper than fighting a fork). If you need Official Account features beyond login (templates, payments, JS-SDK), reach for overtrue/wechat / EasyWeChat.
WeChat returns HTTP 200 on errors. Failures come back as {"errcode":40029,"errmsg":"invalid code"} with a 200 status, so Guzzle won't throw and you'll get a mangled User object instead of an exception. Wrap the callback in try/catch (\Throwable) and log the raw body. Frequent codes: 40029 invalid code, 40163 code already used, 40003 invalid openid. The code is single-use and expires in 5 minutes — a page refresh or a double-fired callback (React StrictMode, double-tap, browser prefetch) will burn it.
Domain whitelisting is a hard blocker, not a config detail. In the 公众号 admin console you set 网页授权域名 — a bare domain, no path, no port, no wildcard. The redirect URI must live under it. For mainland China hosting that domain must be ICP-filed (备案) and registered to the same entity; localhost and *.test cannot be whitelisted. Local development requires an HTTPS tunnel on a filed domain.
The flow only works inside the WeChat browser. snsapi_userinfo opened in Chrome/Safari shows "请在微信客户端打开链接" with no callback. Always detect UA and offer a fallback (QR code for desktop, or a password path) — otherwise every QA engineer files the same bug.
unionid is usually buried in the raw payload. Many Socialite providers don't expose it on the User object. Use $wx->getRaw()['unionid'] and null-check it, rather than assuming $wx->unionid.
state mismatches are the flakiest failure mode. InvalidStateException fires when the session is lost — Android WeChat webviews are inconsistent about cookie persistence, and users habitually press the back button into a stale authorize URL. Catch it separately from real WeChat errors and simply restart the flow. ->stateless() is the escape hatch, but you lose CSRF protection; only use it when the cookie genuinely can't survive.
Store Chinese nicknames in utf8mb4. WeChat nicknames routinely contain emoji. varchar(191) + utf8 will throw Incorrect string value on insert. Also budget openid/unionid at varchar(64).
You will never get an email. $wx->getEmail() returns null. Any flow keyed on email uniqueness needs a different strategy (phone binding via a separate verification step, or a generated placeholder).
Avatars default to 132px. headimgurl ends in /0. Strip the trailing /0 and append /132… or /0 for the 640px original — the CDN 302s, so cache the URL, not the image bytes.
Extension points if you must change behaviour: subclass SocialiteProviders\WeixinWeb\Provider and override getAuthUrl() (e.g. to force lang=zh_CN, or append the #wechat_redirect fragment WeChat expects), getTokenUrl(), or getUserByToken() (to enrich the User object with unionid once and for all). Register the subclass as the driver in the same SocialiteWasCalled listener — that keeps the hack in one file you can diff against upstream.
How can I help you explore Laravel packages today?