1use oauth2_client::{
2 authorization_code_grant::provider_ext::AccessTokenRequestBody,
3 oauth2_core::{
4 access_token_request::GRANT_TYPE_WITH_AUTHORIZATION_CODE_GRANT,
5 re_exports::{
6 AccessTokenResponseErrorBody, AccessTokenResponseErrorBodyError,
7 AccessTokenResponseSuccessfulBody,
8 },
9 types::AccessTokenType,
10 },
11 re_exports::{
12 http::Method, serde_json, serde_qs, thiserror, Body, ClientId, ClientSecret, HttpError,
13 Map, RedirectUri, Request, Response, SerdeJsonError, SerdeQsError, Url, UrlParseError,
14 Value,
15 },
16 Provider, ProviderExtAuthorizationCodeGrant,
17};
18use serde::{Deserialize, Serialize};
19
20use crate::{TiktokScope, AUTHORIZATION_URL, TOKEN_URL};
21
22pub const KEY_OPENID: &str = "open_id";
23
24#[derive(Debug, Clone)]
26pub struct TiktokProviderWithWebApplication {
27 client_id: ClientId,
28 client_secret: ClientSecret,
29 redirect_uri: RedirectUri,
30 token_endpoint_url: Url,
32 authorization_endpoint_url: Url,
33}
34impl TiktokProviderWithWebApplication {
35 pub fn new(
36 client_key: ClientId,
37 client_secret: ClientSecret,
38 redirect_uri: RedirectUri,
39 ) -> Result<Self, UrlParseError> {
40 Ok(Self {
41 client_id: client_key,
42 client_secret,
43 redirect_uri,
44 token_endpoint_url: TOKEN_URL.parse()?,
45 authorization_endpoint_url: AUTHORIZATION_URL.parse()?,
46 })
47 }
48}
49impl Provider for TiktokProviderWithWebApplication {
50 type Scope = TiktokScope;
51
52 fn client_id(&self) -> Option<&ClientId> {
53 Some(&self.client_id)
54 }
55
56 fn client_secret(&self) -> Option<&ClientSecret> {
57 Some(&self.client_secret)
58 }
59
60 fn token_endpoint_url(&self) -> &Url {
61 &self.token_endpoint_url
62 }
63}
64impl ProviderExtAuthorizationCodeGrant for TiktokProviderWithWebApplication {
65 fn redirect_uri(&self) -> Option<&RedirectUri> {
66 Some(&self.redirect_uri)
67 }
68
69 fn scopes_default(&self) -> Option<Vec<<Self as Provider>::Scope>> {
70 Some(vec![TiktokScope::UserInfoBasic, TiktokScope::VideoList])
71 }
72
73 fn authorization_endpoint_url(&self) -> &Url {
74 &self.authorization_endpoint_url
75 }
76
77 fn authorization_request_url_modifying(&self, url: &mut Url) {
78 let query_pairs: Vec<_> = url
79 .query_pairs()
80 .map(|(k, v)| (k.to_string(), v.to_string()))
81 .collect::<Vec<_>>();
82 let mut query_pairs_mut = url.query_pairs_mut();
83 query_pairs_mut.clear();
84 for (k, v) in query_pairs {
85 match k.as_str() {
86 "client_id" => {
87 query_pairs_mut.append_pair("client_key", v.as_str());
88 }
89 "scope" => {
90 query_pairs_mut
91 .append_pair("scope", v.split(' ').collect::<Vec<_>>().join(",").as_str());
92 }
93 _ => {
94 query_pairs_mut.append_pair(k.as_str(), v.as_str());
95 }
96 }
97 }
98 query_pairs_mut.finish();
99 }
100
101 fn access_token_request_rendering(
103 &self,
104 body: &AccessTokenRequestBody,
105 ) -> Option<Result<Request<Body>, Box<dyn std::error::Error + Send + Sync + 'static>>> {
106 fn doing(
107 this: &TiktokProviderWithWebApplication,
108 body: &AccessTokenRequestBody,
109 ) -> Result<Request<Body>, Box<dyn std::error::Error + Send + Sync + 'static>> {
110 let client_key = this.client_id.to_owned();
111 let query = TiktokAccessTokenRequestQuery {
112 client_key,
113 client_secret: this.client_secret.to_owned(),
114 code: body.code.to_owned(),
115 grant_type: GRANT_TYPE_WITH_AUTHORIZATION_CODE_GRANT.to_owned(),
116 };
117 let query_str = serde_qs::to_string(&query)
118 .map_err(AccessTokenRequestRenderingError::SerRequestQueryFailed)?;
119
120 let mut url = this.token_endpoint_url().to_owned();
121 url.set_query(Some(query_str.as_str()));
122
123 let request = Request::builder()
124 .method(Method::POST)
125 .uri(url.as_str())
126 .body(vec![])
127 .map_err(AccessTokenRequestRenderingError::MakeRequestFailed)?;
128
129 Ok(request)
130 }
131
132 Some(doing(self, body))
133 }
134
135 #[allow(clippy::type_complexity)]
137 fn access_token_response_parsing(
138 &self,
139 response: &Response<Body>,
140 ) -> Option<
141 Result<
142 Result<
143 AccessTokenResponseSuccessfulBody<<Self as Provider>::Scope>,
144 AccessTokenResponseErrorBody,
145 >,
146 Box<dyn std::error::Error + Send + Sync + 'static>,
147 >,
148 > {
149 fn doing(
150 response: &Response<Body>,
151 ) -> Result<TiktokAccessTokenResponseBody, Box<dyn std::error::Error + Send + Sync + 'static>>
152 {
153 let body = serde_json::from_slice::<TiktokAccessTokenResponseBody>(response.body())
154 .map_err(AccessTokenResponseParsingError::DeResponseBodyFailed)?;
155
156 Ok(body)
157 }
158
159 Some(doing(response).map(Into::into))
160 }
161}
162
163#[derive(Serialize, Deserialize)]
165pub struct TiktokAccessTokenRequestQuery {
166 pub client_key: String,
167 pub client_secret: String,
168 pub code: String,
169 pub grant_type: String,
170}
171
172#[derive(thiserror::Error, Debug)]
173pub enum AccessTokenRequestRenderingError {
174 #[error("SerRequestQueryFailed {0}")]
175 SerRequestQueryFailed(SerdeQsError),
176 #[error("MakeRequestFailed {0}")]
177 MakeRequestFailed(HttpError),
178}
179
180#[derive(Serialize, Deserialize)]
182#[serde(tag = "message", content = "data")]
183pub enum TiktokAccessTokenResponseBody {
184 #[serde(rename = "success")]
185 Success(TiktokAccessTokenResponseBodySuccessfulData),
186 #[serde(rename = "error")]
187 Error(TiktokAccessTokenResponseBodyErrorData),
188}
189
190#[derive(Serialize, Deserialize)]
191pub struct TiktokAccessTokenResponseBodySuccessfulData {
192 pub open_id: String,
193 pub scope: String,
194 pub access_token: String,
195 pub expires_in: i64,
196 pub refresh_token: String,
197 pub refresh_expires_in: i64,
198}
199
200#[derive(Serialize, Deserialize)]
201pub struct TiktokAccessTokenResponseBodyErrorData {
202 pub captcha: String,
203 pub desc_url: String,
204 pub description: String,
205 pub error_code: i64,
206}
207
208impl From<TiktokAccessTokenResponseBody>
209 for Result<AccessTokenResponseSuccessfulBody<TiktokScope>, AccessTokenResponseErrorBody>
210{
211 fn from(body: TiktokAccessTokenResponseBody) -> Self {
212 match body {
213 TiktokAccessTokenResponseBody::Success(body) => {
214 let scope: Vec<_> = body
215 .scope
216 .split(',')
217 .map(|x| {
218 x.parse::<TiktokScope>()
219 .unwrap_or_else(|_| TiktokScope::Other(x.to_owned()))
220 })
221 .collect();
222
223 let mut map = Map::new();
224 map.insert(
225 KEY_OPENID.to_owned(),
226 Value::String(body.open_id.to_owned()),
227 );
228 map.insert(
229 "refresh_expires_in".to_owned(),
230 Value::Number(body.refresh_expires_in.into()),
231 );
232
233 let mut body = AccessTokenResponseSuccessfulBody::<TiktokScope>::new(
234 body.access_token.to_owned(),
235 AccessTokenType::Bearer,
236 Some(body.expires_in as usize),
237 Some(body.refresh_token),
238 if scope.is_empty() {
239 None
240 } else {
241 Some(scope.into())
242 },
243 );
244 body.set_extra(map);
245
246 Ok(body)
247 }
248 TiktokAccessTokenResponseBody::Error(body) => {
249 let body = AccessTokenResponseErrorBody::new(
250 AccessTokenResponseErrorBodyError::Other(body.error_code.to_string()),
251 Some(body.description),
252 None,
253 );
254
255 Err(body)
256 }
257 }
258 }
259}
260
261#[derive(thiserror::Error, Debug)]
262pub enum AccessTokenResponseParsingError {
263 #[error("DeResponseBodyFailed {0}")]
265 DeResponseBodyFailed(SerdeJsonError),
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 use oauth2_client::{
273 authorization_code_grant::{AccessTokenEndpoint, AuthorizationEndpoint},
274 re_exports::{Endpoint as _, Response},
275 };
276
277 #[test]
278 fn authorization_request() -> Result<(), Box<dyn std::error::Error>> {
279 let provider = TiktokProviderWithWebApplication::new(
280 "CLIENT_KEY".to_owned(),
281 "CLIENT_SECRET".to_owned(),
282 RedirectUri::new("https://client.example.com/cb")?,
283 )?;
284
285 let request = AuthorizationEndpoint::new(
286 &provider,
287 vec![TiktokScope::UserInfoBasic, TiktokScope::VideoList],
288 )
289 .configure(|x| x.state = Some("STATE".to_owned()))
290 .render_request()?;
291
292 assert_eq!(request.uri(), "https://www.tiktok.com/auth/authorize/?response_type=code&client_key=CLIENT_KEY&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb&scope=user.info.basic%2Cvideo.list&state=STATE");
293
294 Ok(())
295 }
296
297 #[test]
298 fn access_token_response() -> Result<(), Box<dyn std::error::Error>> {
299 let provider = TiktokProviderWithWebApplication::new(
300 "CLIENT_KEY".to_owned(),
301 "CLIENT_SECRET".to_owned(),
302 RedirectUri::new("https://client.example.com/cb")?,
303 )?;
304
305 let response_body = include_str!(
307 "../tests/response_body_json_files/access_token_with_authorization_code_grant.json"
308 );
309 let body_ret = AccessTokenEndpoint::new(&provider, "CODE".to_owned())
310 .parse_response(Response::builder().body(response_body.as_bytes().to_vec())?)?;
311
312 match body_ret {
313 Ok(body) => {
314 let map = body.extra().unwrap();
315 assert_eq!(
316 map.get("open_id").unwrap().as_str(),
317 Some("_000fwZ23Mw4RY9cB4lDQyKCgQg4Ft6SyTuE")
318 );
319 }
320 Err(body) => panic!("{body:?}"),
321 }
322
323 let response_body = include_str!(
325 "../tests/response_body_json_files/access_token_failed_with_authorization_code_grant.json"
326 );
327 let body_ret = AccessTokenEndpoint::new(&provider, "CODE".to_owned())
328 .parse_response(Response::builder().body(response_body.as_bytes().to_vec())?)?;
329
330 match body_ret {
331 Ok(body) => {
332 panic!("{body:?}")
333 }
334 Err(body) => assert_eq!(body.error.to_string(), "10007"),
335 }
336
337 Ok(())
338 }
339}