diff --git a/Cargo.toml b/Cargo.toml index b10b455f1..6630e9b52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "yup-oauth2" -version = "0.3.4" +version = "0.3.5" authors = ["Sebastian Thiel "] repository = "https://github.com/Byron/yup-oauth2" description = "A partial oauth2 implementation, providing the 'device' authorization flow" diff --git a/examples/auth.rs b/examples/auth.rs index a2b742025..6c183e219 100644 --- a/examples/auth.rs +++ b/examples/auth.rs @@ -1,4 +1,4 @@ -#![feature(collections, old_io, std_misc, exit_status)] +#![feature(collections, thread_sleep, std_misc, exit_status)] #![allow(deprecated)] extern crate yup_oauth2 as oauth2; extern crate yup_hyper_mock as mock; @@ -13,7 +13,7 @@ use getopts::{HasArg,Options,Occur,Fail}; use std::env; use std::default::Default; use std::time::Duration; -use std::old_io::timer::sleep; +use std::thread::sleep; fn usage(program: &str, opts: &Options, err: Option) { @@ -82,15 +82,17 @@ fn main() { connector: hyper::net::HttpConnector(None) }); - if let Some(t) = oauth2::Authenticator::new(&secret, StdoutHandler, client, - oauth2::NullStorage, None) - .token(&m.free) { + match oauth2::Authenticator::new(&secret, StdoutHandler, client, + oauth2::NullStorage, None).token(&m.free) { + Ok(t) => { println!("Authentication granted !"); println!("You should store the following information for use, or revoke it."); println!("All dates are given in UTC."); println!("{:?}", t); - } else { - println!("Invalid client id, invalid scope, user denied access or request expired"); - env::set_exit_status(10); + }, + Err(err) => { + println!("Access token wasn't obtained: {}", err); + env::set_exit_status(10); + } } } \ No newline at end of file diff --git a/src/common.rs b/src/common.rs index fc01caf37..aad9f4c03 100644 --- a/src/common.rs +++ b/src/common.rs @@ -9,6 +9,12 @@ pub trait Flow : MarkerTrait { fn type_id() -> FlowType; } +#[derive(RustcDecodable)] +pub struct JsonError { + pub error: String, + pub error_description: Option, +} + /// Represents all implemented token types #[derive(Clone, PartialEq, Debug)] pub enum TokenType { diff --git a/src/device.rs b/src/device.rs index 28800b3cc..900cd56fb 100644 --- a/src/device.rs +++ b/src/device.rs @@ -2,6 +2,7 @@ use std::iter::IntoIterator; use std::time::Duration; use std::default::Default; use std::rc::Rc; +use std::fmt; use hyper; use hyper::header::ContentType; @@ -12,7 +13,7 @@ use chrono::{DateTime,UTC}; use std::borrow::BorrowMut; use std::io::Read; -use common::{Token, FlowType, Flow}; +use common::{Token, FlowType, Flow, JsonError}; pub const GOOGLE_TOKEN_URL: &'static str = "https://accounts.google.com/o/oauth2/token"; @@ -55,6 +56,12 @@ pub struct PollInformation { pub server_message: String, } +impl fmt::Display for PollInformation { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + writeln!(f, "Poll result was: '{}'", self.server_message) + } +} + /// Encapsulates all possible results of the `request_token(...)` operation #[derive(Clone)] pub enum RequestResult { @@ -65,22 +72,47 @@ pub enum RequestResult { /// 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), /// Indicates we may enter the next phase ProceedWithPolling(PollInformation), } -impl RequestResult { - fn from_server_message(msg: &str, desc: &str) -> RequestResult { - match msg { +impl From for RequestResult { + fn from(value: JsonError) -> RequestResult { + match &*value.error { "invalid_client" => RequestResult::InvalidClient, - "invalid_scope" => RequestResult::InvalidScope(desc.to_string()), - _ => panic!("'{}' not understood", msg) + "invalid_scope" => RequestResult::InvalidScope( + value.error_description.unwrap_or("no description provided".to_string()) + ), + _ => RequestResult::NegativeServerResponse(value.error, value.error_description), + } + } +} + +impl fmt::Display for RequestResult { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + match *self { + RequestResult::Error(ref err) => err.fmt(f), + RequestResult::InvalidClient => "Invalid Client".fmt(f), + RequestResult::InvalidScope(ref scope) + => writeln!(f, "Invalid Scope: '{}'", scope), + RequestResult::NegativeServerResponse(ref error, ref desc) => { + try!(error.fmt(f)); + if let &Some(ref desc) = desc { + try!(write!(f, ": {}", desc)); + } + "\n".fmt(f) + }, + RequestResult::ProceedWithPolling(ref pi) + => write!(f, "Proceed with polling: {}", pi), } } } /// Encapsulates all possible results of a `poll_token(...)` operation -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum PollResult { /// Connection failure - retry if you think it's worth it Error(Rc), @@ -94,12 +126,27 @@ pub enum PollResult { AccessGranted(Token), } +impl fmt::Display for PollResult { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + match *self { + PollResult::Error(ref err) => err.fmt(f), + PollResult::AuthorizationPending(ref pi) => pi.fmt(f), + PollResult::Expired(ref date) + => writeln!(f, "Authentication expired at {}", date), + PollResult::AccessDenied => "Access denied by user".fmt(f), + PollResult::AccessGranted(ref token) + => writeln!(f, "Access granted by user, expires at {}", token.expiry_date()), + } + } +} + impl Default for PollResult { fn default() -> PollResult { PollResult::Error(Rc::new(hyper::HttpError::HttpStatusError)) } } + impl DeviceFlow where C: BorrowMut { @@ -176,13 +223,6 @@ impl DeviceFlow interval: i64, } - - #[derive(RustcDecodable)] - struct JsonError { - error: String, - error_description: String - } - // This will work once hyper uses std::io::Reader // let decoded: JsonData = rustc_serialize::Decodable::decode( // &mut json::Decoder::new( @@ -196,8 +236,7 @@ impl DeviceFlow match json::decode::(&json_str) { Err(_) => {}, // ignore, move on Ok(res) => { - return RequestResult::from_server_message(&res.error, - &res.error_description) + return RequestResult::from(res) } } diff --git a/src/helper.rs b/src/helper.rs index d11a16279..796aaf73b 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -4,6 +4,9 @@ use std::collections::HashMap; use std::hash::{SipHasher, Hash, Hasher}; use std::thread::sleep; use std::cmp::min; +use std::error::Error; +use std::fmt; +use std::convert::From; use common::{Token, FlowType, ApplicationSecret}; use device::{PollInformation, RequestResult, DeviceFlow, PollResult}; @@ -12,23 +15,42 @@ use chrono::{DateTime, UTC, Duration, Local}; use hyper; -/// Implements a specialised storage to set and retrieve `Token` instances. +/// Implements a specialized storage to set and retrieve `Token` instances. /// The `scope_hash` represents the signature of the scopes for which the given token /// should be stored or retrieved. +/// 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; + /// If `token` is None, it is invalid or revoked and should be removed from storage. - fn set(&mut self, scope_hash: u64, token: Option); + fn set(&mut self, scope_hash: u64, scopes: &Vec<&str>, token: Option) -> Option; /// A `None` result indicates that there is no token for the given scope_hash. - fn get(&self, scope_hash: u64) -> Option; + fn get(&self, scope_hash: u64, scopes: &Vec<&str>) -> Result, Self::Error>; } /// A storage that remembers nothing. #[derive(Default)] pub struct NullStorage; +#[derive(Debug)] +pub struct NullError; + +impl Error for NullError { + fn description(&self) -> &str { + "NULL" + } +} + +impl fmt::Display for NullError { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + "NULL-ERROR".fmt(f) + } +} impl TokenStorage for NullStorage { - fn set(&mut self, _: u64, _: Option) {} - fn get(&self, _: u64) -> Option { None } + type Error = NullError; + fn set(&mut self, _: u64, _: &Vec<&str>, _: Option) -> Option { None } + fn get(&self, _: u64, _: &Vec<&str>) -> Result, NullError> { Ok(None) } } /// A storage that remembers values for one session only. @@ -38,17 +60,20 @@ pub struct MemoryStorage { } impl TokenStorage for MemoryStorage { - fn set(&mut self, scope_hash: u64, token: Option) { + type Error = NullError; + + fn set(&mut self, scope_hash: u64, _: &Vec<&str>, token: Option) -> Option { match token { Some(t) => self.tokens.insert(scope_hash, t), None => self.tokens.remove(&scope_hash), }; + None } - fn get(&self, scope_hash: u64) -> Option { + fn get(&self, scope_hash: u64, _: &Vec<&str>) -> Result, NullError> { match self.tokens.get(&scope_hash) { - Some(t) => Some(t.clone()), - None => None, + Some(t) => Ok(Some(t.clone())), + None => Ok(None), } } } @@ -77,11 +102,54 @@ pub struct Authenticator { secret: ApplicationSecret, } +#[derive(Debug)] +struct StringError { + error: String, +} + +impl fmt::Display for StringError { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + self.description().fmt(f) + } +} + +impl StringError { + fn new(error: String, desc: Option<&String>) -> StringError { + let mut error = error; + if let Some(d) = desc { + error.push_str(": "); + error.push_str(&*d); + } + + StringError { + error: error, + } + } +} + +impl<'a> From<&'a Error> for StringError { + fn from(err: &'a Error) -> StringError { + StringError::new(err.description().to_string(), None) + } +} + +impl From for StringError { + fn from(value: String) -> StringError { + StringError::new(value, None) + } +} + +impl Error for StringError { + fn description(&self) -> &str { + &self.error + } +} + /// 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) -> Option + fn token<'b, I, T>(&mut self, scopes: I) -> Result> where T: AsRef + Ord, I: IntoIterator; @@ -119,7 +187,7 @@ impl Authenticator } } - fn retrieve_device_token(&mut self, scopes: &Vec<&str>) -> Option { + fn retrieve_device_token(&mut self, scopes: &Vec<&str>) -> Result> { let mut flow = DeviceFlow::new(self.client.borrow_mut()); // PHASE 1: REQUEST CODE @@ -129,14 +197,16 @@ impl Authenticator match res { RequestResult::Error(err) => { match self.delegate.connection_error(&*err) { - Retry::Abort => return None, + Retry::Abort|Retry::Skip => return Err(Box::new(StringError::from(&*err as &Error))), Retry::After(d) => sleep(d), } }, RequestResult::InvalidClient + |RequestResult::NegativeServerResponse(_, _) |RequestResult::InvalidScope(_) => { + let serr = StringError::from(res.to_string()); self.delegate.request_failure(res); - return None + return Err(Box::new(serr)) } RequestResult::ProceedWithPolling(pi) => { self.delegate.present_user_code(pi); @@ -147,29 +217,31 @@ impl Authenticator // PHASE 1: POLL TOKEN loop { - match flow.poll_token() { + let pt = flow.poll_token(); + let pts = pt.to_string(); + match pt { PollResult::Error(err) => { match self.delegate.connection_error(&*err) { - Retry::Abort => return None, + Retry::Abort|Retry::Skip => return Err(Box::new(StringError::from(&*err as &Error))), Retry::After(d) => sleep(d), } }, PollResult::Expired(t) => { self.delegate.expired(t); - return None + return Err(Box::new(StringError::from(pts))) }, PollResult::AccessDenied => { self.delegate.denied(); - return None + return Err(Box::new(StringError::from(pts))) }, PollResult::AuthorizationPending(pi) => { match self.delegate.pending(&pi) { - Retry::Abort => return None, + Retry::Abort|Retry::Skip => return Err(Box::new(StringError::new(pts, None))), Retry::After(d) => sleep(min(d, pi.interval)), } }, PollResult::AccessGranted(token) => { - return Some(token) + return Ok(token) }, } } @@ -183,9 +255,10 @@ impl GetToken for Authenticator /// 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 returned token will be None, otherwise it is guaranteed to be - /// valid for the given scopes. - fn token<'b, I, T>(&mut self, scopes: I) -> Option + /// 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, I: IntoIterator { let (scope_key, scopes) = { @@ -193,55 +266,99 @@ impl GetToken for Authenticator .map(|s|s.as_ref()) .collect::>(); sv.sort(); - let s = sv.connect(" "); - let mut sh = SipHasher::new(); - s.hash(&mut sh); + &sv.hash(&mut sh); let sv = sv; (sh.finish(), sv) }; // Get cached token. Yes, let's do an explicit return - return match self.storage.get(scope_key) { - Some(mut t) => { - // t needs refresh ? - if t.expired() { - 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) { - RefreshResult::Error(ref err) => { - match self.delegate.connection_error(err.clone()) { - Retry::Abort => return None, - Retry::After(d) => sleep(d), + 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.borrow_mut()); + loop { + match *rf.refresh_token(self.flow_type, + &self.secret.client_id, + &self.secret.client_secret, + &t.refresh_token, + scopes.iter()) { + RefreshResult::Error(ref err) => { + match self.delegate.connection_error(err) { + Retry::Abort|Retry::Skip => + return Err(Box::new(StringError::new( + err.description().to_string(), + None))), + Retry::After(d) => sleep(d), + } + }, + RefreshResult::RefreshError(ref err_str, ref err_description) => { + self.delegate.token_refresh_failed(&err_str, &err_description); + return Err(Box::new( + StringError::new(err_str.clone(), err_description.as_ref()))) + }, + RefreshResult::Success(ref new_t) => { + t = new_t.clone(); + loop { + if let Some(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)), + Retry::After(d) => { + sleep(d); + continue; + } + } + } + break; // .set() + } + break; // refresh_token loop } - }, - RefreshResult::Refused(_) => { - self.delegate.denied(); - return None - }, - RefreshResult::Success(ref new_t) => { - t = new_t.clone(); - self.storage.set(scope_key, Some(t.clone())); - } - }// RefreshResult handling - }// refresh loop - }// handle expiration - Some(t) - } - None => { - // get new token. The respective sub-routine will do all the logic. - let ot = match self.flow_type { - FlowType::Device => self.retrieve_device_token(&scopes), - }; - // store it, no matter what. If tokens have become invalid, it's ok - // to indicate that to the storage. - self.storage.set(scope_key, ot.clone()); - ot - }, - } + }// RefreshResult handling + }// refresh loop + }// handle expiration + 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 { + FlowType::Device => self.retrieve_device_token(&scopes), + } + { + Ok(token) => { + loop { + if let Some(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)), + Retry::After(d) => { + sleep(d); + continue; + } + } + } + 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::After(d) => { + sleep(d); + continue + } + } + }, + }// end match + }// end loop } fn api_key(&mut self) -> Option { @@ -267,17 +384,36 @@ pub trait AuthenticatorDelegate { 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, RequestResult) {} /// 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. /// @@ -312,7 +448,10 @@ pub enum Retry { /// Signal you don't want to retry Abort, /// Signals you want to retry after the given duration - After(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, } @@ -336,7 +475,7 @@ mod tests { .token(&["https://www.googleapis.com/auth/youtube.upload"]); match res { - Some(t) => assert_eq!(t.access_token, "1/fFAGRNJru1FTz70BzhT3Zg"), + Ok(t) => assert_eq!(t.access_token, "1/fFAGRNJru1FTz70BzhT3Zg"), _ => panic!("Expected to retrieve token in one go"), } } diff --git a/src/lib.rs b/src/lib.rs index 2d2976028..60db12d12 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,12 +31,12 @@ //! ::default(), None) //! .token(&["https://www.googleapis.com/auth/youtube.upload"]); //! match res { -//! Some(t) => { +//! Ok(t) => { //! // now you can use t.access_token to authenticate API calls within your //! // given scopes. It will not be valid forever, which is when you have to //! // refresh it using the `RefreshFlow` //! }, -//! None => println!("user declined"), +//! Err(err) => println!("Failed to acquire token: {}", err), //! } //! # } //! ``` @@ -54,7 +54,8 @@ //! let mut f = RefreshFlow::new(hyper::Client::new()); //! let new_token = match *f.refresh_token(FlowType::Device, //! "my_client_id", "my_secret", -//! "my_refresh_token") { +//! "my_refresh_token", +//! &["https://scope.url"]) { //! RefreshResult::Success(ref t) => t, //! _ => panic!("bad luck ;)") //! }; diff --git a/src/refresh.rs b/src/refresh.rs index 183ebe231..1c0234aea 100644 --- a/src/refresh.rs +++ b/src/refresh.rs @@ -1,4 +1,4 @@ -use common::FlowType; +use common::{FlowType, JsonError}; use chrono::UTC; use hyper; @@ -6,8 +6,10 @@ use hyper::header::ContentType; use rustc_serialize::json; use url::form_urlencoded; use super::Token; +use itertools::Itertools; use std::borrow::BorrowMut; use std::io::Read; +use std::iter::IntoIterator; /// Implements the [Outh2 Refresh Token Flow](https://developers.google.com/youtube/v3/guides/authentication#devices). /// @@ -25,7 +27,7 @@ pub enum RefreshResult { /// Indicates connection failure Error(hyper::HttpError), /// The server did not answer with a new token, providing the server message - Refused(String), + RefreshError(String, Option), /// The refresh operation finished successfully, providing a new `Token` Success(Token), } @@ -42,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::Refused`, your refresh token is invalid + /// 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, /// and you will have to re-authorize using the `DeviceFlow` /// @@ -54,9 +56,15 @@ impl RefreshFlow /// /// # 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<'b, I, T>( &mut self, + flow_type: FlowType, + client_id: &str, + client_secret: &str, + refresh_token: &str, + scopes: I) + -> &RefreshResult + where T: AsRef + Ord, + I: IntoIterator { if let RefreshResult::Success(_) = self.result { return &self.result; } @@ -65,7 +73,12 @@ impl RefreshFlow [("client_id", client_id), ("client_secret", client_secret), ("refresh_token", refresh_token), - ("grant_type", "refresh_token")] + ("grant_type", "refresh_token"), + ("scope", scopes.into_iter() + .map(|s| s.as_ref()) + .intersperse(" ") + .collect::() + .as_ref())] .iter().cloned()); let json_str = @@ -84,11 +97,6 @@ impl RefreshFlow } }; - #[derive(RustcDecodable)] - struct JsonError { - error: String - } - #[derive(RustcDecodable)] struct JsonToken { access_token: String, @@ -99,7 +107,7 @@ impl RefreshFlow match json::decode::(&json_str) { Err(_) => {}, Ok(res) => { - self.result = RefreshResult::Refused(res.error); + self.result = RefreshResult::RefreshError(res.error, res.error_description); return &self.result; } } @@ -145,7 +153,7 @@ mod tests { match *flow.refresh_token(FlowType::Device, - "bogus", "secret", "bogus_refresh_token") { + "bogus", "secret", "bogus_refresh_token", &["scope.url"]) { RefreshResult::Success(ref t) => { assert_eq!(t.access_token, "1/fFAGRNJru1FTz70BzhT3Zg"); assert!(!t.expired());