oauth2_wechat/
authorization_code_grant.rs

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    //
29    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//
207#[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//
228#[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//
245#[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    //
304    #[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}