1use oauth2_client::{
2 authorization_code_grant::provider_ext::{
3 AccessTokenRequestBody, AccessTokenResponseErrorBody, AccessTokenResponseSuccessfulBody,
4 AuthorizationRequestQuery,
5 },
6 oauth2_core::{
7 access_token_request::GRANT_TYPE_WITH_AUTHORIZATION_CODE_GRANT,
8 re_exports::AccessTokenResponseErrorBodyError, types::AccessTokenType,
9 },
10 re_exports::{
11 serde_json, serde_qs, thiserror, Body, ClientId, ClientSecret, Deserialize, HttpError, Map,
12 RedirectUri, Request, Response, SerdeJsonError, SerdeQsError, Serialize, Url,
13 UrlParseError, Value,
14 },
15 Provider, ProviderExtAuthorizationCodeGrant,
16};
17
18use crate::{WechatScope, AUTHORIZATION_URL, TOKEN_URL};
19
20pub const KEY_OPENID: &str = "openid";
21
22#[derive(Debug, Clone)]
23pub struct WechatProviderWithWebApplication {
24 appid: ClientId,
25 secret: ClientSecret,
26 redirect_uri: RedirectUri,
27 pub wechat_redirect: Option<bool>,
28 token_endpoint_url: Url,
30 authorization_endpoint_url: Url,
31}
32impl WechatProviderWithWebApplication {
33 pub fn new(
34 appid: ClientId,
35 secret: ClientSecret,
36 redirect_uri: RedirectUri,
37 ) -> Result<Self, UrlParseError> {
38 Ok(Self {
39 appid,
40 secret,
41 redirect_uri,
42 wechat_redirect: None,
43 token_endpoint_url: TOKEN_URL.parse()?,
44 authorization_endpoint_url: AUTHORIZATION_URL.parse()?,
45 })
46 }
47
48 pub fn configure<F>(mut self, mut f: F) -> Self
49 where
50 F: FnMut(&mut Self),
51 {
52 f(&mut self);
53 self
54 }
55}
56impl Provider for WechatProviderWithWebApplication {
57 type Scope = WechatScope;
58
59 fn client_id(&self) -> Option<&ClientId> {
60 Some(&self.appid)
61 }
62
63 fn client_secret(&self) -> Option<&ClientSecret> {
64 Some(&self.secret)
65 }
66
67 fn token_endpoint_url(&self) -> &Url {
68 &self.token_endpoint_url
69 }
70}
71impl ProviderExtAuthorizationCodeGrant for WechatProviderWithWebApplication {
72 fn redirect_uri(&self) -> Option<&RedirectUri> {
73 Some(&self.redirect_uri)
74 }
75
76 fn scopes_default(&self) -> Option<Vec<<Self as Provider>::Scope>> {
77 Some(vec![WechatScope::SnsapiLogin])
78 }
79
80 fn authorization_endpoint_url(&self) -> &Url {
81 &self.authorization_endpoint_url
82 }
83
84 fn authorization_request_query_serializing(
85 &self,
86 query: &AuthorizationRequestQuery<<Self as Provider>::Scope>,
87 ) -> Option<Result<String, Box<dyn std::error::Error + Send + Sync + 'static>>> {
88 fn doing(
89 query: &AuthorizationRequestQuery<
90 <WechatProviderWithWebApplication as Provider>::Scope,
91 >,
92 ) -> Result<String, Box<dyn std::error::Error + Send + Sync + 'static>> {
93 let redirect_uri = query
94 .redirect_uri
95 .to_owned()
96 .ok_or(AuthorizationRequestQuerySerializingError::RedirectUriMissing)?;
97
98 let scope = query
99 .scope
100 .to_owned()
101 .ok_or(AuthorizationRequestQuerySerializingError::ScopeMissing)?;
102
103 let scope = scope
104 .0
105 .iter()
106 .map(|x| x.to_string())
107 .collect::<Vec<_>>()
108 .join(",");
109
110 let query = WechatAuthorizationRequestQuery {
111 appid: query.client_id.to_owned(),
112 redirect_uri,
113 response_type: query.response_type.to_owned(),
114 scope,
115 state: query.state.to_owned(),
116 };
117
118 let query_str = serde_qs::to_string(&query)
119 .map_err(AuthorizationRequestQuerySerializingError::SerRequestQueryFailed)?;
120
121 Ok(query_str)
122 }
123
124 Some(doing(query))
125 }
126
127 fn authorization_request_url_modifying(&self, url: &mut Url) {
128 if self.wechat_redirect == Some(true) {
129 url.set_fragment(Some("wechat_redirect"));
130 }
131 }
132
133 fn access_token_request_rendering(
134 &self,
135 body: &AccessTokenRequestBody,
136 ) -> Option<Result<Request<Body>, Box<dyn std::error::Error + Send + Sync + 'static>>> {
137 fn doing(
138 this: &WechatProviderWithWebApplication,
139 body: &AccessTokenRequestBody,
140 ) -> Result<Request<Body>, Box<dyn std::error::Error + Send + Sync + 'static>> {
141 let query = WechatAccessTokenRequestQuery {
142 appid: this.appid.to_owned(),
143 secret: this.secret.to_owned(),
144 code: body.code.to_owned(),
145 grant_type: GRANT_TYPE_WITH_AUTHORIZATION_CODE_GRANT.to_owned(),
146 };
147 let query_str = serde_qs::to_string(&query)
148 .map_err(AccessTokenRequestRenderingError::SerRequestQueryFailed)?;
149
150 let mut url = this.token_endpoint_url().to_owned();
151 url.set_query(Some(query_str.as_str()));
152
153 let request = Request::builder()
154 .uri(url.as_str())
155 .body(vec![])
156 .map_err(AccessTokenRequestRenderingError::MakeRequestFailed)?;
157
158 Ok(request)
159 }
160
161 Some(doing(self, body))
162 }
163
164 #[allow(clippy::type_complexity)]
165 fn access_token_response_parsing(
166 &self,
167 response: &Response<Body>,
168 ) -> Option<
169 Result<
170 Result<
171 AccessTokenResponseSuccessfulBody<<Self as Provider>::Scope>,
172 AccessTokenResponseErrorBody,
173 >,
174 Box<dyn std::error::Error + Send + Sync + 'static>,
175 >,
176 > {
177 fn doing(
178 response: &Response<Body>,
179 ) -> Result<
180 Result<WechatAccessTokenResponseSuccessfulBody, WechatAccessTokenResponseErrorBody>,
181 Box<dyn std::error::Error + Send + Sync + 'static>,
182 > {
183 if response.status().is_success() {
184 let map = serde_json::from_slice::<Map<String, Value>>(response.body())
185 .map_err(AccessTokenResponseParsingError::DeResponseBodyFailed)?;
186 if !map.contains_key("errcode") {
187 let body = serde_json::from_slice::<WechatAccessTokenResponseSuccessfulBody>(
188 response.body(),
189 )
190 .map_err(AccessTokenResponseParsingError::DeResponseBodyFailed)?;
191
192 return Ok(Ok(body));
193 }
194 }
195
196 let body =
197 serde_json::from_slice::<WechatAccessTokenResponseErrorBody>(response.body())
198 .map_err(AccessTokenResponseParsingError::DeResponseBodyFailed)?;
199 Ok(Err(body))
200 }
201
202 Some(doing(response).map(|ret| ret.map(Into::into).map_err(Into::into)))
203 }
204}
205
206#[derive(Serialize, Deserialize)]
208pub struct WechatAuthorizationRequestQuery {
209 pub appid: String,
210 pub redirect_uri: String,
211 pub response_type: String,
212 pub scope: String,
213 #[serde(skip_serializing_if = "Option::is_none")]
214 pub state: Option<String>,
215}
216
217#[derive(thiserror::Error, Debug)]
218pub enum AuthorizationRequestQuerySerializingError {
219 #[error("RedirectUriMissing")]
220 RedirectUriMissing,
221 #[error("ScopeMissing")]
222 ScopeMissing,
223 #[error("SerRequestQueryFailed {0}")]
224 SerRequestQueryFailed(SerdeQsError),
225}
226
227#[derive(Serialize, Deserialize)]
229pub struct WechatAccessTokenRequestQuery {
230 pub appid: String,
231 pub secret: String,
232 pub code: String,
233 pub grant_type: String,
234}
235
236#[derive(thiserror::Error, Debug)]
237pub enum AccessTokenRequestRenderingError {
238 #[error("SerRequestQueryFailed {0}")]
239 SerRequestQueryFailed(SerdeQsError),
240 #[error("MakeRequestFailed {0}")]
241 MakeRequestFailed(HttpError),
242}
243
244#[derive(Serialize, Deserialize)]
246pub struct WechatAccessTokenResponseSuccessfulBody {
247 pub access_token: String,
248 pub expires_in: usize,
249 pub refresh_token: String,
250 pub openid: String,
251 pub scope: String,
252}
253impl From<WechatAccessTokenResponseSuccessfulBody>
254 for AccessTokenResponseSuccessfulBody<WechatScope>
255{
256 fn from(body: WechatAccessTokenResponseSuccessfulBody) -> Self {
257 let scope: Vec<_> = body
258 .scope
259 .split(',')
260 .map(|x| {
261 x.parse::<WechatScope>()
262 .unwrap_or_else(|_| WechatScope::Other(x.to_owned()))
263 })
264 .collect();
265
266 let mut map = Map::new();
267 map.insert(KEY_OPENID.to_owned(), Value::String(body.openid.to_owned()));
268
269 let mut body = Self::new(
270 body.access_token.to_owned(),
271 AccessTokenType::Bearer,
272 Some(body.expires_in),
273 Some(body.refresh_token),
274 if scope.is_empty() {
275 None
276 } else {
277 Some(scope.into())
278 },
279 );
280 body.set_extra(map);
281
282 body
283 }
284}
285
286#[derive(Serialize, Deserialize)]
287pub struct WechatAccessTokenResponseErrorBody {
288 pub errcode: usize,
289 pub errmsg: String,
290}
291impl From<WechatAccessTokenResponseErrorBody> for AccessTokenResponseErrorBody {
292 fn from(body: WechatAccessTokenResponseErrorBody) -> Self {
293 Self::new(
294 AccessTokenResponseErrorBodyError::Other(body.errcode.to_string()),
295 Some(body.errmsg),
296 None,
297 )
298 }
299}
300
301#[derive(thiserror::Error, Debug)]
302pub enum AccessTokenResponseParsingError {
303 #[error("DeResponseBodyFailed {0}")]
305 DeResponseBodyFailed(SerdeJsonError),
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 use oauth2_client::{
313 authorization_code_grant::{AccessTokenEndpoint, AuthorizationEndpoint},
314 re_exports::{Endpoint as _, Response},
315 };
316
317 #[test]
318 fn authorization_request() -> Result<(), Box<dyn std::error::Error>> {
319 let provider = WechatProviderWithWebApplication::new(
320 "APPID".to_owned(),
321 "SECRET".to_owned(),
322 RedirectUri::new("https://client.example.com/cb")?,
323 )?
324 .configure(|x| {
325 x.wechat_redirect = Some(true);
326 });
327
328 let request = AuthorizationEndpoint::new(&provider, vec![WechatScope::SnsapiLogin])
329 .configure(|x| x.state = Some("3d6be0a4035d839573b04816624a415e".to_owned()))
330 .render_request()?;
331
332 assert_eq!(request.uri(), "https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb&response_type=code&scope=snsapi_login&state=3d6be0a4035d839573b04816624a415e#wechat_redirect");
333
334 Ok(())
335 }
336
337 #[test]
338 fn access_token_request() -> Result<(), Box<dyn std::error::Error>> {
339 let provider = WechatProviderWithWebApplication::new(
340 "APPID".to_owned(),
341 "SECRET".to_owned(),
342 RedirectUri::new("https://client.example.com/cb")?,
343 )?;
344
345 let request = AccessTokenEndpoint::new(&provider, "CODE".to_owned()).render_request()?;
346
347 assert_eq!(request.method(), "GET");
348 assert_eq!(request.uri(), "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code");
349
350 Ok(())
351 }
352
353 #[test]
354 fn access_token_response() -> Result<(), Box<dyn std::error::Error>> {
355 let provider = WechatProviderWithWebApplication::new(
356 "APPID".to_owned(),
357 "SECRET".to_owned(),
358 RedirectUri::new("https://client.example.com/cb")?,
359 )?;
360
361 let response_body = include_str!(
362 "../tests/response_body_json_files/access_token_with_authorization_code_grant.json"
363 );
364 let body_ret = AccessTokenEndpoint::new(&provider, "CODE".to_owned())
365 .parse_response(Response::builder().body(response_body.as_bytes().to_vec())?)?;
366
367 match body_ret {
368 Ok(body) => {
369 assert_eq!(body.access_token, "ACCESS_TOKEN");
370 assert_eq!(body.token_type, AccessTokenType::Bearer);
371 assert_eq!(body.expires_in, Some(7200));
372 assert_eq!(body.refresh_token, Some("REFRESH_TOKEN".to_owned()));
373 assert_eq!(
374 body.scope,
375 Some(vec![WechatScope::Other("SCOPE".to_owned())].into())
376 );
377 let map = body.extra().unwrap();
378 assert_eq!(map.get("openid").unwrap(), "OPENID");
379 }
380 Err(body) => panic!("{body:?}"),
381 }
382
383 Ok(())
384 }
385}