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#[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 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 #[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#[derive(Debug, Clone)]
140pub struct GoogleProviderForDesktopApps {
141 client_id: ClientId,
142 client_secret: ClientSecret,
143 redirect_uri: RedirectUri,
144 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}