sources/oauth: add WeChat type (#18086)

* Add wechat.

* Refactor comments and formatting in wechat.py

Signed-off-by: Anduin Xue <anduin@aiursoft.com>

* Fix lint.

Signed-off-by: Anduin Xue <anduin@aiursoft.com>

* Fix lint.

* fix: Rename `WeChat` enum member to `Wechat` for consistency

* docs: Add WeChat social login integration guide.

* Docs updates

* Revise WeChat integration instructions

Updated instructions for creating a WeChat Website Application and added details about scopes and user attribute mappings.

Signed-off-by: Anduin Xue <anduin@aiursoft.com>

* Prettier

* Update wechat.py

Signed-off-by: Anduin Xue <anduin@aiursoft.com>

---------

Signed-off-by: Anduin Xue <anduin@aiursoft.com>
Co-authored-by: dewi-tik <dewi@goauthentik.io>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
This commit is contained in:
Anduin Xue
2025-12-10 20:48:12 +08:00
committed by GitHub
parent 4c07b7ae81
commit cd09bff247
10 changed files with 313 additions and 1 deletions
+1
View File
@@ -25,6 +25,7 @@ AUTHENTIK_SOURCES_OAUTH_TYPES = [
"authentik.sources.oauth.types.slack",
"authentik.sources.oauth.types.twitch",
"authentik.sources.oauth.types.twitter",
"authentik.sources.oauth.types.wechat",
]
+9
View File
@@ -307,6 +307,15 @@ class RedditOAuthSource(CreatableType, OAuthSource):
verbose_name_plural = _("Reddit OAuth Sources")
class WeChatOAuthSource(CreatableType, OAuthSource):
"""Social Login using WeChat."""
class Meta:
abstract = True
verbose_name = _("WeChat OAuth Source")
verbose_name_plural = _("WeChat OAuth Sources")
class OAuthSourcePropertyMapping(PropertyMapping):
"""Map OAuth properties to User or Group object attributes"""
@@ -0,0 +1,52 @@
"""WeChat Type tests"""
from django.test import RequestFactory, TestCase
from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.wechat import WeChatType
WECHAT_USER = {
"openid": "OPENID",
"nickname": "NICKNAME",
"sex": 1,
"province": "PROVINCE",
"city": "CITY",
"country": "COUNTRY",
"headimgurl": "https://thirdwx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0",
"privilege": ["PRIVILEGE1", "PRIVILEGE2"],
"unionid": " o6_buyCrymLUUFYHxvDU6M2PHl22",
}
class TestTypeWeChat(TestCase):
"""OAuth Source tests"""
def setUp(self):
self.source = OAuthSource.objects.create(
name="test",
slug="test",
provider_type="wechat",
)
self.factory = RequestFactory()
def test_enroll_context(self):
"""Test WeChat Enrollment context"""
ak_context = WeChatType().get_base_user_properties(
source=self.source, info=WECHAT_USER, client=None, token={}
)
self.assertEqual(ak_context["username"], WECHAT_USER["unionid"])
self.assertIsNone(ak_context["email"])
self.assertEqual(ak_context["name"], WECHAT_USER["nickname"])
self.assertEqual(ak_context["attributes"]["openid"], WECHAT_USER["openid"])
self.assertEqual(ak_context["attributes"]["unionid"], WECHAT_USER["unionid"])
def test_enroll_context_no_unionid(self):
"""Test WeChat Enrollment context without unionid"""
user = WECHAT_USER.copy()
del user["unionid"]
ak_context = WeChatType().get_base_user_properties(
source=self.source, info=user, client=None, token={}
)
self.assertEqual(ak_context["username"], WECHAT_USER["openid"])
self.assertIsNone(ak_context["email"])
self.assertEqual(ak_context["name"], WECHAT_USER["nickname"])
+162
View File
@@ -0,0 +1,162 @@
"""WeChat (Weixin) OAuth Views"""
from typing import Any
from requests.exceptions import RequestException
from authentik.sources.oauth.clients.oauth2 import OAuth2Client
from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect
class WeChatOAuthRedirect(OAuthRedirect):
"""WeChat OAuth2 Redirect"""
def get_additional_parameters(self, source: OAuthSource): # pragma: no cover
# WeChat (Weixin) for Websites official documentation requires 'snsapi_login'
# as the *only* scope for the QR code-based login flow.
# Ref: https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html (Step 1) # noqa: E501
return {
"scope": ["snsapi_login"],
}
class WeChatOAuth2Client(OAuth2Client):
"""
WeChat OAuth2 Client
Handles the non-standard parts of the WeChat OAuth2 flow.
"""
def get_access_token(self, redirect_uri: str, code: str) -> dict[str, Any]:
"""
Get access token from WeChat.
WeChat uses a non-standard GET request for the token exchange,
unlike the standard OAuth2 POST request. The AppID (client_id)
and AppSecret (client_secret) are passed as URL query parameters.
"""
token_url = self.get_access_token_url()
params = {
"appid": self.get_client_id(),
"secret": self.get_client_secret(),
"code": code,
"grant_type": "authorization_code",
}
# Send the GET request using the base class's session handler
response = self.do_request("get", token_url, params=params)
try:
response.raise_for_status()
except RequestException as exc:
self.logger.warning("Unable to fetch wechat token", exc=exc)
raise exc
data = response.json()
# Handle WeChat's specific error format (JSON with 'errcode' and 'errmsg')
if "errcode" in data:
self.logger.warning(
"Unable to fetch wechat token",
errcode=data.get("errcode"),
errmsg=data.get("errmsg"),
)
raise RequestException(data.get("errmsg"))
return data
def get_profile_info(self, token: dict[str, Any]) -> dict[str, Any]:
"""
Get Userinfo from WeChat.
This API call requires both the 'access_token' and the 'openid'
(which was returned during the token exchange).
"""
profile_url = self.get_profile_url()
params = {
"access_token": token.get("access_token"),
"openid": token.get("openid"),
"lang": "en", # or 'zh_CN' (Simplified Chinese), 'zh_TW' (Traditional)
}
response = self.do_request("get", profile_url, params=params)
try:
response.raise_for_status()
except RequestException as exc:
self.logger.warning("Unable to fetch wechat userinfo", exc=exc)
raise exc
data = response.json()
# Handle WeChat's specific error format
if "errcode" in data:
self.logger.warning(
"Unable to fetch wechat userinfo",
errcode=data.get("errcode"),
errmsg=data.get("errmsg"),
)
raise RequestException(data.get("errmsg"))
return data
class WeChatOAuth2Callback(OAuthCallback):
"""WeChat OAuth2 Callback"""
# Specify our custom Client to handle the non-standard WeChat flow
client_class = WeChatOAuth2Client
@registry.register()
class WeChatType(SourceType):
"""WeChat Type definition"""
callback_view = WeChatOAuth2Callback
redirect_view = WeChatOAuthRedirect
verbose_name = "WeChat"
name = "wechat"
# WeChat API URLs are fixed and not customizable
urls_customizable = False
# URLs for the WeChat "Login for Websites" authorization flow
authorization_url = "https://open.weixin.qq.com/connect/qrconnect"
# nosec: B105 This is a public URL, not a hardcoded secret
access_token_url = "https://api.weixin.qq.com/sns/oauth2/access_token" # nosec
profile_url = "https://api.weixin.qq.com/sns/userinfo"
# Note: 'authorization_code_auth_method' is intentionally omitted.
# The base OAuth2Client defaults to POST_BODY, but our custom
# WeChatOAuth2Client overrides get_access_token() to use GET,
# so this setting would be misleading.
def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]:
"""
Map WeChat userinfo to authentik user properties.
"""
# The WeChat userinfo API (sns/userinfo) does *not* return an email address.
# We explicitly set 'email' to None. Authentik will typically
# prompt the user to provide one on their first login if it's required.
# 'unionid' is the preferred unique identifier as it's consistent
# across multiple apps under the same WeChat Open Platform account.
# 'openid' is the fallback, which is only unique to this specific AppID.
return {
"username": info.get("unionid", info.get("openid")),
"email": None, # WeChat API does not provide Email
"name": info.get("nickname"),
"attributes": {
# Save all other relevant info as user attributes
"headimgurl": info.get("headimgurl"),
"sex": info.get("sex"),
"city": info.get("city"),
"province": info.get("province"),
"country": info.get("country"),
"unionid": info.get("unionid"),
"openid": info.get("openid"),
},
}
+2 -1
View File
@@ -11982,7 +11982,8 @@
"reddit",
"slack",
"twitch",
"twitter"
"twitter",
"wechat"
],
"title": "Provider type"
},
+1
View File
@@ -50080,6 +50080,7 @@ components:
- slack
- twitch
- twitter
- wechat
type: string
ProxyMode:
enum:
+1
View File
@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>WeChat</title><path 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.48c-.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"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -71,6 +71,8 @@ export function ProviderToLabel(provider?: ProviderTypeEnum): string {
return "Twitter";
case ProviderTypeEnum.Twitch:
return "Twitch";
case ProviderTypeEnum.Wechat:
return "WeChat";
case ProviderTypeEnum.UnknownDefaultOpenApi:
return msg("Unknown provider type");
}
+1
View File
@@ -615,6 +615,7 @@ const items = [
"users-sources/sources/social-logins/telegram/index",
"users-sources/sources/social-logins/twitch/index",
"users-sources/sources/social-logins/twitter/index",
"users-sources/sources/social-logins/wechat/index",
],
},
],
@@ -0,0 +1,82 @@
---
title: WeChat
tags:
- source
- wechat
---
Allows users to authenticate using their WeChat credentials by configuring WeChat as a federated identity provider via OAuth2.
## Preparation
The following placeholders are used in this guide:
- `authentik.company` is the FQDN of the authentik installation.
## WeChat configuration
To integrate WeChat with authentik you will need to register a "Website Application" (网站应用) on the [WeChat Open Platform](https://open.weixin.qq.com/).
1. Register for a developer account on the [WeChat Open Platform](https://open.weixin.qq.com/).
2. Navigate to the **Management Center** (管理中心) > **Website Application** (网站应用) and click **Create Website Application** (创建网站应用).
3. Submit the application for review.
4. Once approved, you will obtain an **AppID** and **AppSecret**.
5. In the WeChat application settings, configure the **Authorized Callback Domain** (授权回调域) to match your authentik domain (e.g. `authentik.company`).
:::info
This integration uses the WeChat "Website Application" login flow (QR Code login). When users access the login page on a desktop device (Windows/Mac) with the WeChat client installed, they may see a "Fast Login" prompt.
:::
## authentik configuration
To support the integration of WeChat with authentik, you need to create a WeChat OAuth source in authentik.
1. Log in to authentik as an administrator and open the authentik Admin interface.
2. Navigate to **Directory** > **Federation and Social login**, click **Create**, and then configure the following settings:
- **Select type**: select **WeChat OAuth Source** as the source type.
- **Create OAuth Source**: provide a name, a slug (e.g. `wechat`), and set the following required configurations:
- **Protocol settings**
- **Consumer Key**: Enter the **AppID** from the WeChat Open Platform.
- **Consumer Secret**: Enter the **AppSecret** from the WeChat Open Platform.
- **Scopes**: define any further access scopes.
3. Click **Finish**.
:::info Display new source on login screen
For instructions on how to display the new source on the authentik login page, refer to the [Add sources to default login page documentation](../../index.md#add-sources-to-default-login-page).
:::
:::info Embed new source in flow :ak-enterprise
For instructions on embedding the new source within a flow, such as an authorization flow, refer to the [Source Stage documentation](../../../../../add-secure-apps/flows-stages/stages/source/).
:::
## Source property mappings
Source property mappings allow you to modify or gather extra information from sources. See the [overview](../../property-mappings/index.md) for more information.
The following data is retrieved from WeChat and mapped to the user's attributes in authentik:
| WeChat Field | authentik Attribute | Description |
| :---------------------- | :---------------------- | :------------------------------ |
| `unionid` (or `openid`) | `username` | Used as the primary identifier. |
| `nickname` | `name` | The user's display name. |
| `headimgurl` | `attributes.headimgurl` | URL to the user's avatar. |
| `sex` | `attributes.sex` | Gender (1=Male, 2=Female). |
| `city` | `attributes.city` | User's city. |
| `province` | `attributes.province` | User's province. |
| `country` | `attributes.country` | User's country. |
### User Matching
WeChat users are identified by their `unionid` (if available) or `openid`.
- **UnionID**: Unique across multiple applications under the same developer account. authentik prioritizes this as the username.
- **OpenID**: Unique to the specific application. Used as a fallback if `unionid` is not returned.
:::info
WeChat does not provide the user's email address via the API.
:::
## Resources
- [WeChat Open Platform](https://open.weixin.qq.com/)
- [WeChat Login document](https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html)