authentik.sources.oauth.types.wechat
WeChat (Weixin) OAuth Views
1"""WeChat (Weixin) OAuth Views""" 2 3from typing import Any 4 5from requests.exceptions import RequestException 6 7from authentik.sources.oauth.clients.oauth2 import OAuth2Client 8from authentik.sources.oauth.models import OAuthSource 9from authentik.sources.oauth.types.registry import SourceType, registry 10from authentik.sources.oauth.views.callback import OAuthCallback 11from authentik.sources.oauth.views.redirect import OAuthRedirect 12 13 14class WeChatOAuth2Client(OAuth2Client): 15 """ 16 WeChat OAuth2 Client 17 18 Handles the non-standard parts of the WeChat OAuth2 flow. 19 """ 20 21 def get_access_token(self, **request_kwargs) -> dict[str, Any] | None: 22 """ 23 Get access token from WeChat. 24 25 WeChat uses a non-standard GET request for the token exchange, 26 unlike the standard OAuth2 POST request. The AppID (client_id) 27 and AppSecret (client_secret) are passed as URL query parameters. 28 """ 29 if not self.check_application_state(): 30 self.logger.warning("Application state check failed.") 31 return {"error": "State check failed."} 32 33 code = self.get_request_arg("code", None) 34 if not code: 35 return None 36 37 token_url = self.source.source_type.access_token_url 38 params = { 39 "appid": self.get_client_id(), 40 "secret": self.get_client_secret(), 41 "code": code, 42 "grant_type": "authorization_code", 43 } 44 45 # Send the GET request using the base class's session handler 46 try: 47 response = self.do_request("get", token_url, params=params) 48 response.raise_for_status() 49 except RequestException as exc: 50 self.logger.warning("Unable to fetch wechat token", exc=exc) 51 return None 52 53 data = response.json() 54 55 # Handle WeChat's specific error format (JSON with 'errcode' and 'errmsg') 56 if "errcode" in data: 57 self.logger.warning( 58 "Unable to fetch wechat token", 59 errcode=data.get("errcode"), 60 errmsg=data.get("errmsg"), 61 ) 62 return None 63 64 return data 65 66 def get_profile_info(self, token: dict[str, Any]) -> dict[str, Any] | None: 67 """ 68 Get Userinfo from WeChat. 69 70 This API call requires both the 'access_token' and the 'openid' 71 (which was returned during the token exchange). 72 """ 73 profile_url = self.source.source_type.profile_url 74 params = { 75 "access_token": token.get("access_token"), 76 "openid": token.get("openid"), 77 "lang": "en", # or 'zh_CN' (Simplified Chinese), 'zh_TW' (Traditional) 78 } 79 80 response = self.do_request("get", profile_url, params=params) 81 82 try: 83 response.raise_for_status() 84 except RequestException as exc: 85 self.logger.warning("Unable to fetch wechat userinfo", exc=exc) 86 return None 87 88 data = response.json() 89 90 # Handle WeChat's specific error format 91 if "errcode" in data: 92 self.logger.warning( 93 "Unable to fetch wechat userinfo", 94 errcode=data.get("errcode"), 95 errmsg=data.get("errmsg"), 96 ) 97 return None 98 99 return data 100 101 def get_redirect_args(self) -> dict[str, str]: 102 """Get request parameters for redirect url.""" 103 args = super().get_redirect_args() 104 args["appid"] = args.pop("client_id") 105 return args 106 107 108class WeChatOAuthRedirect(OAuthRedirect): 109 """WeChat OAuth2 Redirect""" 110 111 client_class = WeChatOAuth2Client 112 113 def get_additional_parameters(self, source: OAuthSource): # pragma: no cover 114 # WeChat (Weixin) for Websites official documentation requires 'snsapi_login' 115 # as the *only* scope for the QR code-based login flow. 116 # Ref: https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html (Step 1) # noqa: E501 117 return { 118 "scope": ["snsapi_login"], 119 } 120 121 122class WeChatOAuth2Callback(OAuthCallback): 123 """WeChat OAuth2 Callback""" 124 125 # Specify our custom Client to handle the non-standard WeChat flow 126 client_class = WeChatOAuth2Client 127 128 def get_user_id(self, info: dict[str, Any]) -> str | None: 129 return info.get("unionid", info.get("openid")) 130 131 132@registry.register() 133class WeChatType(SourceType): 134 """WeChat Type definition""" 135 136 callback_view = WeChatOAuth2Callback 137 redirect_view = WeChatOAuthRedirect 138 verbose_name = "WeChat" 139 name = "wechat" 140 141 # WeChat API URLs are fixed and not customizable 142 urls_customizable = False 143 144 # URLs for the WeChat "Login for Websites" authorization flow 145 authorization_url = "https://open.weixin.qq.com/connect/qrconnect" 146 # This is a public URL, not a hardcoded secret 147 access_token_url = "https://api.weixin.qq.com/sns/oauth2/access_token" # nosec B105 148 profile_url = "https://api.weixin.qq.com/sns/userinfo" 149 150 # Note: 'authorization_code_auth_method' is intentionally omitted. 151 # The base OAuth2Client defaults to POST_BODY, but our custom 152 # WeChatOAuth2Client overrides get_access_token() to use GET, 153 # so this setting would be misleading. 154 155 def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]: 156 """ 157 Map WeChat userinfo to authentik user properties. 158 """ 159 # The WeChat userinfo API (sns/userinfo) does *not* return an email address. 160 # We explicitly set 'email' to None. Authentik will typically 161 # prompt the user to provide one on their first login if it's required. 162 163 # 'unionid' is the preferred unique identifier as it's consistent 164 # across multiple apps under the same WeChat Open Platform account. 165 # 'openid' is the fallback, which is only unique to this specific AppID. 166 return { 167 "username": info.get("unionid", info.get("openid")), 168 "email": None, # WeChat API does not provide Email 169 "name": info.get("nickname"), 170 "attributes": { 171 # Save all other relevant info as user attributes 172 "headimgurl": info.get("headimgurl"), 173 "sex": info.get("sex"), 174 "city": info.get("city"), 175 "province": info.get("province"), 176 "country": info.get("country"), 177 "unionid": info.get("unionid"), 178 "openid": info.get("openid"), 179 }, 180 }
15class WeChatOAuth2Client(OAuth2Client): 16 """ 17 WeChat OAuth2 Client 18 19 Handles the non-standard parts of the WeChat OAuth2 flow. 20 """ 21 22 def get_access_token(self, **request_kwargs) -> dict[str, Any] | None: 23 """ 24 Get access token from WeChat. 25 26 WeChat uses a non-standard GET request for the token exchange, 27 unlike the standard OAuth2 POST request. The AppID (client_id) 28 and AppSecret (client_secret) are passed as URL query parameters. 29 """ 30 if not self.check_application_state(): 31 self.logger.warning("Application state check failed.") 32 return {"error": "State check failed."} 33 34 code = self.get_request_arg("code", None) 35 if not code: 36 return None 37 38 token_url = self.source.source_type.access_token_url 39 params = { 40 "appid": self.get_client_id(), 41 "secret": self.get_client_secret(), 42 "code": code, 43 "grant_type": "authorization_code", 44 } 45 46 # Send the GET request using the base class's session handler 47 try: 48 response = self.do_request("get", token_url, params=params) 49 response.raise_for_status() 50 except RequestException as exc: 51 self.logger.warning("Unable to fetch wechat token", exc=exc) 52 return None 53 54 data = response.json() 55 56 # Handle WeChat's specific error format (JSON with 'errcode' and 'errmsg') 57 if "errcode" in data: 58 self.logger.warning( 59 "Unable to fetch wechat token", 60 errcode=data.get("errcode"), 61 errmsg=data.get("errmsg"), 62 ) 63 return None 64 65 return data 66 67 def get_profile_info(self, token: dict[str, Any]) -> dict[str, Any] | None: 68 """ 69 Get Userinfo from WeChat. 70 71 This API call requires both the 'access_token' and the 'openid' 72 (which was returned during the token exchange). 73 """ 74 profile_url = self.source.source_type.profile_url 75 params = { 76 "access_token": token.get("access_token"), 77 "openid": token.get("openid"), 78 "lang": "en", # or 'zh_CN' (Simplified Chinese), 'zh_TW' (Traditional) 79 } 80 81 response = self.do_request("get", profile_url, params=params) 82 83 try: 84 response.raise_for_status() 85 except RequestException as exc: 86 self.logger.warning("Unable to fetch wechat userinfo", exc=exc) 87 return None 88 89 data = response.json() 90 91 # Handle WeChat's specific error format 92 if "errcode" in data: 93 self.logger.warning( 94 "Unable to fetch wechat userinfo", 95 errcode=data.get("errcode"), 96 errmsg=data.get("errmsg"), 97 ) 98 return None 99 100 return data 101 102 def get_redirect_args(self) -> dict[str, str]: 103 """Get request parameters for redirect url.""" 104 args = super().get_redirect_args() 105 args["appid"] = args.pop("client_id") 106 return args
WeChat OAuth2 Client
Handles the non-standard parts of the WeChat OAuth2 flow.
def
get_access_token(self, **request_kwargs) -> dict[str, Any] | None:
22 def get_access_token(self, **request_kwargs) -> dict[str, Any] | None: 23 """ 24 Get access token from WeChat. 25 26 WeChat uses a non-standard GET request for the token exchange, 27 unlike the standard OAuth2 POST request. The AppID (client_id) 28 and AppSecret (client_secret) are passed as URL query parameters. 29 """ 30 if not self.check_application_state(): 31 self.logger.warning("Application state check failed.") 32 return {"error": "State check failed."} 33 34 code = self.get_request_arg("code", None) 35 if not code: 36 return None 37 38 token_url = self.source.source_type.access_token_url 39 params = { 40 "appid": self.get_client_id(), 41 "secret": self.get_client_secret(), 42 "code": code, 43 "grant_type": "authorization_code", 44 } 45 46 # Send the GET request using the base class's session handler 47 try: 48 response = self.do_request("get", token_url, params=params) 49 response.raise_for_status() 50 except RequestException as exc: 51 self.logger.warning("Unable to fetch wechat token", exc=exc) 52 return None 53 54 data = response.json() 55 56 # Handle WeChat's specific error format (JSON with 'errcode' and 'errmsg') 57 if "errcode" in data: 58 self.logger.warning( 59 "Unable to fetch wechat token", 60 errcode=data.get("errcode"), 61 errmsg=data.get("errmsg"), 62 ) 63 return None 64 65 return data
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.
def
get_profile_info(self, token: dict[str, typing.Any]) -> dict[str, Any] | None:
67 def get_profile_info(self, token: dict[str, Any]) -> dict[str, Any] | None: 68 """ 69 Get Userinfo from WeChat. 70 71 This API call requires both the 'access_token' and the 'openid' 72 (which was returned during the token exchange). 73 """ 74 profile_url = self.source.source_type.profile_url 75 params = { 76 "access_token": token.get("access_token"), 77 "openid": token.get("openid"), 78 "lang": "en", # or 'zh_CN' (Simplified Chinese), 'zh_TW' (Traditional) 79 } 80 81 response = self.do_request("get", profile_url, params=params) 82 83 try: 84 response.raise_for_status() 85 except RequestException as exc: 86 self.logger.warning("Unable to fetch wechat userinfo", exc=exc) 87 return None 88 89 data = response.json() 90 91 # Handle WeChat's specific error format 92 if "errcode" in data: 93 self.logger.warning( 94 "Unable to fetch wechat userinfo", 95 errcode=data.get("errcode"), 96 errmsg=data.get("errmsg"), 97 ) 98 return None 99 100 return data
Get Userinfo from WeChat.
This API call requires both the 'access_token' and the 'openid' (which was returned during the token exchange).
def
get_redirect_args(self) -> dict[str, str]:
102 def get_redirect_args(self) -> dict[str, str]: 103 """Get request parameters for redirect url.""" 104 args = super().get_redirect_args() 105 args["appid"] = args.pop("client_id") 106 return args
Get request parameters for redirect url.
Inherited Members
109class WeChatOAuthRedirect(OAuthRedirect): 110 """WeChat OAuth2 Redirect""" 111 112 client_class = WeChatOAuth2Client 113 114 def get_additional_parameters(self, source: OAuthSource): # pragma: no cover 115 # WeChat (Weixin) for Websites official documentation requires 'snsapi_login' 116 # as the *only* scope for the QR code-based login flow. 117 # Ref: https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html (Step 1) # noqa: E501 118 return { 119 "scope": ["snsapi_login"], 120 }
WeChat OAuth2 Redirect
client_class =
<class 'WeChatOAuth2Client'>
114 def get_additional_parameters(self, source: OAuthSource): # pragma: no cover 115 # WeChat (Weixin) for Websites official documentation requires 'snsapi_login' 116 # as the *only* scope for the QR code-based login flow. 117 # Ref: https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html (Step 1) # noqa: E501 118 return { 119 "scope": ["snsapi_login"], 120 }
Return additional redirect parameters for this source.
123class WeChatOAuth2Callback(OAuthCallback): 124 """WeChat OAuth2 Callback""" 125 126 # Specify our custom Client to handle the non-standard WeChat flow 127 client_class = WeChatOAuth2Client 128 129 def get_user_id(self, info: dict[str, Any]) -> str | None: 130 return info.get("unionid", info.get("openid"))
WeChat OAuth2 Callback
client_class =
<class 'WeChatOAuth2Client'>
133@registry.register() 134class WeChatType(SourceType): 135 """WeChat Type definition""" 136 137 callback_view = WeChatOAuth2Callback 138 redirect_view = WeChatOAuthRedirect 139 verbose_name = "WeChat" 140 name = "wechat" 141 142 # WeChat API URLs are fixed and not customizable 143 urls_customizable = False 144 145 # URLs for the WeChat "Login for Websites" authorization flow 146 authorization_url = "https://open.weixin.qq.com/connect/qrconnect" 147 # This is a public URL, not a hardcoded secret 148 access_token_url = "https://api.weixin.qq.com/sns/oauth2/access_token" # nosec B105 149 profile_url = "https://api.weixin.qq.com/sns/userinfo" 150 151 # Note: 'authorization_code_auth_method' is intentionally omitted. 152 # The base OAuth2Client defaults to POST_BODY, but our custom 153 # WeChatOAuth2Client overrides get_access_token() to use GET, 154 # so this setting would be misleading. 155 156 def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]: 157 """ 158 Map WeChat userinfo to authentik user properties. 159 """ 160 # The WeChat userinfo API (sns/userinfo) does *not* return an email address. 161 # We explicitly set 'email' to None. Authentik will typically 162 # prompt the user to provide one on their first login if it's required. 163 164 # 'unionid' is the preferred unique identifier as it's consistent 165 # across multiple apps under the same WeChat Open Platform account. 166 # 'openid' is the fallback, which is only unique to this specific AppID. 167 return { 168 "username": info.get("unionid", info.get("openid")), 169 "email": None, # WeChat API does not provide Email 170 "name": info.get("nickname"), 171 "attributes": { 172 # Save all other relevant info as user attributes 173 "headimgurl": info.get("headimgurl"), 174 "sex": info.get("sex"), 175 "city": info.get("city"), 176 "province": info.get("province"), 177 "country": info.get("country"), 178 "unionid": info.get("unionid"), 179 "openid": info.get("openid"), 180 }, 181 }
WeChat Type definition
callback_view =
<class 'WeChatOAuth2Callback'>
redirect_view =
<class 'WeChatOAuthRedirect'>
def
get_base_user_properties(self, info: dict[str, typing.Any], **kwargs) -> dict[str, typing.Any]:
156 def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]: 157 """ 158 Map WeChat userinfo to authentik user properties. 159 """ 160 # The WeChat userinfo API (sns/userinfo) does *not* return an email address. 161 # We explicitly set 'email' to None. Authentik will typically 162 # prompt the user to provide one on their first login if it's required. 163 164 # 'unionid' is the preferred unique identifier as it's consistent 165 # across multiple apps under the same WeChat Open Platform account. 166 # 'openid' is the fallback, which is only unique to this specific AppID. 167 return { 168 "username": info.get("unionid", info.get("openid")), 169 "email": None, # WeChat API does not provide Email 170 "name": info.get("nickname"), 171 "attributes": { 172 # Save all other relevant info as user attributes 173 "headimgurl": info.get("headimgurl"), 174 "sex": info.get("sex"), 175 "city": info.get("city"), 176 "province": info.get("province"), 177 "country": info.get("country"), 178 "unionid": info.get("unionid"), 179 "openid": info.get("openid"), 180 }, 181 }
Map WeChat userinfo to authentik user properties.