oauth2_twitter/
authorization_code_grant.rs

1use 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//
18#[derive(Debug, Clone)]
19pub struct TwitterProviderWithWebApplication {
20    client_id: ClientId,
21    client_secret: ClientSecret,
22    redirect_uri: RedirectUri,
23    //
24    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    // https://developer.twitter.com/en/docs/authentication/oauth-2-0/user-access-token
79    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//
120#[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        //
176        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}