Skip to content

Commit c440221

Browse files
Eric-Song-Nopclaudehimself65
committed
feat(social-provider): add wechat social provider (#5189)
Co-authored-by: Claude <[email protected]> Co-authored-by: Alex Yang <[email protected]>
1 parent 812fd4d commit c440221

File tree

5 files changed

+312
-0
lines changed

5 files changed

+312
-0
lines changed

.cspell/third-party.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,4 @@ vite
4040
electronjs
4141
wagmi
4242
tldts
43+
headimgurl
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
---
2+
title: WeChat
3+
description: WeChat provider setup and usage.
4+
---
5+
6+
<Steps>
7+
<Step>
8+
### Get your WeChat Credentials
9+
To use WeChat sign in, you need to register a website application on the [WeChat Open Platform](https://open.weixin.qq.com/), set the `Authorization Callback Domain` to the better auth domain and get your App ID and App Secret.
10+
</Step>
11+
12+
<Step>
13+
### Configure the provider
14+
To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
15+
16+
```ts title="auth.ts"
17+
import { betterAuth } from "better-auth"
18+
19+
export const auth = betterAuth({
20+
socialProviders: {
21+
wechat: { // [!code highlight]
22+
clientId: process.env.WECHAT_CLIENT_ID, // [!code highlight]
23+
clientSecret: process.env.WECHAT_CLIENT_SECRET, // [!code highlight]
24+
}, // [!code highlight]
25+
},
26+
})
27+
```
28+
29+
#### Optional Configuration
30+
31+
You can customize the WeChat provider with additional options:
32+
33+
```ts title="auth.ts"
34+
export const auth = betterAuth({
35+
socialProviders: {
36+
wechat: {
37+
clientId: process.env.WECHAT_CLIENT_ID,
38+
clientSecret: process.env.WECHAT_CLIENT_SECRET,
39+
// Optional: Set UI language for the WeChat login page
40+
lang: "cn", // or "en" for English
41+
// Optional: Use custom scopes
42+
scope: [], // "snsapi_login" for web QR code login.
43+
},
44+
},
45+
})
46+
```
47+
</Step>
48+
49+
<Step>
50+
### Sign In with WeChat
51+
To sign in with WeChat, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
52+
- `provider`: The provider to use. It should be set to `wechat`.
53+
54+
```ts title="auth-client.ts"
55+
import { createAuthClient } from "better-auth/client"
56+
const authClient = createAuthClient()
57+
58+
const signIn = async () => {
59+
const data = await authClient.signIn.social({
60+
provider: "wechat"
61+
})
62+
}
63+
```
64+
</Step>
65+
66+
</Steps>
67+
68+
## Usage
69+
70+
### Platform Support
71+
72+
WeChat provider currently supports the Website Application platform type, which enables WeChat QR code login for web applications.
73+
74+
### Development Notes
75+
76+
- The redirect URL domain must match the domain configured in the WeChat Open Platform
77+

landing/components/sidebar-content.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1135,6 +1135,24 @@ C0.7,239.6,62.1,0.5,62.2,0.4c0,0,54,13.8,119.9,30.8S302.1,62,302.2,62c0.2,0,0.2,
11351135
</svg>
11361136
),
11371137
},
1138+
{
1139+
title: "WeChat",
1140+
href: "/docs/authentication/wechat",
1141+
isNew: true,
1142+
icon: () => (
1143+
<svg
1144+
xmlns="http://www.w3.org/2000/svg"
1145+
width="1.2em"
1146+
height="1.2em"
1147+
viewBox="0 0 24 24"
1148+
>
1149+
<path
1150+
fill="currentColor"
1151+
d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.504c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.12 2.361-.336a.722.722 0 0 1 .598.082l1.584.926a.272.272 0 0 0 .14.047c.134 0 .24-.111.24-.247 0-.06-.023-.12-.038-.177l-.327-1.233a.582.582 0 0 1-.023-.156.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088V8.89c-.135-.01-.27-.027-.407-.03zm-2.53 3.274c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.97-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982z"
1152+
/>
1153+
</svg>
1154+
),
1155+
},
11381156
{
11391157
title: "Zoom",
11401158
href: "/docs/authentication/zoom",

packages/core/src/social-providers/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { twitch } from "./twitch";
3333
import { twitter } from "./twitter";
3434
import { vercel } from "./vercel";
3535
import { vk } from "./vk";
36+
import { wechat } from "./wechat";
3637
import { zoom } from "./zoom";
3738

3839
export const socialProviders = {
@@ -70,6 +71,7 @@ export const socialProviders = {
7071
polar,
7172
railway,
7273
vercel,
74+
wechat,
7375
};
7476

7577
export const socialProviderList = Object.keys(socialProviders) as [
@@ -126,6 +128,7 @@ export * from "./twitch";
126128
export * from "./twitter";
127129
export * from "./vercel";
128130
export * from "./vk";
131+
export * from "./wechat";
129132
export * from "./zoom";
130133

131134
export type SocialProviderList = typeof socialProviderList;
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import { betterFetch } from "@better-fetch/fetch";
2+
import type { OAuth2Tokens, OAuthProvider, ProviderOptions } from "../oauth2";
3+
4+
/**
5+
* WeChat user profile information
6+
* @see https://developers.weixin.qq.com/doc/oplatform/en/Website_App/WeChat_Login/Wechat_Login.html
7+
*/
8+
export interface WeChatProfile extends Record<string, any> {
9+
/**
10+
* User's unique OpenID
11+
*/
12+
openid: string;
13+
/**
14+
* User's nickname
15+
*/
16+
nickname: string;
17+
/**
18+
* User's avatar image URL
19+
*/
20+
headimgurl: string;
21+
/**
22+
* User's privileges
23+
*/
24+
privilege: string[];
25+
/**
26+
* User's UnionID (unique across the developer's various applications)
27+
*/
28+
unionid?: string;
29+
/** @note Email is currently unsupported by WeChat */
30+
email?: string;
31+
}
32+
33+
export interface WeChatOptions extends ProviderOptions<WeChatProfile> {
34+
/**
35+
* WeChat App ID
36+
*/
37+
clientId: string;
38+
/**
39+
* WeChat App Secret
40+
*/
41+
clientSecret: string;
42+
/**
43+
* Platform type for WeChat login
44+
* - Currently only supports "WebsiteApp" for WeChat Website Application (网站应用)
45+
* @default "WebsiteApp"
46+
*/
47+
platformType?: "WebsiteApp";
48+
49+
/**
50+
* UI language for the WeChat login page
51+
* cn for Simplified Chinese, en for English
52+
* @default "cn" if left undefined
53+
*/
54+
lang?: "cn" | "en";
55+
}
56+
57+
export const wechat = (options: WeChatOptions) => {
58+
return {
59+
id: "wechat",
60+
name: "WeChat",
61+
createAuthorizationURL({ state, scopes, redirectURI }) {
62+
const _scopes = options.disableDefaultScope ? [] : ["snsapi_login"];
63+
options.scope && _scopes.push(...options.scope);
64+
scopes && _scopes.push(...scopes);
65+
66+
// WeChat uses non-standard OAuth2 parameters (appid instead of client_id)
67+
// and requires a fragment (#wechat_redirect), so we construct the URL manually.
68+
const url = new URL("https://open.weixin.qq.com/connect/qrconnect");
69+
url.searchParams.set("scope", _scopes.join(","));
70+
url.searchParams.set("response_type", "code");
71+
url.searchParams.set("appid", options.clientId);
72+
url.searchParams.set("redirect_uri", options.redirectURI || redirectURI);
73+
url.searchParams.set("state", state);
74+
url.searchParams.set("lang", options.lang || "cn");
75+
url.hash = "wechat_redirect";
76+
77+
return url;
78+
},
79+
80+
// WeChat uses non-standard token exchange (appid/secret instead of
81+
// client_id/client_secret, GET instead of POST), so shared helpers
82+
// like validateAuthorizationCode/getOAuth2Tokens cannot be used directly.
83+
validateAuthorizationCode: async ({ code }) => {
84+
const params = new URLSearchParams({
85+
appid: options.clientId,
86+
secret: options.clientSecret,
87+
code: code,
88+
grant_type: "authorization_code",
89+
});
90+
91+
const { data: tokenData, error } = await betterFetch<{
92+
access_token: string;
93+
expires_in: number;
94+
refresh_token: string;
95+
openid: string;
96+
scope: string;
97+
unionid?: string;
98+
errcode?: number;
99+
errmsg?: string;
100+
}>(
101+
"https://api.weixin.qq.com/sns/oauth2/access_token?" +
102+
params.toString(),
103+
{
104+
method: "GET",
105+
},
106+
);
107+
108+
if (error || !tokenData || tokenData.errcode) {
109+
throw new Error(
110+
`Failed to validate authorization code: ${tokenData?.errmsg || error?.message || "Unknown error"}`,
111+
);
112+
}
113+
114+
return {
115+
tokenType: "Bearer" as const,
116+
accessToken: tokenData.access_token,
117+
refreshToken: tokenData.refresh_token,
118+
accessTokenExpiresAt: new Date(
119+
Date.now() + tokenData.expires_in * 1000,
120+
),
121+
scopes: tokenData.scope.split(","),
122+
// WeChat requires openid for the userinfo endpoint, which is
123+
// returned alongside the access token.
124+
openid: tokenData.openid,
125+
unionid: tokenData.unionid,
126+
};
127+
},
128+
129+
refreshAccessToken: options.refreshAccessToken
130+
? options.refreshAccessToken
131+
: async (refreshToken) => {
132+
const params = new URLSearchParams({
133+
appid: options.clientId,
134+
grant_type: "refresh_token",
135+
refresh_token: refreshToken,
136+
});
137+
138+
const { data: tokenData, error } = await betterFetch<{
139+
access_token: string;
140+
expires_in: number;
141+
refresh_token: string;
142+
openid: string;
143+
scope: string;
144+
errcode?: number;
145+
errmsg?: string;
146+
}>(
147+
"https://api.weixin.qq.com/sns/oauth2/refresh_token?" +
148+
params.toString(),
149+
{
150+
method: "GET",
151+
},
152+
);
153+
154+
if (error || !tokenData || tokenData.errcode) {
155+
throw new Error(
156+
`Failed to refresh access token: ${tokenData?.errmsg || error?.message || "Unknown error"}`,
157+
);
158+
}
159+
160+
return {
161+
tokenType: "Bearer" as const,
162+
accessToken: tokenData.access_token,
163+
refreshToken: tokenData.refresh_token,
164+
accessTokenExpiresAt: new Date(
165+
Date.now() + tokenData.expires_in * 1000,
166+
),
167+
scopes: tokenData.scope.split(","),
168+
};
169+
},
170+
171+
async getUserInfo(token) {
172+
if (options.getUserInfo) {
173+
return options.getUserInfo(token);
174+
}
175+
176+
const openid = (token as OAuth2Tokens & { openid?: string }).openid;
177+
178+
if (!openid) {
179+
return null;
180+
}
181+
182+
const params = new URLSearchParams({
183+
access_token: token.accessToken || "",
184+
openid: openid,
185+
lang: "zh_CN",
186+
});
187+
188+
const { data: profile, error } = await betterFetch<
189+
WeChatProfile & { errcode?: number; errmsg?: string }
190+
>("https://api.weixin.qq.com/sns/userinfo?" + params.toString(), {
191+
method: "GET",
192+
});
193+
194+
if (error || !profile || profile.errcode) {
195+
return null;
196+
}
197+
198+
const userMap = await options.mapProfileToUser?.(profile);
199+
return {
200+
user: {
201+
id: profile.unionid || profile.openid || openid,
202+
name: profile.nickname,
203+
email: profile.email || null,
204+
image: profile.headimgurl,
205+
emailVerified: false,
206+
...userMap,
207+
},
208+
data: profile,
209+
};
210+
},
211+
options,
212+
} satisfies OAuthProvider<WeChatProfile, WeChatOptions>;
213+
};

0 commit comments

Comments
 (0)