-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
sources/oauth: add WeChat type #18086
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Anduin2017
wants to merge
8
commits into
goauthentik:main
Choose a base branch
from
Anduin2017:patch-3
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+313
−1
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
2f42ec2
Add wechat.
Anduin2017 432a259
Refactor comments and formatting in wechat.py
Anduin2017 812e4dd
Fix lint.
Anduin2017 db8a4dd
Fix lint.
Anduin2017 82a1454
fix: Rename `WeChat` enum member to `Wechat` for consistency
Anduin2017 a197d27
docs: Add WeChat social login integration guide.
Anduin2017 c4c30a5
Docs updates
dewi-tik 32dc4ca
Revise WeChat integration instructions
Anduin2017 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"]) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"), | ||
| }, | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11332,7 +11332,8 @@ | |
| "patreon", | ||
| "reddit", | ||
| "twitch", | ||
| "twitter" | ||
| "twitter", | ||
| "wechat" | ||
| ], | ||
| "title": "Provider type" | ||
| }, | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47355,6 +47355,7 @@ components: | |
| - twitch | ||
| type: string | ||
| ProxyMode: | ||
| enum: | ||
|
|
||
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
82 changes: 82 additions & 0 deletions
82
website/docs/users-sources/sources/social-logins/wechat/index.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| --- | ||
| title: WeChat | ||
| tags: | ||
| - source | ||
| --- | ||
|
|
||
| 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) | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.