diff --git a/src/helper.rs b/src/authenticator.rs similarity index 56% rename from src/helper.rs rename to src/authenticator.rs index 2b4c2cd..510a8f4 100644 --- a/src/helper.rs +++ b/src/authenticator.rs @@ -6,14 +6,13 @@ use std::cmp::min; use std::error::Error; use std::fmt; use std::convert::From; -use std::io; -use common::{Token, FlowType, ApplicationSecret}; -use device::{PollInformation, RequestError, DeviceFlow, PollError}; +use authenticator_delegate::{AuthenticatorDelegate, PollError, PollInformation}; +use common::{RequestError, Token, FlowType, ApplicationSecret}; +use device::DeviceFlow; use installed::{InstalledFlow, InstalledFlowReturnMethod}; use refresh::{RefreshResult, RefreshFlow}; -use storage::{TokenStorage}; -use chrono::{DateTime, UTC, Local}; +use storage::TokenStorage; use std::time::Duration; use hyper; @@ -61,9 +60,7 @@ impl StringError { error.push_str(&*d); } - StringError { - error: error, - } + StringError { error: error } } } @@ -90,18 +87,17 @@ impl Error for StringError { /// if no user is involved. pub trait GetToken { fn token<'b, I, T>(&mut self, scopes: I) -> Result> - where T: AsRef + Ord + 'b, - I: IntoIterator; + where T: AsRef + Ord + 'b, + I: IntoIterator; fn api_key(&mut self) -> Option; } impl Authenticator - where D: AuthenticatorDelegate, - S: TokenStorage, - C: BorrowMut { - - + where D: AuthenticatorDelegate, + S: TokenStorage, + C: BorrowMut +{ /// Returns a new `Authenticator` instance /// /// # Arguments @@ -116,8 +112,11 @@ impl Authenticator /// 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: C, storage: S, flow_type: Option) - -> Authenticator { + delegate: D, + client: C, + storage: S, + flow_type: Option) + -> Authenticator { Authenticator { flow_type: flow_type.unwrap_or(FlowType::Device), delegate: delegate, @@ -142,9 +141,7 @@ impl Authenticator } let mut flow = InstalledFlow::new(self.client.borrow_mut(), installed_type); - flow.obtain_token(&mut self.delegate, - &self.secret, - scopes.iter()) + flow.obtain_token(&mut self.delegate, &self.secret, scopes.iter()) } fn retrieve_device_token(&mut self, scopes: &Vec<&str>) -> Result> { @@ -154,33 +151,36 @@ impl Authenticator let pi: PollInformation; loop { let res = flow.request_code(&self.secret.client_id, - &self.secret.client_secret, scopes.iter()); + &self.secret.client_secret, + scopes.iter()); pi = match res { - Err(res_err) => { - match res_err { - 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), + Err(res_err) => { + match res_err { + RequestError::HttpError(err) => { + match self.delegate.connection_error(&err) { + Retry::Abort | Retry::Skip => { + return Err(Box::new(StringError::from(&err as &Error))) } - }, - 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)) + Retry::After(d) => sleep(d), } - }; - continue - }, - Ok(pi) => { - self.delegate.present_user_code(&pi); - pi - } - }; - break + } + 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 @@ -191,50 +191,56 @@ impl Authenticator match poll_err { &&PollError::HttpError(ref err) => { match self.delegate.connection_error(err) { - Retry::Abort|Retry::Skip - => return Err(Box::new(StringError::from(err as &Error))), + 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))) - }, + return Err(Box::new(StringError::from(pts))); + } &&PollError::AccessDenied => { self.delegate.denied(); - return Err(Box::new(StringError::from(pts))) - }, + 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) + } + 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: BorrowMut { - + where D: AuthenticatorDelegate, + S: TokenStorage, + C: BorrowMut +{ /// 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) -> Result> - where T: AsRef + Ord + 'b, - I: IntoIterator { + 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::>(); + .map(|s| s.as_ref()) + .collect::>(); sv.sort(); let mut sh = SipHasher::new(); &sv.hash(&mut sh); @@ -251,9 +257,9 @@ impl GetToken for Authenticator let mut rf = RefreshFlow::new(self.client.borrow_mut()); loop { match *rf.refresh_token(self.flow_type, - &self.secret.client_id, - &self.secret.client_secret, - &t.refresh_token) { + &self.secret.client_id, + &self.secret.client_secret, + &t.refresh_token) { RefreshResult::Error(ref err) => { match self.delegate.connection_error(err) { Retry::Abort|Retry::Skip => @@ -262,21 +268,22 @@ impl GetToken for Authenticator None))), 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 Err(Box::new( - StringError::new(storage_err + err_str, err_description.as_ref()))) - }, + let storage_err = match self.storage + .set(scope_key, &scopes, None) { + Ok(_) => String::new(), + Err(err) => err.to_string(), + }; + return Err(Box::new(StringError::new(storage_err + err_str, + err_description.as_ref()))); + } RefreshResult::Success(ref new_t) => { t = new_t.clone(); loop { - if let Err(err) = self.storage.set(scope_key, &scopes, Some(t.clone())) { + 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 Err(Box::new(err)), @@ -298,16 +305,15 @@ impl GetToken for Authenticator 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 { - FlowType::Device => self.retrieve_device_token(&scopes), - FlowType::InstalledInteractive => self.do_installed_flow(&scopes), - FlowType::InstalledRedirect(_) => self.do_installed_flow(&scopes), - } - { + match match self.flow_type { + FlowType::Device => self.retrieve_device_token(&scopes), + 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())) { + 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 Err(Box::new(err)), @@ -320,127 +326,32 @@ impl GetToken for Authenticator break; }// end attempt to save Ok(token) - }, + } Err(err) => 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 => Err(Box::new(err)), Retry::After(d) => { sleep(d); - continue + continue; } } - }, - }// end match + } + };// end match }// end loop } fn api_key(&mut self) -> Option { if self.secret.client_id.len() == 0 { - return None + return None; } Some(self.secret.client_id.clone()) } } - -/// A partially implemented trait to interact with the `Authenticator` -/// -/// 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 { - - /// Called whenever there is an HttpError, usually if there are network problems. - /// - /// Return retry information. - fn connection_error(&mut self, &hyper::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. - /// if `is_set` is true, the failure resulted from `TokenStorage.set(...)`. Otherwise, - /// it was `TokenStorage.get(...)` - fn token_storage_failure(&mut self, is_set: bool, _: &Error) -> Retry { - let _ = is_set; - Retry::Abort - } - - /// 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. - fn token_refresh_failed(&mut self, error: &String, error_description: &Option) { - { let _ = error; } - { let _ = error_description; } - } - - /// 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. - /// - /// If the returned `Retry` variant is a duration. - /// # Notes - /// * Only used in `DeviceFlow`. Return value will only be used if it - /// is larger than the interval desired by the server. - fn pending(&mut self, &PollInformation) -> Retry { - Retry::After(Duration::from_secs(5)) - } - - /// The server has returned a `user_code` which must be shown to the user, - /// along with the `verification_url`. - /// # Notes - /// * Will be called exactly once, provided we didn't abort during `request_code` phase. - /// * Will only be called if the Authenticator's flow_type is `FlowType::Device`. - fn present_user_code(&mut self, pi: &PollInformation) { - println!("Please enter {} at {} and grant access to this application", - pi.user_code, - pi.verification_url); - println!("Do not close this application until you either denied or granted access."); - println!("You have time until {}.", - pi.expires_at.with_timezone(&Local)); - } - - /// Only method currently used by the InstalledFlow. - /// We need the user to navigate to a URL using their browser and potentially paste back a code - /// (or maybe not). Whether they have to enter a code depends on the InstalledFlowReturnMethod - /// used. - fn present_user_url(&mut self, url: &String, need_code: bool) -> Option { - if need_code { - println!("Please direct your browser to {}, follow the instructions and enter the \ - code displayed here: ", - url); - - let mut code = String::new(); - io::stdin().read_line(&mut code).ok().map(|_| code) - } else { - println!("Please direct your browser to {} and follow the instructions displayed \ - there.", - url); - None - } - } -} - -/// Uses all default implementations by AuthenticatorDelegate, and makes the trait's -/// implementation usable in the first place. -pub struct DefaultAuthenticatorDelegate; -impl AuthenticatorDelegate for DefaultAuthenticatorDelegate {} - /// A utility type to indicate how operations DeviceFlowHelper operations should be retried pub enum Retry { /// Signal you don't want to retry @@ -458,7 +369,7 @@ mod tests { use super::*; use super::super::device::tests::MockGoogleAuth; use super::super::common::tests::SECRET; - use super::super::common::{ConsoleApplicationSecret}; + use super::super::common::ConsoleApplicationSecret; use storage::MemoryStorage; use std::default::Default; use hyper; diff --git a/src/authenticator_delegate.rs b/src/authenticator_delegate.rs new file mode 100644 index 0000000..7be4e34 --- /dev/null +++ b/src/authenticator_delegate.rs @@ -0,0 +1,153 @@ +use hyper; + +use std::fmt; +use std::io; +use std::error::Error; + +use authenticator::Retry; +use common::RequestError; + +use chrono::{DateTime, Local, UTC}; +use std::time::Duration; + +/// Contains state of pending authentication requests +#[derive(Clone, Debug, PartialEq)] +pub struct PollInformation { + /// Code the user must enter ... + pub user_code: String, + /// ... at the verification URL + pub verification_url: String, + + /// The `user_code` expires at the given time + /// It's the time the user has left to authenticate your application + pub expires_at: DateTime, + /// The interval in which we may poll for a status change + /// The server responds with errors of we poll too fast. + pub interval: Duration, +} + +impl fmt::Display for PollInformation { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + writeln!(f, "Proceed with polling until {}", self.expires_at) + } +} + +/// 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, +} + +impl fmt::Display for PollError { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + match *self { + 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), + } + } +} + + + +/// A partially implemented trait to interact with the `Authenticator` +/// +/// 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 { + /// Called whenever there is an HttpError, usually if there are network problems. + /// + /// Return retry information. + fn connection_error(&mut self, &hyper::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. + /// if `is_set` is true, the failure resulted from `TokenStorage.set(...)`. Otherwise, + /// it was `TokenStorage.get(...)` + fn token_storage_failure(&mut self, is_set: bool, _: &Error) -> Retry { + let _ = is_set; + Retry::Abort + } + + /// 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. + fn token_refresh_failed(&mut self, error: &String, error_description: &Option) { + { + let _ = error; + } + { + let _ = error_description; + } + } + + /// 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. + /// + /// If the returned `Retry` variant is a duration. + /// # Notes + /// * Only used in `DeviceFlow`. Return value will only be used if it + /// is larger than the interval desired by the server. + fn pending(&mut self, &PollInformation) -> Retry { + Retry::After(Duration::from_secs(5)) + } + + /// The server has returned a `user_code` which must be shown to the user, + /// along with the `verification_url`. + /// # Notes + /// * Will be called exactly once, provided we didn't abort during `request_code` phase. + /// * Will only be called if the Authenticator's flow_type is `FlowType::Device`. + fn present_user_code(&mut self, pi: &PollInformation) { + println!("Please enter {} at {} and grant access to this application", + pi.user_code, + pi.verification_url); + println!("Do not close this application until you either denied or granted access."); + println!("You have time until {}.", + pi.expires_at.with_timezone(&Local)); + } + + /// Only method currently used by the InstalledFlow. + /// We need the user to navigate to a URL using their browser and potentially paste back a code + /// (or maybe not). Whether they have to enter a code depends on the InstalledFlowReturnMethod + /// used. + fn present_user_url(&mut self, url: &String, need_code: bool) -> Option { + if need_code { + println!("Please direct your browser to {}, follow the instructions and enter the \ + code displayed here: ", + url); + + let mut code = String::new(); + io::stdin().read_line(&mut code).ok().map(|_| code) + } else { + println!("Please direct your browser to {} and follow the instructions displayed \ + there.", + url); + None + } + } +} + +/// Uses all default implementations by AuthenticatorDelegate, and makes the trait's +/// implementation usable in the first place. +pub struct DefaultAuthenticatorDelegate; +impl AuthenticatorDelegate for DefaultAuthenticatorDelegate {} diff --git a/src/common.rs b/src/common.rs index 46c1f71..d73d100 100644 --- a/src/common.rs +++ b/src/common.rs @@ -15,6 +15,51 @@ pub struct JsonError { pub error_uri: Option, } +/// Encapsulates all possible results of the `request_token(...)` operation +pub enum RequestError { + /// Indicates connection failure + HttpError(hyper::Error), + /// The OAuth client was not found + InvalidClient, + /// Some requested scopes were invalid. String contains the scopes as part of + /// the server error message + InvalidScope(String), + /// 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), +} + +impl From for RequestError { + fn from(value: JsonError) -> RequestError { + match &*value.error { + "invalid_client" => RequestError::InvalidClient, + "invalid_scope" => { + RequestError::InvalidScope(value.error_description + .unwrap_or("no description provided".to_string())) + } + _ => RequestError::NegativeServerResponse(value.error, value.error_description), + } + } +} + +impl fmt::Display for RequestError { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + match *self { + 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) => { + try!(error.fmt(f)); + if let &Some(ref desc) = desc { + try!(write!(f, ": {}", desc)); + } + "\n".fmt(f) + } + } + } +} + + /// Represents all implemented token types #[derive(Clone, PartialEq, Debug)] pub enum TokenType { @@ -25,7 +70,7 @@ pub enum TokenType { impl AsRef for TokenType { fn as_ref(&self) -> &'static str { match *self { - TokenType::Bearer => "Bearer" + TokenType::Bearer => "Bearer", } } } @@ -35,7 +80,7 @@ impl FromStr for TokenType { fn from_str(s: &str) -> Result { match s { "Bearer" => Ok(TokenType::Bearer), - _ => Err(()) + _ => Err(()), } } } @@ -47,7 +92,7 @@ pub struct Scheme { /// The type of our access token pub token_type: TokenType, /// The token returned by one of the Authorization Flows - pub access_token: String + pub access_token: String, } impl hyper::header::Scheme for Scheme { @@ -65,11 +110,16 @@ impl FromStr for Scheme { fn from_str(s: &str) -> Result { let parts: Vec<&str> = s.split(' ').collect(); if parts.len() != 2 { - return Err("Expected two parts: ") + return Err("Expected two parts: "); } match ::from_str(parts[0]) { - Ok(t) => Ok(Scheme { token_type: t, access_token: parts[1].to_string() }), - Err(_) => Err("Couldn't parse token type") + Ok(t) => { + Ok(Scheme { + token_type: t, + access_token: parts[1].to_string(), + }) + } + Err(_) => Err("Couldn't parse token type"), } } } @@ -103,7 +153,6 @@ pub struct Token { } impl Token { - /// Returns true if we are expired. /// /// # Panics @@ -117,14 +166,16 @@ impl Token { /// Returns a DateTime object representing our expiry date. pub fn expiry_date(&self) -> DateTime { - UTC.timestamp(self.expires_in_timestamp.expect("Tokens without an absolute expiry are invalid"), 0) + UTC.timestamp(self.expires_in_timestamp + .expect("Tokens without an absolute expiry are invalid"), + 0) } /// Adjust our stored expiry format to be absolute, using the current time. pub fn set_expiry_absolute(&mut self) -> &mut Token { if self.expires_in_timestamp.is_some() { assert!(self.expires_in.is_none()); - return self + return self; } self.expires_in_timestamp = Some(UTC::now().timestamp() + self.expires_in.unwrap()); @@ -182,7 +233,7 @@ pub struct ApplicationSecret { /// as ID tokens, signed by the authentication provider. pub auth_provider_x509_cert_url: Option, /// The URL of the public x509 certificate, used to verify JWTs signed by the client. - pub client_x509_cert_url: Option + pub client_x509_cert_url: Option, } /// A type to facilitate reading and writing the json secret file @@ -190,7 +241,7 @@ pub struct ApplicationSecret { #[derive(Deserialize, Serialize, Default)] pub struct ConsoleApplicationSecret { pub web: Option, - pub installed: Option + pub installed: Option, } @@ -199,7 +250,13 @@ pub mod tests { use super::*; use hyper; - pub 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\"}}"; + pub 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 console_secret() { @@ -212,15 +269,20 @@ pub mod tests { #[test] fn schema() { - let s = Scheme {token_type: TokenType::Bearer, access_token: "foo".to_string() }; + let s = Scheme { + token_type: TokenType::Bearer, + access_token: "foo".to_string(), + }; let mut headers = hyper::header::Headers::new(); headers.set(hyper::header::Authorization(s)); - assert_eq!(headers.to_string(), "Authorization: Bearer foo\r\n".to_string()); + assert_eq!(headers.to_string(), + "Authorization: Bearer foo\r\n".to_string()); } #[test] fn parse_schema() { - let auth: hyper::header::Authorization = hyper::header::Header::parse_header(&[b"Bearer foo".to_vec()]).unwrap(); + let auth: hyper::header::Authorization = + hyper::header::Header::parse_header(&[b"Bearer foo".to_vec()]).unwrap(); assert_eq!(auth.0.token_type, TokenType::Bearer); assert_eq!(auth.0.access_token, "foo".to_string()); } diff --git a/src/device.rs b/src/device.rs index 3e1e7ac..b05f790 100644 --- a/src/device.rs +++ b/src/device.rs @@ -1,19 +1,19 @@ use std::iter::IntoIterator; use std::time::Duration; use std::default::Default; -use std::fmt; use hyper; use hyper::header::ContentType; use url::form_urlencoded; use itertools::Itertools; use serde_json as json; -use chrono::{DateTime,UTC, self}; +use chrono::{self, UTC}; use std::borrow::BorrowMut; use std::io::Read; use std::i64; -use common::{Token, FlowType, Flow, JsonError}; +use common::{Token, FlowType, Flow, RequestError, JsonError}; +use authenticator_delegate::{PollError, PollInformation}; pub const GOOGLE_TOKEN_URL: &'static str = "https://accounts.google.com/o/oauth2/token"; @@ -45,100 +45,9 @@ impl Flow for DeviceFlow { FlowType::Device } } - - -/// Contains state of pending authentication requests -#[derive(Clone, Debug, PartialEq)] -pub struct PollInformation { - /// Code the user must enter ... - pub user_code: String, - /// ... at the verification URL - pub verification_url: String, - - /// The `user_code` expires at the given time - /// It's the time the user has left to authenticate your application - pub expires_at: DateTime, - /// The interval in which we may poll for a status change - /// The server responds with errors of we poll too fast. - pub interval: Duration, -} - -impl fmt::Display for PollInformation { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - writeln!(f, "Proceed with polling until {}", self.expires_at) - } -} - -/// Encapsulates all possible results of the `request_token(...)` operation -pub enum RequestError { - /// Indicates connection failure - HttpError(hyper::Error), - /// The OAuth client was not found - InvalidClient, - /// Some requested scopes were invalid. String contains the scopes as part of - /// the server error message - InvalidScope(String), - /// 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), -} - -impl From for RequestError { - fn from(value: JsonError) -> RequestError { - match &*value.error { - "invalid_client" => RequestError::InvalidClient, - "invalid_scope" => RequestError::InvalidScope( - value.error_description.unwrap_or("no description provided".to_string()) - ), - _ => RequestError::NegativeServerResponse(value.error, value.error_description), - } - } -} - -impl fmt::Display for RequestError { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - match *self { - 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) => { - try!(error.fmt(f)); - if let &Some(ref desc) = desc { - try!(write!(f, ": {}", desc)); - } - "\n".fmt(f) - }, - } - } -} - -/// 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, -} - -impl fmt::Display for PollError { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - match *self { - 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), - } - } -} - - impl DeviceFlow - where C: BorrowMut { - + where C: BorrowMut +{ /// # Examples /// ```test_harness /// extern crate hyper; @@ -175,30 +84,36 @@ impl DeviceFlow /// * 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, client_id: &str, client_secret: &str, scopes: I) - -> Result - where T: AsRef + 'b, - I: IntoIterator { + pub fn request_code<'b, T, I>(&mut self, + client_id: &str, + client_secret: &str, + scopes: I) + -> Result + where T: AsRef + 'b, + I: IntoIterator + { 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::serialize( - &[("client_id", client_id), - ("scope", scopes.into_iter() - .map(|s| s.as_ref()) - .intersperse(" ") - .collect::() - .as_ref())]); + let req = form_urlencoded::serialize(&[("client_id", client_id), + ("scope", + scopes.into_iter() + .map(|s| s.as_ref()) + .intersperse(" ") + .collect::() + .as_ref())]); // note: works around bug in rustlang // https://github.com/rust-lang/rust/issues/22252 - let ret = match self.client.borrow_mut().post(FlowType::Device.as_ref()) - .header(ContentType("application/x-www-form-urlencoded".parse().unwrap())) - .body(&*req) - .send() { + let ret = match self.client + .borrow_mut() + .post(FlowType::Device.as_ref()) + .header(ContentType("application/x-www-form-urlencoded".parse().unwrap())) + .body(&*req) + .send() { Err(err) => { return Err(RequestError::HttpError(err)); } @@ -219,10 +134,8 @@ impl DeviceFlow // check for error match json::from_str::(&json_str) { - Err(_) => {}, // ignore, move on - Ok(res) => { - return Err(RequestError::from(res)) - } + Err(_) => {} // ignore, move on + Ok(res) => return Err(RequestError::from(res)), } let decoded: JsonData = json::from_str(&json_str).unwrap(); @@ -231,7 +144,7 @@ impl DeviceFlow let pi = PollInformation { user_code: decoded.user_code, verification_url: decoded.verification_url, - expires_at: UTC::now() + chrono::Duration::seconds(decoded.expires_in), + 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())); @@ -265,33 +178,35 @@ impl DeviceFlow 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) => + 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"), }; 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()) + return Err(&self.error.as_ref().unwrap()); } // We should be ready for a new request - let req = form_urlencoded::serialize( - &[("client_id", &self.id[..]), - ("client_secret", &self.secret), - ("code", &self.device_code), - ("grant_type", "http://oauth.net/grant_type/device/1.0")]); + let req = form_urlencoded::serialize(&[("client_id", &self.id[..]), + ("client_secret", &self.secret), + ("code", &self.device_code), + ("grant_type", + "http://oauth.net/grant_type/device/1.0")]); - let json_str = - match self.client.borrow_mut().post(GOOGLE_TOKEN_URL) - .header(ContentType("application/x-www-form-urlencoded".parse().unwrap())) - .body(&*req) - .send() { + let json_str = match self.client + .borrow_mut() + .post(GOOGLE_TOKEN_URL) + .header(ContentType("application/x-www-form-urlencoded".parse().unwrap())) + .body(&*req) + .send() { Err(err) => { self.error = Some(PollError::HttpError(err)); return Err(self.error.as_ref().unwrap()); @@ -305,18 +220,18 @@ impl DeviceFlow #[derive(Deserialize)] struct JsonError { - error: String + error: String, } match json::from_str::(&json_str) { - Err(_) => {}, // ignore, move on, it's not an error + 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()) - }, + return Err(self.error.as_ref().unwrap()); + } "authorization_pending" => return Ok(None), _ => panic!("server message '{}' not understood", res.error), }; @@ -329,7 +244,7 @@ impl DeviceFlow let res = Ok(Some(t.clone())); self.state = Some(DeviceFlowState::Success(t)); - return res + return res; } } @@ -356,24 +271,23 @@ pub mod tests { \"verification_url\" : \"http://www.google.com/device\",\r\n\ \"expires_in\" : 1800,\r\n\ \"interval\" : 0\r\n\ - }".to_string()); + }" + .to_string()); c.0.content.push("HTTP/1.1 200 OK\r\n\ Server: BOGUS\r\n\ \r\n\ {\r\n\ \"error\" : \"authorization_pending\"\r\n\ - }".to_string()); + }" + .to_string()); - c.0.content.push("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\ - \"refresh_token\":\"1/6BMfW9j53gdGImsixUH6kU5RsR4zwI9lUVX-tqf8JXQ\"\r\n\ - }".to_string()); + c.0.content.push("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}" + .to_string()); c } @@ -400,18 +314,17 @@ pub mod tests { } match flow.poll_token() { - Ok(None) => {}, + Ok(None) => {} _ => unreachable!(), } - let t = - match flow.poll_token() { - Ok(Some(t)) => { - assert_eq!(t.access_token, "1/fFAGRNJru1FTz70BzhT3Zg"); - t - }, - _ => 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 diff --git a/src/installed.rs b/src/installed.rs index 3097ecd..c6f2022 100644 --- a/src/installed.rs +++ b/src/installed.rs @@ -20,7 +20,7 @@ use url::form_urlencoded; use url::percent_encoding::{percent_encode, QUERY_ENCODE_SET}; use common::{ApplicationSecret, Token}; -use helper::AuthenticatorDelegate; +use authenticator_delegate::AuthenticatorDelegate; const OOB_REDIRECT_URI: &'static str = "urn:ietf:wg:oauth:2.0:oob"; @@ -76,7 +76,8 @@ pub enum InstalledFlowReturnMethod { HTTPRedirect(u32), } -impl InstalledFlow where C: BorrowMut +impl InstalledFlow + where C: BorrowMut { /// Starts a new Installed App auth flow. /// If HTTPRedirect is chosen as method and the server can't be started, the flow falls @@ -99,9 +100,8 @@ impl InstalledFlow where C: BorrowMut Result::Err(_) => default, Result::Ok(server) => { let (tx, rx) = channel(); - let listening = server.handle(InstalledFlowHandler { - auth_code_snd: Mutex::new(tx), - }); + let listening = + server.handle(InstalledFlowHandler { auth_code_snd: Mutex::new(tx) }); match listening { Result::Err(_) => default, @@ -146,7 +146,7 @@ impl InstalledFlow where C: BorrowMut expires_in: tokens.expires_in, expires_in_timestamp: None, }; - + token.set_expiry_absolute(); Result::Ok(token) } else { @@ -154,7 +154,7 @@ impl InstalledFlow where C: BorrowMut format!("Token API error: {} {}", tokens.error.unwrap_or("".to_string()), tokens.error_description - .unwrap_or("".to_string())) + .unwrap_or("".to_string())) .as_str()); Result::Err(Box::new(err)) } @@ -230,13 +230,12 @@ impl InstalledFlow where C: BorrowMut ("grant_type".to_string(), "authorization_code".to_string())]); - let result: Result = - self.client - .borrow_mut() - .post(&appsecret.token_uri) - .body(&body) - .header(header::ContentType("application/x-www-form-urlencoded".parse().unwrap())) - .send(); + let result: Result = self.client + .borrow_mut() + .post(&appsecret.token_uri) + .body(&body) + .header(header::ContentType("application/x-www-form-urlencoded".parse().unwrap())) + .send(); let mut resp = String::new(); @@ -292,9 +291,10 @@ impl server::Handler for InstalledFlowHandler { } else { self.handle_url(url.unwrap()); *rp.status_mut() = status::StatusCode::Ok; - let _ = rp.send("SuccessYou may now \ - close this window." - .as_ref()); + let _ = + rp.send("SuccessYou may now \ + close this window." + .as_ref()); } } _ => { diff --git a/src/lib.rs.in b/src/lib.rs.in index 4723c55..4dcadd2 100644 --- a/src/lib.rs.in +++ b/src/lib.rs.in @@ -12,17 +12,19 @@ extern crate mime; extern crate url; extern crate itertools; +mod authenticator_delegate; +mod authenticator; mod device; mod storage; mod installed; -mod helper; mod refresh; mod common; -pub use device::{DeviceFlow, PollInformation, PollError}; +pub use device::DeviceFlow; pub use refresh::{RefreshFlow, RefreshResult}; pub use common::{Token, FlowType, ApplicationSecret, ConsoleApplicationSecret, Scheme, TokenType}; pub use installed::{InstalledFlow, InstalledFlowReturnMethod}; pub use storage::{TokenStorage, NullStorage, MemoryStorage, DiskTokenStorage}; -pub use helper::{Authenticator, AuthenticatorDelegate, - Retry, DefaultAuthenticatorDelegate, GetToken}; +pub use authenticator::{Authenticator, Retry, GetToken}; +pub use authenticator_delegate::{AuthenticatorDelegate, DefaultAuthenticatorDelegate, PollError, + PollInformation}; diff --git a/src/refresh.rs b/src/refresh.rs index 6c18183..30edf85 100644 --- a/src/refresh.rs +++ b/src/refresh.rs @@ -11,7 +11,7 @@ use std::borrow::BorrowMut; use std::io::Read; /// Implements the [Outh2 Refresh Token Flow](https://developers.google.com/youtube/v3/guides/authentication#devices). -/// +/// /// 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. @@ -32,8 +32,8 @@ pub enum RefreshResult { } impl RefreshFlow - where C: BorrowMut { - + where C: BorrowMut +{ pub fn new(client: C) -> RefreshFlow { RefreshFlow { client: client, @@ -44,7 +44,7 @@ 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 - /// or your authorization was revoked. Therefore no further attempt shall be made, + /// or your authorization was revoked. Therefore no further attempt shall be made, /// and you will have to re-authorize using the `DeviceFlow` /// /// # Arguments @@ -52,31 +52,32 @@ impl RefreshFlow /// your refresh_token in the first place. /// * `client_id` & `client_secret` - as obtained when [registering your application](https://developers.google.com/youtube/registering_an_application) /// * `refresh_token` - obtained during previous call to `DeviceFlow::poll_token()` or equivalent - /// + /// /// # Examples /// Please see the crate landing page for an example. - pub fn refresh_token(&mut self, - flow_type: FlowType, - client_id: &str, - client_secret: &str, - refresh_token: &str) -> &RefreshResult { + pub fn refresh_token(&mut self, + flow_type: FlowType, + client_id: &str, + client_secret: &str, + refresh_token: &str) + -> &RefreshResult { let _ = flow_type; if let RefreshResult::Success(_) = self.result { return &self.result; } - let req = form_urlencoded::serialize( - &[("client_id", client_id), - ("client_secret", client_secret), - ("refresh_token", refresh_token), - ("grant_type", "refresh_token")]); + let req = form_urlencoded::serialize(&[("client_id", client_id), + ("client_secret", client_secret), + ("refresh_token", refresh_token), + ("grant_type", "refresh_token")]); - let json_str = - match self.client.borrow_mut().post(GOOGLE_TOKEN_URL) - .header(ContentType("application/x-www-form-urlencoded".parse().unwrap())) - .body(&*req) - .send() { - Err(err) => { + let json_str = match self.client + .borrow_mut() + .post(GOOGLE_TOKEN_URL) + .header(ContentType("application/x-www-form-urlencoded".parse().unwrap())) + .body(&*req) + .send() { + Err(err) => { self.result = RefreshResult::Error(err); return &self.result; } @@ -95,7 +96,7 @@ impl RefreshFlow } match json::from_str::(&json_str) { - Err(_) => {}, + Err(_) => {} Ok(res) => { self.result = RefreshResult::RefreshError(res.error, res.error_description); return &self.result; @@ -137,7 +138,8 @@ mod tests { \"access_token\":\"1/fFAGRNJru1FTz70BzhT3Zg\",\r\n\ \"expires_in\":3920,\r\n\ \"token_type\":\"Bearer\"\r\n\ - }".to_string()); + }" + .to_string()); c } @@ -154,17 +156,15 @@ mod tests { #[test] fn refresh_flow() { let mut c = hyper::Client::with_connector(::default()); - let mut flow = RefreshFlow::new( - &mut c); + let mut flow = RefreshFlow::new(&mut c); - match *flow.refresh_token(FlowType::Device, - "bogus", "secret", "bogus_refresh_token") { + match *flow.refresh_token(FlowType::Device, "bogus", "secret", "bogus_refresh_token") { RefreshResult::Success(ref t) => { assert_eq!(t.access_token, "1/fFAGRNJru1FTz70BzhT3Zg"); assert!(!t.expired()); - }, - _ => unreachable!() + } + _ => unreachable!(), } } } diff --git a/src/storage.rs b/src/storage.rs index 28f4a2c..2794cab 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -1,8 +1,7 @@ -/* - * partially (c) 2016 Google Inc. (Lewin Bormann, lewinb@google.com) - * - * See project root for licensing information. - */ +// partially (c) 2016 Google Inc. (Lewin Bormann, lewinb@google.com) +// +// See project root for licensing information. +// extern crate serde_json; @@ -172,10 +171,10 @@ impl DiskTokenStorage { } let mut f = try!(fs::OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(&self.location)); + .create(true) + .write(true) + .truncate(true) + .open(&self.location)); f.write(serialized.as_ref()).map(|_| ()) } }