oauth2_twitter/
authorization_code_grant.rs1use oauth2_client::{
2 authorization_code_grant::provider_ext::{
3 AccessTokenRequestBody, ProviderExtAuthorizationCodeGrantPkceSupportType,
4 },
5 oauth2_core::access_token_request::GRANT_TYPE_WITH_AUTHORIZATION_CODE_GRANT,
6 re_exports::{
7 http::{header::AUTHORIZATION, Method},
8 serde_urlencoded, thiserror, Body, ClientId, ClientSecret, HttpError, RedirectUri, Request,
9 SerdeUrlencodedSerError, Url, UrlParseError,
10 },
11 Provider, ProviderExtAuthorizationCodeGrant,
12};
13use serde::{Deserialize, Serialize};
14
15use crate::{TwitterScope, AUTHORIZATION_URL, TOKEN_URL};
16
17#[derive(Debug, Clone)]
19pub struct TwitterProviderWithWebApplication {
20 client_id: ClientId,
21 client_secret: ClientSecret,
22 redirect_uri: RedirectUri,
23 token_endpoint_url: Url,
25 authorization_endpoint_url: Url,
26}
27impl TwitterProviderWithWebApplication {
28 pub fn new(
29 client_id: ClientId,
30 client_secret: ClientSecret,
31 redirect_uri: RedirectUri,
32 ) -> Result<Self, UrlParseError> {
33 Ok(Self {
34 client_id,
35 client_secret,
36 redirect_uri,
37 token_endpoint_url: TOKEN_URL.parse()?,
38 authorization_endpoint_url: AUTHORIZATION_URL.parse()?,
39 })
40 }
41}
42impl Provider for TwitterProviderWithWebApplication {
43 type Scope = TwitterScope;
44
45 fn client_id(&self) -> Option<&ClientId> {
46 Some(&self.client_id)
47 }
48
49 fn client_secret(&self) -> Option<&ClientSecret> {
50 Some(&self.client_secret)
51 }
52
53 fn token_endpoint_url(&self) -> &Url {
54 &self.token_endpoint_url
55 }
56}
57impl ProviderExtAuthorizationCodeGrant for TwitterProviderWithWebApplication {
58 fn redirect_uri(&self) -> Option<&RedirectUri> {
59 Some(&self.redirect_uri)
60 }
61
62 fn pkce_support_type(&self) -> Option<ProviderExtAuthorizationCodeGrantPkceSupportType> {
63 Some(ProviderExtAuthorizationCodeGrantPkceSupportType::Yes)
64 }
65
66 fn scopes_default(&self) -> Option<Vec<<Self as Provider>::Scope>> {
67 Some(vec![
68 TwitterScope::TweetRead,
69 TwitterScope::TweetWrite,
70 TwitterScope::OfflineAccess,
71 ])
72 }
73
74 fn authorization_endpoint_url(&self) -> &Url {
75 &self.authorization_endpoint_url
76 }
77
78 fn access_token_request_rendering(
80 &self,
81 body: &AccessTokenRequestBody,
82 ) -> Option<Result<Request<Body>, Box<dyn std::error::Error + Send + Sync + 'static>>> {
83 fn doing(
84 this: &TwitterProviderWithWebApplication,
85 body: &AccessTokenRequestBody,
86 ) -> Result<Request<Body>, Box<dyn std::error::Error + Send + Sync + 'static>> {
87 let body = TwitterAccessTokenRequestBody {
88 grant_type: GRANT_TYPE_WITH_AUTHORIZATION_CODE_GRANT.to_owned(),
89 code: body.code.to_owned(),
90 redirect_uri: this.redirect_uri.to_string(),
91 code_verifier: body
92 .code_verifier
93 .to_owned()
94 .ok_or(AccessTokenRequestRenderingError::CodeVerifierMissing)?,
95 };
96 let body_str = serde_urlencoded::to_string(body)
97 .map_err(AccessTokenRequestRenderingError::SerRequestBodyFailed)?;
98
99 let url = this.token_endpoint_url().to_owned();
100
101 let request = Request::builder()
102 .method(Method::POST)
103 .uri(url.as_str())
104 .header(
105 AUTHORIZATION,
106 http_authentication::Credentials::basic(&this.client_id, &this.client_secret)
107 .to_string(),
108 )
109 .body(body_str.as_bytes().to_vec())
110 .map_err(AccessTokenRequestRenderingError::MakeRequestFailed)?;
111
112 Ok(request)
113 }
114
115 Some(doing(self, body))
116 }
117}
118
119#[derive(Serialize, Deserialize)]
121pub struct TwitterAccessTokenRequestBody {
122 pub grant_type: String,
123 pub code: String,
124 pub redirect_uri: String,
125 pub code_verifier: String,
126}
127
128#[derive(thiserror::Error, Debug)]
129pub enum AccessTokenRequestRenderingError {
130 #[error("CodeVerifierMissing")]
131 CodeVerifierMissing,
132 #[error("SerRequestBodyFailed {0}")]
133 SerRequestBodyFailed(SerdeUrlencodedSerError),
134 #[error("MakeRequestFailed {0}")]
135 MakeRequestFailed(HttpError),
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 use oauth2_client::{
143 authorization_code_grant::{AccessTokenEndpoint, AuthorizationEndpoint},
144 re_exports::{Endpoint as _, Response},
145 };
146
147 #[test]
148 fn authorization_request() -> Result<(), Box<dyn std::error::Error>> {
149 let provider = TwitterProviderWithWebApplication::new(
150 "CLIENT_ID".to_owned(),
151 "CLIENT_SECRET".to_owned(),
152 RedirectUri::new("https://client.example.com/cb")?,
153 )?;
154
155 let request = AuthorizationEndpoint::new(
156 &provider,
157 vec![TwitterScope::TweetRead, TwitterScope::UsersRead],
158 )
159 .configure(|x| x.state = Some("STATE".to_owned()))
160 .render_request()?;
161
162 assert_eq!(request.uri(), "https://twitter.com/i/oauth2/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb&scope=tweet.read+users.read&state=STATE");
163
164 Ok(())
165 }
166
167 #[test]
168 fn access_token_response() -> Result<(), Box<dyn std::error::Error>> {
169 let provider = TwitterProviderWithWebApplication::new(
170 "CLIENT_ID".to_owned(),
171 "CLIENT_SECRET".to_owned(),
172 RedirectUri::new("https://client.example.com/cb")?,
173 )?;
174
175 let response_body = include_str!(
177 "../tests/response_body_json_files/access_token_with_authorization_code_grant.json"
178 );
179 let body_ret = AccessTokenEndpoint::new(&provider, "CODE".to_owned())
180 .parse_response(Response::builder().body(response_body.as_bytes().to_vec())?)?;
181
182 match body_ret {
183 Ok(body) => {
184 assert_eq!(body.expires_in, Some(7200));
185 }
186 Err(body) => panic!("{body:?}"),
187 }
188
189 Ok(())
190 }
191}