just_auth/
qq.rs

1//! https://wikinew.open.qq.com/index.html#/iwiki/901251864
2use crate::{
3    auth_server_builder, error::Result, utils, AuthAction, AuthConfig, AuthUrlProvider, AuthUser,
4    GenericAuthAction,
5};
6use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use serde_with::{formats::CommaSeparator, serde_as, StringWithSeparator};
10use std::collections::HashMap;
11
12pub struct AuthorizationServer {
13    config: AuthConfig,
14}
15
16auth_server_builder!();
17
18impl AuthUrlProvider for AuthorizationServer {
19    type AuthRequest = AuthRequest;
20    type TokenRequest = GetTokenRequest;
21    type UserInfoRequest = GetUserInfoRequest;
22
23    fn authorize_url(request: Self::AuthRequest) -> Result<String> {
24        let query = serde_urlencoded::to_string(request)?;
25        Ok(format!(
26            "https://graph.qq.com/oauth2.0/authorize?response_type=token&{query}"
27        ))
28    }
29
30    fn access_token_url(request: Self::TokenRequest) -> Result<String> {
31        let query = serde_urlencoded::to_string(request)?;
32        Ok(format!(
33            "https://graph.qq.com/oauth2.0/token?grant_type=authorization_code&{query}"
34        ))
35    }
36
37    fn user_info_url(request: Self::UserInfoRequest) -> Result<String> {
38        let query = serde_urlencoded::to_string(request)?;
39        Ok(format!("https://graph.qq.com/user/get_user_info?{query}"))
40    }
41}
42
43#[async_trait]
44impl AuthAction for AuthorizationServer {
45    type AuthCallback = AuthCallback;
46    type AuthToken = TokenResponse;
47    type AuthUser = UserInfoResponse;
48
49    async fn get_access_token(&self, callback: Self::AuthCallback) -> Result<Self::AuthToken> {
50        let AuthConfig {
51            client_id,
52            client_secret,
53            redirect_uri,
54            ..
55        } = &self.config;
56        let access_token_url = Self::access_token_url(GetTokenRequest {
57            client_id: client_id.to_string(),
58            client_secret: client_secret.clone().expect("client_secret is empty"),
59            code: callback.code,
60            redirect_uri: redirect_uri.to_string(),
61            fmt: Some(ResponseFormat::Json),
62        })?;
63        Ok(reqwest::get(access_token_url).await?.json().await?)
64    }
65
66    async fn get_user_info(&self, token: Self::AuthToken) -> Result<Self::AuthUser> {
67        let AuthConfig { client_id, .. } = &self.config;
68        let access_token = token.access_token;
69        let value = self.get_open_id(&access_token).await?;
70        let user_info_url = Self::user_info_url(GetUserInfoRequest {
71            openid: value.openid,
72            access_token: access_token,
73            oauth_consumer_key: client_id.to_string(),
74        })?;
75        Ok(reqwest::get(user_info_url).await?.json().await?)
76    }
77}
78
79#[async_trait]
80impl GenericAuthAction for AuthorizationServer {
81    async fn authorize<S: Into<String> + Send>(&self, state: S) -> Result<String> {
82        let AuthConfig {
83            client_id,
84            redirect_uri,
85            scope,
86            ..
87        } = &self.config;
88        Self::authorize_url(AuthRequest {
89            client_id: client_id.to_string(),
90            redirect_uri: redirect_uri.to_string(),
91            state: state.into(),
92            scope: scope.clone().or_else(|| Some(vec!["get_user_info".into()])),
93            ..Default::default()
94        })
95    }
96
97    async fn login<S: Into<String> + Send>(&self, callback: S) -> Result<AuthUser> {
98        let callback: AuthCallback = serde_urlencoded::from_str(&callback.into())?;
99        let AuthConfig { client_id, .. } = &self.config;
100        let token = self.get_access_token(callback).await?;
101        let access_token = token.access_token;
102        let open_id = self.get_open_id(&access_token).await?;
103        let user_info_url = Self::user_info_url(GetUserInfoRequest {
104            openid: open_id.openid.clone(),
105            access_token: access_token.clone(),
106            oauth_consumer_key: client_id.to_string(),
107        })?;
108        let user: UserInfoResponse = reqwest::get(user_info_url).await?.json().await?;
109        Ok(AuthUser {
110            user_id: open_id.openid,
111            name: user.nickname,
112            access_token: access_token,
113            refresh_token: token.refresh_token,
114            expires_in: token.expires_in.into(),
115            extra: user.extra,
116        })
117    }
118}
119
120impl AuthorizationServer {
121    async fn get_open_id(&self, access_token: &str) -> Result<OpenIdResp> {
122        let jsonp = reqwest::get(format!(
123            "https://graph.qq.com/oauth2.0/me?access_token={access_token}"
124        ))
125        .await?
126        .text()
127        .await?;
128        let json =
129            utils::substr_between(&jsonp, "callback(", ");").expect("jsonp response is valid");
130        Ok(serde_json::from_str(json)?)
131    }
132}
133
134#[serde_as]
135#[derive(Debug, Default, Serialize, Deserialize)]
136pub struct AuthRequest {
137    client_id: String,
138    redirect_uri: String,
139    state: String,
140    #[serde_as(as = "Option<StringWithSeparator::<CommaSeparator, String>>")]
141    scope: Option<Vec<String>>,
142    display: Option<QQDisplayStyle>,
143}
144
145#[derive(Debug, Serialize, Deserialize)]
146#[serde(rename_all = "lowercase")]
147pub enum QQDisplayStyle {
148    PC,
149    Mobile,
150}
151
152#[derive(Debug, Serialize, Deserialize)]
153pub struct AuthCallback {
154    code: String,
155    state: String,
156}
157
158#[derive(Debug, Serialize, Deserialize)]
159pub struct GetTokenRequest {
160    client_id: String,
161    client_secret: String,
162    code: String,
163    redirect_uri: String,
164    fmt: Option<ResponseFormat>,
165}
166
167#[derive(Debug, Serialize, Deserialize)]
168pub enum ResponseFormat {
169    #[serde(rename = "x-www-form-urlencoded")]
170    UrlEncoded,
171    #[serde(rename = "json")]
172    Json,
173}
174
175#[derive(Debug, Serialize, Deserialize)]
176pub struct RefreshTokenRequest {
177    grant_type: String,
178    client_id: String,
179    client_secret: String,
180    refresh_token: String,
181    fmt: Option<ResponseFormat>,
182}
183
184#[derive(Debug, Serialize, Deserialize)]
185pub struct TokenResponse {
186    access_token: String,
187    expires_in: i32,
188    refresh_token: String,
189}
190
191#[derive(Debug, Serialize, Deserialize)]
192pub struct OpenIdResp {
193    client_id: String,
194    openid: String,
195}
196
197#[derive(Debug, Serialize, Deserialize)]
198pub struct GetUserInfoRequest {
199    access_token: String,
200    oauth_consumer_key: String,
201    openid: String,
202}
203
204/// https://wiki.connect.qq.com/get_user_info
205#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
206#[serde(rename_all = "camelCase")]
207pub struct UserInfoResponse {
208    pub nickname: String,
209    #[serde(flatten)]
210    pub extra: HashMap<String, Value>,
211}