oauth2_google/
authorization_code_grant.rs

1use oauth2_client::{
2    authorization_code_grant::provider_ext::ProviderExtAuthorizationCodeGrantOidcSupportType,
3    re_exports::{
4        thiserror, ClientId, ClientSecret, Map, RedirectUri, Serialize_enum_str, Url,
5        UrlParseError, Value,
6    },
7    Provider, ProviderExtAuthorizationCodeGrant,
8};
9
10use crate::{GoogleScope, AUTHORIZATION_URL, TOKEN_URL};
11
12//
13//
14//
15#[derive(Debug, Clone)]
16pub struct GoogleProviderForWebServerApps {
17    client_id: ClientId,
18    client_secret: ClientSecret,
19    redirect_uri: RedirectUri,
20    pub access_type: Option<GoogleProviderForWebServerAppsAccessType>,
21    pub include_granted_scopes: Option<bool>,
22    //
23    token_endpoint_url: Url,
24    authorization_endpoint_url: Url,
25}
26
27#[derive(Serialize_enum_str, Debug, Clone)]
28pub enum GoogleProviderForWebServerAppsAccessType {
29    #[serde(rename = "online")]
30    Online,
31    #[serde(rename = "offline")]
32    Offline,
33}
34
35impl GoogleProviderForWebServerApps {
36    pub fn new(
37        client_id: ClientId,
38        client_secret: ClientSecret,
39        redirect_uri: RedirectUri,
40    ) -> Result<Self, GoogleProviderForWebServerAppsNewError> {
41        if !matches!(redirect_uri, RedirectUri::Url(_)) {
42            return Err(GoogleProviderForWebServerAppsNewError::RedirectUriShouldBeAUrl);
43        }
44
45        Ok(Self {
46            client_id,
47            client_secret,
48            redirect_uri,
49            access_type: None,
50            include_granted_scopes: None,
51            token_endpoint_url: TOKEN_URL.parse()?,
52            authorization_endpoint_url: AUTHORIZATION_URL.parse()?,
53        })
54    }
55
56    pub fn configure<F>(mut self, mut f: F) -> Self
57    where
58        F: FnMut(&mut Self),
59    {
60        f(&mut self);
61        self
62    }
63}
64
65#[derive(thiserror::Error, Debug)]
66pub enum GoogleProviderForWebServerAppsNewError {
67    #[error("UrlParseError {0}")]
68    UrlParseError(#[from] UrlParseError),
69    //
70    #[error("RedirectUriShouldBeAUrl")]
71    RedirectUriShouldBeAUrl,
72}
73
74impl Provider for GoogleProviderForWebServerApps {
75    type Scope = GoogleScope;
76
77    fn client_id(&self) -> Option<&ClientId> {
78        Some(&self.client_id)
79    }
80
81    fn client_secret(&self) -> Option<&ClientSecret> {
82        Some(&self.client_secret)
83    }
84
85    fn token_endpoint_url(&self) -> &Url {
86        &self.token_endpoint_url
87    }
88}
89impl ProviderExtAuthorizationCodeGrant for GoogleProviderForWebServerApps {
90    fn redirect_uri(&self) -> Option<&RedirectUri> {
91        Some(&self.redirect_uri)
92    }
93
94    fn oidc_support_type(&self) -> Option<ProviderExtAuthorizationCodeGrantOidcSupportType> {
95        Some(ProviderExtAuthorizationCodeGrantOidcSupportType::Yes)
96    }
97
98    fn scopes_default(&self) -> Option<Vec<<Self as Provider>::Scope>> {
99        Some(vec![
100            GoogleScope::Profile,
101            GoogleScope::Email,
102            GoogleScope::Openid,
103        ])
104    }
105
106    fn authorization_endpoint_url(&self) -> &Url {
107        &self.authorization_endpoint_url
108    }
109
110    fn authorization_request_query_extra(&self) -> Option<Map<String, Value>> {
111        let mut map = Map::new();
112
113        if let Some(access_type) = &self.access_type {
114            map.insert(
115                "access_type".to_owned(),
116                Value::String(access_type.to_string()),
117            );
118        }
119        if let Some(include_granted_scopes) = &self.include_granted_scopes {
120            if *include_granted_scopes {
121                map.insert(
122                    "include_granted_scopes".to_owned(),
123                    Value::String(true.to_string()),
124                );
125            }
126        }
127
128        if map.is_empty() {
129            None
130        } else {
131            Some(map)
132        }
133    }
134}
135
136//
137//
138//
139#[derive(Debug, Clone)]
140pub struct GoogleProviderForDesktopApps {
141    client_id: ClientId,
142    client_secret: ClientSecret,
143    redirect_uri: RedirectUri,
144    //
145    token_endpoint_url: Url,
146    authorization_endpoint_url: Url,
147}
148
149impl GoogleProviderForDesktopApps {
150    pub fn new(
151        client_id: ClientId,
152        client_secret: ClientSecret,
153        redirect_uri: RedirectUri,
154    ) -> Result<Self, UrlParseError> {
155        Ok(Self {
156            client_id,
157            client_secret,
158            redirect_uri,
159            token_endpoint_url: TOKEN_URL.parse()?,
160            authorization_endpoint_url: AUTHORIZATION_URL.parse()?,
161        })
162    }
163
164    pub fn configure<F>(mut self, mut f: F) -> Self
165    where
166        F: FnMut(&mut Self),
167    {
168        f(&mut self);
169        self
170    }
171}
172
173impl Provider for GoogleProviderForDesktopApps {
174    type Scope = GoogleScope;
175
176    fn client_id(&self) -> Option<&ClientId> {
177        Some(&self.client_id)
178    }
179
180    fn client_secret(&self) -> Option<&ClientSecret> {
181        Some(&self.client_secret)
182    }
183
184    fn token_endpoint_url(&self) -> &Url {
185        &self.token_endpoint_url
186    }
187}
188impl ProviderExtAuthorizationCodeGrant for GoogleProviderForDesktopApps {
189    fn redirect_uri(&self) -> Option<&RedirectUri> {
190        Some(&self.redirect_uri)
191    }
192
193    fn scopes_default(&self) -> Option<Vec<<Self as Provider>::Scope>> {
194        Some(vec![
195            GoogleScope::Email,
196            GoogleScope::Profile,
197            GoogleScope::Openid,
198        ])
199    }
200
201    fn authorization_endpoint_url(&self) -> &Url {
202        &self.authorization_endpoint_url
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    use oauth2_client::{
211        authorization_code_grant::{AccessTokenEndpoint, AuthorizationEndpoint},
212        re_exports::{Endpoint as _, Response},
213    };
214
215    #[test]
216    fn test_new() {
217        match GoogleProviderForWebServerApps::new(
218            "CLIENT_ID".to_owned(),
219            "CLIENT_SECRET".to_owned(),
220            RedirectUri::Oob,
221        ) {
222            Ok(p) => panic!("{p:?}"),
223            Err(GoogleProviderForWebServerAppsNewError::RedirectUriShouldBeAUrl) => {}
224            Err(err) => panic!("{err}"),
225        }
226    }
227
228    #[test]
229    fn authorization_request() -> Result<(), Box<dyn std::error::Error>> {
230        let provider = GoogleProviderForWebServerApps::new(
231            "CLIENT_ID".to_owned(),
232            "CLIENT_SECRET".to_owned(),
233            RedirectUri::new("https://client.example.com/cb")?,
234        )?
235        .configure(|x| {
236            x.access_type = Some(GoogleProviderForWebServerAppsAccessType::Offline);
237            x.include_granted_scopes = Some(true);
238        });
239
240        let request = AuthorizationEndpoint::new(&provider, vec![GoogleScope::Email])
241            .configure(|x| x.state = Some("STATE".to_owned()))
242            .render_request()?;
243
244        assert_eq!(request.uri(), "https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=CLIENT_ID&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb&scope=email&state=STATE&access_type=offline&include_granted_scopes=true");
245
246        Ok(())
247    }
248
249    #[test]
250    fn access_token_request() -> Result<(), Box<dyn std::error::Error>> {
251        let provider = GoogleProviderForWebServerApps::new(
252            "CLIENT_ID".to_owned(),
253            "CLIENT_SECRET".to_owned(),
254            RedirectUri::new("https://client.example.com/cb")?,
255        )?;
256
257        let request = AccessTokenEndpoint::new(&provider, "CODE".to_owned()).render_request()?;
258
259        assert_eq!(request.body(), b"grant_type=authorization_code&code=CODE&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb&client_id=CLIENT_ID&client_secret=CLIENT_SECRET");
260
261        Ok(())
262    }
263
264    #[test]
265    fn access_token_response() -> Result<(), Box<dyn std::error::Error>> {
266        let provider = GoogleProviderForWebServerApps::new(
267            "CLIENT_ID".to_owned(),
268            "CLIENT_SECRET".to_owned(),
269            RedirectUri::new("https://client.example.com/cb")?,
270        )?;
271
272        let response_body = include_str!(
273            "../tests/response_body_json_files/access_token_with_authorization_code_grant.json"
274        );
275        let body_ret = AccessTokenEndpoint::new(&provider, "CODE".to_owned())
276            .parse_response(Response::builder().body(response_body.as_bytes().to_vec())?)?;
277
278        match body_ret {
279            Ok(body) => {
280                assert!(body.id_token.is_some());
281            }
282            Err(body) => panic!("{body:?}"),
283        }
284
285        Ok(())
286    }
287}