Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions authentik/sources/oauth/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"authentik.sources.oauth.types.reddit",
"authentik.sources.oauth.types.twitch",
"authentik.sources.oauth.types.twitter",
"authentik.sources.oauth.types.wechat",
]


Expand Down
9 changes: 9 additions & 0 deletions authentik/sources/oauth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,15 @@ class Meta:
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"""

Expand Down
52 changes: 52 additions & 0 deletions authentik/sources/oauth/tests/test_type_wechat.py
Original file line number Diff line number Diff line change
@@ -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 changes: 162 additions & 0 deletions authentik/sources/oauth/types/wechat.py
Original file line number Diff line number Diff line change
@@ -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"),
},
}
3 changes: 2 additions & 1 deletion blueprints/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -11332,7 +11332,8 @@
"patreon",
"reddit",
"twitch",
"twitter"
"twitter",
"wechat"
],
"title": "Provider type"
},
Expand Down
1 change: 1 addition & 0 deletions schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47355,6 +47355,7 @@ components:
- reddit
- twitch
- twitter
- wechat
type: string
ProxyMode:
enum:
Expand Down
1 change: 1 addition & 0 deletions web/authentik/sources/wechat.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions web/src/admin/sources/oauth/OAuthSourceViewPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,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");
}
Expand Down
1 change: 1 addition & 0 deletions website/docs/sidebar.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,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",
],
},
],
Expand Down
Original file line number Diff line number Diff line change
@@ -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)