From 79f66402e115bb8edfad77d24695b46939029e64 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Sun, 9 Jun 2019 10:55:53 +0200 Subject: [PATCH 01/41] First step towards futures in yup-oauth2 --- src/authenticator.rs | 46 +++++++++++++++++++++++++++++------------- src/service_account.rs | 28 ++++++++++++++++--------- 2 files changed, 51 insertions(+), 23 deletions(-) diff --git a/src/authenticator.rs b/src/authenticator.rs index 5863d0c..d3240e9 100644 --- a/src/authenticator.rs +++ b/src/authenticator.rs @@ -14,6 +14,7 @@ use crate::refresh::{RefreshFlow, RefreshResult}; use crate::storage::TokenStorage; use crate::types::{ApplicationSecret, FlowType, RequestError, StringError, Token}; +use futures::{future, prelude::*}; use hyper; /// A generalized authenticator which will keep tokens valid and store them. @@ -44,7 +45,7 @@ pub struct Authenticator { /// The `api_key()` method is an alternative in case there are no scopes or /// if no user is involved. pub trait GetToken { - fn token<'b, I, T>(&mut self, scopes: I) -> Result> + fn token<'b, I, T>(&mut self, scopes: I) -> Box>> where T: AsRef + Ord + 'b, I: IntoIterator; @@ -199,7 +200,10 @@ where /// In any failure case, the delegate will be provided with additional information, and /// the caller will be informed about storage related errors. /// Otherwise it is guaranteed to be valid for the given scopes. - fn token<'b, I, T>(&mut self, scopes: I) -> Result> + fn token<'b, I, T>( + &mut self, + scopes: I, + ) -> Box>> where T: AsRef + Ord + 'b, I: IntoIterator, @@ -235,10 +239,13 @@ where RefreshResult::Error(ref err) => { match self.delegate.client_error(err) { Retry::Abort | Retry::Skip => { - return Err(Box::new(StringError::new( - err.description().to_string(), - None, - ))); + return Box::new(future::err( + Box::new(StringError::new( + err.description().to_string(), + None, + )) + as Box, + )); } Retry::After(d) => sleep(d), } @@ -250,10 +257,11 @@ where Ok(_) => String::new(), Err(err) => err.to_string(), }; - return Err(Box::new(StringError::new( + return Box::new(future::err(Box::new(StringError::new( storage_err + err_str, err_description.as_ref(), - ))); + )) + as Box)); } RefreshResult::Success(ref new_t) => { t = new_t.clone(); @@ -263,7 +271,11 @@ where { match self.delegate.token_storage_failure(true, &err) { Retry::Skip => break, - Retry::Abort => return Err(Box::new(err)), + Retry::Abort => { + return Box::new(future::err( + Box::new(err) as Box + )) + } Retry::After(d) => { sleep(d); continue; @@ -277,7 +289,7 @@ where } // RefreshResult handling } // refresh loop } // handle expiration - Ok(t) + Box::new(future::ok(t)) } Ok(None) => { // Nothing was in storage - get a new token @@ -294,7 +306,11 @@ where { match self.delegate.token_storage_failure(true, &err) { Retry::Skip => break, - Retry::Abort => return Err(Box::new(err)), + Retry::Abort => { + return Box::new(future::err( + Box::new(err) as Box + )) + } Retry::After(d) => { sleep(d); continue; @@ -303,13 +319,15 @@ where } break; } // end attempt to save - Ok(token) + Box::new(future::ok(token)) } - Err(err) => Err(err), + Err(err) => Box::new(future::err(err)), } // end match token retrieve result } Err(err) => match self.delegate.token_storage_failure(false, &err) { - Retry::Abort | Retry::Skip => Err(Box::new(err)), + Retry::Abort | Retry::Skip => { + Box::new(future::err(Box::new(err) as Box)) + } Retry::After(d) => { sleep(d); continue; diff --git a/src/service_account.rs b/src/service_account.rs index dea4bbb..0ede6e5 100644 --- a/src/service_account.rs +++ b/src/service_account.rs @@ -22,7 +22,7 @@ use crate::storage::{hash_scopes, MemoryStorage, TokenStorage}; use crate::types::{StringError, Token}; use futures::stream::Stream; -use futures::Future; +use futures::{future, prelude::*}; use hyper::header; use url::form_urlencoded; @@ -311,23 +311,33 @@ impl GetToken for ServiceAccountAccess where C: hyper::client::connect::Connect, { - fn token<'b, I, T>(&mut self, scopes: I) -> result::Result> + fn token<'b, I, T>( + &mut self, + scopes: I, + ) -> Box>> where T: AsRef + Ord + 'b, I: IntoIterator, { let (hash, scps) = hash_scopes(scopes); - if let Some(token) = self.cache.get(hash, &scps)? { - if !token.expired() { - return Ok(token); + match self.cache.get(hash, &scps) { + Ok(Some(token)) => { + if !token.expired() { + return Box::new(future::ok(token)); + } } + Err(e) => return Box::new(future::err(Box::new(e) as Box)), + _ => {} } - let token = self.request_token(&scps)?; - let _ = self.cache.set(hash, &scps, Some(token.clone())); - - Ok(token) + match self.request_token(&scps) { + Ok(token) => { + let _ = self.cache.set(hash, &scps, Some(token.clone())); + Box::new(future::ok(token)) + } + Err(e) => Box::new(future::err(e)), + } } fn api_key(&mut self) -> Option { From 9f061a0a10c23957fda7b0d8227067a688b38140 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Sun, 9 Jun 2019 11:25:53 +0200 Subject: [PATCH 02/41] Work on Installed flow for futures --- src/authenticator.rs | 2 +- src/installed.rs | 72 +++++++++++++++++++++++--------------------- 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/src/authenticator.rs b/src/authenticator.rs index d3240e9..a48b0f1 100644 --- a/src/authenticator.rs +++ b/src/authenticator.rs @@ -102,7 +102,7 @@ where } let mut flow = InstalledFlow::new(self.client.clone(), installed_type); - flow.obtain_token(&mut self.delegate, &self.secret, scopes.iter()) + flow.obtain_token(self.delegate, self.secret.clone(), scopes.iter()) } fn retrieve_device_token( diff --git a/src/installed.rs b/src/installed.rs index 07b4695..f964091 100644 --- a/src/installed.rs +++ b/src/installed.rs @@ -7,10 +7,9 @@ use std::error::Error; use std::io; use std::sync::{Arc, Mutex}; -use futures; use futures::stream::Stream; use futures::sync::oneshot; -use futures::Future; +use futures::{future, prelude::*}; use hyper; use hyper::{header, StatusCode, Uri}; use serde_json::error; @@ -116,51 +115,54 @@ where /// It's recommended not to use the DefaultAuthenticatorDelegate, but a specialized one. pub fn obtain_token<'a, AD: AuthenticatorDelegate, S, T>( &mut self, - auth_delegate: &mut AD, - appsecret: &ApplicationSecret, + auth_delegate: AD, + appsecret: ApplicationSecret, scopes: S, - ) -> Result> + ) -> impl Future> where T: AsRef + 'a, S: Iterator, { - let authcode = self.get_authorization_code(auth_delegate, &appsecret, scopes)?; - let tokens = self.request_token(&appsecret, &authcode, auth_delegate.redirect_uri())?; + self.get_authorization_code(auth_delegate, appsecret, scopes) + .and_then(|authcode| { + self.request_token(&appsecret, &authcode, auth_delegate.redirect_uri()) + }) + .and_then(|tokens| { + // Successful response + if tokens.access_token.is_some() { + let mut token = Token { + access_token: tokens.access_token.unwrap(), + refresh_token: tokens.refresh_token.unwrap(), + token_type: tokens.token_type.unwrap(), + expires_in: tokens.expires_in, + expires_in_timestamp: None, + }; - // Successful response - if tokens.access_token.is_some() { - let mut token = Token { - access_token: tokens.access_token.unwrap(), - refresh_token: tokens.refresh_token.unwrap(), - token_type: tokens.token_type.unwrap(), - expires_in: tokens.expires_in, - expires_in_timestamp: None, - }; - - token.set_expiry_absolute(); - Result::Ok(token) - } else { - let err = io::Error::new( - io::ErrorKind::Other, - format!( - "Token API error: {} {}", - tokens.error.unwrap_or("".to_string()), - tokens.error_description.unwrap_or("".to_string()) - ) - .as_str(), - ); - Result::Err(Box::new(err)) - } + token.set_expiry_absolute(); + Result::Ok(token) + } else { + let err = Box::new(io::Error::new( + io::ErrorKind::Other, + format!( + "Token API error: {} {}", + tokens.error.unwrap_or("".to_string()), + tokens.error_description.unwrap_or("".to_string()) + ) + .as_str(), + )) as Box; + Result::Err(err) + } + }) } /// Obtains an authorization code either interactively or via HTTP redirect (see /// InstalledFlowReturnMethod). fn get_authorization_code<'a, AD: AuthenticatorDelegate, S, T>( &mut self, - auth_delegate: &mut AD, - appsecret: &ApplicationSecret, + auth_delegate: AD, + appsecret: ApplicationSecret, scopes: S, - ) -> Result> + ) -> impl Future> where T: AsRef + 'a, S: Iterator, @@ -211,7 +213,7 @@ where } }; - result + result.into_future() } /// Sends the authorization code to the provider in order to obtain access and refresh tokens. From c2fbee4dc871ed3d52357457c4f90c66c594f444 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Tue, 11 Jun 2019 23:29:12 +0200 Subject: [PATCH 03/41] rewrite(installed): Make the InstalledFlow asynchronous with futures. --- Cargo.toml | 5 +- src/authenticator.rs | 396 ---------------------------------- src/authenticator_delegate.rs | 36 +++- src/installed.rs | 311 +++++++++++++------------- src/lib.rs | 2 - src/service_account.rs | 3 +- src/types.rs | 14 ++ 7 files changed, 203 insertions(+), 564 deletions(-) delete mode 100644 src/authenticator.rs diff --git a/Cargo.toml b/Cargo.toml index 84c1eb7..d9141ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,9 +24,12 @@ serde_derive = "1.0" url = "1" futures = "0.1" tokio-threadpool = "0.1" +tokio = "0.1" [dev-dependencies] getopts = "0.2" open = "1.1" yup-hyper-mock = "3.14" -tokio = "0.1" + +[workspace] +members = ["examples/test-installed/"] diff --git a/src/authenticator.rs b/src/authenticator.rs deleted file mode 100644 index a48b0f1..0000000 --- a/src/authenticator.rs +++ /dev/null @@ -1,396 +0,0 @@ -use std::cmp::min; -use std::collections::hash_map::DefaultHasher; -use std::convert::From; -use std::error::Error; -use std::hash::{Hash, Hasher}; -use std::iter::IntoIterator; -use std::thread::sleep; -use std::time::Duration; - -use crate::authenticator_delegate::{AuthenticatorDelegate, PollError, PollInformation}; -use crate::device::{DeviceFlow, GOOGLE_DEVICE_CODE_URL}; -use crate::installed::{InstalledFlow, InstalledFlowReturnMethod}; -use crate::refresh::{RefreshFlow, RefreshResult}; -use crate::storage::TokenStorage; -use crate::types::{ApplicationSecret, FlowType, RequestError, StringError, Token}; - -use futures::{future, prelude::*}; -use hyper; - -/// A generalized authenticator which will keep tokens valid and store them. -/// -/// It is the go-to helper to deal with any kind of supported authentication flow, -/// which will be kept valid and usable. -/// -/// # Device Flow -/// This involves polling the authentication server in the given intervals -/// until there is a definitive result. -/// -/// These results will be passed the `DeviceFlowHelperDelegate` implementation to deal with -/// * presenting the user code -/// * inform the user about the progress or errors -/// * abort the operation -/// -/// # Usage -/// Please have a look at the library's landing page. -pub struct Authenticator { - flow_type: FlowType, - delegate: D, - storage: S, - client: hyper::Client, - secret: ApplicationSecret, -} - -/// A provider for authorization tokens, yielding tokens valid for a given scope. -/// The `api_key()` method is an alternative in case there are no scopes or -/// if no user is involved. -pub trait GetToken { - fn token<'b, I, T>(&mut self, scopes: I) -> Box>> - where - T: AsRef + Ord + 'b, - I: IntoIterator; - - fn api_key(&mut self) -> Option; -} - -impl<'a, D, S, C: 'static> Authenticator -where - D: AuthenticatorDelegate, - S: TokenStorage, - C: hyper::client::connect::Connect, -{ - /// Returns a new `Authenticator` instance - /// - /// # Arguments - /// * `secret` - usually obtained from a client secret file produced by the - /// [developer console][dev-con] - /// * `delegate` - Used to further refine the flow of the authentication. - /// * `client` - used for all authentication https requests - /// * `storage` - used to cache authorization tokens tokens permanently. However, - /// the implementation doesn't have any particular semantic requirement, which - /// is why `NullStorage` and `MemoryStorage` can be used as well. - /// * `flow_type` - the kind of authentication to use to obtain a token for the - /// required scopes. If unset, it will be derived from the secret. - /// [dev-con]: https://console.developers.google.com - pub fn new( - secret: &ApplicationSecret, - delegate: D, - client: hyper::Client, - storage: S, - flow_type: Option, - ) -> Authenticator { - Authenticator { - flow_type: flow_type.unwrap_or(FlowType::Device(GOOGLE_DEVICE_CODE_URL.to_string())), - delegate: delegate, - storage: storage, - client: client, - secret: secret.clone(), - } - } - - fn do_installed_flow(&mut self, scopes: &Vec<&str>) -> Result> { - let installed_type; - - match self.flow_type { - FlowType::InstalledInteractive => { - installed_type = Some(InstalledFlowReturnMethod::Interactive) - } - FlowType::InstalledRedirect(port) => { - installed_type = Some(InstalledFlowReturnMethod::HTTPRedirect(port)) - } - _ => installed_type = None, - } - - let mut flow = InstalledFlow::new(self.client.clone(), installed_type); - flow.obtain_token(self.delegate, self.secret.clone(), scopes.iter()) - } - - fn retrieve_device_token( - &mut self, - scopes: &Vec<&str>, - code_url: String, - ) -> Result> { - let mut flow = DeviceFlow::new(self.client.clone(), &self.secret, &code_url); - - // PHASE 1: REQUEST CODE - let pi: PollInformation; - loop { - let res = flow.request_code(scopes.iter()); - - pi = match res { - Err(res_err) => { - match res_err { - RequestError::ClientError(err) => match self.delegate.client_error(&err) { - Retry::Abort | Retry::Skip => { - return Err(Box::new(StringError::from(&err as &Error))); - } - Retry::After(d) => sleep(d), - }, - RequestError::HttpError(err) => { - match self.delegate.connection_error(&err) { - Retry::Abort | Retry::Skip => { - return Err(Box::new(StringError::from(&err as &Error))); - } - Retry::After(d) => sleep(d), - } - } - RequestError::InvalidClient - | RequestError::NegativeServerResponse(_, _) - | RequestError::InvalidScope(_) => { - let serr = StringError::from(res_err.to_string()); - self.delegate.request_failure(res_err); - return Err(Box::new(serr)); - } - }; - continue; - } - Ok(pi) => { - self.delegate.present_user_code(&pi); - pi - } - }; - break; - } - - // PHASE 1: POLL TOKEN - loop { - match flow.poll_token() { - Err(ref poll_err) => { - let pts = poll_err.to_string(); - match poll_err { - &&PollError::HttpError(ref err) => match self.delegate.client_error(err) { - Retry::Abort | Retry::Skip => { - return Err(Box::new(StringError::from(err as &Error))); - } - Retry::After(d) => sleep(d), - }, - &&PollError::Expired(ref t) => { - self.delegate.expired(t); - return Err(Box::new(StringError::from(pts))); - } - &&PollError::AccessDenied => { - self.delegate.denied(); - return Err(Box::new(StringError::from(pts))); - } - }; // end match poll_err - } - Ok(None) => match self.delegate.pending(&pi) { - Retry::Abort | Retry::Skip => { - return Err(Box::new(StringError::new( - "Pending authentication aborted".to_string(), - None, - ))); - } - Retry::After(d) => sleep(min(d, pi.interval)), - }, - Ok(Some(token)) => return Ok(token), - } - } - } -} - -impl GetToken for Authenticator -where - D: AuthenticatorDelegate, - S: TokenStorage, - C: hyper::client::connect::Connect, -{ - /// Blocks until a token was retrieved from storage, from the server, or until the delegate - /// decided to abort the attempt, or the user decided not to authorize the application. - /// In any failure case, the delegate will be provided with additional information, and - /// the caller will be informed about storage related errors. - /// Otherwise it is guaranteed to be valid for the given scopes. - fn token<'b, I, T>( - &mut self, - scopes: I, - ) -> Box>> - where - T: AsRef + Ord + 'b, - I: IntoIterator, - { - let (scope_key, scopes) = { - let mut sv: Vec<&str> = scopes - .into_iter() - .map(|s| s.as_ref()) - .collect::>(); - sv.sort(); - let mut sh = DefaultHasher::new(); - &sv.hash(&mut sh); - let sv = sv; - (sh.finish(), sv) - }; - - // Get cached token. Yes, let's do an explicit return - loop { - return match self.storage.get(scope_key, &scopes) { - Ok(Some(mut t)) => { - // t needs refresh ? - if t.expired() { - let mut rf = RefreshFlow::new(self.client.clone()); - loop { - match *rf.refresh_token( - self.flow_type.clone(), - &self.secret, - &t.refresh_token, - ) { - RefreshResult::Uninitialized => { - panic!("Token flow should never get here"); - } - RefreshResult::Error(ref err) => { - match self.delegate.client_error(err) { - Retry::Abort | Retry::Skip => { - return Box::new(future::err( - Box::new(StringError::new( - err.description().to_string(), - None, - )) - as Box, - )); - } - Retry::After(d) => sleep(d), - } - } - RefreshResult::RefreshError(ref err_str, ref err_description) => { - self.delegate.token_refresh_failed(err_str, err_description); - let storage_err = - match self.storage.set(scope_key, &scopes, None) { - Ok(_) => String::new(), - Err(err) => err.to_string(), - }; - return Box::new(future::err(Box::new(StringError::new( - storage_err + err_str, - err_description.as_ref(), - )) - as Box)); - } - RefreshResult::Success(ref new_t) => { - t = new_t.clone(); - loop { - if let Err(err) = - self.storage.set(scope_key, &scopes, Some(t.clone())) - { - match self.delegate.token_storage_failure(true, &err) { - Retry::Skip => break, - Retry::Abort => { - return Box::new(future::err( - Box::new(err) as Box - )) - } - Retry::After(d) => { - sleep(d); - continue; - } - } - } - break; // .set() - } - break; // refresh_token loop - } - } // RefreshResult handling - } // refresh loop - } // handle expiration - Box::new(future::ok(t)) - } - Ok(None) => { - // Nothing was in storage - get a new token - // get new token. The respective sub-routine will do all the logic. - match match self.flow_type.clone() { - FlowType::Device(url) => self.retrieve_device_token(&scopes, url), - FlowType::InstalledInteractive => self.do_installed_flow(&scopes), - FlowType::InstalledRedirect(_) => self.do_installed_flow(&scopes), - } { - Ok(token) => { - loop { - if let Err(err) = - self.storage.set(scope_key, &scopes, Some(token.clone())) - { - match self.delegate.token_storage_failure(true, &err) { - Retry::Skip => break, - Retry::Abort => { - return Box::new(future::err( - Box::new(err) as Box - )) - } - Retry::After(d) => { - sleep(d); - continue; - } - } - } - break; - } // end attempt to save - Box::new(future::ok(token)) - } - Err(err) => Box::new(future::err(err)), - } // end match token retrieve result - } - Err(err) => match self.delegate.token_storage_failure(false, &err) { - Retry::Abort | Retry::Skip => { - Box::new(future::err(Box::new(err) as Box)) - } - Retry::After(d) => { - sleep(d); - continue; - } - }, - }; // end match - } // end loop - } - - fn api_key(&mut self) -> Option { - if self.secret.client_id.len() == 0 { - return None; - } - Some(self.secret.client_id.clone()) - } -} - -/// A utility type to indicate how operations DeviceFlowHelper operations should be retried -pub enum Retry { - /// Signal you don't want to retry - Abort, - /// Signals you want to retry after the given duration - After(Duration), - /// Instruct the caller to attempt to keep going, or choose an alternate path. - /// If this is not supported, it will have the same effect as `Abort` - Skip, -} - -#[cfg(test)] -mod tests { - use super::super::device::tests::MockGoogleAuth; - use super::super::types::tests::SECRET; - use super::super::types::ConsoleApplicationSecret; - use super::*; - use crate::authenticator_delegate::DefaultAuthenticatorDelegate; - use crate::storage::MemoryStorage; - use hyper; - use std::default::Default; - - #[test] - fn test_flow() { - use serde_json as json; - - let runtime = tokio::runtime::Runtime::new().unwrap(); - let secret = json::from_str::(SECRET) - .unwrap() - .installed - .unwrap(); - let client = hyper::Client::builder() - .executor(runtime.executor()) - .build(MockGoogleAuth::default()); - let res = Authenticator::new( - &secret, - DefaultAuthenticatorDelegate, - client, - ::default(), - None, - ) - .token(&["https://www.googleapis.com/auth/youtube.upload"]); - - match res { - Ok(t) => assert_eq!(t.access_token, "1/fFAGRNJru1FTz70BzhT3Zg"), - Err(err) => panic!("Expected to retrieve token in one go: {}", err), - } - } -} diff --git a/src/authenticator_delegate.rs b/src/authenticator_delegate.rs index 0d70d5b..6b9784b 100644 --- a/src/authenticator_delegate.rs +++ b/src/authenticator_delegate.rs @@ -4,12 +4,25 @@ use std::error::Error; use std::fmt; use std::io; -use crate::authenticator::Retry; use crate::types::RequestError; use chrono::{DateTime, Local, Utc}; use std::time::Duration; +use futures::{future, prelude::*}; +use tokio::io as tio; + +/// A utility type to indicate how operations DeviceFlowHelper operations should be retried +pub enum Retry { + /// Signal you don't want to retry + Abort, + /// Signals you want to retry after the given duration + After(Duration), + /// Instruct the caller to attempt to keep going, or choose an alternate path. + /// If this is not supported, it will have the same effect as `Abort` + Skip, +} + /// Contains state of pending authentication requests #[derive(Clone, Debug, PartialEq)] pub struct PollInformation { @@ -151,7 +164,7 @@ pub trait AuthenticatorDelegate { &mut self, url: S, need_code: bool, - ) -> Option { + ) -> Box, Error = Box> + Send> { if need_code { println!( "Please direct your browser to {}, follow the instructions and enter the \ @@ -159,19 +172,24 @@ pub trait AuthenticatorDelegate { url ); - let mut code = String::new(); - io::stdin().read_line(&mut code).ok().map(|_| { - // Remove newline - code.pop(); - code - }) + Box::new( + tio::lines(io::BufReader::new(tio::stdin())) + .into_future() + .map_err(|(e, _)| { + println!("{:?}", e); + Box::new(e) as Box + }) + .and_then(|(l, _)| { + Ok(l) + }), + ) } else { println!( "Please direct your browser to {} and follow the instructions displayed \ there.", url ); - None + Box::new(future::ok(None)) } } } diff --git a/src/installed.rs b/src/installed.rs index f964091..e760cd7 100644 --- a/src/installed.rs +++ b/src/installed.rs @@ -7,9 +7,9 @@ use std::error::Error; use std::io; use std::sync::{Arc, Mutex}; +use futures::prelude::*; use futures::stream::Stream; use futures::sync::oneshot; -use futures::{future, prelude::*}; use hyper; use hyper::{header, StatusCode, Uri}; use serde_json::error; @@ -46,6 +46,7 @@ where url.push_str(auth_uri); vec![ format!("?scope={}", scopes_string), + format!("&access_type=offline"), format!( "&redirect_uri={}", redirect_uri.unwrap_or(OOB_REDIRECT_URI.to_string()) @@ -60,9 +61,9 @@ where }) } -pub struct InstalledFlow { - client: hyper::Client, - server: Option, +pub struct InstalledFlow { + method: InstalledFlowReturnMethod, + client: hyper::client::Client, } /// cf. https://developers.google.com/identity/protocols/OAuth2InstalledApp#choosingredirecturi @@ -76,57 +77,91 @@ pub enum InstalledFlowReturnMethod { HTTPRedirect(u16), } -impl InstalledFlow -where - C: hyper::client::connect::Connect, -{ +impl<'c, C: 'c + hyper::client::connect::Connect> InstalledFlow { /// Starts a new Installed App auth flow. /// If HTTPRedirect is chosen as method and the server can't be started, the flow falls /// back to Interactive. pub fn new( - client: hyper::Client, - method: Option, + client: hyper::client::Client, + method: InstalledFlowReturnMethod, ) -> InstalledFlow { - let default = InstalledFlow { + InstalledFlow { + method: method, client: client, - server: None, - }; - match method { - None => default, - Some(InstalledFlowReturnMethod::Interactive) => default, - // Start server on localhost to accept auth code. - Some(InstalledFlowReturnMethod::HTTPRedirect(port)) => { - match InstalledFlowServer::new(port) { - Result::Err(_) => default, - Result::Ok(server) => InstalledFlow { - client: default.client, - server: Some(server), - }, - } - } } } /// Handles the token request flow; it consists of the following steps: - /// . Obtain a auhorization code with user cooperation or internal redirect. + /// . Obtain a authorization code with user cooperation or internal redirect. /// . Obtain a token and refresh token using that code. /// . Return that token /// /// It's recommended not to use the DefaultAuthenticatorDelegate, but a specialized one. - pub fn obtain_token<'a, AD: AuthenticatorDelegate, S, T>( + pub fn obtain_token<'a, AD: 'a + AuthenticatorDelegate + Send>( &mut self, auth_delegate: AD, appsecret: ApplicationSecret, - scopes: S, - ) -> impl Future> - where - T: AsRef + 'a, - S: Iterator, - { - self.get_authorization_code(auth_delegate, appsecret, scopes) - .and_then(|authcode| { - self.request_token(&appsecret, &authcode, auth_delegate.redirect_uri()) + scopes: Vec, + ) -> impl 'a + Future> + Send { + let rduri = auth_delegate.redirect_uri(); + // Start server on localhost to accept auth code. + let server = if let InstalledFlowReturnMethod::HTTPRedirect(port) = self.method { + match InstalledFlowServer::new(port) { + Result::Err(e) => Err(Box::new(e) as Box), + Result::Ok(server) => Ok(Some(server)), + } + } else { + Ok(None) + }; + let port = if let Ok(Some(ref srv)) = server { + Some(srv.port) + } else { + None + }; + let client = self.client.clone(); + let appsecclone = appsecret.clone(); + server + .into_future() + // First: Obtain authorization code from user. + .and_then(move |server| { + Self::ask_authorization_code(server, auth_delegate, &appsecclone, scopes.iter()) }) + // Exchange the authorization code provided by Google for a refresh and an access + // token. + .and_then(move |authcode| { + let request = Self::request_token(appsecret, authcode, rduri, port); + let result = client.request(request); + // Handle result here, it makes ownership tracking easier. + result + .map_err(|e| Box::new(e) as Box) + .and_then(move |r| { + let result = r + .into_body() + .concat2() + .wait() + .map(|c| String::from_utf8(c.into_bytes().to_vec()).unwrap()); // TODO: error handling + + let resp = match result { + Result::Err(e) => { + return Result::Err(Box::new(e) as Box) + } + Result::Ok(s) => s, + }; + + let token_resp: Result = + serde_json::from_str(&resp); + + match token_resp { + Result::Err(e) => { + return Result::Err(Box::new(e) as Box); + } + Result::Ok(tok) => { + Result::Ok(tok) as Result> + } + } + }) + }) + // Return the combined token. .and_then(|tokens| { // Successful response if tokens.access_token.is_some() { @@ -149,83 +184,82 @@ where tokens.error_description.unwrap_or("".to_string()) ) .as_str(), - )) as Box; + )) as Box; Result::Err(err) } }) } - /// Obtains an authorization code either interactively or via HTTP redirect (see - /// InstalledFlowReturnMethod). - fn get_authorization_code<'a, AD: AuthenticatorDelegate, S, T>( - &mut self, - auth_delegate: AD, - appsecret: ApplicationSecret, + fn ask_authorization_code<'a, AD: AuthenticatorDelegate, S, T>( + server: Option, + mut auth_delegate: AD, + appsecret: &ApplicationSecret, scopes: S, - ) -> impl Future> + ) -> Box> + Send> where T: AsRef + 'a, S: Iterator, { - let server = self.server.take(); // Will shutdown the server if present when goes out of scope - let result: Result> = match server { - None => { - let url = build_authentication_request_url( - &appsecret.auth_uri, - &appsecret.client_id, - scopes, - auth_delegate.redirect_uri(), - ); - match auth_delegate.present_user_url(&url, true /* need_code */) { - None => Result::Err(Box::new(io::Error::new( - io::ErrorKind::UnexpectedEof, - "couldn't read code", - ))), - Some(mut code) => { - // Partial backwards compatibilty in case an implementation adds a new line - // due to previous behaviour. - let ends_with_newline = - code.chars().last().map(|c| c == '\n').unwrap_or(false); - if ends_with_newline { - code.pop(); + if server.is_none() { + let url = build_authentication_request_url( + &appsecret.auth_uri, + &appsecret.client_id, + scopes, + auth_delegate.redirect_uri(), + ); + Box::new( + auth_delegate + .present_user_url(&url, true /* need_code */) + .then(|r| { + match r { + Ok(Some(mut code)) => { + // Partial backwards compatibilty in case an implementation adds a new line + // due to previous behaviour. + let ends_with_newline = + code.chars().last().map(|c| c == '\n').unwrap_or(false); + if ends_with_newline { + code.pop(); + } + Ok(code) + } + _ => Err(Box::new(io::Error::new( + io::ErrorKind::UnexpectedEof, + "couldn't read code", + )) as Box), } - Result::Ok(code) - } - } - } - Some(mut server) => { - // The redirect URI must be this very localhost URL, otherwise Google refuses - // authorization. - let url = build_authentication_request_url( - &appsecret.auth_uri, - &appsecret.client_id, - scopes, - auth_delegate - .redirect_uri() - .or_else(|| Some(format!("http://localhost:{}", server.port))), - ); - auth_delegate.present_user_url(&url, false /* need_code */); - - match server.block_till_auth() { - Result::Err(e) => Result::Err(Box::new(e)), - Result::Ok(s) => Result::Ok(s), - } - } - }; - - result.into_future() + }), + ) + } else { + let mut server = server.unwrap(); + // The redirect URI must be this very localhost URL, otherwise Google refuses + // authorization. + let url = build_authentication_request_url( + &appsecret.auth_uri, + &appsecret.client_id, + scopes, + auth_delegate + .redirect_uri() + .or_else(|| Some(format!("http://localhost:{}", server.port))), + ); + Box::new( + auth_delegate + .present_user_url(&url, false /* need_code */) + .then(move |_| server.block_till_auth()) + .map_err(|e| Box::new(e) as Box), + ) + } } /// Sends the authorization code to the provider in order to obtain access and refresh tokens. - fn request_token( - &self, - appsecret: &ApplicationSecret, - authcode: &str, + fn request_token<'a>( + appsecret: ApplicationSecret, + authcode: String, custom_redirect_uri: Option, - ) -> Result> { - let redirect_uri = custom_redirect_uri.unwrap_or_else(|| match &self.server { + port: Option, + ) -> hyper::Request { + let redirect_uri = custom_redirect_uri.unwrap_or_else(|| match port { None => OOB_REDIRECT_URI.to_string(), - Some(server) => format!("http://localhost:{}", server.port), + Some(port) => format!("http://localhost:{}", port), }); let body = form_urlencoded::Serializer::new(String::new()) @@ -238,37 +272,11 @@ where ]) .finish(); - let request = hyper::Request::post(&appsecret.token_uri) + let request = hyper::Request::post(appsecret.token_uri) .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded") .body(hyper::Body::from(body)) .unwrap(); // TODO: error check - - let result = self.client.request(request).wait(); - - let resp = String::new(); - - match result { - Result::Err(e) => return Result::Err(Box::new(e)), - Result::Ok(res) => { - let result = res - .into_body() - .concat2() - .wait() - .map(|c| String::from_utf8(c.into_bytes().to_vec()).unwrap()); // TODO: error handling - - match result { - Result::Err(e) => return Result::Err(Box::new(e)), - Result::Ok(_) => (), - } - } - } - - let token_resp: Result = serde_json::from_str(&resp); - - match token_resp { - Result::Err(e) => return Result::Err(Box::new(e)), - Result::Ok(tok) => Result::Ok(tok) as Result>, - } + request } } @@ -291,37 +299,32 @@ struct InstalledFlowServer { } impl InstalledFlowServer { - fn new(port: u16) -> Result { - let bound_port = hyper::server::conn::AddrIncoming::bind(&([127, 0, 0, 1], port).into()); - match bound_port { - Result::Err(_) => Result::Err(()), - Result::Ok(bound_port) => { - let port = bound_port.local_addr().port(); + fn new(port: u16) -> Result { + let (auth_code_tx, auth_code_rx) = oneshot::channel::(); + let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>(); - let (auth_code_tx, auth_code_rx) = oneshot::channel::(); - let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>(); + let threadpool = tokio_threadpool::Builder::new() + .pool_size(1) + .name_prefix("InstalledFlowServer-") + .build(); + let service_maker = InstalledFlowServiceMaker::new(auth_code_tx); - let threadpool = tokio_threadpool::Builder::new() - .pool_size(1) - .name_prefix("InstalledFlowServer-") - .build(); - let service_maker = InstalledFlowServiceMaker::new(auth_code_tx); - let server = hyper::server::Server::builder(bound_port) - .http1_only(true) - .serve(service_maker) - .with_graceful_shutdown(shutdown_rx) - .map_err(|err| panic!("Failed badly: {}", err)); // TODO: Error handling + let addr = format!("127.0.0.1:{}", port); + let builder = hyper::server::Server::try_bind(&addr.parse().unwrap())?; + let server = builder + .http1_only(true) + .serve(service_maker) + .with_graceful_shutdown(shutdown_rx) + .map_err(|err| panic!("Failed badly: {}", err)); - threadpool.spawn(server); + threadpool.spawn(server); - Result::Ok(InstalledFlowServer { - port, - shutdown_tx: Option::Some(shutdown_tx), - auth_code_rx: Option::Some(auth_code_rx), - threadpool: Option::Some(threadpool), - }) - } - } + Result::Ok(InstalledFlowServer { + port, + shutdown_tx: Some(shutdown_tx), + auth_code_rx: Some(auth_code_rx), + threadpool: Some(threadpool), + }) } fn block_till_auth(&mut self) -> Result { @@ -471,7 +474,7 @@ impl InstalledFlowService { fn handle_url(&mut self, url: hyper::Uri) { // Google redirects to the specified localhost URL, appending the authorization // code, like this: http://localhost:8080/xyz/?code=4/731fJ3BheyCouCniPufAd280GHNV5Ju35yYcGs - // We take that code and send it to the get_authorization_code() function that + // We take that code and send it to the ask_authorization_code() function that // waits for it. for (param, val) in form_urlencoded::parse(url.query().unwrap_or("").as_bytes()) { if param == "code".to_string() { diff --git a/src/lib.rs b/src/lib.rs index 12d5e46..22b5e63 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,7 +69,6 @@ extern crate serde_derive; #[macro_use] extern crate yup_hyper_mock as hyper_mock; -mod authenticator; mod authenticator_delegate; mod device; mod helper; @@ -79,7 +78,6 @@ mod service_account; mod storage; mod types; -pub use crate::authenticator::{Authenticator, GetToken, Retry}; pub use crate::authenticator_delegate::{ AuthenticatorDelegate, DefaultAuthenticatorDelegate, PollError, PollInformation, }; diff --git a/src/service_account.rs b/src/service_account.rs index 0ede6e5..68b234e 100644 --- a/src/service_account.rs +++ b/src/service_account.rs @@ -17,9 +17,8 @@ use std::result; use std::str; use std::sync::{Arc, RwLock}; -use crate::authenticator::GetToken; use crate::storage::{hash_scopes, MemoryStorage, TokenStorage}; -use crate::types::{StringError, Token}; +use crate::types::{StringError, GetToken, Token}; use futures::stream::Stream; use futures::{future, prelude::*}; diff --git a/src/types.rs b/src/types.rs index 42746c1..ca039b3 100644 --- a/src/types.rs +++ b/src/types.rs @@ -4,6 +4,8 @@ use std::error::Error; use std::fmt; use std::str::FromStr; +use futures::{future, prelude::*}; + /// A marker trait for all Flows pub trait Flow { fn type_id() -> FlowType; @@ -179,6 +181,18 @@ impl FromStr for Scheme { } } +/// A provider for authorization tokens, yielding tokens valid for a given scope. +/// The `api_key()` method is an alternative in case there are no scopes or +/// if no user is involved. +pub trait GetToken { + fn token<'b, I, T>(&mut self, scopes: I) -> Box>> + where + T: AsRef + Ord + 'b, + I: IntoIterator; + + fn api_key(&mut self) -> Option; +} + /// Represents a token as returned by OAuth2 servers. /// /// It is produced by all authentication flows. From 4b32c0f097cc2c4b96481c32cc6d8c2320129210 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Tue, 11 Jun 2019 23:29:58 +0200 Subject: [PATCH 04/41] example(installed): Add small example for testing the InstalledFlow --- examples/test-installed/Cargo.toml | 12 ++++++++++++ examples/test-installed/src/main.rs | 28 ++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 examples/test-installed/Cargo.toml create mode 100644 examples/test-installed/src/main.rs diff --git a/examples/test-installed/Cargo.toml b/examples/test-installed/Cargo.toml new file mode 100644 index 0000000..104183a --- /dev/null +++ b/examples/test-installed/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "test-installed" +version = "0.1.0" +authors = ["Lewin Bormann "] +edition = "2018" + +[dependencies] +yup-oauth2 = { path = "../../" } +hyper = "0.12" +hyper-tls = "0.3" +futures = "0.1" +tokio = "0.1" diff --git a/examples/test-installed/src/main.rs b/examples/test-installed/src/main.rs new file mode 100644 index 0000000..592c150 --- /dev/null +++ b/examples/test-installed/src/main.rs @@ -0,0 +1,28 @@ +use yup_oauth2::InstalledFlow; + +use futures::prelude::*; + +use hyper::client::Client; +use hyper_tls::HttpsConnector; + +use std::path::Path; + +fn main() { + let https = HttpsConnector::new(1).expect("tls"); + let client = Client::builder().build::<_, hyper::Body>(https); + let mut inf = InstalledFlow::new(client, yup_oauth2::InstalledFlowReturnMethod::HTTPRedirect(8081)); + let ad = yup_oauth2::DefaultAuthenticatorDelegate; + let secret = yup_oauth2::read_application_secret(Path::new("clientsecret.json")) + .expect("clientsecret.json"); + let s = "https://www.googleapis.com/auth/drive.file".to_string(); + let scopes = vec![s]; + + let tok = inf.obtain_token(ad, secret, scopes); + let fut = tok.map_err(|e| println!("error: {:?}", e)).and_then(|t| { + println!("The token is {:?}", t); + Ok(()) + }); + + let mut rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(fut).unwrap(); +} From 7c1731cac99d08ce2eafb6c79f0d23fb3760eee2 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Tue, 11 Jun 2019 23:41:55 +0200 Subject: [PATCH 05/41] chore(rustfmt): cargo fmt --- examples/test-installed/src/main.rs | 5 ++++- src/authenticator_delegate.rs | 4 +--- src/installed.rs | 14 +++++--------- src/service_account.rs | 2 +- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/examples/test-installed/src/main.rs b/examples/test-installed/src/main.rs index 592c150..bd793a7 100644 --- a/examples/test-installed/src/main.rs +++ b/examples/test-installed/src/main.rs @@ -10,7 +10,10 @@ use std::path::Path; fn main() { let https = HttpsConnector::new(1).expect("tls"); let client = Client::builder().build::<_, hyper::Body>(https); - let mut inf = InstalledFlow::new(client, yup_oauth2::InstalledFlowReturnMethod::HTTPRedirect(8081)); + let mut inf = InstalledFlow::new( + client, + yup_oauth2::InstalledFlowReturnMethod::HTTPRedirect(8081), + ); let ad = yup_oauth2::DefaultAuthenticatorDelegate; let secret = yup_oauth2::read_application_secret(Path::new("clientsecret.json")) .expect("clientsecret.json"); diff --git a/src/authenticator_delegate.rs b/src/authenticator_delegate.rs index 6b9784b..150f186 100644 --- a/src/authenticator_delegate.rs +++ b/src/authenticator_delegate.rs @@ -179,9 +179,7 @@ pub trait AuthenticatorDelegate { println!("{:?}", e); Box::new(e) as Box }) - .and_then(|(l, _)| { - Ok(l) - }), + .and_then(|(l, _)| Ok(l)), ) } else { println!( diff --git a/src/installed.rs b/src/installed.rs index e760cd7..c7282b0 100644 --- a/src/installed.rs +++ b/src/installed.rs @@ -142,22 +142,18 @@ impl<'c, C: 'c + hyper::client::connect::Connect> InstalledFlow { .map(|c| String::from_utf8(c.into_bytes().to_vec()).unwrap()); // TODO: error handling let resp = match result { - Result::Err(e) => { - return Result::Err(Box::new(e) as Box) - } - Result::Ok(s) => s, + Err(e) => return Err(Box::new(e) as Box), + Ok(s) => s, }; let token_resp: Result = serde_json::from_str(&resp); match token_resp { - Result::Err(e) => { - return Result::Err(Box::new(e) as Box); - } - Result::Ok(tok) => { - Result::Ok(tok) as Result> + Err(e) => { + return Err(Box::new(e) as Box); } + Ok(tok) => Ok(tok) as Result>, } }) }) diff --git a/src/service_account.rs b/src/service_account.rs index 68b234e..c27aed3 100644 --- a/src/service_account.rs +++ b/src/service_account.rs @@ -18,7 +18,7 @@ use std::str; use std::sync::{Arc, RwLock}; use crate::storage::{hash_scopes, MemoryStorage, TokenStorage}; -use crate::types::{StringError, GetToken, Token}; +use crate::types::{GetToken, StringError, Token}; use futures::stream::Stream; use futures::{future, prelude::*}; From f3774e4b74efde8e2ab1927305445ee6e5340b81 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Wed, 12 Jun 2019 00:02:47 +0200 Subject: [PATCH 06/41] fix(tests): Disable unused tests and fix failing ones. --- .travis.yml | 8 -------- examples/{ => old}/auth.rs | 0 src/installed.rs | 12 ++++++------ src/lib.rs | 28 ++-------------------------- src/service_account.rs | 4 ++-- 5 files changed, 10 insertions(+), 42 deletions(-) rename examples/{ => old}/auth.rs (100%) diff --git a/.travis.yml b/.travis.yml index 42e492e..f63d99c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,6 @@ rust: stages: - name: test - - name: examples - name: lint - name: coverage @@ -45,13 +44,6 @@ jobs: - cargo fmt --all -- --check - cargo clippy --all-targets --all-features -- -D warnings || true - - stage: examples - if: os = linux - rust: stable - script: - - cargo build --manifest-path examples/drive_example/Cargo.toml - - cargo build --manifest-path examples/service_account/Cargo.toml - - stage: coverage if: os = linux sudo: true diff --git a/examples/auth.rs b/examples/old/auth.rs similarity index 100% rename from examples/auth.rs rename to examples/old/auth.rs diff --git a/src/installed.rs b/src/installed.rs index c7282b0..d1497d7 100644 --- a/src/installed.rs +++ b/src/installed.rs @@ -307,16 +307,16 @@ impl InstalledFlowServer { let addr = format!("127.0.0.1:{}", port); let builder = hyper::server::Server::try_bind(&addr.parse().unwrap())?; - let server = builder - .http1_only(true) - .serve(service_maker) + let server = builder.http1_only(true).serve(service_maker); + let port = server.local_addr().port(); + let server_future = server .with_graceful_shutdown(shutdown_rx) .map_err(|err| panic!("Failed badly: {}", err)); - threadpool.spawn(server); + threadpool.spawn(server_future); Result::Ok(InstalledFlowServer { - port, + port: port, shutdown_tx: Some(shutdown_tx), auth_code_rx: Some(auth_code_rx), threadpool: Some(threadpool), @@ -497,7 +497,7 @@ mod tests { fn test_request_url_builder() { assert_eq!( "https://accounts.google.\ - com/o/oauth2/auth?scope=email%20profile&redirect_uri=urn:ietf:wg:oauth:2.0:\ + com/o/oauth2/auth?scope=email%20profile&access_type=offline&redirect_uri=urn:ietf:wg:oauth:2.0:\ oob&response_type=code&client_id=812741506391-h38jh0j4fv0ce1krdkiq0hfvt6n5amr\ f.apps.googleusercontent.com", build_authentication_request_url( diff --git a/src/lib.rs b/src/lib.rs index 22b5e63..944f5d4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,31 +35,7 @@ //! authorize future API requests to the same scopes. //! //! ```test_harness,no_run -//! #[macro_use] -//! extern crate serde_derive; -//! -//! use yup_oauth2::{Authenticator, DefaultAuthenticatorDelegate, PollInformation, ConsoleApplicationSecret, MemoryStorage, GetToken}; -//! use serde_json as json; -//! use std::default::Default; -//! use hyper::Client; -//! use hyper_tls::HttpsConnector; -//! # const SECRET: &'static str = "{\"installed\":{\"auth_uri\":\"https://accounts.google.com/o/oauth2/auth\",\"client_secret\":\"UqkDJd5RFwnHoiG5x5Rub8SI\",\"token_uri\":\"https://accounts.google.com/o/oauth2/token\",\"client_email\":\"\",\"redirect_uris\":[\"urn:ietf:wg:oauth:2.0:oob\",\"oob\"],\"client_x509_cert_url\":\"\",\"client_id\":\"14070749909-vgip2f1okm7bkvajhi9jugan6126io9v.apps.googleusercontent.com\",\"auth_provider_x509_cert_url\":\"https://www.googleapis.com/oauth2/v1/certs\"}}"; -//! -//! # #[test] fn device() { -//! let secret = json::from_str::(SECRET).unwrap().installed.unwrap(); -//! let res = Authenticator::new(&secret, DefaultAuthenticatorDelegate, -//! Client::builder().build(HttpsConnector::new(4).unwrap()), -//! ::default(), None) -//! .token(&["https://www.googleapis.com/auth/youtube.upload"]); -//! match res { -//! Ok(t) => { -//! // now you can use t.access_token to authenticate API calls within your -//! // given scopes. It will not be valid forever, but Authenticator will automatically -//! // refresh the token for you. -//! }, -//! Err(err) => println!("Failed to acquire token: {}", err), -//! } -//! # } +//! // TODO: Rewrite example here once new authenticator works. //! ``` //! #[macro_use] @@ -88,5 +64,5 @@ pub use crate::refresh::{RefreshFlow, RefreshResult}; pub use crate::service_account::*; pub use crate::storage::{DiskTokenStorage, MemoryStorage, NullStorage, TokenStorage}; pub use crate::types::{ - ApplicationSecret, ConsoleApplicationSecret, FlowType, Scheme, Token, TokenType, + ApplicationSecret, ConsoleApplicationSecret, FlowType, GetToken, Scheme, Token, TokenType, }; diff --git a/src/service_account.rs b/src/service_account.rs index c27aed3..037c3e6 100644 --- a/src/service_account.rs +++ b/src/service_account.rs @@ -347,8 +347,8 @@ where #[cfg(test)] mod tests { use super::*; - use crate::authenticator::GetToken; use crate::helper::service_account_key_from_file; + use crate::types::GetToken; use hyper; use hyper_tls::HttpsConnector; @@ -369,7 +369,7 @@ mod tests { println!( "{:?}", acc.token(vec![&"https://www.googleapis.com/auth/pubsub"]) - .unwrap() + .wait() ); } From 39fe5f1d25eaeadad4578392acf0e76ee03ca374 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Wed, 12 Jun 2019 00:05:32 +0200 Subject: [PATCH 07/41] chore(syntax): Use `dyn` everywhere and remove unused imports. --- src/installed.rs | 7 ++++--- src/service_account.rs | 9 ++++++--- src/types.rs | 11 +++++++---- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/installed.rs b/src/installed.rs index d1497d7..2460770 100644 --- a/src/installed.rs +++ b/src/installed.rs @@ -153,7 +153,7 @@ impl<'c, C: 'c + hyper::client::connect::Connect> InstalledFlow { Err(e) => { return Err(Box::new(e) as Box); } - Ok(tok) => Ok(tok) as Result>, + Ok(tok) => Ok(tok) as Result>, } }) }) @@ -341,14 +341,15 @@ impl std::ops::Drop for InstalledFlowServer { pub struct InstalledFlowHandlerResponseFuture { inner: Box< - futures::Future, Error = hyper::http::Error> + Send, + dyn futures::Future, Error = hyper::http::Error> + Send, >, } impl InstalledFlowHandlerResponseFuture { fn new( fut: Box< - futures::Future, Error = hyper::http::Error> + Send, + dyn futures::Future, Error = hyper::http::Error> + + Send, >, ) -> Self { Self { inner: fut } diff --git a/src/service_account.rs b/src/service_account.rs index 037c3e6..bd5334b 100644 --- a/src/service_account.rs +++ b/src/service_account.rs @@ -46,7 +46,7 @@ fn encode_base64>(s: T) -> String { base64::encode_config(s.as_ref(), base64::URL_SAFE) } -fn decode_rsa_key(pem_pkcs8: &str) -> Result> { +fn decode_rsa_key(pem_pkcs8: &str) -> Result> { let private = pem_pkcs8.to_string().replace("\\n", "\n").into_bytes(); let mut private_reader: &[u8] = private.as_ref(); let private_keys = pemfile::pkcs8_private_keys(&mut private_reader); @@ -121,7 +121,7 @@ impl JWT { head } - fn sign(&self, private_key: &str) -> Result> { + fn sign(&self, private_key: &str) -> Result> { let mut jwt_head = self.encode_claims(); let key = decode_rsa_key(private_key)?; let signing_key = sign::RSASigningKey::new(&key) @@ -236,7 +236,10 @@ where } } - fn request_token(&mut self, scopes: &Vec<&str>) -> result::Result> { + fn request_token( + &mut self, + scopes: &Vec<&str>, + ) -> result::Result> { let mut claims = init_claims_from_key(&self.key, scopes); claims.sub = self.sub.clone(); let signed = JWT::new(claims).sign(self.key.private_key.as_ref().unwrap())?; diff --git a/src/types.rs b/src/types.rs index ca039b3..be16f88 100644 --- a/src/types.rs +++ b/src/types.rs @@ -4,7 +4,7 @@ use std::error::Error; use std::fmt; use std::str::FromStr; -use futures::{future, prelude::*}; +use futures::prelude::*; /// A marker trait for all Flows pub trait Flow { @@ -101,8 +101,8 @@ impl StringError { } } -impl<'a> From<&'a Error> for StringError { - fn from(err: &'a Error) -> StringError { +impl<'a> From<&'a dyn Error> for StringError { + fn from(err: &'a dyn Error) -> StringError { StringError::new(err.description().to_string(), None) } } @@ -185,7 +185,10 @@ impl FromStr for Scheme { /// The `api_key()` method is an alternative in case there are no scopes or /// if no user is involved. pub trait GetToken { - fn token<'b, I, T>(&mut self, scopes: I) -> Box>> + fn token<'b, I, T>( + &mut self, + scopes: I, + ) -> Box>> where T: AsRef + Ord + 'b, I: IntoIterator; From 59b2b03b7d00134d70144986689db60677e33640 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Wed, 12 Jun 2019 13:50:56 +0200 Subject: [PATCH 08/41] rewrite(serviceaccount): Rewrite ServiceAccountAccess to use futures. Also add example/test to check if obtaining tokens using JWTs works. --- Cargo.toml | 2 +- examples/test-installed/src/main.rs | 2 +- examples/test-svc-acct/Cargo.toml | 12 ++ examples/test-svc-acct/src/main.rs | 28 ++++ src/installed.rs | 2 +- src/service_account.rs | 224 +++++++++++++++------------- src/storage.rs | 4 +- src/types.rs | 2 +- 8 files changed, 166 insertions(+), 110 deletions(-) create mode 100644 examples/test-svc-acct/Cargo.toml create mode 100644 examples/test-svc-acct/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index d9141ee..680af8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,4 +32,4 @@ open = "1.1" yup-hyper-mock = "3.14" [workspace] -members = ["examples/test-installed/"] +members = ["examples/test-installed/", "examples/test-svc-acct/"] diff --git a/examples/test-installed/src/main.rs b/examples/test-installed/src/main.rs index bd793a7..2e99263 100644 --- a/examples/test-installed/src/main.rs +++ b/examples/test-installed/src/main.rs @@ -20,7 +20,7 @@ fn main() { let s = "https://www.googleapis.com/auth/drive.file".to_string(); let scopes = vec![s]; - let tok = inf.obtain_token(ad, secret, scopes); + let tok = inf.obtain_token(ad, secret, scopes.clone()); let fut = tok.map_err(|e| println!("error: {:?}", e)).and_then(|t| { println!("The token is {:?}", t); Ok(()) diff --git a/examples/test-svc-acct/Cargo.toml b/examples/test-svc-acct/Cargo.toml new file mode 100644 index 0000000..3b58bfd --- /dev/null +++ b/examples/test-svc-acct/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "test-svc-acct" +version = "0.1.0" +authors = ["Lewin Bormann "] +edition = "2018" + +[dependencies] +yup-oauth2 = { path = "../../" } +hyper = "0.12" +hyper-tls = "0.3" +futures = "0.1" +tokio = "0.1" diff --git a/examples/test-svc-acct/src/main.rs b/examples/test-svc-acct/src/main.rs new file mode 100644 index 0000000..88a1eda --- /dev/null +++ b/examples/test-svc-acct/src/main.rs @@ -0,0 +1,28 @@ +use yup_oauth2; + +use futures::prelude::*; +use yup_oauth2::GetToken; + +use hyper::client::Client; +use hyper_tls::HttpsConnector; +use tokio; + +use std::path; + +fn main() { + let creds = + yup_oauth2::service_account_key_from_file(path::Path::new("serviceaccount.json")).unwrap(); + let https = HttpsConnector::new(1).expect("tls"); + let client = Client::builder().build::<_, hyper::Body>(https); + + let mut sa = yup_oauth2::ServiceAccountAccess::new(creds, client); + + let fut = sa + .token(["https://www.googleapis.com/auth/pubsub"].iter()) + .and_then(|tok| { + println!("token is: {:?}", tok); + Ok(()) + }); + let mut rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(fut).unwrap() +} diff --git a/src/installed.rs b/src/installed.rs index 2460770..a4cd61a 100644 --- a/src/installed.rs +++ b/src/installed.rs @@ -101,7 +101,7 @@ impl<'c, C: 'c + hyper::client::connect::Connect> InstalledFlow { &mut self, auth_delegate: AD, appsecret: ApplicationSecret, - scopes: Vec, + scopes: Vec, // Note: I haven't found a better way to give a list of strings here, due to ownership issues with futures. ) -> impl 'a + Future> + Send { let rduri = auth_delegate.redirect_uri(); // Start server on localhost to accept auth code. diff --git a/src/service_account.rs b/src/service_account.rs index bd5334b..01cbd6d 100644 --- a/src/service_account.rs +++ b/src/service_account.rs @@ -13,9 +13,7 @@ use std::default::Default; use std::error; -use std::result; -use std::str; -use std::sync::{Arc, RwLock}; +use std::sync::{Arc, Mutex}; use crate::storage::{hash_scopes, MemoryStorage, TokenStorage}; use crate::types::{GetToken, StringError, Token}; @@ -41,12 +39,13 @@ use serde_json; const GRANT_TYPE: &'static str = "urn:ietf:params:oauth:grant-type:jwt-bearer"; const GOOGLE_RS256_HEAD: &'static str = "{\"alg\":\"RS256\",\"typ\":\"JWT\"}"; -// Encodes s as Base64 +/// Encodes s as Base64 fn encode_base64>(s: T) -> String { base64::encode_config(s.as_ref(), base64::URL_SAFE) } -fn decode_rsa_key(pem_pkcs8: &str) -> Result> { +/// Decode a PKCS8 formatted RSA key. +fn decode_rsa_key(pem_pkcs8: &str) -> Result> { let private = pem_pkcs8.to_string().replace("\\n", "\n").into_bytes(); let mut private_reader: &[u8] = private.as_ref(); let private_keys = pemfile::pkcs8_private_keys(&mut private_reader); @@ -88,6 +87,8 @@ pub struct ServiceAccountKey { pub client_x509_cert_url: Option, } +/// Permissions requested for a JWT. +/// See https://developers.google.com/identity/protocols/OAuth2ServiceAccount#authorizingrequests. #[derive(Serialize, Debug)] struct Claims { iss: String, @@ -98,20 +99,25 @@ struct Claims { scope: String, } +/// A JSON Web Token ready for signing. struct JWT { + /// The value of GOOGLE_RS256_HEAD. header: String, + /// A Claims struct, expressing the set of desired permissions etc. claims: Claims, } impl JWT { + /// Create a new JWT from claims. fn new(claims: Claims) -> JWT { JWT { header: GOOGLE_RS256_HEAD.to_string(), claims: claims, } } - // Encodes the first two parts (header and claims) to base64 and assembles them into a form - // ready to be signed. + + /// Encodes the first two parts (header and claims) to base64 and assembles them into a form + /// ready to be signed. fn encode_claims(&self) -> String { let mut head = encode_base64(&self.header); let claims = encode_base64(serde_json::to_string(&self.claims).unwrap()); @@ -121,18 +127,25 @@ impl JWT { head } - fn sign(&self, private_key: &str) -> Result> { + /// Sign a JWT base string with `private_key`, which is a PKCS8 string. + fn sign(&self, private_key: &str) -> Result> { let mut jwt_head = self.encode_claims(); let key = decode_rsa_key(private_key)?; - let signing_key = sign::RSASigningKey::new(&key) - .map_err(|_| io::Error::new(io::ErrorKind::Other, "Couldn't initialize signer"))?; + let signing_key = sign::RSASigningKey::new(&key).map_err(|_| { + Box::new(io::Error::new( + io::ErrorKind::Other, + "Couldn't initialize signer", + )) as Box + })?; let signer = signing_key .choose_scheme(&[rustls::SignatureScheme::RSA_PKCS1_SHA256]) - .ok_or(io::Error::new( + .ok_or(Box::new(io::Error::new( io::ErrorKind::Other, "Couldn't choose signing scheme", - ))?; - let signature = signer.sign(jwt_head.as_bytes())?; + )) as Box)?; + let signature = signer + .sign(jwt_head.as_bytes()) + .map_err(|e| Box::new(e) as Box)?; let signature_b64 = encode_base64(signature); jwt_head.push_str("."); @@ -142,6 +155,8 @@ impl JWT { } } +/// Set `iss`, `aud`, `exp`, `iat`, `scope` field in the returned `Claims`. `scopes` is an iterator +/// yielding strings with OAuth scopes. fn init_claims_from_key<'a, I, T>(key: &ServiceAccountKey, scopes: I) -> Claims where T: AsRef + 'a, @@ -167,19 +182,12 @@ where } } -/// See "Additional claims" at https://developers.google.com/identity/protocols/OAuth2ServiceAccount -#[allow(dead_code)] -fn set_sub_claim(mut claims: Claims, sub: String) -> Claims { - claims.sub = Some(sub); - claims -} - /// A token source (`GetToken`) yielding OAuth tokens for services that use ServiceAccount authorization. /// This token source caches token and automatically renews expired ones. pub struct ServiceAccountAccess { client: hyper::Client, key: ServiceAccountKey, - cache: MemoryStorage, + cache: Arc>, sub: Option, } @@ -205,10 +213,7 @@ impl TokenResponse { } } -impl<'a, C: 'static> ServiceAccountAccess -where - C: hyper::client::connect::Connect, -{ +impl<'a, C: 'static + hyper::client::connect::Connect> ServiceAccountAccess { /// Returns a new `ServiceAccountAccess` token source. #[allow(dead_code)] pub fn new( @@ -218,11 +223,13 @@ where ServiceAccountAccess { client: client, key: key, - cache: MemoryStorage::default(), + cache: Arc::new(Mutex::new(MemoryStorage::default())), sub: None, } } + /// Set `sub` claim in new `ServiceAccountKey` (see + /// https://developers.google.com/identity/protocols/OAuth2ServiceAccount#authorizingrequests). pub fn with_sub( key: ServiceAccountKey, client: hyper::Client, @@ -231,81 +238,72 @@ where ServiceAccountAccess { client: client, key: key, - cache: MemoryStorage::default(), + cache: Arc::new(Mutex::new(MemoryStorage::default())), sub: Some(sub), } } + /// fn request_token( - &mut self, - scopes: &Vec<&str>, - ) -> result::Result> { - let mut claims = init_claims_from_key(&self.key, scopes); - claims.sub = self.sub.clone(); - let signed = JWT::new(claims).sign(self.key.private_key.as_ref().unwrap())?; - - let body = form_urlencoded::Serializer::new(String::new()) - .extend_pairs(vec![ - ("grant_type".to_string(), GRANT_TYPE.to_string()), - ("assertion".to_string(), signed), - ]) - .finish(); - - let request = hyper::Request::post(self.key.token_uri.as_ref().unwrap()) - .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded") - .body(hyper::Body::from(body)) - .unwrap(); // TOOD: error handling - - let body = { - let result: Arc>> = - Arc::new(RwLock::new(Ok(Default::default()))); - let ok = Arc::clone(&result); - let err = Arc::clone(&result); - hyper::rt::run( - self.client + client: hyper::client::Client, + sub: Option, + key: ServiceAccountKey, + scopes: Vec, + ) -> impl Future> { + let mut claims = init_claims_from_key(&key, &scopes); + claims.sub = sub.clone(); + let signed = JWT::new(claims) + .sign(key.private_key.as_ref().unwrap()) + .into_future(); + signed + .map(|signed| { + form_urlencoded::Serializer::new(String::new()) + .extend_pairs(vec![ + ("grant_type".to_string(), GRANT_TYPE.to_string()), + ("assertion".to_string(), signed), + ]) + .finish() + }) + .map(|rqbody| { + hyper::Request::post(key.token_uri.unwrap()) + .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded") + .body(hyper::Body::from(rqbody)) + .unwrap() + }) + .and_then(move |request| { + client .request(request) - .and_then(|response| response.into_body().concat2()) - .map(move |body| { - *ok.write().unwrap_or_else(|e| unreachable!("{}", e)) = Ok(body); - - () - }) - .map_err(move |e| { - *err.write().unwrap_or_else(|e| unreachable!("{}", e)) = Err(e); - - () - }), - ); - - Arc::try_unwrap(result) - .unwrap_or_else(|e| unreachable!("{:?}", e)) - .into_inner() - .unwrap_or_else(|e| unreachable!("{}", e)) - }; - - let json_str = body + .map_err(|e| Box::new(e) as Box) + }) + .and_then(|response| { + response + .into_body() + .concat2() + .map_err(|e| Box::new(e) as Box) + }) .map(|c| String::from_utf8(c.into_bytes().to_vec()).unwrap()) - .unwrap(); // TODO: error handling - - let token: Result = - serde_json::from_str(&json_str); - - match token { - Err(e) => return Err(Box::new(e)), - Ok(token) => { - if token.access_token.is_none() - || token.token_type.is_none() - || token.expires_in.is_none() - { - Err(Box::new(StringError::new( - "Token response lacks fields".to_string(), - Some(&format!("{:?}", token)), - ))) - } else { - Ok(token.to_oauth_token()) - } - } - } + .and_then(|s| { + serde_json::from_str(&s).map_err(|e| Box::new(e) as Box) + }) + .then( + |token: Result>| match token { + Err(e) => return Err(e), + Ok(token) => { + if token.access_token.is_none() + || token.token_type.is_none() + || token.expires_in.is_none() + { + Err(Box::new(StringError::new( + "Token response lacks fields".to_string(), + Some(&format!("{:?}", token)), + )) + as Box) + } else { + Ok(token.to_oauth_token()) + } + } + }, + ) } } @@ -316,30 +314,48 @@ where fn token<'b, I, T>( &mut self, scopes: I, - ) -> Box>> + ) -> Box> + Send> where T: AsRef + Ord + 'b, I: IntoIterator, { let (hash, scps) = hash_scopes(scopes); - match self.cache.get(hash, &scps) { + match self + .cache + .lock() + .unwrap() + .get(hash, &scps.iter().map(|s| s.as_str()).collect()) + { Ok(Some(token)) => { if !token.expired() { return Box::new(future::ok(token)); } } - Err(e) => return Box::new(future::err(Box::new(e) as Box)), + Err(e) => return Box::new(future::err(Box::new(e) as Box)), _ => {} } - match self.request_token(&scps) { - Ok(token) => { - let _ = self.cache.set(hash, &scps, Some(token.clone())); - Box::new(future::ok(token)) - } - Err(e) => Box::new(future::err(e)), - } + let cache = self.cache.clone(); + Box::new( + Self::request_token( + self.client.clone(), + self.sub.clone(), + self.key.clone(), + scps.iter().map(|s| s.to_string()).collect(), + ) + .then(move |r| match r { + Ok(token) => { + let _ = cache.lock().unwrap().set( + hash, + &scps.iter().map(|s| s.as_str()).collect(), + Some(token.clone()), + ); + Box::new(future::ok(token)) + } + Err(e) => Box::new(future::err(e)), + }), + ) } fn api_key(&mut self) -> Option { diff --git a/src/storage.rs b/src/storage.rs index 770d8bd..c7adf87 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -35,7 +35,7 @@ pub trait TokenStorage { } /// Calculate a hash value describing the scopes, and return a sorted Vec of the scopes. -pub fn hash_scopes<'a, I, T>(scopes: I) -> (u64, Vec<&'a str>) +pub fn hash_scopes<'a, I, T>(scopes: I) -> (u64, Vec) where T: AsRef + Ord + 'a, I: IntoIterator, @@ -47,7 +47,7 @@ where sv.sort(); let mut sh = DefaultHasher::new(); &sv.hash(&mut sh); - let sv = sv; + let sv = sv.iter().map(|s| s.to_string()).collect(); (sh.finish(), sv) } diff --git a/src/types.rs b/src/types.rs index be16f88..93e3dcc 100644 --- a/src/types.rs +++ b/src/types.rs @@ -188,7 +188,7 @@ pub trait GetToken { fn token<'b, I, T>( &mut self, scopes: I, - ) -> Box>> + ) -> Box> + Send> where T: AsRef + Ord + 'b, I: IntoIterator; From 732e5949624ddf151586becab475e66dcf82be61 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Wed, 12 Jun 2019 14:40:08 +0200 Subject: [PATCH 09/41] refactor(InstalledFlow): Implement GetToken for InstalledFlow --- examples/test-installed/src/main.rs | 16 +++++---- src/authenticator_delegate.rs | 3 +- src/installed.rs | 52 +++++++++++++++++++++++------ src/service_account.rs | 4 +-- src/types.rs | 2 +- 5 files changed, 55 insertions(+), 22 deletions(-) diff --git a/examples/test-installed/src/main.rs b/examples/test-installed/src/main.rs index 2e99263..47a8215 100644 --- a/examples/test-installed/src/main.rs +++ b/examples/test-installed/src/main.rs @@ -1,6 +1,6 @@ -use yup_oauth2::InstalledFlow; - use futures::prelude::*; +use yup_oauth2::GetToken; +use yup_oauth2::InstalledFlow; use hyper::client::Client; use hyper_tls::HttpsConnector; @@ -10,17 +10,19 @@ use std::path::Path; fn main() { let https = HttpsConnector::new(1).expect("tls"); let client = Client::builder().build::<_, hyper::Body>(https); - let mut inf = InstalledFlow::new( - client, - yup_oauth2::InstalledFlowReturnMethod::HTTPRedirect(8081), - ); let ad = yup_oauth2::DefaultAuthenticatorDelegate; let secret = yup_oauth2::read_application_secret(Path::new("clientsecret.json")) .expect("clientsecret.json"); + let mut inf = InstalledFlow::new( + client, + ad, + secret, + yup_oauth2::InstalledFlowReturnMethod::HTTPRedirect(8081), + ); let s = "https://www.googleapis.com/auth/drive.file".to_string(); let scopes = vec![s]; - let tok = inf.obtain_token(ad, secret, scopes.clone()); + let tok = inf.token(scopes.iter()); let fut = tok.map_err(|e| println!("error: {:?}", e)).and_then(|t| { println!("The token is {:?}", t); Ok(()) diff --git a/src/authenticator_delegate.rs b/src/authenticator_delegate.rs index 150f186..6d0de4f 100644 --- a/src/authenticator_delegate.rs +++ b/src/authenticator_delegate.rs @@ -70,7 +70,7 @@ impl fmt::Display for PollError { /// /// The only method that needs to be implemented manually is `present_user_code(...)`, /// as no assumptions are made on how this presentation should happen. -pub trait AuthenticatorDelegate { +pub trait AuthenticatorDelegate: Clone { /// Called whenever there is an client, usually if there are network problems. /// /// Return retry information. @@ -194,5 +194,6 @@ pub trait AuthenticatorDelegate { /// Uses all default implementations by AuthenticatorDelegate, and makes the trait's /// implementation usable in the first place. +#[derive(Clone)] pub struct DefaultAuthenticatorDelegate; impl AuthenticatorDelegate for DefaultAuthenticatorDelegate {} diff --git a/src/installed.rs b/src/installed.rs index a4cd61a..7e32c5c 100644 --- a/src/installed.rs +++ b/src/installed.rs @@ -17,7 +17,7 @@ use url::form_urlencoded; use url::percent_encoding::{percent_encode, QUERY_ENCODE_SET}; use crate::authenticator_delegate::AuthenticatorDelegate; -use crate::types::{ApplicationSecret, Token}; +use crate::types::{ApplicationSecret, GetToken, Token}; const OOB_REDIRECT_URI: &'static str = "urn:ietf:wg:oauth:2.0:oob"; @@ -61,9 +61,31 @@ where }) } -pub struct InstalledFlow { +impl< + AD: AuthenticatorDelegate + 'static + Send + Clone, + C: hyper::client::connect::Connect + 'static, + > GetToken for InstalledFlow +{ + fn token<'b, I, T>( + &mut self, + scopes: I, + ) -> Box> + Send> + where + T: AsRef + Ord + 'b, + I: Iterator, + { + Box::new(self.obtain_token(scopes.into_iter().map(|s| s.as_ref().to_string()).collect())) + } + fn api_key(&mut self) -> Option { + None + } +} + +pub struct InstalledFlow { method: InstalledFlowReturnMethod, client: hyper::client::Client, + ad: AD, + appsecret: ApplicationSecret, } /// cf. https://developers.google.com/identity/protocols/OAuth2InstalledApp#choosingredirecturi @@ -77,16 +99,25 @@ pub enum InstalledFlowReturnMethod { HTTPRedirect(u16), } -impl<'c, C: 'c + hyper::client::connect::Connect> InstalledFlow { +impl< + 'c, + AD: 'static + AuthenticatorDelegate + Clone + Send, + C: 'c + hyper::client::connect::Connect, + > InstalledFlow +{ /// Starts a new Installed App auth flow. /// If HTTPRedirect is chosen as method and the server can't be started, the flow falls /// back to Interactive. pub fn new( client: hyper::client::Client, + ad: AD, + secret: ApplicationSecret, method: InstalledFlowReturnMethod, - ) -> InstalledFlow { + ) -> InstalledFlow { InstalledFlow { method: method, + ad: ad, + appsecret: secret, client: client, } } @@ -97,13 +128,11 @@ impl<'c, C: 'c + hyper::client::connect::Connect> InstalledFlow { /// . Return that token /// /// It's recommended not to use the DefaultAuthenticatorDelegate, but a specialized one. - pub fn obtain_token<'a, AD: 'a + AuthenticatorDelegate + Send>( + pub fn obtain_token<'a>( &mut self, - auth_delegate: AD, - appsecret: ApplicationSecret, scopes: Vec, // Note: I haven't found a better way to give a list of strings here, due to ownership issues with futures. ) -> impl 'a + Future> + Send { - let rduri = auth_delegate.redirect_uri(); + let rduri = self.ad.redirect_uri(); // Start server on localhost to accept auth code. let server = if let InstalledFlowReturnMethod::HTTPRedirect(port) = self.method { match InstalledFlowServer::new(port) { @@ -119,7 +148,8 @@ impl<'c, C: 'c + hyper::client::connect::Connect> InstalledFlow { None }; let client = self.client.clone(); - let appsecclone = appsecret.clone(); + let (appsecclone, appsecclone2) = (self.appsecret.clone(), self.appsecret.clone()); + let auth_delegate = self.ad.clone(); server .into_future() // First: Obtain authorization code from user. @@ -129,7 +159,7 @@ impl<'c, C: 'c + hyper::client::connect::Connect> InstalledFlow { // Exchange the authorization code provided by Google for a refresh and an access // token. .and_then(move |authcode| { - let request = Self::request_token(appsecret, authcode, rduri, port); + let request = Self::request_token(appsecclone2, authcode, rduri, port); let result = client.request(request); // Handle result here, it makes ownership tracking easier. result @@ -186,7 +216,7 @@ impl<'c, C: 'c + hyper::client::connect::Connect> InstalledFlow { }) } - fn ask_authorization_code<'a, AD: AuthenticatorDelegate, S, T>( + fn ask_authorization_code<'a, S, T>( server: Option, mut auth_delegate: AD, appsecret: &ApplicationSecret, diff --git a/src/service_account.rs b/src/service_account.rs index 01cbd6d..9539f1d 100644 --- a/src/service_account.rs +++ b/src/service_account.rs @@ -317,7 +317,7 @@ where ) -> Box> + Send> where T: AsRef + Ord + 'b, - I: IntoIterator, + I: Iterator, { let (hash, scps) = hash_scopes(scopes); @@ -387,7 +387,7 @@ mod tests { let mut acc = ServiceAccountAccess::new(key, client); println!( "{:?}", - acc.token(vec![&"https://www.googleapis.com/auth/pubsub"]) + acc.token(vec!["https://www.googleapis.com/auth/pubsub"].iter()) .wait() ); } diff --git a/src/types.rs b/src/types.rs index 93e3dcc..520388d 100644 --- a/src/types.rs +++ b/src/types.rs @@ -191,7 +191,7 @@ pub trait GetToken { ) -> Box> + Send> where T: AsRef + Ord + 'b, - I: IntoIterator; + I: Iterator; fn api_key(&mut self) -> Option; } From 58383f9a031e54b7ea559d67fdb896218462da7a Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Wed, 12 Jun 2019 18:43:30 +0200 Subject: [PATCH 10/41] refactor(DeviceFlow): Make DeviceFlow work with Futures --- Cargo.toml | 4 +- examples/test-device/Cargo.toml | 12 ++ examples/test-device/src/main.rs | 26 +++ src/authenticator_delegate.rs | 14 +- src/device.rs | 309 +++++++++++++++++-------------- src/types.rs | 11 ++ 6 files changed, 240 insertions(+), 136 deletions(-) create mode 100644 examples/test-device/Cargo.toml create mode 100644 examples/test-device/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 680af8b..f124e0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ edition = "2018" [dependencies] base64 = "0.10" chrono = "0.4" +http = "0.1" hyper = {version = "0.12", default-features = false} hyper-tls = "0.3" itertools = "0.8" @@ -25,6 +26,7 @@ url = "1" futures = "0.1" tokio-threadpool = "0.1" tokio = "0.1" +tokio-timer = "0.2" [dev-dependencies] getopts = "0.2" @@ -32,4 +34,4 @@ open = "1.1" yup-hyper-mock = "3.14" [workspace] -members = ["examples/test-installed/", "examples/test-svc-acct/"] +members = ["examples/test-installed/", "examples/test-svc-acct/", "examples/test-device/"] diff --git a/examples/test-device/Cargo.toml b/examples/test-device/Cargo.toml new file mode 100644 index 0000000..648213b --- /dev/null +++ b/examples/test-device/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "test-device" +version = "0.1.0" +authors = ["Lewin Bormann "] +edition = "2018" + +[dependencies] +yup-oauth2 = { path = "../../" } +hyper = "0.12" +hyper-tls = "0.3" +futures = "0.1" +tokio = "0.1" diff --git a/examples/test-device/src/main.rs b/examples/test-device/src/main.rs new file mode 100644 index 0000000..0b0b565 --- /dev/null +++ b/examples/test-device/src/main.rs @@ -0,0 +1,26 @@ +use futures::prelude::*; +use yup_oauth2; + +use hyper::client::Client; +use hyper_tls::HttpsConnector; +use std::path; +use tokio; + +fn main() { + let creds = yup_oauth2::read_application_secret(path::Path::new("clientsecret.json")) + .expect("clientsecret"); + let https = HttpsConnector::new(1).expect("tls"); + let client = Client::builder().build::<_, hyper::Body>(https); + + let scopes = &["https://www.googleapis.com/auth/youtube.readonly".to_string()]; + + let ad = yup_oauth2::DefaultAuthenticatorDelegate; + let mut df = yup_oauth2::DeviceFlow::new::(client, creds, ad, None); + let mut rt = tokio::runtime::Runtime::new().unwrap(); + + let fut = df + .retrieve_device_token(scopes.to_vec()) + .and_then(|tok| Ok(println!("{:?}", tok))); + + rt.block_on(fut).unwrap() +} diff --git a/src/authenticator_delegate.rs b/src/authenticator_delegate.rs index 6d0de4f..e918fd1 100644 --- a/src/authenticator_delegate.rs +++ b/src/authenticator_delegate.rs @@ -50,10 +50,12 @@ impl fmt::Display for PollInformation { pub enum PollError { /// Connection failure - retry if you think it's worth it HttpError(hyper::Error), - /// indicates we are expired, including the expiration date + /// Indicates we are expired, including the expiration date Expired(DateTime), /// Indicates that the user declined access. String is server response AccessDenied, + /// Indicates that too many attempts failed. + TimedOut, } impl fmt::Display for PollError { @@ -62,6 +64,16 @@ impl fmt::Display for PollError { PollError::HttpError(ref err) => err.fmt(f), PollError::Expired(ref date) => writeln!(f, "Authentication expired at {}", date), PollError::AccessDenied => "Access denied by user".fmt(f), + PollError::TimedOut => "Timed out waiting for token".fmt(f), + } + } +} + +impl Error for PollError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match *self { + PollError::HttpError(ref e) => Some(e), + _ => None, } } } diff --git a/src/device.rs b/src/device.rs index a4d72bd..6dbb022 100644 --- a/src/device.rs +++ b/src/device.rs @@ -1,71 +1,107 @@ -use std::default::Default; +use std::error::Error; use std::iter::IntoIterator; use std::time::Duration; use chrono::{self, Utc}; use futures::stream::Stream; -use futures::Future; +use futures::{future, prelude::*}; +use http; use hyper; use hyper::header; use itertools::Itertools; use serde_json as json; +use tokio_timer; use url::form_urlencoded; -use crate::authenticator_delegate::{PollError, PollInformation}; +use crate::authenticator_delegate::{AuthenticatorDelegate, PollError, PollInformation}; use crate::types::{ApplicationSecret, Flow, FlowType, JsonError, RequestError, Token}; pub const GOOGLE_DEVICE_CODE_URL: &'static str = "https://accounts.google.com/o/oauth2/device/code"; -/// Encapsulates all possible states of the Device Flow -enum DeviceFlowState { - /// We failed to poll a result - Error, - /// We received poll information and will periodically poll for a token - Pending(PollInformation), - /// The flow finished successfully, providing token information - Success(Token), -} - /// Implements the [Oauth2 Device Flow](https://developers.google.com/youtube/v3/guides/authentication#devices) /// It operates in two steps: /// * obtain a code to show to the user /// * (repeatedly) poll for the user to authenticate your application -pub struct DeviceFlow { +pub struct DeviceFlow { client: hyper::Client, - device_code: String, - state: Option, - error: Option, application_secret: ApplicationSecret, + /// Usually GOOGLE_DEVICE_CODE_URL device_code_url: String, + ad: AD, } -impl Flow for DeviceFlow { +impl Flow for DeviceFlow { fn type_id() -> FlowType { FlowType::Device(String::new()) } } -impl DeviceFlow +impl DeviceFlow where C: hyper::client::connect::Connect + Sync + 'static, C::Transport: 'static, C::Future: 'static, + AD: AuthenticatorDelegate + Clone + Send + 'static, { - pub fn new>( + pub fn new>( client: hyper::Client, - secret: &ApplicationSecret, - device_code_url: S, - ) -> DeviceFlow { + secret: ApplicationSecret, + ad: AD, + device_code_url: Option, + ) -> DeviceFlow { DeviceFlow { client: client, - device_code: Default::default(), - application_secret: secret.clone(), - device_code_url: device_code_url.as_ref().to_string(), - state: None, - error: None, + application_secret: secret, + device_code_url: device_code_url + .as_ref() + .map(|s| s.as_ref().to_string()) + .unwrap_or(GOOGLE_DEVICE_CODE_URL.to_string()), + ad: ad, } } + pub fn retrieve_device_token<'a>( + &mut self, + scopes: Vec, + ) -> Box, Error = Box> + Send> { + let mut ad = self.ad.clone(); + let application_secret = self.application_secret.clone(); + let client = self.client.clone(); + let request_code = Self::request_code( + application_secret.clone(), + client.clone(), + self.device_code_url.clone(), + scopes, + ) + .and_then(move |(pollinf, device_code)| { + println!("presenting, {}", device_code); + ad.present_user_code(&pollinf); + Ok((pollinf, device_code)) + }); + Box::new(request_code.and_then(|(pollinf, device_code)| { + future::loop_fn(0, move |i| { + // Make a copy of everything every time, because the loop function needs to be + // repeatable, i.e. we can't move anything out. + // + let pt = Self::poll_token( + application_secret.clone(), + client.clone(), + device_code.clone(), + pollinf.clone(), + ); + println!("waiting {:?}", pollinf.interval); + tokio_timer::sleep(pollinf.interval) + .then(|_| pt) + .then(move |r| match r { + Ok(None) if i < 10 => Ok(future::Loop::Continue(i + 1)), + Ok(Some(tok)) => Ok(future::Loop::Break(Some(tok))), + Err(_) if i < 10 => Ok(future::Loop::Continue(i + 1)), + _ => Ok(future::Loop::Break(None)), + }) + }) + })) + } + /// The first step involves asking the server for a code that the user /// can type into a field at a specified URL. It is called only once, assuming /// there was no connection error. Otherwise, it may be called again until @@ -81,26 +117,23 @@ where /// * If called after a successful result was returned at least once. /// # Examples /// See test-cases in source code for a more complete example. - pub fn request_code<'b, T, I>(&mut self, scopes: I) -> Result - where - T: AsRef + 'b, - I: IntoIterator, + fn request_code( + application_secret: ApplicationSecret, + client: hyper::Client, + device_code_url: String, + scopes: Vec, + ) -> impl Future> { - if self.state.is_some() { - panic!("Must not be called after we have obtained a token and have no error"); - } - // note: cloned() shouldn't be needed, see issue // https://github.com/servo/rust-url/issues/81 let req = form_urlencoded::Serializer::new(String::new()) .extend_pairs(&[ - ("client_id", &self.application_secret.client_id), + ("client_id", application_secret.client_id.clone()), ( "scope", - &scopes + scopes .into_iter() - .map(|s| s.as_ref()) - .intersperse(" ") + .intersperse(" ".to_string()) .collect::(), ), ]) @@ -108,54 +141,67 @@ where // note: works around bug in rustlang // https://github.com/rust-lang/rust/issues/22252 - let request = hyper::Request::post(&self.device_code_url) + let request = hyper::Request::post(device_code_url) .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded") - .body(hyper::Body::from(req))?; + .body(hyper::Body::from(req)) + .into_future(); + request + .then( + move |request: Result, http::Error>| { + let request = request.unwrap(); + println!("request: {:?}", request); + client.request(request) + }, + ) + .then( + |r: Result, hyper::error::Error>| { + match r { + Err(err) => { + return Err( + Box::new(RequestError::ClientError(err)) as Box + ); + } + Ok(res) => { + #[derive(Deserialize)] + struct JsonData { + device_code: String, + user_code: String, + verification_url: String, + expires_in: i64, + interval: i64, + } - // TODO: move the ? on request - let ret = match self.client.request(request).wait() { - Err(err) => { - return Err(RequestError::ClientError(err)); // TODO: failed here - } - Ok(res) => { - #[derive(Deserialize)] - struct JsonData { - device_code: String, - user_code: String, - verification_url: String, - expires_in: i64, - interval: i64, - } + let json_str: String = res + .into_body() + .concat2() + .wait() + .map(|c| String::from_utf8(c.into_bytes().to_vec()).unwrap()) + .unwrap(); // TODO: error handling - let json_str: String = res - .into_body() - .concat2() - .wait() - .map(|c| String::from_utf8(c.into_bytes().to_vec()).unwrap()) - .unwrap(); // TODO: error handling + // check for error + match json::from_str::(&json_str) { + Err(_) => {} // ignore, move on + Ok(res) => { + return Err( + Box::new(RequestError::from(res)) as Box + ) + } + } - // check for error - match json::from_str::(&json_str) { - Err(_) => {} // ignore, move on - Ok(res) => return Err(RequestError::from(res)), - } + let decoded: JsonData = json::from_str(&json_str).unwrap(); - let decoded: JsonData = json::from_str(&json_str).unwrap(); - - self.device_code = decoded.device_code; - let pi = PollInformation { - user_code: decoded.user_code, - verification_url: decoded.verification_url, - expires_at: Utc::now() + chrono::Duration::seconds(decoded.expires_in), - interval: Duration::from_secs(i64::abs(decoded.interval) as u64), - }; - self.state = Some(DeviceFlowState::Pending(pi.clone())); - - Ok(pi) - } - }; - - ret + let pi = PollInformation { + user_code: decoded.user_code, + verification_url: decoded.verification_url, + expires_at: Utc::now() + + chrono::Duration::seconds(decoded.expires_in), + interval: Duration::from_secs(i64::abs(decoded.interval) as u64), + }; + Ok((pi, decoded.device_code)) + } + } + }, + ) } /// If the first call is successful, this method may be called. @@ -175,78 +221,73 @@ where /// /// # Examples /// See test-cases in source code for a more complete example. - pub fn poll_token(&mut self) -> Result, &PollError> { - // clone, as we may re-assign our state later - let pi = match self.state { - Some(ref s) => match *s { - DeviceFlowState::Pending(ref pi) => pi.clone(), - DeviceFlowState::Error => return Err(self.error.as_ref().unwrap()), - DeviceFlowState::Success(ref t) => return Ok(Some(t.clone())), - }, - _ => panic!("You have to call request_code() beforehand"), + fn poll_token<'a>( + application_secret: ApplicationSecret, + client: hyper::Client, + device_code: String, + pi: PollInformation, + ) -> impl Future, Error = Box> { + let expired = if pi.expires_at <= Utc::now() { + Err(PollError::Expired(pi.expires_at)).into_future() + } else { + Ok(()).into_future() }; - if pi.expires_at <= Utc::now() { - self.error = Some(PollError::Expired(pi.expires_at)); - self.state = Some(DeviceFlowState::Error); - return Err(&self.error.as_ref().unwrap()); - } - // We should be ready for a new request let req = form_urlencoded::Serializer::new(String::new()) .extend_pairs(&[ - ("client_id", &self.application_secret.client_id[..]), - ("client_secret", &self.application_secret.client_secret), - ("code", &self.device_code), + ("client_id", &application_secret.client_id[..]), + ("client_secret", &application_secret.client_secret), + ("code", &device_code), ("grant_type", "http://oauth.net/grant_type/device/1.0"), ]) .finish(); - let request = hyper::Request::post(&self.application_secret.token_uri) + let request = hyper::Request::post(&application_secret.token_uri) .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded") .body(hyper::Body::from(req)) .unwrap(); // TODO: Error checking - let json_str: String = match self.client.request(request).wait() { - Err(err) => { - self.error = Some(PollError::HttpError(err)); - return Err(self.error.as_ref().unwrap()); - } - Ok(res) => { + expired + .map_err(|e| Box::new(e) as Box) + .and_then(move |_| { + client + .request(request) + .map_err(|e| Box::new(e) as Box) + }) + .map(|res| { res.into_body() .concat2() .wait() .map(|c| String::from_utf8(c.into_bytes().to_vec()).unwrap()) .unwrap() // TODO: error handling - } - }; + }) + .and_then(|json_str: String| { + #[derive(Deserialize)] + struct JsonError { + error: String, + } - #[derive(Deserialize)] - struct JsonError { - error: String, - } - - match json::from_str::(&json_str) { - Err(_) => {} // ignore, move on, it's not an error - Ok(res) => { - match res.error.as_ref() { - "access_denied" => { - self.error = Some(PollError::AccessDenied); - self.state = Some(DeviceFlowState::Error); - return Err(self.error.as_ref().unwrap()); + match json::from_str::(&json_str) { + Err(_) => {} // ignore, move on, it's not an error + Ok(res) => { + match res.error.as_ref() { + "access_denied" => { + return Err( + Box::new(PollError::AccessDenied) as Box + ); + } + "authorization_pending" => return Ok(None), + _ => panic!("server message '{}' not understood", res.error), + }; } - "authorization_pending" => return Ok(None), - _ => panic!("server message '{}' not understood", res.error), - }; - } - } + } - // yes, we expect that ! - let mut t: Token = json::from_str(&json_str).unwrap(); - t.set_expiry_absolute(); + // yes, we expect that ! + let mut t: Token = json::from_str(&json_str).unwrap(); + t.set_expiry_absolute(); - let res = Ok(Some(t.clone())); - self.state = Some(DeviceFlowState::Success(t)); - return res; + Ok(Some(t.clone())) + }) } } diff --git a/src/types.rs b/src/types.rs index 520388d..2f353f9 100644 --- a/src/types.rs +++ b/src/types.rs @@ -19,6 +19,7 @@ pub struct JsonError { } /// Encapsulates all possible results of the `request_token(...)` operation +#[derive(Debug)] pub enum RequestError { /// Indicates connection failure ClientError(hyper::Error), @@ -78,6 +79,16 @@ impl fmt::Display for RequestError { } } +impl Error for RequestError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match *self { + RequestError::ClientError(ref err) => Some(err), + RequestError::HttpError(ref err) => Some(err), + _ => None, + } + } +} + #[derive(Debug)] pub struct StringError { error: String, From e7a89fae07e69f097c0ae4ef141fb6a337e1a059 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Wed, 12 Jun 2019 18:49:14 +0200 Subject: [PATCH 11/41] refactor(cleanup): Remove obsolete tests. DeviceFlow now works in a different way, so remove old test. --- src/device.rs | 66 --------------------------------------------------- 1 file changed, 66 deletions(-) diff --git a/src/device.rs b/src/device.rs index 6dbb022..cbf3e8f 100644 --- a/src/device.rs +++ b/src/device.rs @@ -290,69 +290,3 @@ where }) } } - -#[cfg(test)] -pub mod tests { - use super::*; - - mock_connector_in_order!(MockGoogleAuth { - "HTTP/1.1 200 OK\r\n\ - Server: BOGUS\r\n\ - \r\n\ - {\r\n\ - \"device_code\" : \"4/L9fTtLrhY96442SEuf1Rl3KLFg3y\",\r\n\ - \"user_code\" : \"a9xfwk9c\",\r\n\ - \"verification_url\" : \"http://www.google.com/device\",\r\n\ - \"expires_in\" : 1800,\r\n\ - \"interval\" : 0\r\n\ - }" - "HTTP/1.1 200 OK\r\n\ - Server: BOGUS\r\n\ - \r\n\ - {\r\n\ - \"error\" : \"authorization_pending\"\r\n\ - }" - "HTTP/1.1 200 OK\r\nServer: \ - BOGUS\r\n\r\n{\r\n\"access_token\":\"1/fFAGRNJru1FTz70BzhT3Zg\",\ - \r\n\"expires_in\":3920,\r\n\"token_type\":\"Bearer\",\ - \r\n\"refresh_token\":\ - \"1/6BMfW9j53gdGImsixUH6kU5RsR4zwI9lUVX-tqf8JXQ\"\r\n}" - }); - - const TEST_APP_SECRET: &'static str = r#"{"installed":{"client_id":"384278056379-tr5pbot1mil66749n639jo54i4840u77.apps.googleusercontent.com","project_id":"sanguine-rhythm-105020","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://accounts.google.com/o/oauth2/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"QeQUnhzsiO4t--ZGmj9muUAu","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}}"#; - - #[test] - fn working_flow() { - use crate::helper::parse_application_secret; - - let runtime = tokio::runtime::Runtime::new().unwrap(); - let appsecret = parse_application_secret(&TEST_APP_SECRET.to_string()).unwrap(); - let client = hyper::Client::builder() - .executor(runtime.executor()) - .build(MockGoogleAuth::default()); - - let mut flow = DeviceFlow::new(client, &appsecret, GOOGLE_DEVICE_CODE_URL); - - match flow.request_code(&["https://www.googleapis.com/auth/youtube.upload"]) { - Ok(pi) => assert_eq!(pi.interval, Duration::from_secs(0)), - Err(err) => assert!(false, "request_code failed: {}", err), - } - - match flow.poll_token() { - Ok(None) => {} - _ => unreachable!(), - } - - let t = match flow.poll_token() { - Ok(Some(t)) => { - assert_eq!(t.access_token, "1/fFAGRNJru1FTz70BzhT3Zg"); - t - } - _ => unreachable!(), - }; - - // from now on, all calls will yield the same result - // As our mock has only 3 items, we would panic on this call - assert_eq!(flow.poll_token().unwrap(), Some(t)); - } -} From 46e1f1b880c8b61637289d8a9bc203e06156f235 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Wed, 12 Jun 2019 19:28:37 +0200 Subject: [PATCH 12/41] feat(DeviceFlow): Proper timeout handling for the DeviceFlow. --- examples/test-device/src/main.rs | 8 ++++-- src/device.rs | 48 ++++++++++++++++++++++++-------- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/examples/test-device/src/main.rs b/examples/test-device/src/main.rs index 0b0b565..d5f8b07 100644 --- a/examples/test-device/src/main.rs +++ b/examples/test-device/src/main.rs @@ -1,9 +1,10 @@ use futures::prelude::*; -use yup_oauth2; +use yup_oauth2::{self, GetToken}; use hyper::client::Client; use hyper_tls::HttpsConnector; use std::path; +use std::time::Duration; use tokio; fn main() { @@ -16,11 +17,12 @@ fn main() { let ad = yup_oauth2::DefaultAuthenticatorDelegate; let mut df = yup_oauth2::DeviceFlow::new::(client, creds, ad, None); + df.set_wait_duration(Duration::from_secs(120)); let mut rt = tokio::runtime::Runtime::new().unwrap(); let fut = df - .retrieve_device_token(scopes.to_vec()) + .token(scopes.iter()) .and_then(|tok| Ok(println!("{:?}", tok))); - rt.block_on(fut).unwrap() + println!("{:?}", rt.block_on(fut)); } diff --git a/src/device.rs b/src/device.rs index cbf3e8f..535055a 100644 --- a/src/device.rs +++ b/src/device.rs @@ -1,5 +1,5 @@ use std::error::Error; -use std::iter::IntoIterator; +use std::iter::{FromIterator, IntoIterator}; use std::time::Duration; use chrono::{self, Utc}; @@ -14,7 +14,7 @@ use tokio_timer; use url::form_urlencoded; use crate::authenticator_delegate::{AuthenticatorDelegate, PollError, PollInformation}; -use crate::types::{ApplicationSecret, Flow, FlowType, JsonError, RequestError, Token}; +use crate::types::{ApplicationSecret, Flow, FlowType, GetToken, JsonError, RequestError, Token}; pub const GOOGLE_DEVICE_CODE_URL: &'static str = "https://accounts.google.com/o/oauth2/device/code"; @@ -28,6 +28,7 @@ pub struct DeviceFlow { /// Usually GOOGLE_DEVICE_CODE_URL device_code_url: String, ad: AD, + wait: Duration, } impl Flow for DeviceFlow { @@ -36,6 +37,26 @@ impl Flow for DeviceFlow { } } +impl< + AD: AuthenticatorDelegate + Clone + Send + 'static, + C: hyper::client::connect::Connect + Sync + 'static, + > GetToken for DeviceFlow +{ + fn token<'b, I, T>( + &mut self, + scopes: I, + ) -> Box> + Send> + where + T: AsRef + Ord + 'b, + I: Iterator, + { + self.retrieve_device_token(Vec::from_iter(scopes.map(|s| s.as_ref().to_string()))) + } + fn api_key(&mut self) -> Option { + None + } +} + impl DeviceFlow where C: hyper::client::connect::Connect + Sync + 'static, @@ -57,16 +78,23 @@ where .map(|s| s.as_ref().to_string()) .unwrap_or(GOOGLE_DEVICE_CODE_URL.to_string()), ad: ad, + wait: Duration::from_secs(120), } } + /// Set the time to wait for the user to authorize us. The default is 120 seconds. + pub fn set_wait_duration(&mut self, wait: Duration) { + self.wait = wait; + } + pub fn retrieve_device_token<'a>( &mut self, scopes: Vec, - ) -> Box, Error = Box> + Send> { + ) -> Box> + Send> { let mut ad = self.ad.clone(); let application_secret = self.application_secret.clone(); let client = self.client.clone(); + let wait = self.wait; let request_code = Self::request_code( application_secret.clone(), client.clone(), @@ -74,11 +102,10 @@ where scopes, ) .and_then(move |(pollinf, device_code)| { - println!("presenting, {}", device_code); ad.present_user_code(&pollinf); Ok((pollinf, device_code)) }); - Box::new(request_code.and_then(|(pollinf, device_code)| { + Box::new(request_code.and_then(move |(pollinf, device_code)| { future::loop_fn(0, move |i| { // Make a copy of everything every time, because the loop function needs to be // repeatable, i.e. we can't move anything out. @@ -89,14 +116,14 @@ where device_code.clone(), pollinf.clone(), ); - println!("waiting {:?}", pollinf.interval); + let maxn = wait.as_secs() / pollinf.interval.as_secs(); tokio_timer::sleep(pollinf.interval) .then(|_| pt) .then(move |r| match r { - Ok(None) if i < 10 => Ok(future::Loop::Continue(i + 1)), - Ok(Some(tok)) => Ok(future::Loop::Break(Some(tok))), - Err(_) if i < 10 => Ok(future::Loop::Continue(i + 1)), - _ => Ok(future::Loop::Break(None)), + Ok(None) if i < maxn => Ok(future::Loop::Continue(i + 1)), + Ok(Some(tok)) => Ok(future::Loop::Break(tok)), + Err(_) if i < maxn => Ok(future::Loop::Continue(i + 1)), + _ => Err(Box::new(PollError::TimedOut) as Box), }) }) })) @@ -149,7 +176,6 @@ where .then( move |request: Result, http::Error>| { let request = request.unwrap(); - println!("request: {:?}", request); client.request(request) }, ) From 71a45f059ed0a71ea0d5f4b5df989f287a0a6ae7 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Wed, 12 Jun 2019 21:16:28 +0200 Subject: [PATCH 13/41] refactor(delegate): Split AuthenticatorDelegate to have FlowDelegate --- src/authenticator_delegate.rs | 3 ++- src/device.rs | 29 +++++++++++++++-------------- src/installed.rs | 28 ++++++++++++++-------------- 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/authenticator_delegate.rs b/src/authenticator_delegate.rs index e918fd1..1dd37e1 100644 --- a/src/authenticator_delegate.rs +++ b/src/authenticator_delegate.rs @@ -134,7 +134,9 @@ pub trait AuthenticatorDelegate: Clone { let _ = error_description; } } +} +pub trait FlowDelegate: Clone { /// Called as long as we are waiting for the user to authorize us. /// Can be used to print progress information, or decide to time-out. /// @@ -150,7 +152,6 @@ pub trait AuthenticatorDelegate: Clone { fn redirect_uri(&self) -> Option { None } - /// The server has returned a `user_code` which must be shown to the user, /// along with the `verification_url`. /// # Notes diff --git a/src/device.rs b/src/device.rs index 535055a..f35dcd2 100644 --- a/src/device.rs +++ b/src/device.rs @@ -13,7 +13,7 @@ use serde_json as json; use tokio_timer; use url::form_urlencoded; -use crate::authenticator_delegate::{AuthenticatorDelegate, PollError, PollInformation}; +use crate::authenticator_delegate::{FlowDelegate, PollError, PollInformation}; use crate::types::{ApplicationSecret, Flow, FlowType, GetToken, JsonError, RequestError, Token}; pub const GOOGLE_DEVICE_CODE_URL: &'static str = "https://accounts.google.com/o/oauth2/device/code"; @@ -22,25 +22,25 @@ pub const GOOGLE_DEVICE_CODE_URL: &'static str = "https://accounts.google.com/o/ /// It operates in two steps: /// * obtain a code to show to the user /// * (repeatedly) poll for the user to authenticate your application -pub struct DeviceFlow { +pub struct DeviceFlow { client: hyper::Client, application_secret: ApplicationSecret, /// Usually GOOGLE_DEVICE_CODE_URL device_code_url: String, - ad: AD, + fd: FD, wait: Duration, } -impl Flow for DeviceFlow { +impl Flow for DeviceFlow { fn type_id() -> FlowType { FlowType::Device(String::new()) } } impl< - AD: AuthenticatorDelegate + Clone + Send + 'static, + FD: FlowDelegate + Clone + Send + 'static, C: hyper::client::connect::Connect + Sync + 'static, - > GetToken for DeviceFlow + > GetToken for DeviceFlow { fn token<'b, I, T>( &mut self, @@ -57,19 +57,19 @@ impl< } } -impl DeviceFlow +impl DeviceFlow where C: hyper::client::connect::Connect + Sync + 'static, C::Transport: 'static, C::Future: 'static, - AD: AuthenticatorDelegate + Clone + Send + 'static, + FD: FlowDelegate + Clone + Send + 'static, { pub fn new>( client: hyper::Client, secret: ApplicationSecret, - ad: AD, + fd: FD, device_code_url: Option, - ) -> DeviceFlow { + ) -> DeviceFlow { DeviceFlow { client: client, application_secret: secret, @@ -77,7 +77,7 @@ where .as_ref() .map(|s| s.as_ref().to_string()) .unwrap_or(GOOGLE_DEVICE_CODE_URL.to_string()), - ad: ad, + fd: fd, wait: Duration::from_secs(120), } } @@ -87,11 +87,13 @@ where self.wait = wait; } + /// Essentially what `GetToken::token` does: Retrieve a token for the given scopes without + /// caching. pub fn retrieve_device_token<'a>( &mut self, scopes: Vec, ) -> Box> + Send> { - let mut ad = self.ad.clone(); + let mut fd = self.fd.clone(); let application_secret = self.application_secret.clone(); let client = self.client.clone(); let wait = self.wait; @@ -102,14 +104,13 @@ where scopes, ) .and_then(move |(pollinf, device_code)| { - ad.present_user_code(&pollinf); + fd.present_user_code(&pollinf); Ok((pollinf, device_code)) }); Box::new(request_code.and_then(move |(pollinf, device_code)| { future::loop_fn(0, move |i| { // Make a copy of everything every time, because the loop function needs to be // repeatable, i.e. we can't move anything out. - // let pt = Self::poll_token( application_secret.clone(), client.clone(), diff --git a/src/installed.rs b/src/installed.rs index 7e32c5c..1a5a0d1 100644 --- a/src/installed.rs +++ b/src/installed.rs @@ -16,7 +16,7 @@ use serde_json::error; use url::form_urlencoded; use url::percent_encoding::{percent_encode, QUERY_ENCODE_SET}; -use crate::authenticator_delegate::AuthenticatorDelegate; +use crate::authenticator_delegate::FlowDelegate; use crate::types::{ApplicationSecret, GetToken, Token}; const OOB_REDIRECT_URI: &'static str = "urn:ietf:wg:oauth:2.0:oob"; @@ -62,9 +62,9 @@ where } impl< - AD: AuthenticatorDelegate + 'static + Send + Clone, + FD: FlowDelegate + 'static + Send + Clone, C: hyper::client::connect::Connect + 'static, - > GetToken for InstalledFlow + > GetToken for InstalledFlow { fn token<'b, I, T>( &mut self, @@ -81,10 +81,10 @@ impl< } } -pub struct InstalledFlow { +pub struct InstalledFlow { method: InstalledFlowReturnMethod, client: hyper::client::Client, - ad: AD, + fd: FD, appsecret: ApplicationSecret, } @@ -101,22 +101,22 @@ pub enum InstalledFlowReturnMethod { impl< 'c, - AD: 'static + AuthenticatorDelegate + Clone + Send, + FD: 'static + FlowDelegate + Clone + Send, C: 'c + hyper::client::connect::Connect, - > InstalledFlow + > InstalledFlow { /// Starts a new Installed App auth flow. /// If HTTPRedirect is chosen as method and the server can't be started, the flow falls /// back to Interactive. pub fn new( client: hyper::client::Client, - ad: AD, + fd: FD, secret: ApplicationSecret, method: InstalledFlowReturnMethod, - ) -> InstalledFlow { + ) -> InstalledFlow { InstalledFlow { method: method, - ad: ad, + fd: fd, appsecret: secret, client: client, } @@ -127,12 +127,12 @@ impl< /// . Obtain a token and refresh token using that code. /// . Return that token /// - /// It's recommended not to use the DefaultAuthenticatorDelegate, but a specialized one. + /// It's recommended not to use the DefaultFlowDelegate, but a specialized one. pub fn obtain_token<'a>( &mut self, scopes: Vec, // Note: I haven't found a better way to give a list of strings here, due to ownership issues with futures. ) -> impl 'a + Future> + Send { - let rduri = self.ad.redirect_uri(); + let rduri = self.fd.redirect_uri(); // Start server on localhost to accept auth code. let server = if let InstalledFlowReturnMethod::HTTPRedirect(port) = self.method { match InstalledFlowServer::new(port) { @@ -149,7 +149,7 @@ impl< }; let client = self.client.clone(); let (appsecclone, appsecclone2) = (self.appsecret.clone(), self.appsecret.clone()); - let auth_delegate = self.ad.clone(); + let auth_delegate = self.fd.clone(); server .into_future() // First: Obtain authorization code from user. @@ -218,7 +218,7 @@ impl< fn ask_authorization_code<'a, S, T>( server: Option, - mut auth_delegate: AD, + mut auth_delegate: FD, appsecret: &ApplicationSecret, scopes: S, ) -> Box> + Send> From 505d759ba09eee95b21ec9861274b1f5c993eafc Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Wed, 12 Jun 2019 22:13:58 +0200 Subject: [PATCH 14/41] refactor(Refresh): Convert Refresh flow to be simpler and use futures. --- src/refresh.rs | 121 ++++++++++++++++++++++--------------------------- 1 file changed, 54 insertions(+), 67 deletions(-) diff --git a/src/refresh.rs b/src/refresh.rs index 2eef3f1..412c96f 100644 --- a/src/refresh.rs +++ b/src/refresh.rs @@ -1,5 +1,7 @@ use crate::types::{ApplicationSecret, FlowType, JsonError}; +use std::error::Error; + use super::Token; use chrono::Utc; use futures::stream::Stream; @@ -14,10 +16,7 @@ use url::form_urlencoded; /// Refresh an expired access token, as obtained by any other authentication flow. /// This flow is useful when your `Token` is expired and allows to obtain a new /// and valid access token. -pub struct RefreshFlow { - client: hyper::Client, - result: RefreshResult, -} +pub struct RefreshFlow; /// All possible outcomes of the refresh flow pub enum RefreshResult { @@ -31,17 +30,7 @@ pub enum RefreshResult { Success(Token), } -impl RefreshFlow -where - C: hyper::client::connect::Connect, -{ - pub fn new(client: hyper::Client) -> RefreshFlow { - RefreshFlow { - client: client, - result: RefreshResult::Uninitialized, - } - } - +impl RefreshFlow { /// Attempt to refresh the given token, and obtain a new, valid one. /// If the `RefreshResult` is `RefreshResult::Error`, you may retry within an interval /// of your choice. If it is `RefreshResult:RefreshError`, your refresh token is invalid @@ -56,69 +45,67 @@ where /// /// # Examples /// Please see the crate landing page for an example. - pub fn refresh_token( - &mut self, - _flow_type: FlowType, - client_secret: &ApplicationSecret, - refresh_token: &str, - ) -> &RefreshResult { - if let RefreshResult::Success(_) = self.result { - return &self.result; - } - + pub fn refresh_token<'a, C: 'static + hyper::client::connect::Connect>( + client: hyper::Client, + client_secret: &'a ApplicationSecret, + refresh_token: &'a str, + ) -> impl 'a + Future> { let req = form_urlencoded::Serializer::new(String::new()) .extend_pairs(&[ - ("client_id", client_secret.client_id.as_ref()), - ("client_secret", client_secret.client_secret.as_ref()), - ("refresh_token", refresh_token), - ("grant_type", "refresh_token"), + ("client_id", client_secret.client_id.clone()), + ("client_secret", client_secret.client_secret.clone()), + ("refresh_token", refresh_token.to_string()), + ("grant_type", "refresh_token".to_string()), ]) .finish(); - let request = hyper::Request::post(&client_secret.token_uri) + let request = hyper::Request::post(client_secret.token_uri.clone()) .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded") .body(hyper::Body::from(req)) .unwrap(); // TODO: error handling - let json_str: String = match self.client.request(request).wait() { - Err(err) => { - self.result = RefreshResult::Error(err); - return &self.result; - } - Ok(res) => { - res.into_body() - .concat2() - .wait() - .map(|c| String::from_utf8(c.into_bytes().to_vec()).unwrap()) - .unwrap() // TODO: error handling - } - }; + client + .request(request) + .then(|r| { + match r { + Err(err) => return Err(Box::new(err) as Box), + Ok(res) => { + Ok(res + .into_body() + .concat2() + .wait() + .map(|c| String::from_utf8(c.into_bytes().to_vec()).unwrap()) + .unwrap()) // TODO: error handling + } + } + }) + .and_then(|json_str: String| { + #[derive(Deserialize)] + struct JsonToken { + access_token: String, + token_type: String, + expires_in: i64, + } - #[derive(Deserialize)] - struct JsonToken { - access_token: String, - token_type: String, - expires_in: i64, - } + match json::from_str::(&json_str) { + Err(_) => {} + Ok(res) => { + return Ok(RefreshResult::RefreshError( + res.error, + res.error_description, + )) + } + } - match json::from_str::(&json_str) { - Err(_) => {} - Ok(res) => { - self.result = RefreshResult::RefreshError(res.error, res.error_description); - return &self.result; - } - } - - let t: JsonToken = json::from_str(&json_str).unwrap(); - self.result = RefreshResult::Success(Token { - access_token: t.access_token, - token_type: t.token_type, - refresh_token: refresh_token.to_string(), - expires_in: None, - expires_in_timestamp: Some(Utc::now().timestamp() + t.expires_in), - }); - - &self.result + let t: JsonToken = json::from_str(&json_str).unwrap(); + Ok(RefreshResult::Success(Token { + access_token: t.access_token, + token_type: t.token_type, + refresh_token: refresh_token.to_string(), + expires_in: None, + expires_in_timestamp: Some(Utc::now().timestamp() + t.expires_in), + })) + }) } } From 9efad9b08626c302b511226812fa3328aee73feb Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Thu, 13 Jun 2019 15:27:41 +0200 Subject: [PATCH 15/41] chore(version): Preemptively change version to 3.0.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f124e0a..4d8d5ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "yup-oauth2" -version = "2.0.1" +version = "3.0.0" authors = ["Sebastian Thiel ", "Lewin Bormann "] repository = "https://github.com/dermesser/yup-oauth2" description = "An oauth2 implementation, providing the 'device', 'service account' and 'installed' authorization flows" From 86e71cca5d45a2b0f9db36b6864e769eb211ba4d Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Thu, 13 Jun 2019 15:28:25 +0200 Subject: [PATCH 16/41] feat(DefaultFlowDelegate): Introduce DefaultFlowDelegate type. This was necessary after splitting traits. --- src/authenticator_delegate.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/authenticator_delegate.rs b/src/authenticator_delegate.rs index 1dd37e1..b5723ac 100644 --- a/src/authenticator_delegate.rs +++ b/src/authenticator_delegate.rs @@ -210,3 +210,7 @@ pub trait FlowDelegate: Clone { #[derive(Clone)] pub struct DefaultAuthenticatorDelegate; impl AuthenticatorDelegate for DefaultAuthenticatorDelegate {} + +#[derive(Clone)] +pub struct DefaultFlowDelegate; +impl FlowDelegate for DefaultFlowDelegate {} From a656df6b74fbfaa5eec0a45b0957ae547f44c449 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Thu, 13 Jun 2019 15:29:18 +0200 Subject: [PATCH 17/41] feat(GetToken): Add application_secret method to GetToken trait. This makes decoupling Authenticator and individual flows easier while allowing for refreshing tokens. --- src/device.rs | 3 +++ src/installed.rs | 16 +++++++--------- src/service_account.rs | 8 +++++++- src/types.rs | 4 ++++ 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/device.rs b/src/device.rs index f35dcd2..b58a68a 100644 --- a/src/device.rs +++ b/src/device.rs @@ -55,6 +55,9 @@ impl< fn api_key(&mut self) -> Option { None } + fn application_secret(&self) -> ApplicationSecret { + self.application_secret.clone() + } } impl DeviceFlow diff --git a/src/installed.rs b/src/installed.rs index 1a5a0d1..9190edd 100644 --- a/src/installed.rs +++ b/src/installed.rs @@ -61,10 +61,8 @@ where }) } -impl< - FD: FlowDelegate + 'static + Send + Clone, - C: hyper::client::connect::Connect + 'static, - > GetToken for InstalledFlow +impl + GetToken for InstalledFlow { fn token<'b, I, T>( &mut self, @@ -79,6 +77,9 @@ impl< fn api_key(&mut self) -> Option { None } + fn application_secret(&self) -> ApplicationSecret { + self.appsecret.clone() + } } pub struct InstalledFlow { @@ -99,11 +100,8 @@ pub enum InstalledFlowReturnMethod { HTTPRedirect(u16), } -impl< - 'c, - FD: 'static + FlowDelegate + Clone + Send, - C: 'c + hyper::client::connect::Connect, - > InstalledFlow +impl<'c, FD: 'static + FlowDelegate + Clone + Send, C: 'c + hyper::client::connect::Connect> + InstalledFlow { /// Starts a new Installed App auth flow. /// If HTTPRedirect is chosen as method and the server can't be started, the flow falls diff --git a/src/service_account.rs b/src/service_account.rs index 9539f1d..eeb78e0 100644 --- a/src/service_account.rs +++ b/src/service_account.rs @@ -16,7 +16,7 @@ use std::error; use std::sync::{Arc, Mutex}; use crate::storage::{hash_scopes, MemoryStorage, TokenStorage}; -use crate::types::{GetToken, StringError, Token}; +use crate::types::{ApplicationSecret, GetToken, StringError, Token}; use futures::stream::Stream; use futures::{future, prelude::*}; @@ -358,6 +358,12 @@ where ) } + /// Returns an empty ApplicationSecret as tokens for service accounts don't need to be + /// refreshed (they are simply reissued). + fn application_secret(&self) -> ApplicationSecret { + Default::default() + } + fn api_key(&mut self) -> Option { None } diff --git a/src/types.rs b/src/types.rs index 2f353f9..3ba1586 100644 --- a/src/types.rs +++ b/src/types.rs @@ -205,6 +205,10 @@ pub trait GetToken { I: Iterator; fn api_key(&mut self) -> Option; + + /// Return an application secret with at least token_uri, client_secret, and client_id filled + /// in. This is used for refreshing tokens without interaction from the flow. + fn application_secret(&self) -> ApplicationSecret; } /// Represents a token as returned by OAuth2 servers. From 18411a06108e561d2bef918b80880b85a2d628e9 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Thu, 13 Jun 2019 15:30:50 +0200 Subject: [PATCH 18/41] imp(Storage): Implement Default for MemoryStorage and make errors sendable --- src/storage.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/storage.rs b/src/storage.rs index c7adf87..427c92c 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -20,7 +20,7 @@ use crate::types::Token; /// For completeness, the underlying, sorted scopes are provided as well. They might be /// useful for presentation to the user. pub trait TokenStorage { - type Error: 'static + Error; + type Error: 'static + Error + Send; /// If `token` is None, it is invalid or revoked and should be removed from storage. /// Otherwise, it should be saved. @@ -85,6 +85,12 @@ pub struct MemoryStorage { pub tokens: HashMap, } +impl MemoryStorage { + pub fn new() -> MemoryStorage { + Default::default() + } +} + impl TokenStorage for MemoryStorage { type Error = NullError; From 48cf83e4da8e0ca018a1dce882bde9189be26f20 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Thu, 13 Jun 2019 15:32:31 +0200 Subject: [PATCH 19/41] feat(Authenticator): Implement new Authenticator. --- src/authenticator.rs | 190 +++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 5 +- src/refresh.rs | 70 +++------------- 3 files changed, 204 insertions(+), 61 deletions(-) create mode 100644 src/authenticator.rs diff --git a/src/authenticator.rs b/src/authenticator.rs new file mode 100644 index 0000000..557cdf3 --- /dev/null +++ b/src/authenticator.rs @@ -0,0 +1,190 @@ +use crate::authenticator_delegate::{AuthenticatorDelegate, Retry}; +use crate::refresh::{RefreshFlow, RefreshResult}; +use crate::storage::{hash_scopes, DiskTokenStorage, MemoryStorage, TokenStorage}; +use crate::types::{ApplicationSecret, GetToken, StringError, Token}; + +use futures::{future, prelude::*}; +use tokio_timer; + +use std::error::Error; +use std::io; +use std::sync::{Arc, Mutex}; + +/// Authenticator abstracts different `GetToken` implementations behind one type and handles +/// caching received tokens. It's important to use it (instead of the flows directly) because +/// otherwise the user needs to be asked for new authorization every time a token is generated. +/// +/// `ServiceAccountAccess` does not need (and does not work) with `Authenticator`, given that it +/// does not require interaction and implements its own caching. Use it directly. +pub struct Authenticator< + T: GetToken, + S: TokenStorage, + AD: AuthenticatorDelegate, + C: hyper::client::connect::Connect, +> { + client: hyper::Client, + inner: Arc>, + store: Arc>, + delegate: AD, +} + +impl + Authenticator +{ + /// Create an Authenticator caching tokens for the duration of this authenticator. + pub fn new( + client: hyper::Client, + inner: T, + delegate: AD, + ) -> Authenticator { + Authenticator { + client: client, + inner: Arc::new(Mutex::new(inner)), + store: Arc::new(Mutex::new(MemoryStorage::new())), + delegate: delegate, + } + } +} + +impl + Authenticator +{ + /// Create an Authenticator using the store at `path`. + pub fn new_disk>( + client: hyper::Client, + inner: T, + delegate: AD, + token_storage_path: P, + ) -> io::Result> { + Ok(Authenticator { + client: client, + inner: Arc::new(Mutex::new(inner)), + store: Arc::new(Mutex::new(DiskTokenStorage::new(token_storage_path)?)), + delegate: delegate, + }) + } +} + +impl< + GT: 'static + GetToken + Send, + S: 'static + TokenStorage + Send, + AD: 'static + AuthenticatorDelegate + Send, + C: 'static + hyper::client::connect::Connect + Clone + Send, + > GetToken for Authenticator +{ + /// Returns the API Key of the inner flow. + fn api_key(&mut self) -> Option { + self.inner.lock().unwrap().api_key() + } + /// Returns the application secret of the inner flow. + fn application_secret(&self) -> ApplicationSecret { + self.inner.lock().unwrap().application_secret() + } + + fn token<'b, I, T>( + &mut self, + scopes: I, + ) -> Box> + Send> + where + T: AsRef + Ord + 'b, + I: Iterator, + { + let (scope_key, scopes) = hash_scopes(scopes); + let store = self.store.clone(); + let mut delegate = self.delegate.clone(); + let client = self.client.clone(); + let appsecret = self.inner.lock().unwrap().application_secret(); + let gettoken = self.inner.clone(); + let loopfn = move |()| -> Box< + dyn Future, Error = Box> + Send, + > { + // How well does this work with tokio? + match store.lock().unwrap().get( + scope_key.clone(), + &scopes.iter().map(|s| s.as_str()).collect(), + ) { + Ok(Some(t)) => { + if !t.expired() { + return Box::new(Ok(future::Loop::Break(t)).into_future()); + } + // Implement refresh flow. + let refresh_token = t.refresh_token.clone(); + let mut delegate = delegate.clone(); + let refresh_fut = RefreshFlow::refresh_token( + client.clone(), + appsecret.clone(), + refresh_token, + ) + .and_then(move |rr| match rr { + RefreshResult::Error(e) => { + delegate.token_refresh_failed( + format!("{}", e.description().to_string()), + &Some("the request has likely timed out".to_string()), + ); + Err(Box::new(e) as Box) + } + RefreshResult::RefreshError(ref s, ref ss) => { + delegate.token_refresh_failed( + format!("{} {}", s, ss.clone().map(|s| format!("({})", s)).unwrap_or("".to_string())), + &Some("the refresh token is likely invalid and your authorization has been revoked".to_string()), + ); + Err(Box::new(StringError::new(s.to_string(), ss.as_ref())) as Box) + } + RefreshResult::Success(t) => Ok(future::Loop::Break(t)), + }); + Box::new(refresh_fut) + } + Ok(None) => { + let store = store.clone(); + let scopes = scopes.clone(); + let mut delegate = delegate.clone(); + Box::new( + gettoken + .lock() + .unwrap() + .token(scopes.iter()) + .and_then(move |t| { + if let Err(e) = store.lock().unwrap().set( + scope_key, + &scopes.iter().map(|s| s.as_str()).collect(), + Some(t.clone()), + ) { + match delegate.token_storage_failure(true, &e) { + Retry::Skip => { + Box::new(Ok(future::Loop::Break(t)).into_future()) + } + Retry::Abort => Box::new( + Err(Box::new(e) as Box).into_future(), + ), + Retry::After(d) => Box::new( + tokio_timer::sleep(d) + .then(|_| Ok(future::Loop::Continue(()))), + ) + as Box< + dyn Future< + Item = future::Loop, + Error = Box, + > + Send, + >, + } + } else { + Box::new(Ok(future::Loop::Break(t)).into_future()) + } + }), + ) + } + Err(err) => match delegate.token_storage_failure(false, &err) { + Retry::Abort | Retry::Skip => { + return Box::new(future::err(Box::new(err) as Box)) + } + Retry::After(d) => { + return Box::new( + tokio_timer::sleep(d).then(|_| Ok(future::Loop::Continue(()))), + ) + } + }, + } + }; + Box::new(future::loop_fn((), loopfn)) + } +} diff --git a/src/lib.rs b/src/lib.rs index 944f5d4..bd4aa87 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,6 +45,7 @@ extern crate serde_derive; #[macro_use] extern crate yup_hyper_mock as hyper_mock; +mod authenticator; mod authenticator_delegate; mod device; mod helper; @@ -54,8 +55,10 @@ mod service_account; mod storage; mod types; +pub use crate::authenticator::Authenticator; pub use crate::authenticator_delegate::{ - AuthenticatorDelegate, DefaultAuthenticatorDelegate, PollError, PollInformation, + AuthenticatorDelegate, DefaultAuthenticatorDelegate, DefaultFlowDelegate, FlowDelegate, + PollError, PollInformation, }; pub use crate::device::{DeviceFlow, GOOGLE_DEVICE_CODE_URL}; pub use crate::helper::*; diff --git a/src/refresh.rs b/src/refresh.rs index 412c96f..ed8f28c 100644 --- a/src/refresh.rs +++ b/src/refresh.rs @@ -1,4 +1,4 @@ -use crate::types::{ApplicationSecret, FlowType, JsonError}; +use crate::types::{ApplicationSecret, JsonError}; use std::error::Error; @@ -19,9 +19,8 @@ use url::form_urlencoded; pub struct RefreshFlow; /// All possible outcomes of the refresh flow +#[derive(Debug)] pub enum RefreshResult { - // Indicates no attempt has been made to refresh yet - Uninitialized, /// Indicates connection failure Error(hyper::Error), /// The server did not answer with a new token, providing the server message @@ -47,8 +46,8 @@ impl RefreshFlow { /// Please see the crate landing page for an example. pub fn refresh_token<'a, C: 'static + hyper::client::connect::Connect>( client: hyper::Client, - client_secret: &'a ApplicationSecret, - refresh_token: &'a str, + client_secret: ApplicationSecret, + refresh_token: String, ) -> impl 'a + Future> { let req = form_urlencoded::Serializer::new(String::new()) .extend_pairs(&[ @@ -68,7 +67,7 @@ impl RefreshFlow { .request(request) .then(|r| { match r { - Err(err) => return Err(Box::new(err) as Box), + Err(err) => return Err(RefreshResult::Error(err)), Ok(res) => { Ok(res .into_body() @@ -79,7 +78,11 @@ impl RefreshFlow { } } }) - .and_then(|json_str: String| { + .then(move |maybe_json_str: Result| { + if let Err(e) = maybe_json_str { + return Ok(e); + } + let json_str = maybe_json_str.unwrap(); #[derive(Deserialize)] struct JsonToken { access_token: String, @@ -108,56 +111,3 @@ impl RefreshFlow { }) } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::device::GOOGLE_DEVICE_CODE_URL; - use crate::helper::parse_application_secret; - - mock_connector!(MockGoogleRefresh { - "https://accounts.google.com" => - "HTTP/1.1 200 OK\r\n\ - Server: BOGUS\r\n\ - \r\n\ - {\r\n\ - \"access_token\":\"1/fFAGRNJru1FTz70BzhT3Zg\",\r\n\ - \"expires_in\":3920,\r\n\ - \"token_type\":\"Bearer\"\r\n\ - }" - }); - - const TEST_APP_SECRET: &'static str = r#"{"installed":{"client_id":"384278056379-tr5pbot1mil66749n639jo54i4840u77.apps.googleusercontent.com","project_id":"sanguine-rhythm-105020","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://accounts.google.com/o/oauth2/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"QeQUnhzsiO4t--ZGmj9muUAu","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}}"#; - - #[test] - fn refresh_flow() { - let appsecret = parse_application_secret(TEST_APP_SECRET).unwrap(); - - let runtime = tokio::runtime::Runtime::new().unwrap(); - let client = hyper::Client::builder() - .executor(runtime.executor()) - .build(MockGoogleRefresh::default()); - let mut flow = RefreshFlow::new(client); - let device_flow = FlowType::Device(GOOGLE_DEVICE_CODE_URL.to_string()); - - match flow.refresh_token(device_flow, &appsecret, "bogus_refresh_token") { - RefreshResult::Success(ref t) => { - assert_eq!(t.access_token, "1/fFAGRNJru1FTz70BzhT3Zg"); - assert!(!t.expired()); - } - RefreshResult::Error(err) => { - assert!(false, "Refresh flow failed: RefreshResult::Error({})", err); - } - RefreshResult::RefreshError(msg, err) => { - assert!( - false, - "Refresh flow failed: RefreshResult::RefreshError({}, {:?})", - msg, err - ); - } - RefreshResult::Uninitialized => { - assert!(false, "Refresh flow failed: RefreshResult::Uninitialized"); - } - } - } -} From bdb0bd92e7bcbecd0b4ce96467a7286f507526e2 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Thu, 13 Jun 2019 15:32:48 +0200 Subject: [PATCH 20/41] fix(examples): Update examples to use Authenticator. --- examples/test-device/src/main.rs | 17 ++++++++++++----- examples/test-installed/src/main.rs | 17 ++++++++++++----- examples/test-svc-acct/src/main.rs | 9 ++++++++- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/examples/test-device/src/main.rs b/examples/test-device/src/main.rs index d5f8b07..f24068e 100644 --- a/examples/test-device/src/main.rs +++ b/examples/test-device/src/main.rs @@ -1,5 +1,5 @@ use futures::prelude::*; -use yup_oauth2::{self, GetToken}; +use yup_oauth2::{self, Authenticator, GetToken}; use hyper::client::Client; use hyper_tls::HttpsConnector; @@ -15,12 +15,19 @@ fn main() { let scopes = &["https://www.googleapis.com/auth/youtube.readonly".to_string()]; - let ad = yup_oauth2::DefaultAuthenticatorDelegate; - let mut df = yup_oauth2::DeviceFlow::new::(client, creds, ad, None); + let ad = yup_oauth2::DefaultFlowDelegate; + let mut df = yup_oauth2::DeviceFlow::new::(client.clone(), creds, ad, None); df.set_wait_duration(Duration::from_secs(120)); - let mut rt = tokio::runtime::Runtime::new().unwrap(); + let mut auth = Authenticator::new_disk( + client, + df, + yup_oauth2::DefaultAuthenticatorDelegate, + "tokenstorage.json", + ) + .expect("authenticator"); - let fut = df + let mut rt = tokio::runtime::Runtime::new().unwrap(); + let fut = auth .token(scopes.iter()) .and_then(|tok| Ok(println!("{:?}", tok))); diff --git a/examples/test-installed/src/main.rs b/examples/test-installed/src/main.rs index 47a8215..18a2440 100644 --- a/examples/test-installed/src/main.rs +++ b/examples/test-installed/src/main.rs @@ -1,6 +1,6 @@ use futures::prelude::*; use yup_oauth2::GetToken; -use yup_oauth2::InstalledFlow; +use yup_oauth2::{Authenticator, InstalledFlow}; use hyper::client::Client; use hyper_tls::HttpsConnector; @@ -10,19 +10,26 @@ use std::path::Path; fn main() { let https = HttpsConnector::new(1).expect("tls"); let client = Client::builder().build::<_, hyper::Body>(https); - let ad = yup_oauth2::DefaultAuthenticatorDelegate; + let ad = yup_oauth2::DefaultFlowDelegate; let secret = yup_oauth2::read_application_secret(Path::new("clientsecret.json")) .expect("clientsecret.json"); - let mut inf = InstalledFlow::new( - client, + let inf = InstalledFlow::new( + client.clone(), ad, secret, yup_oauth2::InstalledFlowReturnMethod::HTTPRedirect(8081), ); + let mut auth = Authenticator::new_disk( + client, + inf, + yup_oauth2::DefaultAuthenticatorDelegate, + "tokencache.json", + ) + .unwrap(); let s = "https://www.googleapis.com/auth/drive.file".to_string(); let scopes = vec![s]; - let tok = inf.token(scopes.iter()); + let tok = auth.token(scopes.iter()); let fut = tok.map_err(|e| println!("error: {:?}", e)).and_then(|t| { println!("The token is {:?}", t); Ok(()) diff --git a/examples/test-svc-acct/src/main.rs b/examples/test-svc-acct/src/main.rs index 88a1eda..b3fd150 100644 --- a/examples/test-svc-acct/src/main.rs +++ b/examples/test-svc-acct/src/main.rs @@ -23,6 +23,13 @@ fn main() { println!("token is: {:?}", tok); Ok(()) }); + let fut2 = sa + .token(["https://www.googleapis.com/auth/pubsub"].iter()) + .and_then(|tok| { + println!("cached token is {:?} and should be identical", tok); + Ok(()) + }); + let all = fut.join(fut2); let mut rt = tokio::runtime::Runtime::new().unwrap(); - rt.block_on(fut).unwrap() + rt.block_on(all).unwrap(); } From 4cfbc6e5fcb0d794bdd16d153d506c836659f6d1 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Thu, 13 Jun 2019 16:07:32 +0200 Subject: [PATCH 21/41] imp(Device): Honor FlowDelegate's opinion on pending authorization. --- src/authenticator_delegate.rs | 18 ++++++------- src/device.rs | 50 ++++++++++++++++++++++++++++------- 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/src/authenticator_delegate.rs b/src/authenticator_delegate.rs index b5723ac..b993758 100644 --- a/src/authenticator_delegate.rs +++ b/src/authenticator_delegate.rs @@ -110,15 +110,6 @@ pub trait AuthenticatorDelegate: Clone { /// The server denied the attempt to obtain a request code fn request_failure(&mut self, _: RequestError) {} - /// Called if the request code is expired. You will have to start over in this case. - /// This will be the last call the delegate receives. - /// Given `DateTime` is the expiration date - fn expired(&mut self, _: &DateTime) {} - - /// Called if the user denied access. You would have to start over. - /// This will be the last call the delegate receives. - fn denied(&mut self) {} - /// Called if we could not acquire a refresh token for a reason possibly specified /// by the server. /// This call is made for the delegate's information only. @@ -137,6 +128,15 @@ pub trait AuthenticatorDelegate: Clone { } pub trait FlowDelegate: Clone { + /// Called if the request code is expired. You will have to start over in this case. + /// This will be the last call the delegate receives. + /// Given `DateTime` is the expiration date + fn expired(&mut self, _: &DateTime) {} + + /// Called if the user denied access. You would have to start over. + /// This will be the last call the delegate receives. + fn denied(&mut self) {} + /// Called as long as we are waiting for the user to authorize us. /// Can be used to print progress information, or decide to time-out. /// diff --git a/src/device.rs b/src/device.rs index b58a68a..3182ef8 100644 --- a/src/device.rs +++ b/src/device.rs @@ -13,8 +13,10 @@ use serde_json as json; use tokio_timer; use url::form_urlencoded; -use crate::authenticator_delegate::{FlowDelegate, PollError, PollInformation}; -use crate::types::{ApplicationSecret, Flow, FlowType, GetToken, JsonError, RequestError, Token}; +use crate::authenticator_delegate::{FlowDelegate, PollError, PollInformation, Retry}; +use crate::types::{ + ApplicationSecret, Flow, FlowType, GetToken, JsonError, RequestError, StringError, Token, +}; pub const GOOGLE_DEVICE_CODE_URL: &'static str = "https://accounts.google.com/o/oauth2/device/code"; @@ -81,7 +83,7 @@ where .map(|s| s.as_ref().to_string()) .unwrap_or(GOOGLE_DEVICE_CODE_URL.to_string()), fd: fd, - wait: Duration::from_secs(120), + wait: Duration::from_secs(1200), } } @@ -96,10 +98,10 @@ where &mut self, scopes: Vec, ) -> Box> + Send> { - let mut fd = self.fd.clone(); let application_secret = self.application_secret.clone(); let client = self.client.clone(); let wait = self.wait; + let mut fd = self.fd.clone(); let request_code = Self::request_code( application_secret.clone(), client.clone(), @@ -110,6 +112,7 @@ where fd.present_user_code(&pollinf); Ok((pollinf, device_code)) }); + let fd = self.fd.clone(); Box::new(request_code.and_then(move |(pollinf, device_code)| { future::loop_fn(0, move |i| { // Make a copy of everything every time, because the loop function needs to be @@ -119,15 +122,41 @@ where client.clone(), device_code.clone(), pollinf.clone(), + fd.clone(), ); let maxn = wait.as_secs() / pollinf.interval.as_secs(); + let mut fd = fd.clone(); + let pollinf = pollinf.clone(); tokio_timer::sleep(pollinf.interval) .then(|_| pt) .then(move |r| match r { - Ok(None) if i < maxn => Ok(future::Loop::Continue(i + 1)), - Ok(Some(tok)) => Ok(future::Loop::Break(tok)), - Err(_) if i < maxn => Ok(future::Loop::Continue(i + 1)), - _ => Err(Box::new(PollError::TimedOut) as Box), + Ok(None) if i < maxn => match fd.pending(&pollinf) { + Retry::Abort | Retry::Skip => Box::new( + Err(Box::new(StringError::new( + "Pending authentication aborted".to_string(), + None, + )) as Box) + .into_future(), + ), + Retry::After(d) => Box::new( + tokio_timer::sleep(d) + .then(move |_| Ok(future::Loop::Continue(i + 1))), + ) + as Box< + dyn Future< + Item = future::Loop, + Error = Box, + > + Send, + >, + }, + Ok(Some(tok)) => Box::new(Ok(future::Loop::Break(tok)).into_future()), + Err(_) if i < maxn => { + Box::new(Ok(future::Loop::Continue(i + 1)).into_future()) + } + _ => Box::new( + Err(Box::new(PollError::TimedOut) as Box) + .into_future(), + ), }) }) })) @@ -256,8 +285,10 @@ where client: hyper::Client, device_code: String, pi: PollInformation, + mut fd: FD, ) -> impl Future, Error = Box> { let expired = if pi.expires_at <= Utc::now() { + fd.expired(&pi.expires_at); Err(PollError::Expired(pi.expires_at)).into_future() } else { Ok(()).into_future() @@ -291,7 +322,7 @@ where .map(|c| String::from_utf8(c.into_bytes().to_vec()).unwrap()) .unwrap() // TODO: error handling }) - .and_then(|json_str: String| { + .and_then(move |json_str: String| { #[derive(Deserialize)] struct JsonError { error: String, @@ -302,6 +333,7 @@ where Ok(res) => { match res.error.as_ref() { "access_denied" => { + fd.denied(); return Err( Box::new(PollError::AccessDenied) as Box ); From 6b05056b057e8ecf7da7ba4d6538f9ea0f9ba9b0 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Thu, 13 Jun 2019 15:51:04 +0200 Subject: [PATCH 22/41] imp(expiry): Treat tokens with < 1 minute life left as expired. Fixes #78. --- src/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.rs b/src/types.rs index 3ba1586..54606ad 100644 --- a/src/types.rs +++ b/src/types.rs @@ -248,7 +248,7 @@ impl Token { if self.access_token.len() == 0 { panic!("called expired() on unset token"); } - self.expiry_date() <= Utc::now() + self.expiry_date() - chrono::Duration::minutes(1) <= Utc::now() } /// Returns a DateTime object representing our expiry date. From f034b8bea470640b47595af62905f8bccd88bb9d Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Thu, 13 Jun 2019 16:16:08 +0200 Subject: [PATCH 23/41] imp(ServiceAccountAccess): Print exact error if server returns one. Prevents #76. --- src/service_account.rs | 12 ++++++++++-- src/types.rs | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/service_account.rs b/src/service_account.rs index eeb78e0..71590c1 100644 --- a/src/service_account.rs +++ b/src/service_account.rs @@ -16,7 +16,7 @@ use std::error; use std::sync::{Arc, Mutex}; use crate::storage::{hash_scopes, MemoryStorage, TokenStorage}; -use crate::types::{ApplicationSecret, GetToken, StringError, Token}; +use crate::types::{ApplicationSecret, GetToken, JsonError, StringError, Token}; use futures::stream::Stream; use futures::{future, prelude::*}; @@ -283,7 +283,15 @@ impl<'a, C: 'static + hyper::client::connect::Connect> ServiceAccountAccess { }) .map(|c| String::from_utf8(c.into_bytes().to_vec()).unwrap()) .and_then(|s| { - serde_json::from_str(&s).map_err(|e| Box::new(e) as Box) + if let Ok(jse) = serde_json::from_str::(&s) { + Err( + Box::new(StringError::new(jse.error, jse.error_description.as_ref())) + as Box, + ) + } else { + serde_json::from_str(&s) + .map_err(|e| Box::new(e) as Box) + } }) .then( |token: Result>| match token { diff --git a/src/types.rs b/src/types.rs index 54606ad..0feb929 100644 --- a/src/types.rs +++ b/src/types.rs @@ -11,7 +11,7 @@ pub trait Flow { fn type_id() -> FlowType; } -#[derive(Deserialize)] +#[derive(Deserialize, Debug)] pub struct JsonError { pub error: String, pub error_description: Option, From 0eb1268567c173fc7b4e2398357a024edd3295bf Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Thu, 13 Jun 2019 18:41:09 +0200 Subject: [PATCH 24/41] doc(tokio): Set keep_alive to false on hyper clients. This prevents hanging event loops. --- examples/test-device/src/main.rs | 5 +++-- examples/test-installed/src/main.rs | 7 ++++--- examples/test-svc-acct/src/main.rs | 9 +++++---- src/authenticator.rs | 4 ++++ 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/examples/test-device/src/main.rs b/examples/test-device/src/main.rs index f24068e..c24a032 100644 --- a/examples/test-device/src/main.rs +++ b/examples/test-device/src/main.rs @@ -11,8 +11,9 @@ fn main() { let creds = yup_oauth2::read_application_secret(path::Path::new("clientsecret.json")) .expect("clientsecret"); let https = HttpsConnector::new(1).expect("tls"); - let client = Client::builder().build::<_, hyper::Body>(https); - + let client = Client::builder() + .keep_alive(false) + .build::<_, hyper::Body>(https); let scopes = &["https://www.googleapis.com/auth/youtube.readonly".to_string()]; let ad = yup_oauth2::DefaultFlowDelegate; diff --git a/examples/test-installed/src/main.rs b/examples/test-installed/src/main.rs index 18a2440..d11df4e 100644 --- a/examples/test-installed/src/main.rs +++ b/examples/test-installed/src/main.rs @@ -9,7 +9,9 @@ use std::path::Path; fn main() { let https = HttpsConnector::new(1).expect("tls"); - let client = Client::builder().build::<_, hyper::Body>(https); + let client = Client::builder() + .keep_alive(false) + .build::<_, hyper::Body>(https); let ad = yup_oauth2::DefaultFlowDelegate; let secret = yup_oauth2::read_application_secret(Path::new("clientsecret.json")) .expect("clientsecret.json"); @@ -35,6 +37,5 @@ fn main() { Ok(()) }); - let mut rt = tokio::runtime::Runtime::new().unwrap(); - rt.block_on(fut).unwrap(); + tokio::run(fut) } diff --git a/examples/test-svc-acct/src/main.rs b/examples/test-svc-acct/src/main.rs index b3fd150..f9a9ee0 100644 --- a/examples/test-svc-acct/src/main.rs +++ b/examples/test-svc-acct/src/main.rs @@ -13,7 +13,9 @@ fn main() { let creds = yup_oauth2::service_account_key_from_file(path::Path::new("serviceaccount.json")).unwrap(); let https = HttpsConnector::new(1).expect("tls"); - let client = Client::builder().build::<_, hyper::Body>(https); + let client = Client::builder() + .keep_alive(false) + .build::<_, hyper::Body>(https); let mut sa = yup_oauth2::ServiceAccountAccess::new(creds, client); @@ -29,7 +31,6 @@ fn main() { println!("cached token is {:?} and should be identical", tok); Ok(()) }); - let all = fut.join(fut2); - let mut rt = tokio::runtime::Runtime::new().unwrap(); - rt.block_on(all).unwrap(); + let all = fut.join(fut2).then(|_| Ok(())); + tokio::run(all) } diff --git a/src/authenticator.rs b/src/authenticator.rs index 557cdf3..cc166a8 100644 --- a/src/authenticator.rs +++ b/src/authenticator.rs @@ -16,6 +16,10 @@ use std::sync::{Arc, Mutex}; /// /// `ServiceAccountAccess` does not need (and does not work) with `Authenticator`, given that it /// does not require interaction and implements its own caching. Use it directly. +/// +/// NOTE: It is recommended to use a client constructed like this in order to prevent functions +/// like `hyper::run()` from hanging: `let client = hyper::Client::builder().keep_alive(false);`. +/// Due to token requests being rare, this should not result in a too bad performance problem. pub struct Authenticator< T: GetToken, S: TokenStorage, From 5a568f23587f8ad950cacaf2f5138507793d57b3 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Thu, 13 Jun 2019 18:52:13 +0200 Subject: [PATCH 25/41] fix(refresh): Write refreshed tokens back to cache. Tested manually. --- src/authenticator.rs | 57 +++++++++++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/src/authenticator.rs b/src/authenticator.rs index cc166a8..38dd91e 100644 --- a/src/authenticator.rs +++ b/src/authenticator.rs @@ -114,28 +114,51 @@ impl< // Implement refresh flow. let refresh_token = t.refresh_token.clone(); let mut delegate = delegate.clone(); + let store = store.clone(); + let scopes = scopes.clone(); let refresh_fut = RefreshFlow::refresh_token( client.clone(), appsecret.clone(), refresh_token, ) - .and_then(move |rr| match rr { - RefreshResult::Error(e) => { - delegate.token_refresh_failed( - format!("{}", e.description().to_string()), - &Some("the request has likely timed out".to_string()), - ); - Err(Box::new(e) as Box) - } - RefreshResult::RefreshError(ref s, ref ss) => { - delegate.token_refresh_failed( - format!("{} {}", s, ss.clone().map(|s| format!("({})", s)).unwrap_or("".to_string())), - &Some("the refresh token is likely invalid and your authorization has been revoked".to_string()), - ); - Err(Box::new(StringError::new(s.to_string(), ss.as_ref())) as Box) - } - RefreshResult::Success(t) => Ok(future::Loop::Break(t)), - }); + .and_then(move |rr| -> Box, Error=Box> + Send> { + match rr { + RefreshResult::Error(e) => { + delegate.token_refresh_failed( + format!("{}", e.description().to_string()), + &Some("the request has likely timed out".to_string()), + ); + Box::new(Err(Box::new(e) as Box).into_future()) + } + RefreshResult::RefreshError(ref s, ref ss) => { + delegate.token_refresh_failed( + format!("{} {}", s, ss.clone().map(|s| format!("({})", s)).unwrap_or("".to_string())), + &Some("the refresh token is likely invalid and your authorization has been revoked".to_string()), + ); + Box::new(Err(Box::new(StringError::new(s.to_string(), ss.as_ref())) as Box).into_future()) + } + RefreshResult::Success(t) => { + if let Err(e) = store.lock().unwrap().set(scope_key, &scopes.iter().map(|s| s.as_str()).collect(), Some(t.clone())) { + match delegate.token_storage_failure(true, &e) { + Retry::Skip => Box::new(Ok(future::Loop::Break(t)).into_future()), + Retry::Abort => Box::new(Err(Box::new(e) as Box).into_future()), + Retry::After(d) => Box::new( + tokio_timer::sleep(d) + .then(|_| Ok(future::Loop::Continue(()))), + ) + as Box< + dyn Future< + Item = future::Loop, + Error = Box, + > + Send, + >, + } + } else { + Box::new(Ok(future::Loop::Break(t)).into_future()) + } + }, + } + }); Box::new(refresh_fut) } Ok(None) => { From d3f1f87760cdc3d15b25408eb1759616ef28b224 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Thu, 13 Jun 2019 21:46:40 +0200 Subject: [PATCH 26/41] chore(version): Mark 3.0.0 as -alpha for publishing. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4d8d5ed..a30b7c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "yup-oauth2" -version = "3.0.0" +version = "3.0.0-alpha" authors = ["Sebastian Thiel ", "Lewin Bormann "] repository = "https://github.com/dermesser/yup-oauth2" description = "An oauth2 implementation, providing the 'device', 'service account' and 'installed' authorization flows" From 9e4a7e6d49e605aa5f3bb6e94a50be81628e19b9 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Fri, 14 Jun 2019 10:44:44 +0200 Subject: [PATCH 27/41] refactor(StringError): Take more comfortable types in StringError::new --- src/authenticator.rs | 2 +- src/service_account.rs | 4 ++-- src/types.rs | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/authenticator.rs b/src/authenticator.rs index 38dd91e..d3e1008 100644 --- a/src/authenticator.rs +++ b/src/authenticator.rs @@ -135,7 +135,7 @@ impl< format!("{} {}", s, ss.clone().map(|s| format!("({})", s)).unwrap_or("".to_string())), &Some("the refresh token is likely invalid and your authorization has been revoked".to_string()), ); - Box::new(Err(Box::new(StringError::new(s.to_string(), ss.as_ref())) as Box).into_future()) + Box::new(Err(Box::new(StringError::new(s, ss.as_ref())) as Box).into_future()) } RefreshResult::Success(t) => { if let Err(e) = store.lock().unwrap().set(scope_key, &scopes.iter().map(|s| s.as_str()).collect(), Some(t.clone())) { diff --git a/src/service_account.rs b/src/service_account.rs index 71590c1..b685a05 100644 --- a/src/service_account.rs +++ b/src/service_account.rs @@ -285,7 +285,7 @@ impl<'a, C: 'static + hyper::client::connect::Connect> ServiceAccountAccess { .and_then(|s| { if let Ok(jse) = serde_json::from_str::(&s) { Err( - Box::new(StringError::new(jse.error, jse.error_description.as_ref())) + Box::new(StringError::new(&jse.error, jse.error_description.as_ref())) as Box, ) } else { @@ -303,7 +303,7 @@ impl<'a, C: 'static + hyper::client::connect::Connect> ServiceAccountAccess { { Err(Box::new(StringError::new( "Token response lacks fields".to_string(), - Some(&format!("{:?}", token)), + Some(format!("{:?}", token)), )) as Box) } else { diff --git a/src/types.rs b/src/types.rs index 0feb929..4c670a0 100644 --- a/src/types.rs +++ b/src/types.rs @@ -101,11 +101,11 @@ impl fmt::Display for StringError { } impl StringError { - pub fn new(error: String, desc: Option<&String>) -> StringError { - let mut error = error; + pub fn new>(error: S, desc: Option) -> StringError { + let mut error = error.as_ref().to_string(); if let Some(d) = desc { error.push_str(": "); - error.push_str(&*d); + error.push_str(d.as_ref()); } StringError { error: error } From 534d5edc1236a90f29dbc4f1d7762aa8b551d258 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Wed, 19 Jun 2019 18:22:03 +0200 Subject: [PATCH 28/41] refactor(google): Make some things less google-specific. --- src/installed.rs | 10 +++++----- src/service_account.rs | 5 +++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/installed.rs b/src/installed.rs index 9190edd..51bef21 100644 --- a/src/installed.rs +++ b/src/installed.rs @@ -154,8 +154,8 @@ impl<'c, FD: 'static + FlowDelegate + Clone + Send, C: 'c + hyper::client::conne .and_then(move |server| { Self::ask_authorization_code(server, auth_delegate, &appsecclone, scopes.iter()) }) - // Exchange the authorization code provided by Google for a refresh and an access - // token. + // Exchange the authorization code provided by Google/the provider for a refresh and an + // access token. .and_then(move |authcode| { let request = Self::request_token(appsecclone2, authcode, rduri, port); let result = client.request(request); @@ -255,8 +255,8 @@ impl<'c, FD: 'static + FlowDelegate + Clone + Send, C: 'c + hyper::client::conne ) } else { let mut server = server.unwrap(); - // The redirect URI must be this very localhost URL, otherwise Google refuses - // authorization. + // The redirect URI must be this very localhost URL, otherwise authorization is refused + // by certain providers. let url = build_authentication_request_url( &appsecret.auth_uri, &appsecret.client_id, @@ -497,7 +497,7 @@ impl hyper::service::Service for InstalledFlowService { impl InstalledFlowService { fn handle_url(&mut self, url: hyper::Uri) { - // Google redirects to the specified localhost URL, appending the authorization + // The provider redirects to the specified localhost URL, appending the authorization // code, like this: http://localhost:8080/xyz/?code=4/731fJ3BheyCouCniPufAd280GHNV5Ju35yYcGs // We take that code and send it to the ask_authorization_code() function that // waits for it. diff --git a/src/service_account.rs b/src/service_account.rs index b685a05..20e3cc2 100644 --- a/src/service_account.rs +++ b/src/service_account.rs @@ -116,6 +116,11 @@ impl JWT { } } + /// Set JWT header. Default is `{"alg":"RS256","typ":"JWT"}`. + pub fn set_header(&mut self, head: String) { + self.header = head; + } + /// Encodes the first two parts (header and claims) to base64 and assembles them into a form /// ready to be signed. fn encode_claims(&self) -> String { From 5e76c2258f11dba7d27095d1c176d0dce4b032eb Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Wed, 19 Jun 2019 18:24:27 +0200 Subject: [PATCH 29/41] docs(README): Update README about provider specificity. --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 69ae42e..0751216 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,17 @@ Status](https://travis-ci.org/dermesser/yup-oauth2.svg)](https://travis-ci.org/d (However, you're able to use it with raw HTTP requests as well; the flows are implemented as token sources yielding HTTP Bearer tokens). +The provider we have been testing the code against is also Google. However, the code itself is +generic, and any OAuth provider behaving like Google will work as well. If you find one that +doesn't, please let us know and/or contribute a fix! + ### Supported authorization types * Device flow (user enters code on authorization page) * Installed application flow (user visits URL, copies code to application, application uses code to obtain token). Used for services like GMail, Drive, ... -* Service account flow: Non-interactive for server-to-server communication based on public key - cryptography. Used for services like Cloud Pubsub, Cloud Storage, ... +* Service account flow: Non-interactive authorization of server-to-server communication based on + public key cryptography. Used for services like Cloud Pubsub, Cloud Storage, ... ### Usage From c2b41c3da2416ebda54f8a9815625970799c9ee9 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Fri, 21 Jun 2019 11:12:59 +0200 Subject: [PATCH 30/41] fix(test-svc-acct): Non-lazy cache lookup resulted in two requests. --- examples/test-svc-acct/src/main.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/examples/test-svc-acct/src/main.rs b/examples/test-svc-acct/src/main.rs index f9a9ee0..cf6b066 100644 --- a/examples/test-svc-acct/src/main.rs +++ b/examples/test-svc-acct/src/main.rs @@ -25,12 +25,15 @@ fn main() { println!("token is: {:?}", tok); Ok(()) }); - let fut2 = sa - .token(["https://www.googleapis.com/auth/pubsub"].iter()) - .and_then(|tok| { - println!("cached token is {:?} and should be identical", tok); - Ok(()) - }); - let all = fut.join(fut2).then(|_| Ok(())); + let mut sa2 = sa.clone(); + let all = fut + .then(move |_| { + sa2.token(["https://www.googleapis.com/auth/pubsub"].iter()) + .and_then(|tok| { + println!("cached token is {:?} and should be identical", tok); + Ok(()) + }) + }) + .then(|_| Ok(())); tokio::run(all) } From e0f32989049c0c9744815d17a23d30f0e220c754 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Fri, 21 Jun 2019 11:22:24 +0200 Subject: [PATCH 31/41] test(ServiceAccount): Add test with internal mockito web server. --- Cargo.toml | 1 + src/lib.rs | 4 --- src/service_account.rs | 56 ++++++++++++++++++++++++++++++++++++++++-- src/storage.rs | 2 +- 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a30b7c6..bd3ebf0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ tokio-timer = "0.2" getopts = "0.2" open = "1.1" yup-hyper-mock = "3.14" +mockito = "0.17" [workspace] members = ["examples/test-installed/", "examples/test-svc-acct/", "examples/test-device/"] diff --git a/src/lib.rs b/src/lib.rs index bd4aa87..d0fd0a1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,10 +41,6 @@ #[macro_use] extern crate serde_derive; -#[cfg(test)] -#[macro_use] -extern crate yup_hyper_mock as hyper_mock; - mod authenticator; mod authenticator_delegate; mod device; diff --git a/src/service_account.rs b/src/service_account.rs index 20e3cc2..cabf075 100644 --- a/src/service_account.rs +++ b/src/service_account.rs @@ -189,6 +189,7 @@ where /// A token source (`GetToken`) yielding OAuth tokens for services that use ServiceAccount authorization. /// This token source caches token and automatically renews expired ones. +#[derive(Clone)] pub struct ServiceAccountAccess { client: hyper::Client, key: ServiceAccountKey, @@ -248,7 +249,7 @@ impl<'a, C: 'static + hyper::client::connect::Connect> ServiceAccountAccess { } } - /// + /// Send a request for a new Bearer token to the OAuth provider. fn request_token( client: hyper::client::Client, sub: Option, @@ -387,10 +388,61 @@ mod tests { use super::*; use crate::helper::service_account_key_from_file; use crate::types::GetToken; + use hyper; use hyper_tls::HttpsConnector; + use mockito::{self, mock}; + use tokio; - // This is a valid but deactivated key. + #[test] + fn test_mocked_http() { + let server_url = &mockito::server_url(); + let client_secret = r#"{ + "type": "service_account", + "project_id": "yup-test-243420", + "private_key_id": "26de294916614a5ebdf7a065307ed3ea9941902b", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDemmylrvp1KcOn\n9yTAVVKPpnpYznvBvcAU8Qjwr2fSKylpn7FQI54wCk5VJVom0jHpAmhxDmNiP8yv\nHaqsef+87Oc0n1yZ71/IbeRcHZc2OBB33/LCFqf272kThyJo3qspEqhuAw0e8neg\nLQb4jpm9PsqR8IjOoAtXQSu3j0zkXemMYFy93PWHjVpPEUX16NGfsWH7oxspBHOk\n9JPGJL8VJdbiAoDSDgF0y9RjJY5I52UeHNhMsAkTYs6mIG4kKXt2+T9tAyHw8aho\nwmuytQAfydTflTfTG8abRtliF3nil2taAc5VB07dP1b4dVYy/9r6M8Z0z4XM7aP+\nNdn2TKm3AgMBAAECggEAWi54nqTlXcr2M5l535uRb5Xz0f+Q/pv3ceR2iT+ekXQf\n+mUSShOr9e1u76rKu5iDVNE/a7H3DGopa7ZamzZvp2PYhSacttZV2RbAIZtxU6th\n7JajPAM+t9klGh6wj4jKEcE30B3XVnbHhPJI9TCcUyFZoscuPXt0LLy/z8Uz0v4B\nd5JARwyxDMb53VXwukQ8nNY2jP7WtUig6zwE5lWBPFMbi8GwGkeGZOruAK5sPPwY\nGBAlfofKANI7xKx9UXhRwisB4+/XI1L0Q6xJySv9P+IAhDUI6z6kxR+WkyT/YpG3\nX9gSZJc7qEaxTIuDjtep9GTaoEqiGntjaFBRKoe+VQKBgQDzM1+Ii+REQqrGlUJo\nx7KiVNAIY/zggu866VyziU6h5wjpsoW+2Npv6Dv7nWvsvFodrwe50Y3IzKtquIal\nVd8aa50E72JNImtK/o5Nx6xK0VySjHX6cyKENxHRDnBmNfbALRM+vbD9zMD0lz2q\nmns/RwRGq3/98EqxP+nHgHSr9QKBgQDqUYsFAAfvfT4I75Glc9svRv8IsaemOm07\nW1LCwPnj1MWOhsTxpNF23YmCBupZGZPSBFQobgmHVjQ3AIo6I2ioV6A+G2Xq/JCF\nmzfbvZfqtbbd+nVgF9Jr1Ic5T4thQhAvDHGUN77BpjEqZCQLAnUWJx9x7e2xvuBl\n1A6XDwH/ewKBgQDv4hVyNyIR3nxaYjFd7tQZYHTOQenVffEAd9wzTtVbxuo4sRlR\nNM7JIRXBSvaATQzKSLHjLHqgvJi8LITLIlds1QbNLl4U3UVddJbiy3f7WGTqPFfG\nkLhUF4mgXpCpkMLxrcRU14Bz5vnQiDmQRM4ajS7/kfwue00BZpxuZxst3QKBgQCI\nRI3FhaQXyc0m4zPfdYYVc4NjqfVmfXoC1/REYHey4I1XetbT9Nb/+ow6ew0UbgSC\nUZQjwwJ1m1NYXU8FyovVwsfk9ogJ5YGiwYb1msfbbnv/keVq0c/Ed9+AG9th30qM\nIf93hAfClITpMz2mzXIMRQpLdmQSR4A2l+E4RjkSOwKBgQCB78AyIdIHSkDAnCxz\nupJjhxEhtQ88uoADxRoEga7H/2OFmmPsqfytU4+TWIdal4K+nBCBWRvAX1cU47vH\nJOlSOZI0gRKe0O4bRBQc8GXJn/ubhYSxI02IgkdGrIKpOb5GG10m85ZvqsXw3bKn\nRVHMD0ObF5iORjZUqD0yRitAdg==\n-----END PRIVATE KEY-----\n", + "client_email": "yup-test-sa-1@yup-test-243420.iam.gserviceaccount.com", + "client_id": "102851967901799660408", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/yup-test-sa-1%40yup-test-243420.iam.gserviceaccount.com" +}"#; + let mut key: ServiceAccountKey = serde_json::from_str(client_secret).unwrap(); + key.token_uri = Some(format!("{}/token", server_url)); + + let json_response = r#"{ + "access_token": "ya29.c.ElouBywiys0LyNaZoLPJcp1Fdi2KjFMxzvYKLXkTdvM-rDfqKlvEq6PiMhGoGHx97t5FAvz3eb_ahdwlBjSStxHtDVQB4ZPRJQ_EOi-iS7PnayahU2S9Jp8S6rk", + "expires_in": 3600, + "token_type": "Bearer" +}"#; + let _m = mock("POST", "/token") + .with_status(200) + .with_header("content-type", "text/json") + .with_body(json_response) + .create(); + let https = HttpsConnector::new(1).unwrap(); + let client = hyper::Client::builder() + .keep_alive(false) + .build::<_, hyper::Body>(https); + let mut acc = ServiceAccountAccess::new(key, client); + let fut = acc + .token(vec!["https://www.googleapis.com/auth/pubsub"].iter()) + .then(|tok| { + assert!(tok + .as_ref() + .unwrap() + .access_token + .contains("ya29.c.ElouBywiys0Ly")); + assert_eq!(Some(3600), tok.unwrap().expires_in); + Ok(()) + }); + tokio::run(fut); + _m.assert(); + } + + // Valid but deactivated key. const TEST_PRIVATE_KEY_PATH: &'static str = "examples/Sanguine-69411a0c0eea.json"; // Uncomment this test to verify that we can successfully obtain tokens. diff --git a/src/storage.rs b/src/storage.rs index 427c92c..30eabe9 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -80,7 +80,7 @@ impl TokenStorage for NullStorage { } /// A storage that remembers values for one session only. -#[derive(Default)] +#[derive(Debug, Default)] pub struct MemoryStorage { pub tokens: HashMap, } From c321f6d2e61ff1597e8e56ec8bc6a75e0583c2e0 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Fri, 21 Jun 2019 18:39:51 +0200 Subject: [PATCH 32/41] fix(ServiceAccount): Make cache behavior more intuitive. Now the cache is only checked for a token when the future is polled, not at future creation time. This also allows for reverting c2b41c3. --- examples/test-svc-acct/src/main.rs | 17 +++---- src/service_account.rs | 75 ++++++++++++++++++------------ 2 files changed, 51 insertions(+), 41 deletions(-) diff --git a/examples/test-svc-acct/src/main.rs b/examples/test-svc-acct/src/main.rs index cf6b066..f9a9ee0 100644 --- a/examples/test-svc-acct/src/main.rs +++ b/examples/test-svc-acct/src/main.rs @@ -25,15 +25,12 @@ fn main() { println!("token is: {:?}", tok); Ok(()) }); - let mut sa2 = sa.clone(); - let all = fut - .then(move |_| { - sa2.token(["https://www.googleapis.com/auth/pubsub"].iter()) - .and_then(|tok| { - println!("cached token is {:?} and should be identical", tok); - Ok(()) - }) - }) - .then(|_| Ok(())); + let fut2 = sa + .token(["https://www.googleapis.com/auth/pubsub"].iter()) + .and_then(|tok| { + println!("cached token is {:?} and should be identical", tok); + Ok(()) + }); + let all = fut.join(fut2).then(|_| Ok(())); tokio::run(all) } diff --git a/src/service_account.rs b/src/service_account.rs index cabf075..435ff9a 100644 --- a/src/service_account.rs +++ b/src/service_account.rs @@ -333,43 +333,56 @@ where T: AsRef + Ord + 'b, I: Iterator, { - let (hash, scps) = hash_scopes(scopes); + let (hash, scps0) = hash_scopes(scopes); + let cache = self.cache.clone(); + let scps = scps0.clone(); - match self - .cache - .lock() - .unwrap() - .get(hash, &scps.iter().map(|s| s.as_str()).collect()) - { - Ok(Some(token)) => { - if !token.expired() { - return Box::new(future::ok(token)); + let cache_lookup = futures::lazy(move || { + match cache + .lock() + .unwrap() + .get(hash, &scps.iter().map(|s| s.as_str()).collect()) + { + Ok(Some(token)) => { + if !token.expired() { + return Ok(token); + } + return Err(Box::new(StringError::new("expired token in cache", None)) + as Box); + } + Err(e) => return Err(Box::new(e) as Box), + Ok(None) => { + return Err(Box::new(StringError::new("no token in cache", None)) + as Box) } } - Err(e) => return Box::new(future::err(Box::new(e) as Box)), - _ => {} - } + }); let cache = self.cache.clone(); - Box::new( - Self::request_token( - self.client.clone(), - self.sub.clone(), - self.key.clone(), - scps.iter().map(|s| s.to_string()).collect(), - ) - .then(move |r| match r { - Ok(token) => { - let _ = cache.lock().unwrap().set( - hash, - &scps.iter().map(|s| s.as_str()).collect(), - Some(token.clone()), - ); - Box::new(future::ok(token)) - } - Err(e) => Box::new(future::err(e)), - }), + let req_token = Self::request_token( + self.client.clone(), + self.sub.clone(), + self.key.clone(), + scps0.iter().map(|s| s.to_string()).collect(), ) + .then(move |r| match r { + Ok(token) => { + let _ = cache.lock().unwrap().set( + hash, + &scps0.iter().map(|s| s.as_str()).collect(), + Some(token.clone()), + ); + Box::new(future::ok(token)) + } + Err(e) => Box::new(future::err(e)), + }); + + Box::new(cache_lookup.then(|r| match r { + Ok(t) => Box::new(Ok(t).into_future()) + as Box> + Send>, + Err(_) => Box::new(req_token) + as Box> + Send>, + })) } /// Returns an empty ApplicationSecret as tokens for service accounts don't need to be From 33babd3d53adf5626fec839c028e6e49b76b6bf2 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Fri, 21 Jun 2019 19:32:49 +0200 Subject: [PATCH 33/41] test(ServiceAccount): Add tests for error paths. --- Cargo.toml | 1 + src/service_account.rs | 91 +++++++++++++++++++++++++++++++++--------- 2 files changed, 73 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bd3ebf0..92ae597 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ getopts = "0.2" open = "1.1" yup-hyper-mock = "3.14" mockito = "0.17" +env_logger = "0.6" [workspace] members = ["examples/test-installed/", "examples/test-svc-acct/", "examples/test-device/"] diff --git a/src/service_account.rs b/src/service_account.rs index 435ff9a..ec8bab3 100644 --- a/src/service_account.rs +++ b/src/service_account.rs @@ -117,6 +117,7 @@ impl JWT { } /// Set JWT header. Default is `{"alg":"RS256","typ":"JWT"}`. + #[allow(dead_code)] pub fn set_header(&mut self, head: String) { self.header = head; } @@ -409,6 +410,7 @@ mod tests { #[test] fn test_mocked_http() { + env_logger::try_init().unwrap(); let server_url = &mockito::server_url(); let client_secret = r#"{ "type": "service_account", @@ -430,29 +432,80 @@ mod tests { "expires_in": 3600, "token_type": "Bearer" }"#; - let _m = mock("POST", "/token") - .with_status(200) - .with_header("content-type", "text/json") - .with_body(json_response) - .create(); + let bad_json_response = r#"{ + "access_token": "ya29.c.ElouBywiys0LyNaZoLPJcp1Fdi2KjFMxzvYKLXkTdvM-rDfqKlvEq6PiMhGoGHx97t5FAvz3eb_ahdwlBjSStxHtDVQB4ZPRJQ_EOi-iS7PnayahU2S9Jp8S6rk", + "token_type": "Bearer" +}"#; + let https = HttpsConnector::new(1).unwrap(); let client = hyper::Client::builder() .keep_alive(false) .build::<_, hyper::Body>(https); - let mut acc = ServiceAccountAccess::new(key, client); - let fut = acc - .token(vec!["https://www.googleapis.com/auth/pubsub"].iter()) - .then(|tok| { - assert!(tok - .as_ref() - .unwrap() - .access_token - .contains("ya29.c.ElouBywiys0Ly")); - assert_eq!(Some(3600), tok.unwrap().expires_in); - Ok(()) - }); - tokio::run(fut); - _m.assert(); + let mut rt = tokio::runtime::Builder::new() + .core_threads(1) + .panic_handler(|e| std::panic::resume_unwind(e)) + .build() + .unwrap(); + + // Successful path. + { + let _m = mock("POST", "/token") + .with_status(200) + .with_header("content-type", "text/json") + .with_body(json_response) + .expect(1) + .create(); + let mut acc = ServiceAccountAccess::new(key.clone(), client.clone()); + let fut = acc + .token(vec!["https://www.googleapis.com/auth/pubsub"].iter()) + .and_then(|tok| { + println!("{:?}", tok); + assert!(tok.access_token.contains("ya29.c.ElouBywiys0Ly")); + assert_eq!(Some(3600), tok.expires_in); + Ok(()) + }); + rt.block_on(fut).expect("block_on"); + + assert!(acc + .cache + .lock() + .unwrap() + .get( + 3502164897243251857, + &vec!["https://www.googleapis.com/auth/pubsub"] + ) + .unwrap() + .is_some()); + // Test that token is in cache (otherwise mock will tell us) + let fut = acc + .token(vec!["https://www.googleapis.com/auth/pubsub"].iter()) + .and_then(|tok| { + assert!(tok.access_token.contains("ya29.c.ElouBywiys0Ly")); + assert_eq!(Some(3600), tok.expires_in); + Ok(()) + }); + rt.block_on(fut).expect("block_on 2"); + + _m.assert(); + } + // Malformed response. + { + let _m = mock("POST", "/token") + .with_status(200) + .with_header("content-type", "text/json") + .with_body(bad_json_response) + .create(); + let mut acc = ServiceAccountAccess::new(key.clone(), client.clone()); + let fut = acc + .token(vec!["https://www.googleapis.com/auth/pubsub"].iter()) + .then(|result| { + assert!(result.is_err()); + Ok(()) as Result<(), ()> + }); + rt.block_on(fut).expect("block_on"); + _m.assert(); + } + rt.shutdown_on_idle().wait().expect("shutdown"); } // Valid but deactivated key. From 16b76b8726bc0b90ea3598b4428e9a47867c276e Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Fri, 21 Jun 2019 21:47:35 +0200 Subject: [PATCH 34/41] test(Installed): Add end-to-end test for Installed flow. Also using mockito. We test both the interactive and the local-HTTP-redirect paths, as well as the interaction with the token provider. --- src/installed.rs | 160 +++++++++++++++++++++++++++++++++++++++++ src/service_account.rs | 1 - 2 files changed, 160 insertions(+), 1 deletion(-) diff --git a/src/installed.rs b/src/installed.rs index 51bef21..b8042ec 100644 --- a/src/installed.rs +++ b/src/installed.rs @@ -520,7 +520,167 @@ impl InstalledFlowService { #[cfg(test)] mod tests { + use std::fmt; + use std::str::FromStr; + + use hyper; + use hyper::client::connect::HttpConnector; + use hyper_tls::HttpsConnector; + use mockito::{self, mock}; + use tokio; + use super::*; + use crate::authenticator_delegate::FlowDelegate; + use crate::helper::*; + use crate::types::StringError; + + #[test] + fn test_end2end() { + #[derive(Clone)] + struct FD( + String, + hyper::Client, hyper::Body>, + ); + impl FlowDelegate for FD { + /// Depending on need_code, return the pre-set code or send the code to the server at + /// the redirect_uri given in the url. + fn present_user_url + fmt::Display>( + &mut self, + url: S, + need_code: bool, + ) -> Box, Error = Box> + Send> + { + if need_code { + Box::new(Ok(Some(self.0.clone())).into_future()) + } else { + // Parse presented url to obtain redirect_uri with location of local + // code-accepting server. + let uri = Uri::from_str(url.as_ref()).unwrap(); + let query = uri.query().unwrap(); + let parsed = form_urlencoded::parse(query.as_bytes()).into_owned(); + let mut rduri = None; + for (k, v) in parsed { + if k == "redirect_uri" { + rduri = Some(v); + break; + } + } + if rduri.is_none() { + return Box::new( + Err(Box::new(StringError::new("no redirect uri!", None)) + as Box) + .into_future(), + ); + } + let mut rduri = rduri.unwrap(); + rduri.push_str(&format!("?code={}", self.0)); + let rduri = Uri::from_str(rduri.as_ref()).unwrap(); + // Hit server. + return Box::new( + self.1 + .get(rduri) + .map_err(|e| Box::new(e) as Box) + .map(|_| None), + ); + } + } + } + + let server_url = mockito::server_url(); + let app_secret = r#"{"installed":{"client_id":"902216714886-k2v9uei3p1dk6h686jbsn9mo96tnbvto.apps.googleusercontent.com","project_id":"yup-test-243420","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"iuMPN6Ne1PD7cos29Tk9rlqH","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}}"#; + let mut app_secret = parse_application_secret(app_secret).unwrap(); + app_secret.token_uri = format!("{}/token", server_url); + + let https = HttpsConnector::new(1).expect("tls"); + let client = hyper::Client::builder() + .keep_alive(false) + .build::<_, hyper::Body>(https); + + let fd = FD("authorizationcode".to_string(), client.clone()); + let mut inf = InstalledFlow::new( + client.clone(), + fd, + app_secret.clone(), + InstalledFlowReturnMethod::Interactive, + ); + + let mut rt = tokio::runtime::Builder::new() + .core_threads(1) + .panic_handler(|e| std::panic::resume_unwind(e)) + .build() + .unwrap(); + + // Successful path. + { + let _m = mock("POST", "/token") + .match_body(mockito::Matcher::Regex(".*code=authorizationcode.*client_id=9022167.*".to_string())) + .with_body(r#"{"access_token": "accesstoken", "refresh_token": "refreshtoken", "token_type": "Bearer", "expires_in": 12345678}"#) + .expect(1) + .create(); + + let fut = inf + .token(vec!["https://googleapis.com/some/scope"].iter()) + .and_then(|tok| { + assert_eq!("accesstoken", tok.access_token); + assert_eq!("refreshtoken", tok.refresh_token); + assert_eq!("Bearer", tok.token_type); + Ok(()) + }); + rt.block_on(fut).expect("block on"); + _m.assert(); + } + // Successful path with HTTP redirect. + { + let mut inf = InstalledFlow::new( + client.clone(), + FD( + "authorizationcodefromlocalserver".to_string(), + client.clone(), + ), + app_secret, + InstalledFlowReturnMethod::HTTPRedirect(8081), + ); + let _m = mock("POST", "/token") + .match_body(mockito::Matcher::Regex(".*code=authorizationcodefromlocalserver.*client_id=9022167.*".to_string())) + .with_body(r#"{"access_token": "accesstoken", "refresh_token": "refreshtoken", "token_type": "Bearer", "expires_in": 12345678}"#) + .expect(1) + .create(); + + let fut = inf + .token(vec!["https://googleapis.com/some/scope"].iter()) + .and_then(|tok| { + assert_eq!("accesstoken", tok.access_token); + assert_eq!("refreshtoken", tok.refresh_token); + assert_eq!("Bearer", tok.token_type); + Ok(()) + }); + rt.block_on(fut).expect("block on"); + _m.assert(); + } + // Error from server. + { + let _m = mock("POST", "/token") + .match_body(mockito::Matcher::Regex( + ".*code=authorizationcode.*client_id=9022167.*".to_string(), + )) + .with_status(400) + .with_body(r#"{"error": "invalid_code"}"#) + .expect(1) + .create(); + + let fut = + inf.token(vec!["https://googleapis.com/some/scope"].iter()) + .then(|tokr| { + assert!(tokr.is_err()); + assert!(format!("{}", tokr.unwrap_err()) + .contains("Token API error: invalid_code")); + Ok(()) as Result<(), ()> + }); + rt.block_on(fut).expect("block on"); + _m.assert(); + } + rt.shutdown_on_idle().wait().expect("shutdown"); + } #[test] fn test_request_url_builder() { diff --git a/src/service_account.rs b/src/service_account.rs index ec8bab3..e0b1b56 100644 --- a/src/service_account.rs +++ b/src/service_account.rs @@ -459,7 +459,6 @@ mod tests { let fut = acc .token(vec!["https://www.googleapis.com/auth/pubsub"].iter()) .and_then(|tok| { - println!("{:?}", tok); assert!(tok.access_token.contains("ya29.c.ElouBywiys0Ly")); assert_eq!(Some(3600), tok.expires_in); Ok(()) From bfe481c93bf32c168abfe060cc254fa0a39b43a1 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Sat, 22 Jun 2019 00:06:11 +0200 Subject: [PATCH 35/41] test(RefreshFlow): Add end-to-end test. This flow is not very complex, but now we have appropriate coverage. --- src/refresh.rs | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/src/refresh.rs b/src/refresh.rs index ed8f28c..85de29a 100644 --- a/src/refresh.rs +++ b/src/refresh.rs @@ -111,3 +111,82 @@ impl RefreshFlow { }) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::helper; + + use hyper; + use hyper_tls::HttpsConnector; + use mockito; + use tokio; + + #[test] + fn test_refresh_end2end() { + let server_url = mockito::server_url(); + + let app_secret = r#"{"installed":{"client_id":"902216714886-k2v9uei3p1dk6h686jbsn9mo96tnbvto.apps.googleusercontent.com","project_id":"yup-test-243420","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"iuMPN6Ne1PD7cos29Tk9rlqH","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}}"#; + let mut app_secret = helper::parse_application_secret(app_secret).unwrap(); + app_secret.token_uri = format!("{}/token", server_url); + let refresh_token = "my-refresh-token".to_string(); + + let https = HttpsConnector::new(1).unwrap(); + let client = hyper::Client::builder() + .keep_alive(false) + .build::<_, hyper::Body>(https); + + // Success + { + let _m = mockito::mock("POST", "/token") + .match_body( + mockito::Matcher::Regex(".*client_id=902216714886-k2v9uei3p1dk6h686jbsn9mo96tnbvto.apps.googleusercontent.com.*refresh_token=my-refresh-token.*".to_string())) + .with_status(200) + .with_body(r#"{"access_token": "new-access-token", "token_type": "Bearer", "expires_in": 1234567}"#) + .create(); + + let fut = RefreshFlow::refresh_token( + client.clone(), + app_secret.clone(), + refresh_token.clone(), + ) + .then(|rr| { + let rr = rr.unwrap(); + match rr { + RefreshResult::Success(tok) => { + assert_eq!("new-access-token", tok.access_token); + assert_eq!("Bearer", tok.token_type); + } + _ => panic!(format!("unexpected RefreshResult {:?}", rr)), + } + Ok(()) + }); + + tokio::run(fut); + _m.assert(); + } + // Refresh error. + { + let _m = mockito::mock("POST", "/token") + .match_body( + mockito::Matcher::Regex(".*client_id=902216714886-k2v9uei3p1dk6h686jbsn9mo96tnbvto.apps.googleusercontent.com.*refresh_token=my-refresh-token.*".to_string())) + .with_status(400) + .with_body(r#"{"error": "invalid_token"}"#) + .create(); + + let fut = RefreshFlow::refresh_token(client, app_secret, refresh_token).then(|rr| { + let rr = rr.unwrap(); + match rr { + RefreshResult::RefreshError(e, None) => { + assert_eq!(e, "invalid_token"); + } + _ => panic!(format!("unexpected RefreshResult {:?}", rr)), + } + Ok(()) + }); + + tokio::run(fut); + _m.assert(); + } + } +} From 45431d83ff8aca7a013aeea7af690574a810c194 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Sat, 22 Jun 2019 12:17:23 +0200 Subject: [PATCH 36/41] test(Device): Add tests for Device flow --- src/authenticator_delegate.rs | 3 + src/device.rs | 166 +++++++++++++++++++++++++++++++--- 2 files changed, 156 insertions(+), 13 deletions(-) diff --git a/src/authenticator_delegate.rs b/src/authenticator_delegate.rs index b993758..df53017 100644 --- a/src/authenticator_delegate.rs +++ b/src/authenticator_delegate.rs @@ -56,6 +56,8 @@ pub enum PollError { AccessDenied, /// Indicates that too many attempts failed. TimedOut, + /// Other type of error. + Other(String) } impl fmt::Display for PollError { @@ -65,6 +67,7 @@ impl fmt::Display for PollError { PollError::Expired(ref date) => writeln!(f, "Authentication expired at {}", date), PollError::AccessDenied => "Access denied by user".fmt(f), PollError::TimedOut => "Timed out waiting for token".fmt(f), + PollError::Other(ref s) => format!("Unknown server error: {}", s).fmt(f) } } } diff --git a/src/device.rs b/src/device.rs index 3182ef8..3778ebb 100644 --- a/src/device.rs +++ b/src/device.rs @@ -150,10 +150,16 @@ where >, }, Ok(Some(tok)) => Box::new(Ok(future::Loop::Break(tok)).into_future()), + Err(e @ PollError::AccessDenied) + | Err(e @ PollError::TimedOut) + | Err(e @ PollError::Expired(_)) => { + Box::new(Err(Box::new(e) as Box).into_future()) + } Err(_) if i < maxn => { Box::new(Ok(future::Loop::Continue(i + 1)).into_future()) } - _ => Box::new( + // Too many attempts. + Ok(None) | Err(_) => Box::new( Err(Box::new(PollError::TimedOut) as Box) .into_future(), ), @@ -273,7 +279,8 @@ where /// /// Do not call after `PollError::Expired|PollError::AccessDenied` was among the /// `Err(PollError)` variants as the flow will not do anything anymore. - /// Thus in any unsuccessful case which is not `PollError::HttpError`, you will have to start /// over the entire flow, which requires a new instance of this type. + /// Thus in any unsuccessful case which is not `PollError::HttpError`, you will have to start + /// over the entire flow, which requires a new instance of this type. /// /// > ⚠️ **Warning**: We assume the caller doesn't call faster than `interval` and are not /// > protected against this kind of mis-use. @@ -286,7 +293,7 @@ where device_code: String, pi: PollInformation, mut fd: FD, - ) -> impl Future, Error = Box> { + ) -> impl Future, Error = PollError> { let expired = if pi.expires_at <= Utc::now() { fd.expired(&pi.expires_at); Err(PollError::Expired(pi.expires_at)).into_future() @@ -309,12 +316,7 @@ where .body(hyper::Body::from(req)) .unwrap(); // TODO: Error checking expired - .map_err(|e| Box::new(e) as Box) - .and_then(move |_| { - client - .request(request) - .map_err(|e| Box::new(e) as Box) - }) + .and_then(move |_| client.request(request).map_err(|e| PollError::HttpError(e))) .map(|res| { res.into_body() .concat2() @@ -334,12 +336,15 @@ where match res.error.as_ref() { "access_denied" => { fd.denied(); - return Err( - Box::new(PollError::AccessDenied) as Box - ); + return Err(PollError::AccessDenied); } "authorization_pending" => return Ok(None), - _ => panic!("server message '{}' not understood", res.error), + s => { + return Err(PollError::Other(format!( + "server message '{}' not understood", + s + ))) + } }; } } @@ -352,3 +357,138 @@ where }) } } + +#[cfg(test)] +mod tests { + use hyper; + use hyper_tls::HttpsConnector; + use mockito; + use tokio; + + use super::*; + use crate::helper::parse_application_secret; + + #[test] + fn test_device_end2end() { + #[derive(Clone)] + struct FD; + impl FlowDelegate for FD { + fn present_user_code(&mut self, pi: &PollInformation) { + assert_eq!("https://example.com/verify", pi.verification_url); + } + } + + let server_url = mockito::server_url(); + let app_secret = r#"{"installed":{"client_id":"902216714886-k2v9uei3p1dk6h686jbsn9mo96tnbvto.apps.googleusercontent.com","project_id":"yup-test-243420","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"iuMPN6Ne1PD7cos29Tk9rlqH","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}}"#; + let mut app_secret = parse_application_secret(app_secret).unwrap(); + app_secret.token_uri = format!("{}/token", server_url); + let device_code_url = format!("{}/code", server_url); + + let https = HttpsConnector::new(1).expect("tls"); + let client = hyper::Client::builder() + .keep_alive(false) + .build::<_, hyper::Body>(https); + + let mut flow = DeviceFlow::new(client.clone(), app_secret, FD, Some(device_code_url)); + + let mut rt = tokio::runtime::Builder::new() + .core_threads(1) + .panic_handler(|e| std::panic::resume_unwind(e)) + .build() + .unwrap(); + + // Successful path + { + let code_response = r#"{"device_code": "devicecode", "user_code": "usercode", "verification_url": "https://example.com/verify", "expires_in": 1234567, "interval": 1}"#; + let _m = mockito::mock("POST", "/code") + .match_body(mockito::Matcher::Regex( + ".*client_id=902216714886.*".to_string(), + )) + .with_status(200) + .with_body(code_response) + .create(); + let token_response = r#"{"access_token": "accesstoken", "refresh_token": "refreshtoken", "token_type": "Bearer", "expires_in": 1234567}"#; + let _m = mockito::mock("POST", "/token") + .match_body(mockito::Matcher::Regex( + ".*client_secret=iuMPN6Ne1PD7cos29Tk9rlqH&code=devicecode.*".to_string(), + )) + .with_status(200) + .with_body(token_response) + .create(); + + let fut = flow + .token(vec!["https://www.googleapis.com/scope/1"].iter()) + .then(|token| { + let token = token.unwrap(); + assert_eq!("accesstoken", token.access_token); + Ok(()) as Result<(), ()> + }); + rt.block_on(fut).expect("block_on"); + + _m.assert(); + } + // Code is not delivered. + { + let code_response = + r#"{"error": "invalid_client_id", "error_description": "description"}"#; + let _m = mockito::mock("POST", "/code") + .match_body(mockito::Matcher::Regex( + ".*client_id=902216714886.*".to_string(), + )) + .with_status(400) + .with_body(code_response) + .create(); + let token_response = r#"{"access_token": "accesstoken", "refresh_token": "refreshtoken", "token_type": "Bearer", "expires_in": 1234567}"#; + let _m = mockito::mock("POST", "/token") + .match_body(mockito::Matcher::Regex( + ".*client_secret=iuMPN6Ne1PD7cos29Tk9rlqH&code=devicecode.*".to_string(), + )) + .with_status(200) + .with_body(token_response) + .expect(0) // Never called! + .create(); + + let fut = flow + .token(vec!["https://www.googleapis.com/scope/1"].iter()) + .then(|token| { + assert!(token.is_err()); + assert!(format!("{}", token.unwrap_err()).contains("invalid_client_id")); + Ok(()) as Result<(), ()> + }); + rt.block_on(fut).expect("block_on"); + + _m.assert(); + } + // Token is not delivered. + { + let code_response = r#"{"device_code": "devicecode", "user_code": "usercode", "verification_url": "https://example.com/verify", "expires_in": 1234567, "interval": 1}"#; + let _m = mockito::mock("POST", "/code") + .match_body(mockito::Matcher::Regex( + ".*client_id=902216714886.*".to_string(), + )) + .with_status(200) + .with_body(code_response) + .create(); + let token_response = r#"{"error": "access_denied"}"#; + let _m = mockito::mock("POST", "/token") + .match_body(mockito::Matcher::Regex( + ".*client_secret=iuMPN6Ne1PD7cos29Tk9rlqH&code=devicecode.*".to_string(), + )) + .with_status(400) + .with_body(token_response) + .expect(1) + .create(); + + let fut = flow + .token(vec!["https://www.googleapis.com/scope/1"].iter()) + .then(|token| { + assert!(token.is_err()); + assert!(format!("{}", token.unwrap_err()).contains("Access denied by user")); + Ok(()) as Result<(), ()> + }); + rt.block_on(fut).expect("block_on"); + + _m.assert(); + } + } +} From d1952e9d67e1954df7c73a43f0076270a25c85fb Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Sat, 22 Jun 2019 12:17:42 +0200 Subject: [PATCH 37/41] test(Refresh): Properly process panics in RefreshFlow test. --- src/refresh.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/refresh.rs b/src/refresh.rs index 85de29a..e6944c9 100644 --- a/src/refresh.rs +++ b/src/refresh.rs @@ -136,6 +136,12 @@ mod tests { .keep_alive(false) .build::<_, hyper::Body>(https); + let mut rt = tokio::runtime::Builder::new() + .core_threads(1) + .panic_handler(|e| std::panic::resume_unwind(e)) + .build() + .unwrap(); + // Success { let _m = mockito::mock("POST", "/token") @@ -144,7 +150,6 @@ mod tests { .with_status(200) .with_body(r#"{"access_token": "new-access-token", "token_type": "Bearer", "expires_in": 1234567}"#) .create(); - let fut = RefreshFlow::refresh_token( client.clone(), app_secret.clone(), @@ -159,10 +164,10 @@ mod tests { } _ => panic!(format!("unexpected RefreshResult {:?}", rr)), } - Ok(()) + Ok(()) as Result<(), ()> }); - tokio::run(fut); + rt.block_on(fut).expect("block_on"); _m.assert(); } // Refresh error. From ff015daf2d502a39c884c93f64e2547155303041 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Sat, 22 Jun 2019 12:33:38 +0200 Subject: [PATCH 38/41] chore(fmt): Make rustfmt on Travis happy. --- src/authenticator_delegate.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/authenticator_delegate.rs b/src/authenticator_delegate.rs index df53017..fb19d38 100644 --- a/src/authenticator_delegate.rs +++ b/src/authenticator_delegate.rs @@ -57,7 +57,7 @@ pub enum PollError { /// Indicates that too many attempts failed. TimedOut, /// Other type of error. - Other(String) + Other(String), } impl fmt::Display for PollError { @@ -67,7 +67,7 @@ impl fmt::Display for PollError { PollError::Expired(ref date) => writeln!(f, "Authentication expired at {}", date), PollError::AccessDenied => "Access denied by user".fmt(f), PollError::TimedOut => "Timed out waiting for token".fmt(f), - PollError::Other(ref s) => format!("Unknown server error: {}", s).fmt(f) + PollError::Other(ref s) => format!("Unknown server error: {}", s).fmt(f), } } } From 8d6085375f0925a1b7901cadeedad6e5086d89d7 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Sat, 22 Jun 2019 20:25:47 +0200 Subject: [PATCH 39/41] doc(Installed): More documentation about InstalledFlow and new example --- src/installed.rs | 5 ++++ src/lib.rs | 63 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/installed.rs b/src/installed.rs index b8042ec..52170e7 100644 --- a/src/installed.rs +++ b/src/installed.rs @@ -82,6 +82,11 @@ impl { method: InstalledFlowReturnMethod, client: hyper::client::Client, diff --git a/src/lib.rs b/src/lib.rs index d0fd0a1..752df38 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,8 +34,69 @@ //! The returned `Token` is stored permanently in the given token storage in order to //! authorize future API requests to the same scopes. //! +//! The following example, which is derived from the (actual and runnable) example in +//! `examples/test-installed/`, shows the basics of using this crate: +//! //! ```test_harness,no_run -//! // TODO: Rewrite example here once new authenticator works. +//! use futures::prelude::*; +//! use yup_oauth2::GetToken; +//! use yup_oauth2::{Authenticator, InstalledFlow}; +//! +//! use hyper::client::Client; +//! use hyper_tls::HttpsConnector; +//! +//! use std::path::Path; +//! +//! fn main() { +//! // Boilerplate: Set up hyper HTTP client and TLS. +//! let https = HttpsConnector::new(1).expect("tls"); +//! let client = Client::builder() +//! .keep_alive(false) +//! .build::<_, hyper::Body>(https); +//! +//! // Read application secret from a file. Sometimes it's easier to compile it directly into +//! // the binary. The clientsecret file contains JSON like `{"installed":{"client_id": ... }}` +//! let secret = yup_oauth2::read_application_secret(Path::new("clientsecret.json")) +//! .expect("clientsecret.json"); +//! +//! // There are two types of delegates; FlowDelegate and AuthenticatorDelegate. See the +//! // respective documentation; all you need to know here is that they determine how the user +//! // is asked to visit the OAuth flow URL or how to read back the provided code. +//! let ad = yup_oauth2::DefaultFlowDelegate; +//! +//! // InstalledFlow handles OAuth flows of that type. They are usually the ones where a user +//! // grants access to their personal account (think Google Drive, Github API, etc.). +//! let inf = InstalledFlow::new( +//! client.clone(), +//! ad, +//! secret, +//! yup_oauth2::InstalledFlowReturnMethod::HTTPRedirect(8081), +//! ); +//! // You could already use InstalledFlow by itself, but usually you want to cache tokens and +//! // refresh them, rather than ask the user every time to log in again. Authenticator wraps +//! // other flows and handles these. +//! // This type of authenticator caches tokens in a JSON file on disk. +//! let mut auth = Authenticator::new_disk( +//! client, +//! inf, +//! yup_oauth2::DefaultAuthenticatorDelegate, +//! "tokencache.json", +//! ) +//! .unwrap(); +//! let s = "https://www.googleapis.com/auth/drive.file".to_string(); +//! let scopes = vec![s]; +//! +//! // token() is the one important function of this crate; it does everything to +//! // obtain a token that can be sent e.g. as Bearer token. +//! let tok = auth.token(scopes.iter()); +//! // Finally we print the token. +//! let fut = tok.map_err(|e| println!("error: {:?}", e)).and_then(|t| { +//! println!("The token is {:?}", t); +//! Ok(()) +//! }); +//! +//! tokio::run(fut) +//! } //! ``` //! #[macro_use] From 602ea1565d834f05fa7bc98a7d594e5c0add2c0a Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Sat, 22 Jun 2019 21:53:55 +0200 Subject: [PATCH 40/41] refactor(errors): Move almost everything to RequestError. This is nicer than stupid Box everywhere. --- src/authenticator.rs | 28 +++++---- src/authenticator_delegate.rs | 24 +------- src/device.rs | 43 +++++--------- src/installed.rs | 86 ++++++++++++++-------------- src/lib.rs | 6 +- src/refresh.rs | 18 +----- src/service_account.rs | 103 +++++++++++++++------------------- src/types.rs | 68 ++++++++++++++++++---- 8 files changed, 178 insertions(+), 198 deletions(-) diff --git a/src/authenticator.rs b/src/authenticator.rs index d3e1008..fa68076 100644 --- a/src/authenticator.rs +++ b/src/authenticator.rs @@ -1,7 +1,7 @@ use crate::authenticator_delegate::{AuthenticatorDelegate, Retry}; -use crate::refresh::{RefreshFlow, RefreshResult}; +use crate::refresh::RefreshFlow; use crate::storage::{hash_scopes, DiskTokenStorage, MemoryStorage, TokenStorage}; -use crate::types::{ApplicationSecret, GetToken, StringError, Token}; +use crate::types::{ApplicationSecret, GetToken, RefreshResult, RequestError, Token}; use futures::{future, prelude::*}; use tokio_timer; @@ -88,7 +88,7 @@ impl< fn token<'b, I, T>( &mut self, scopes: I, - ) -> Box> + Send> + ) -> Box + Send> where T: AsRef + Ord + 'b, I: Iterator, @@ -100,7 +100,7 @@ impl< let appsecret = self.inner.lock().unwrap().application_secret(); let gettoken = self.inner.clone(); let loopfn = move |()| -> Box< - dyn Future, Error = Box> + Send, + dyn Future, Error = RequestError> + Send, > { // How well does this work with tokio? match store.lock().unwrap().get( @@ -121,27 +121,27 @@ impl< appsecret.clone(), refresh_token, ) - .and_then(move |rr| -> Box, Error=Box> + Send> { + .and_then(move |rr| -> Box, Error=RequestError> + Send> { match rr { - RefreshResult::Error(e) => { + RefreshResult::Error(ref e) => { delegate.token_refresh_failed( format!("{}", e.description().to_string()), &Some("the request has likely timed out".to_string()), ); - Box::new(Err(Box::new(e) as Box).into_future()) + Box::new(Err(RequestError::Refresh(rr)).into_future()) } RefreshResult::RefreshError(ref s, ref ss) => { delegate.token_refresh_failed( format!("{} {}", s, ss.clone().map(|s| format!("({})", s)).unwrap_or("".to_string())), &Some("the refresh token is likely invalid and your authorization has been revoked".to_string()), ); - Box::new(Err(Box::new(StringError::new(s, ss.as_ref())) as Box).into_future()) + Box::new(Err(RequestError::Refresh(rr)).into_future()) } RefreshResult::Success(t) => { if let Err(e) = store.lock().unwrap().set(scope_key, &scopes.iter().map(|s| s.as_str()).collect(), Some(t.clone())) { match delegate.token_storage_failure(true, &e) { Retry::Skip => Box::new(Ok(future::Loop::Break(t)).into_future()), - Retry::Abort => Box::new(Err(Box::new(e) as Box).into_future()), + Retry::Abort => Box::new(Err(RequestError::Cache(Box::new(e))).into_future()), Retry::After(d) => Box::new( tokio_timer::sleep(d) .then(|_| Ok(future::Loop::Continue(()))), @@ -149,9 +149,7 @@ impl< as Box< dyn Future< Item = future::Loop, - Error = Box, - > + Send, - >, + Error = RequestError> + Send>, } } else { Box::new(Ok(future::Loop::Break(t)).into_future()) @@ -181,7 +179,7 @@ impl< Box::new(Ok(future::Loop::Break(t)).into_future()) } Retry::Abort => Box::new( - Err(Box::new(e) as Box).into_future(), + Err(RequestError::Cache(Box::new(e))).into_future(), ), Retry::After(d) => Box::new( tokio_timer::sleep(d) @@ -190,7 +188,7 @@ impl< as Box< dyn Future< Item = future::Loop, - Error = Box, + Error = RequestError, > + Send, >, } @@ -202,7 +200,7 @@ impl< } Err(err) => match delegate.token_storage_failure(false, &err) { Retry::Abort | Retry::Skip => { - return Box::new(future::err(Box::new(err) as Box)) + return Box::new(Err(RequestError::Cache(Box::new(err))).into_future()) } Retry::After(d) => { return Box::new( diff --git a/src/authenticator_delegate.rs b/src/authenticator_delegate.rs index fb19d38..132b511 100644 --- a/src/authenticator_delegate.rs +++ b/src/authenticator_delegate.rs @@ -4,7 +4,7 @@ use std::error::Error; use std::fmt; use std::io; -use crate::types::RequestError; +use crate::types::{PollError, RequestError}; use chrono::{DateTime, Local, Utc}; use std::time::Duration; @@ -45,21 +45,6 @@ impl fmt::Display for PollInformation { } } -/// Encapsulates all possible results of a `poll_token(...)` operation -#[derive(Debug)] -pub enum PollError { - /// Connection failure - retry if you think it's worth it - HttpError(hyper::Error), - /// Indicates we are expired, including the expiration date - Expired(DateTime), - /// Indicates that the user declined access. String is server response - AccessDenied, - /// Indicates that too many attempts failed. - TimedOut, - /// Other type of error. - Other(String), -} - impl fmt::Display for PollError { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { match *self { @@ -93,13 +78,6 @@ pub trait AuthenticatorDelegate: Clone { Retry::Abort } - /// Called whenever there is an HttpError, usually if there are network problems. - /// - /// Return retry information. - fn connection_error(&mut self, _: &hyper::http::Error) -> Retry { - Retry::Abort - } - /// Called whenever we failed to retrieve a token or set a token due to a storage error. /// You may use it to either ignore the incident or retry. /// This can be useful if the underlying `TokenStorage` may fail occasionally. diff --git a/src/device.rs b/src/device.rs index 3778ebb..c4b60df 100644 --- a/src/device.rs +++ b/src/device.rs @@ -1,4 +1,3 @@ -use std::error::Error; use std::iter::{FromIterator, IntoIterator}; use std::time::Duration; @@ -13,9 +12,9 @@ use serde_json as json; use tokio_timer; use url::form_urlencoded; -use crate::authenticator_delegate::{FlowDelegate, PollError, PollInformation, Retry}; +use crate::authenticator_delegate::{FlowDelegate, PollInformation, Retry}; use crate::types::{ - ApplicationSecret, Flow, FlowType, GetToken, JsonError, RequestError, StringError, Token, + ApplicationSecret, Flow, FlowType, GetToken, JsonError, PollError, RequestError, Token, }; pub const GOOGLE_DEVICE_CODE_URL: &'static str = "https://accounts.google.com/o/oauth2/device/code"; @@ -47,7 +46,7 @@ impl< fn token<'b, I, T>( &mut self, scopes: I, - ) -> Box> + Send> + ) -> Box + Send> where T: AsRef + Ord + 'b, I: Iterator, @@ -97,7 +96,7 @@ where pub fn retrieve_device_token<'a>( &mut self, scopes: Vec, - ) -> Box> + Send> { + ) -> Box + Send> { let application_secret = self.application_secret.clone(); let client = self.client.clone(); let wait = self.wait; @@ -131,13 +130,9 @@ where .then(|_| pt) .then(move |r| match r { Ok(None) if i < maxn => match fd.pending(&pollinf) { - Retry::Abort | Retry::Skip => Box::new( - Err(Box::new(StringError::new( - "Pending authentication aborted".to_string(), - None, - )) as Box) - .into_future(), - ), + Retry::Abort | Retry::Skip => { + Box::new(Err(RequestError::Poll(PollError::TimedOut)).into_future()) + } Retry::After(d) => Box::new( tokio_timer::sleep(d) .then(move |_| Ok(future::Loop::Continue(i + 1))), @@ -145,7 +140,7 @@ where as Box< dyn Future< Item = future::Loop, - Error = Box, + Error = RequestError, > + Send, >, }, @@ -153,16 +148,15 @@ where Err(e @ PollError::AccessDenied) | Err(e @ PollError::TimedOut) | Err(e @ PollError::Expired(_)) => { - Box::new(Err(Box::new(e) as Box).into_future()) + Box::new(Err(RequestError::Poll(e)).into_future()) } Err(_) if i < maxn => { Box::new(Ok(future::Loop::Continue(i + 1)).into_future()) } // Too many attempts. - Ok(None) | Err(_) => Box::new( - Err(Box::new(PollError::TimedOut) as Box) - .into_future(), - ), + Ok(None) | Err(_) => { + Box::new(Err(RequestError::Poll(PollError::TimedOut)).into_future()) + } }) }) })) @@ -188,8 +182,7 @@ where client: hyper::Client, device_code_url: String, scopes: Vec, - ) -> impl Future> - { + ) -> impl Future { // note: cloned() shouldn't be needed, see issue // https://github.com/servo/rust-url/issues/81 let req = form_urlencoded::Serializer::new(String::new()) @@ -222,9 +215,7 @@ where |r: Result, hyper::error::Error>| { match r { Err(err) => { - return Err( - Box::new(RequestError::ClientError(err)) as Box - ); + return Err(RequestError::ClientError(err)); } Ok(res) => { #[derive(Deserialize)] @@ -246,11 +237,7 @@ where // check for error match json::from_str::(&json_str) { Err(_) => {} // ignore, move on - Ok(res) => { - return Err( - Box::new(RequestError::from(res)) as Box - ) - } + Ok(res) => return Err(RequestError::from(res)), } let decoded: JsonData = json::from_str(&json_str).unwrap(); diff --git a/src/installed.rs b/src/installed.rs index 52170e7..d734887 100644 --- a/src/installed.rs +++ b/src/installed.rs @@ -3,8 +3,6 @@ // Refer to the project root for licensing information. // use std::convert::AsRef; -use std::error::Error; -use std::io; use std::sync::{Arc, Mutex}; use futures::prelude::*; @@ -12,12 +10,11 @@ use futures::stream::Stream; use futures::sync::oneshot; use hyper; use hyper::{header, StatusCode, Uri}; -use serde_json::error; use url::form_urlencoded; use url::percent_encoding::{percent_encode, QUERY_ENCODE_SET}; use crate::authenticator_delegate::FlowDelegate; -use crate::types::{ApplicationSecret, GetToken, Token}; +use crate::types::{ApplicationSecret, GetToken, RequestError, Token}; const OOB_REDIRECT_URI: &'static str = "urn:ietf:wg:oauth:2.0:oob"; @@ -67,7 +64,7 @@ impl( &mut self, scopes: I, - ) -> Box> + Send> + ) -> Box + Send> where T: AsRef + Ord + 'b, I: Iterator, @@ -134,12 +131,12 @@ impl<'c, FD: 'static + FlowDelegate + Clone + Send, C: 'c + hyper::client::conne pub fn obtain_token<'a>( &mut self, scopes: Vec, // Note: I haven't found a better way to give a list of strings here, due to ownership issues with futures. - ) -> impl 'a + Future> + Send { + ) -> impl 'a + Future + Send { let rduri = self.fd.redirect_uri(); // Start server on localhost to accept auth code. let server = if let InstalledFlowReturnMethod::HTTPRedirect(port) = self.method { match InstalledFlowServer::new(port) { - Result::Err(e) => Err(Box::new(e) as Box), + Result::Err(e) => Err(RequestError::ClientError(e)), Result::Ok(server) => Ok(Some(server)), } } else { @@ -166,27 +163,34 @@ impl<'c, FD: 'static + FlowDelegate + Clone + Send, C: 'c + hyper::client::conne let result = client.request(request); // Handle result here, it makes ownership tracking easier. result - .map_err(|e| Box::new(e) as Box) .and_then(move |r| { - let result = r - .into_body() + r.into_body() .concat2() - .wait() - .map(|c| String::from_utf8(c.into_bytes().to_vec()).unwrap()); // TODO: error handling - - let resp = match result { - Err(e) => return Err(Box::new(e) as Box), + .map(|c| String::from_utf8(c.into_bytes().to_vec()).unwrap()) // TODO: error handling + }) + .then(|body_or| { + let resp = match body_or { + Err(e) => return Err(RequestError::ClientError(e)), Ok(s) => s, }; - let token_resp: Result = + let token_resp: Result = serde_json::from_str(&resp); match token_resp { Err(e) => { - return Err(Box::new(e) as Box); + return Err(RequestError::JSONError(e)); + } + Ok(tok) => { + if tok.error.is_some() { + Err(RequestError::NegativeServerResponse( + tok.error.unwrap(), + tok.error_description, + )) + } else { + Ok(tok) + } } - Ok(tok) => Ok(tok) as Result>, } }) }) @@ -203,18 +207,12 @@ impl<'c, FD: 'static + FlowDelegate + Clone + Send, C: 'c + hyper::client::conne }; token.set_expiry_absolute(); - Result::Ok(token) + Ok(token) } else { - let err = Box::new(io::Error::new( - io::ErrorKind::Other, - format!( - "Token API error: {} {}", - tokens.error.unwrap_or("".to_string()), - tokens.error_description.unwrap_or("".to_string()) - ) - .as_str(), - )) as Box; - Result::Err(err) + Err(RequestError::NegativeServerResponse( + tokens.error.unwrap(), + tokens.error_description, + )) } }) } @@ -224,7 +222,7 @@ impl<'c, FD: 'static + FlowDelegate + Clone + Send, C: 'c + hyper::client::conne mut auth_delegate: FD, appsecret: &ApplicationSecret, scopes: S, - ) -> Box> + Send> + ) -> Box + Send> where T: AsRef + 'a, S: Iterator, @@ -251,10 +249,7 @@ impl<'c, FD: 'static + FlowDelegate + Clone + Send, C: 'c + hyper::client::conne } Ok(code) } - _ => Err(Box::new(io::Error::new( - io::ErrorKind::UnexpectedEof, - "couldn't read code", - )) as Box), + _ => Err(RequestError::UserError("couldn't read code".to_string())), } }), ) @@ -274,7 +269,12 @@ impl<'c, FD: 'static + FlowDelegate + Clone + Send, C: 'c + hyper::client::conne auth_delegate .present_user_url(&url, false /* need_code */) .then(move |_| server.block_till_auth()) - .map_err(|e| Box::new(e) as Box), + .map_err(|e| { + RequestError::UserError(format!( + "could not obtain token via redirect: {}", + e + )) + }), ) } } @@ -525,6 +525,7 @@ impl InstalledFlowService { #[cfg(test)] mod tests { + use std::error::Error; use std::fmt; use std::str::FromStr; @@ -673,14 +674,13 @@ mod tests { .expect(1) .create(); - let fut = - inf.token(vec!["https://googleapis.com/some/scope"].iter()) - .then(|tokr| { - assert!(tokr.is_err()); - assert!(format!("{}", tokr.unwrap_err()) - .contains("Token API error: invalid_code")); - Ok(()) as Result<(), ()> - }); + let fut = inf + .token(vec!["https://googleapis.com/some/scope"].iter()) + .then(|tokr| { + assert!(tokr.is_err()); + assert!(format!("{}", tokr.unwrap_err()).contains("invalid_code")); + Ok(()) as Result<(), ()> + }); rt.block_on(fut).expect("block on"); _m.assert(); } diff --git a/src/lib.rs b/src/lib.rs index 752df38..26b59cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -115,14 +115,14 @@ mod types; pub use crate::authenticator::Authenticator; pub use crate::authenticator_delegate::{ AuthenticatorDelegate, DefaultAuthenticatorDelegate, DefaultFlowDelegate, FlowDelegate, - PollError, PollInformation, + PollInformation, }; pub use crate::device::{DeviceFlow, GOOGLE_DEVICE_CODE_URL}; pub use crate::helper::*; pub use crate::installed::{InstalledFlow, InstalledFlowReturnMethod}; -pub use crate::refresh::{RefreshFlow, RefreshResult}; pub use crate::service_account::*; pub use crate::storage::{DiskTokenStorage, MemoryStorage, NullStorage, TokenStorage}; pub use crate::types::{ - ApplicationSecret, ConsoleApplicationSecret, FlowType, GetToken, Scheme, Token, TokenType, + ApplicationSecret, ConsoleApplicationSecret, FlowType, GetToken, PollError, RefreshResult, + RequestError, Scheme, Token, TokenType, }; diff --git a/src/refresh.rs b/src/refresh.rs index e6944c9..ac1fc72 100644 --- a/src/refresh.rs +++ b/src/refresh.rs @@ -1,6 +1,4 @@ -use crate::types::{ApplicationSecret, JsonError}; - -use std::error::Error; +use crate::types::{ApplicationSecret, JsonError, RefreshResult, RequestError}; use super::Token; use chrono::Utc; @@ -18,17 +16,6 @@ use url::form_urlencoded; /// and valid access token. pub struct RefreshFlow; -/// All possible outcomes of the refresh flow -#[derive(Debug)] -pub enum RefreshResult { - /// Indicates connection failure - Error(hyper::Error), - /// The server did not answer with a new token, providing the server message - RefreshError(String, Option), - /// The refresh operation finished successfully, providing a new `Token` - Success(Token), -} - impl RefreshFlow { /// Attempt to refresh the given token, and obtain a new, valid one. /// If the `RefreshResult` is `RefreshResult::Error`, you may retry within an interval @@ -48,7 +35,7 @@ impl RefreshFlow { client: hyper::Client, client_secret: ApplicationSecret, refresh_token: String, - ) -> impl 'a + Future> { + ) -> impl 'a + Future { let req = form_urlencoded::Serializer::new(String::new()) .extend_pairs(&[ ("client_id", client_secret.client_id.clone()), @@ -109,6 +96,7 @@ impl RefreshFlow { expires_in_timestamp: Some(Utc::now().timestamp() + t.expires_in), })) }) + .map_err(RequestError::Refresh) } } diff --git a/src/service_account.rs b/src/service_account.rs index e0b1b56..6eb4411 100644 --- a/src/service_account.rs +++ b/src/service_account.rs @@ -12,11 +12,10 @@ //! use std::default::Default; -use std::error; use std::sync::{Arc, Mutex}; use crate::storage::{hash_scopes, MemoryStorage, TokenStorage}; -use crate::types::{ApplicationSecret, GetToken, JsonError, StringError, Token}; +use crate::types::{ApplicationSecret, GetToken, JsonError, RequestError, StringError, Token}; use futures::stream::Stream; use futures::{future, prelude::*}; @@ -45,7 +44,7 @@ fn encode_base64>(s: T) -> String { } /// Decode a PKCS8 formatted RSA key. -fn decode_rsa_key(pem_pkcs8: &str) -> Result> { +fn decode_rsa_key(pem_pkcs8: &str) -> Result { let private = pem_pkcs8.to_string().replace("\\n", "\n").into_bytes(); let mut private_reader: &[u8] = private.as_ref(); let private_keys = pemfile::pkcs8_private_keys(&mut private_reader); @@ -54,16 +53,16 @@ fn decode_rsa_key(pem_pkcs8: &str) -> Result 0 { Ok(pk[0].clone()) } else { - Err(Box::new(io::Error::new( + Err(io::Error::new( io::ErrorKind::InvalidInput, "Not enough private keys in PEM", - ))) + )) } } else { - Err(Box::new(io::Error::new( + Err(io::Error::new( io::ErrorKind::InvalidInput, "Error reading key from PEM", - ))) + )) } } @@ -134,24 +133,20 @@ impl JWT { } /// Sign a JWT base string with `private_key`, which is a PKCS8 string. - fn sign(&self, private_key: &str) -> Result> { + fn sign(&self, private_key: &str) -> Result { let mut jwt_head = self.encode_claims(); let key = decode_rsa_key(private_key)?; - let signing_key = sign::RSASigningKey::new(&key).map_err(|_| { - Box::new(io::Error::new( - io::ErrorKind::Other, - "Couldn't initialize signer", - )) as Box - })?; + let signing_key = sign::RSASigningKey::new(&key) + .map_err(|_| io::Error::new(io::ErrorKind::Other, "Couldn't initialize signer"))?; let signer = signing_key .choose_scheme(&[rustls::SignatureScheme::RSA_PKCS1_SHA256]) - .ok_or(Box::new(io::Error::new( + .ok_or(io::Error::new( io::ErrorKind::Other, "Couldn't choose signing scheme", - )) as Box)?; + ))?; let signature = signer .sign(jwt_head.as_bytes()) - .map_err(|e| Box::new(e) as Box)?; + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("{}", e)))?; let signature_b64 = encode_base64(signature); jwt_head.push_str("."); @@ -256,13 +251,14 @@ impl<'a, C: 'static + hyper::client::connect::Connect> ServiceAccountAccess { sub: Option, key: ServiceAccountKey, scopes: Vec, - ) -> impl Future> { + ) -> impl Future { let mut claims = init_claims_from_key(&key, &scopes); claims.sub = sub.clone(); let signed = JWT::new(claims) .sign(key.private_key.as_ref().unwrap()) .into_future(); signed + .map_err(RequestError::LowLevelError) .map(|signed| { form_urlencoded::Serializer::new(String::new()) .extend_pairs(vec![ @@ -277,48 +273,40 @@ impl<'a, C: 'static + hyper::client::connect::Connect> ServiceAccountAccess { .body(hyper::Body::from(rqbody)) .unwrap() }) - .and_then(move |request| { - client - .request(request) - .map_err(|e| Box::new(e) as Box) - }) + .and_then(move |request| client.request(request).map_err(RequestError::ClientError)) .and_then(|response| { response .into_body() .concat2() - .map_err(|e| Box::new(e) as Box) + .map_err(RequestError::ClientError) }) .map(|c| String::from_utf8(c.into_bytes().to_vec()).unwrap()) .and_then(|s| { if let Ok(jse) = serde_json::from_str::(&s) { - Err( - Box::new(StringError::new(&jse.error, jse.error_description.as_ref())) - as Box, - ) + Err(RequestError::NegativeServerResponse( + jse.error, + jse.error_description, + )) } else { - serde_json::from_str(&s) - .map_err(|e| Box::new(e) as Box) + serde_json::from_str(&s).map_err(RequestError::JSONError) } }) - .then( - |token: Result>| match token { - Err(e) => return Err(e), - Ok(token) => { - if token.access_token.is_none() - || token.token_type.is_none() - || token.expires_in.is_none() - { - Err(Box::new(StringError::new( - "Token response lacks fields".to_string(), - Some(format!("{:?}", token)), - )) - as Box) - } else { - Ok(token.to_oauth_token()) - } + .then(|token: Result| match token { + Err(e) => return Err(e), + Ok(token) => { + if token.access_token.is_none() + || token.token_type.is_none() + || token.expires_in.is_none() + { + Err(RequestError::BadServerResponse(format!( + "Token response lacks fields: {:?}", + token + ))) + } else { + Ok(token.to_oauth_token()) } - }, - ) + } + }) } } @@ -329,7 +317,7 @@ where fn token<'b, I, T>( &mut self, scopes: I, - ) -> Box> + Send> + ) -> Box + Send> where T: AsRef + Ord + 'b, I: Iterator, @@ -348,14 +336,10 @@ where if !token.expired() { return Ok(token); } - return Err(Box::new(StringError::new("expired token in cache", None)) - as Box); - } - Err(e) => return Err(Box::new(e) as Box), - Ok(None) => { - return Err(Box::new(StringError::new("no token in cache", None)) - as Box) + return Err(StringError::new("expired token in cache", None)); } + Err(e) => return Err(StringError::new(format!("cache lookup error: {}", e), None)), + Ok(None) => return Err(StringError::new("no token in cache", None)), } }); @@ -380,9 +364,10 @@ where Box::new(cache_lookup.then(|r| match r { Ok(t) => Box::new(Ok(t).into_future()) - as Box> + Send>, - Err(_) => Box::new(req_token) - as Box> + Send>, + as Box + Send>, + Err(_) => { + Box::new(req_token) as Box + Send> + } })) } diff --git a/src/types.rs b/src/types.rs index 4c670a0..a4d200e 100644 --- a/src/types.rs +++ b/src/types.rs @@ -2,6 +2,7 @@ use chrono::{DateTime, TimeZone, Utc}; use hyper; use std::error::Error; use std::fmt; +use std::io; use std::str::FromStr; use futures::prelude::*; @@ -18,13 +19,37 @@ pub struct JsonError { pub error_uri: Option, } -/// Encapsulates all possible results of the `request_token(...)` operation +/// All possible outcomes of the refresh flow +#[derive(Debug)] +pub enum RefreshResult { + /// Indicates connection failure + Error(hyper::Error), + /// The server did not answer with a new token, providing the server message + RefreshError(String, Option), + /// The refresh operation finished successfully, providing a new `Token` + Success(Token), +} + +/// Encapsulates all possible results of a `poll_token(...)` operation in the Device flow. +#[derive(Debug)] +pub enum PollError { + /// Connection failure - retry if you think it's worth it + HttpError(hyper::Error), + /// Indicates we are expired, including the expiration date + Expired(DateTime), + /// Indicates that the user declined access. String is server response + AccessDenied, + /// Indicates that too many attempts failed. + TimedOut, + /// Other type of error. + Other(String), +} + +/// Encapsulates all possible results of the `token(...)` operation #[derive(Debug)] pub enum RequestError { /// Indicates connection failure ClientError(hyper::Error), - /// Indicates HTTP status failure - HttpError(hyper::http::Error), /// The OAuth client was not found InvalidClient, /// Some requested scopes were invalid. String contains the scopes as part of @@ -33,6 +58,20 @@ pub enum RequestError { /// A 'catch-all' variant containing the server error and description /// First string is the error code, the second may be a more detailed description NegativeServerResponse(String, Option), + /// A malformed server response. + BadServerResponse(String), + /// Error while decoding a JSON response. + JSONError(serde_json::error::Error), + /// Error within user input. + UserError(String), + /// A lower level IO error. + LowLevelError(io::Error), + /// A poll error occurred in the DeviceFlow. + Poll(PollError), + /// An error occurred while refreshing tokens. + Refresh(RefreshResult), + /// Error in token cache layer + Cache(Box), } impl From for RequestError { @@ -41,12 +80,6 @@ impl From for RequestError { } } -impl From for RequestError { - fn from(error: hyper::http::Error) -> RequestError { - RequestError::HttpError(error) - } -} - impl From for RequestError { fn from(value: JsonError) -> RequestError { match &*value.error { @@ -65,7 +98,6 @@ impl fmt::Display for RequestError { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { match *self { RequestError::ClientError(ref err) => err.fmt(f), - RequestError::HttpError(ref err) => err.fmt(f), RequestError::InvalidClient => "Invalid Client".fmt(f), RequestError::InvalidScope(ref scope) => writeln!(f, "Invalid Scope: '{}'", scope), RequestError::NegativeServerResponse(ref error, ref desc) => { @@ -75,6 +107,17 @@ impl fmt::Display for RequestError { } "\n".fmt(f) } + RequestError::BadServerResponse(ref s) => s.fmt(f), + RequestError::JSONError(ref e) => format!( + "JSON Error; this might be a bug with unexpected server responses! {}", + e + ) + .fmt(f), + RequestError::UserError(ref s) => s.fmt(f), + RequestError::LowLevelError(ref e) => e.fmt(f), + RequestError::Poll(ref pe) => pe.fmt(f), + RequestError::Refresh(ref rr) => format!("{:?}", rr).fmt(f), + RequestError::Cache(ref e) => e.fmt(f), } } } @@ -83,7 +126,8 @@ impl Error for RequestError { fn source(&self) -> Option<&(dyn Error + 'static)> { match *self { RequestError::ClientError(ref err) => Some(err), - RequestError::HttpError(ref err) => Some(err), + RequestError::LowLevelError(ref err) => Some(err), + RequestError::JSONError(ref err) => Some(err), _ => None, } } @@ -199,7 +243,7 @@ pub trait GetToken { fn token<'b, I, T>( &mut self, scopes: I, - ) -> Box> + Send> + ) -> Box + Send> where T: AsRef + Ord + 'b, I: Iterator; From 2d94e043d80ee8b5b2e69b5b09099f991287cae2 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Sat, 22 Jun 2019 22:03:26 +0200 Subject: [PATCH 41/41] doc(misc): Add some small missing pieces. --- src/authenticator_delegate.rs | 3 +++ src/service_account.rs | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/authenticator_delegate.rs b/src/authenticator_delegate.rs index 132b511..9c1e22a 100644 --- a/src/authenticator_delegate.rs +++ b/src/authenticator_delegate.rs @@ -108,6 +108,8 @@ pub trait AuthenticatorDelegate: Clone { } } +/// FlowDelegate methods are called when an OAuth flow needs to ask the application what to do in +/// certain cases. pub trait FlowDelegate: Clone { /// Called if the request code is expired. You will have to start over in this case. /// This will be the last call the delegate receives. @@ -192,6 +194,7 @@ pub trait FlowDelegate: Clone { pub struct DefaultAuthenticatorDelegate; impl AuthenticatorDelegate for DefaultAuthenticatorDelegate {} +/// Uses all default implementations in the FlowDelegate trait. #[derive(Clone)] pub struct DefaultFlowDelegate; impl FlowDelegate for DefaultFlowDelegate {} diff --git a/src/service_account.rs b/src/service_account.rs index 6eb4411..7dd3fa8 100644 --- a/src/service_account.rs +++ b/src/service_account.rs @@ -184,7 +184,8 @@ where } /// A token source (`GetToken`) yielding OAuth tokens for services that use ServiceAccount authorization. -/// This token source caches token and automatically renews expired ones. +/// This token source caches token and automatically renews expired ones, meaning you do not need +/// (and you also should not) use this with `Authenticator`. Just use it directly. #[derive(Clone)] pub struct ServiceAccountAccess { client: hyper::Client,