drive_v3/credentials.rs
1use std::{fs, path::Path};
2use serde::{Deserialize, Serialize};
3
4use crate::{Error, ErrorKind};
5use crate::AccessToken;
6use crate::ClientSecrets;
7
8/// Contains the credentials ( [`ClientSecrets`](crate::ClientSecrets) and
9/// [`AccessToken`](crate::AccessToken) ) for a authorizing requests to the Google Drive API using
10/// [OAuth 2.0](https://developers.google.com/identity/protocols/oauth2).
11///
12/// # Note:
13///
14/// When creating credentials a `client_secrets` JSON file is required in order to use OAuth2 to authorize your API.
15///
16/// If you do not have a `client_secrets` JSON file, follow
17/// [these steps](https://developers.google.com/drive/api/quickstart/python#set_up_your_environment), once you complete the
18/// [Authorize credentials for a desktop application](https://developers.google.com/drive/api/quickstart/python#authorize_credentials_for_a_desktop_application)
19/// section you can use the downloaded secrets file.
20///
21/// # Examples
22///
23/// ```no_run
24/// use drive_v3::Credentials;
25/// # use drive_v3::Error;
26///
27/// let secrets_path = "my_client_secrets.json";
28/// # let secrets_path = "../.secure-files/google_drive_secrets.json";
29/// let scopes = ["https://www.googleapis.com/auth/drive.metadata.readonly"];
30///
31/// let credentials = Credentials::from_client_secrets_file(&secrets_path, &scopes)?;
32///
33/// assert!( credentials.are_valid() );
34/// # Ok::<(), Error>(())
35/// ```
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37pub struct Credentials {
38 /// Representation of the `client_secret.json` file that contains the authorization info for your API.
39 pub client_secrets: ClientSecrets,
40
41 /// Token used to authenticate and provide authorization information to Google APIs.
42 pub access_token: AccessToken,
43}
44
45impl Credentials {
46 /// Creates new [`Credentials`] using `client_secrets` and `access_token`.
47 ///
48 /// Examples:
49 ///
50 /// ```no_run
51 /// use drive_v3::AccessToken;
52 /// use drive_v3::{Credentials, Drive};
53 /// use drive_v3::ClientSecrets;
54 /// # use drive_v3::Error;
55 ///
56 /// // Load your client_secrets file
57 /// let secrets_path = "my_client_secrets.json";
58 /// # let secrets_path = "../.secure-files/google_drive_secrets.json";
59 /// let my_client_secrets = ClientSecrets::from_file(secrets_path)?;
60 ///
61 /// // Request an access token, this will prompt you to authorize via the browser
62 /// let scopes = ["https://www.googleapis.com/auth/drive.metadata.readonly"];
63 /// let my_access_token = AccessToken::request(&my_client_secrets, &scopes)?;
64 ///
65 /// // Create Credentials
66 /// let my_credentials = Credentials::new(&my_client_secrets, &my_access_token);
67 ///
68 /// // now you can create a Drive to make requests
69 /// let drive = Drive::new(&my_credentials);
70 /// let files = drive.files.list();
71 ///
72 /// println!("files in drive: {:#?}", files);
73 ///
74 /// # Ok::<(), Error>(())
75 /// ```
76 pub fn new( client_secrets: &ClientSecrets, access_token: &AccessToken ) -> Self {
77 Self {
78 client_secrets: client_secrets.clone(),
79 access_token: access_token.clone(),
80 }
81 }
82
83 /// Gets the [access token](https://cloud.google.com/docs/authentication/token-types#access) which is sent as a
84 /// [HTTP Authorization request header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) to the
85 /// Google Drive API to authenticate access.
86 ///
87 /// Examples:
88 ///
89 /// ```no_run
90 /// # use drive_v3::Credentials;
91 /// # use drive_v3::Error;
92 /// #
93 /// # let scopes = ["https://www.googleapis.com/auth/drive.metadata.readonly"];
94 /// # let credentials_path = "../.secure-files/google_drive_credentials.json";
95 /// #
96 /// # let mut credentials = Credentials::from_file(&credentials_path, &scopes)?;
97 /// #
98 /// // You can use this token to make a requests (with reqwest for example) using it for authorization
99 /// let client = reqwest::blocking::Client::new();
100 ///
101 /// let body = client.get("google-api-endpoint")
102 /// .bearer_auth( &credentials.get_access_token() )
103 /// .send()?
104 /// .text()?;
105 ///
106 /// println!("response: {:?}", body);
107 /// # Ok::<(), Error>(())
108 /// ```
109 pub fn get_access_token( &self ) -> String {
110 self.access_token.access_token.to_string()
111 }
112
113 /// Saves [`Credentials`] as a JSON file in `path`.
114 ///
115 /// # Examples
116 ///
117 /// ```rust
118 /// use drive_v3::Credentials;
119 /// #
120 /// # use drive_v3::Error;
121 /// #
122 /// # let scopes = ["https://www.googleapis.com/auth/drive.metadata.readonly"];
123 /// # let credentials_path = "../.secure-files/google_drive_credentials.json";
124 /// #
125 /// # let mut credentials = Credentials::from_file(&credentials_path, &scopes)?;
126 ///
127 /// // Store credentials for the next time you need authorization
128 /// credentials.store("credentials_new.json")?;
129 ///
130 /// let required_scopes = ["https://www.googleapis.com/auth/drive.metadata.readonly"];
131 /// let stored_credentials = Credentials::from_file("credentials_new.json", &required_scopes)?;
132 ///
133 /// assert_eq!(credentials, stored_credentials);
134 /// # Ok::<(), Error>(())
135 /// ```
136 ///
137 /// # Errors
138 ///
139 /// - a [`Json`](crate::ErrorKind::Json) error, if unable to parse the [`Credentials`] to JSON.
140 /// - an [`IO`](crate::ErrorKind::IO) error, if unable to write to `path`.
141 pub fn store<T: AsRef<Path>> ( &self, path: T ) -> crate::Result<()> {
142 let contents = serde_json::to_string(&self)?;
143 fs::write(&path, contents)?;
144
145 Ok(())
146 }
147
148 /// Refreshes expired or invalid [`Credentials`].
149 ///
150 /// # Note
151 ///
152 /// The refreshed credentials are updated in place, you should save them to a file using the [`store`](Credentials::store)
153 /// function to avoid requesting to many new credentials.
154 ///
155 /// # Examples
156 ///
157 /// ```no_run
158 /// # use drive_v3::{Credentials, Error};
159 /// #
160 /// # let credentials_path = "../.secure-files/google_drive_credentials.json";
161 /// # let scopes = ["https://www.googleapis.com/auth/drive.metadata.readonly"];
162 /// #
163 /// # let mut credentials = Credentials::from_file(&credentials_path, &scopes)?;
164 /// #
165 /// if !credentials.are_valid() {
166 /// credentials.refresh()?;
167 /// }
168 ///
169 /// assert!( credentials.are_valid() );
170 /// # Ok::<(), Error>(())
171 /// ```
172 ///
173 /// # Errors
174 ///
175 /// - a [`Request`](crate::ErrorKind::Request) error, if unable to send the refresh request or get a body from the response.
176 /// - a [`Json`](crate::ErrorKind::Json) error, if unable to parse the received token from JSON.
177 pub fn refresh( &mut self ) -> crate::Result<()> {
178 self.access_token.refresh( &self.client_secrets )
179 }
180
181 /// Checks if these [`Credentials`] are valid by making a request to the
182 /// [tokeninfo](https://developers.google.com/identity/sign-in/web/backend-auth#calling-the-tokeninfo-endpoint) endpoint of
183 /// the Google Drive API.
184 ///
185 /// # Note
186 ///
187 /// [`are_valid`](Credentials::are_valid) will return false if the credentials have expired or have been revoked, but it will
188 /// also return false if the tokeninfo endpoint cannot be reached or if the request returned an error response of any kind.
189 ///
190 /// # Examples
191 ///
192 /// ```rust
193 /// # use drive_v3::{Credentials, Error};
194 /// #
195 /// # let credentials_path = "../.secure-files/google_drive_credentials.json";
196 /// # let scopes = ["https://www.googleapis.com/auth/drive.metadata.readonly"];
197 /// #
198 /// # let credentials = Credentials::from_file(&credentials_path, &scopes)?;
199 /// #
200 /// if credentials.are_valid() {
201 /// println!("credentials are valid!, you can start making requests");
202 /// } else {
203 /// println!("credentials are invalid :c, try calling 'credentials.refresh()' to update them");
204 /// }
205 /// # Ok::<(), Error>(())
206 /// ```
207 pub fn are_valid( &self ) -> bool {
208 self.access_token.is_valid()
209 }
210
211 /// Retrieves previously stored [`Credentials`] from `file` and checks that they have the required `scopes` enabled.
212 ///
213 /// # Note
214 ///
215 /// [`from_file`](Credentials::from_file) only parses the stored credentials it does not validate them, before using them to
216 /// make requests you must ensure that they are still valid using the [`are_valid`](Credentials::are_valid) function.
217 /// If they are no longer valid use the [`refresh`](Credentials::refresh) function to update them.
218 ///
219 /// # Examples
220 ///
221 /// ```rust
222 /// use drive_v3::Credentials;
223 /// # use drive_v3::Error;
224 ///
225 /// let credentials_path = "my_credentials.json";
226 /// # let credentials_path = "../.secure-files/google_drive_credentials.json";
227 /// let scopes = ["https://www.googleapis.com/auth/drive.metadata.readonly"];
228 ///
229 /// let credentials = Credentials::from_file(&credentials_path, &scopes)?;
230 /// # Ok::<(), Error>(())
231 /// ```
232 ///
233 /// # Errors
234 ///
235 /// - an [`IO`](crate::ErrorKind::IO) error, if the file does not exist.
236 /// - a [`Json`](crate::ErrorKind::Json) error, if parsing of the file from JSON failed.
237 /// - a [`MismatchedScopes`](crate::ErrorKind::MismatchedScopes) error, if the scopes in the access token are different
238 /// to the `scopes` passed to the function.
239 pub fn from_file<T, U> ( file: T, scopes: &[U] ) -> crate::Result<Self>
240 where
241 T: AsRef<Path>,
242 U: AsRef<str>,
243 {
244 let contents = fs::read_to_string(&file)?;
245 let credentials: Credentials = serde_json::from_str(&contents)?;
246
247 if !credentials.access_token.has_scopes(scopes) {
248 return Err(
249 Error::new(ErrorKind::MismatchedScopes, "stored access token does not contain the requested scopes")
250 )
251 }
252
253 Ok(credentials)
254 }
255
256 /// Attempts to get [`Credentials`] by prompting the user for authorization.
257 ///
258 /// # Note:
259 ///
260 /// A `client_secrets` json file is required in order to use OAuth2 to authorize your API.
261 ///
262 /// If you do not have a `client_secrets` json file, follow
263 /// [these steps](https://developers.google.com/drive/api/quickstart/python#set_up_your_environment), once you complete the
264 /// [Authorize credentials for a desktop application](https://developers.google.com/drive/api/quickstart/python#authorize_credentials_for_a_desktop_application)
265 /// section you can use the downloaded secrets file.
266 ///
267 /// # Examples
268 ///
269 /// ```no_run
270 /// use drive_v3::Credentials;
271 /// # use drive_v3::Error;
272 ///
273 /// let secrets_path = "my_client_secrets.json";
274 /// # let secrets_path = "../.secure-files/google_drive_secrets.json";
275 /// let scopes = ["https://www.googleapis.com/auth/drive.metadata.readonly"];
276 ///
277 /// let credentials = Credentials::from_client_secrets_file(&secrets_path, &scopes)?;
278 ///
279 /// assert!( credentials.are_valid() );
280 /// # Ok::<(), Error>(())
281 /// ```
282 ///
283 /// # Errors
284 ///
285 /// - an [`IO`](crate::ErrorKind::IO) or [`Json`](crate::ErrorKind::Json) error, if unable get
286 /// [`ClientSecrets`](crate::ClientSecrets) from `file`.
287 /// - a [`HexDecoding`](crate::ErrorKind::HexDecoding), [`UrlParsing`](crate::ErrorKind::UrlParsing),
288 /// [`Request`](crate::ErrorKind::Request) or [`Response`](crate::ErrorKind::Response) error, if the
289 /// get access token request fails.
290 /// - a [`MismatchedScopes`](crate::ErrorKind::MismatchedScopes) error, if the scopes in the created access token are
291 /// different to the `scopes` passed to the function.
292 #[cfg(not(tarpaulin_include))]
293 pub fn from_client_secrets_file<T, U> ( file: T, scopes: &[U] ) -> crate::Result<Self>
294 where
295 T: AsRef<Path>,
296 U: AsRef<str>,
297 {
298 let client_secrets = ClientSecrets::from_file(&file)?;
299 let access_token = AccessToken::request(&client_secrets, scopes)?;
300
301 Ok( Self::new(&client_secrets, &access_token) )
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use serde_json;
308 use super::ErrorKind;
309 use super::Credentials;
310 use std::process::Command;
311 use testfile::{self, TestFile};
312 use crate::utils::test::{VALID_CREDENTIALS, VALID_SECRETS};
313
314 fn get_test_json() -> String {
315 String::from("{
316 \"client_secrets\": {
317 \"client_id\": \"test\",
318 \"project_id\": \"test\",
319 \"auth_uri\": \"test\",
320 \"token_uri\": \"test\",
321 \"auth_provider_x509_cert_url\": \"test\",
322 \"client_secret\": \"test\",
323 \"redirect_uris\": [\"test\"]
324 },
325 \"access_token\": {
326 \"access_token\": \"test\",
327 \"expires_in\": 0,
328 \"refresh_token\": \"test\",
329 \"scope\": \"test-scope other-test-scope unchecked-scope\",
330 \"token_type\": \"test\"
331 }
332 }")
333 }
334
335 fn get_test_scopes() -> Vec<String> {
336 vec![
337 String::from("test-scope"),
338 String::from("other-test-scope"),
339 ]
340 }
341
342 fn get_test_credentials_file() -> TestFile {
343 testfile::from( &get_test_json() )
344 }
345
346 #[test]
347 fn new_test() {
348 let client_secrets = VALID_SECRETS.clone();
349 let access_token = VALID_CREDENTIALS.access_token.clone();
350
351 let credentials = Credentials::new(&client_secrets, &access_token);
352
353 assert_eq!(credentials.client_secrets, client_secrets);
354 assert_eq!(credentials.access_token, access_token);
355 }
356
357 #[test]
358 fn get_access_token_test() {
359 let credentials = VALID_CREDENTIALS.clone();
360
361 assert_eq!( credentials.access_token.access_token, credentials.get_access_token() );
362 }
363
364 #[test]
365 fn refresh_test() {
366 let mut credentials = VALID_CREDENTIALS.clone();
367 let result = credentials.refresh();
368
369 assert!( result.is_ok() );
370 }
371
372 #[test]
373 fn store_test() {
374 let scopes: [&str; 0] = [];
375 let credentials = VALID_CREDENTIALS.clone();
376
377 credentials.store("test-stored-credentials.json").unwrap();
378
379 let saved_credentials = Credentials::from_file("test-stored-credentials.json", &scopes).unwrap();
380
381 Command::new("rm").arg("test-stored-credentials.json").output().unwrap();
382
383 assert_eq!(saved_credentials, credentials);
384 }
385
386 #[test]
387 fn are_valid_test() {
388 let scopes = get_test_scopes();
389 let test_file = get_test_credentials_file();
390
391 let credentials = Credentials::from_file(&test_file, &scopes).unwrap();
392
393 assert!( !credentials.are_valid() );
394 }
395
396 #[test]
397 fn from_file_test() {
398 let scopes = get_test_scopes();
399 let test_file = get_test_credentials_file();
400 let expected_credentials = serde_json::from_str( &get_test_json() ).unwrap();
401
402 let credentials = Credentials::from_file(&test_file, &scopes).unwrap();
403
404 assert_eq!(credentials, expected_credentials);
405 }
406
407 #[test]
408 fn from_file_invalid_scopes_test() {
409 let scopes = ["invalid-scope"];
410 let test_file = get_test_credentials_file();
411
412 let result = Credentials::from_file(&test_file, &scopes);
413
414 assert!( result.is_err() );
415 assert_eq!( result.unwrap_err().kind, ErrorKind::MismatchedScopes );
416 }
417
418 #[test]
419 fn from_file_non_existent_test() {
420 let scopes = get_test_scopes();
421 let test_file = "non-existent.json";
422
423 let result = Credentials::from_file(&test_file, &scopes);
424
425 assert!( result.is_err() );
426 assert_eq!( result.unwrap_err().kind, ErrorKind::IO );
427 }
428
429 #[test]
430 fn from_file_invalid_json_test() {
431 let scopes = get_test_scopes();
432 let test_file = testfile::from("This is not JSON");
433
434 let result = Credentials::from_file(&test_file, &scopes);
435
436 assert!( result.is_err() );
437 assert_eq!( result.unwrap_err().kind, ErrorKind::Json );
438 }
439
440 #[test]
441 fn from_file_no_longer_valid_test() {
442 let scopes = get_test_scopes();
443 let test_file = get_test_credentials_file();
444
445 let credentials = Credentials::from_file(&test_file, &scopes).unwrap();
446
447 assert_eq!( credentials.are_valid(), false );
448 }
449}