From 4486bd595fc4a3e6fecafbc5323fc0d6398a9ff9 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 27 Feb 2015 19:58:06 +0100 Subject: [PATCH 1/7] feat(refresh): &mut Client instead of Client Breaking: This changes the existing API ... maybe Borrow can be used to hide the type of client. --- src/common.rs | 7 ++++++- src/device.rs | 8 +++++++- src/lib.rs | 6 ++++-- src/refresh.rs | 12 ++++++------ src/util.rs | 23 +++++++++++++++++++++++ 5 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 src/util.rs diff --git a/src/common.rs b/src/common.rs index 4316e8e3e..628bd2c6d 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,4 +1,10 @@ use chrono::{DateTime, UTC, TimeZone}; +use std::marker::MarkerTrait; + +/// A marker trait for all Flows +pub trait Flow : MarkerTrait { + fn type_id() -> AuthenticationType; +} /// Represents a token as returned by OAuth2 servers. /// @@ -23,7 +29,6 @@ pub struct Token { /// access_token will expire after this amount of time. /// Prefer using expiry_date() pub expires_in: Option, - /// timestamp is seconds since epoch indicating when the token will expire in absolute terms. /// use expiry_date() to convert to DateTime. pub expires_in_timestamp: Option, diff --git a/src/device.rs b/src/device.rs index f5d47bf5a..34d3b212f 100644 --- a/src/device.rs +++ b/src/device.rs @@ -11,7 +11,7 @@ use itertools::Itertools; use rustc_serialize::json; use chrono::{DateTime,UTC}; -use common::{Token, AuthenticationType}; +use common::{Token, AuthenticationType, Flow}; pub const GOOGLE_TOKEN_URL: &'static str = "https://accounts.google.com/o/oauth2/token"; @@ -27,6 +27,12 @@ pub struct DeviceFlow { id: String, } +impl Flow for DeviceFlow { + fn type_id() -> AuthenticationType { + AuthenticationType::Device + } +} + /// Contains state of pending authentication requests #[derive(Clone, Debug, PartialEq)] diff --git a/src/lib.rs b/src/lib.rs index 93cf8a7a3..4dfe23431 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,7 +51,8 @@ //! use oauth2::{RefreshFlow, AuthenticationType, RefreshResult}; //! //! # #[test] fn refresh() { -//! let mut f = RefreshFlow::new(hyper::Client::new()); +//! let mut c = hyper::Client::new(); +//! let mut f = RefreshFlow::new(&mut c); //! let new_token = match *f.refresh_token(AuthenticationType::Device, //! "my_client_id", "my_secret", //! "my_refresh_token") { @@ -78,9 +79,10 @@ extern crate "rustc-serialize" as rustc_serialize; mod device; mod refresh; mod common; +mod util; pub use device::{DeviceFlow, PollInformation, PollResult, DeviceFlowHelper, DeviceFlowHelperDelegate, Retry}; pub use refresh::{RefreshFlow, RefreshResult}; pub use common::{Token, AuthenticationType, ApplicationSecret, ConsoleApplicationSecret}; - +pub use util::TokenStorage; diff --git a/src/refresh.rs b/src/refresh.rs index 9d883ffcf..83fc37180 100644 --- a/src/refresh.rs +++ b/src/refresh.rs @@ -12,8 +12,8 @@ use super::Token; /// 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, +pub struct RefreshFlow<'a, NC> where NC: 'a { + client: &'a mut hyper::Client, result: RefreshResult, } @@ -28,10 +28,10 @@ pub enum RefreshResult { Success(Token), } -impl RefreshFlow +impl<'a, NC> RefreshFlow<'a, NC> where NC: hyper::net::NetworkConnector { - pub fn new(client: hyper::Client) -> RefreshFlow { + pub fn new(client: &'a mut hyper::Client) -> RefreshFlow { RefreshFlow { client: client, result: RefreshResult::Error(hyper::HttpError::HttpStatusError), @@ -135,9 +135,9 @@ mod tests { #[test] fn refresh_flow() { + let mut c = hyper::Client::with_connector(::default()); let mut flow = RefreshFlow::new( - hyper::Client::with_connector( - ::default())); + &mut c); match *flow.refresh_token(AuthenticationType::Device, diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 000000000..f671bf89d --- /dev/null +++ b/src/util.rs @@ -0,0 +1,23 @@ +use common::{Token, AuthenticationType}; + +/// Implements a specialised 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. +pub trait TokenStorage { + /// If `token` is None, it is invalid or revoked and should be removed from storage. + fn set(&mut self, scope_hash: i64, token: Option); + /// A `None` result indicates that there is no token for the given scope_hash. + /// It is assumed that a token previously `set` will be retrievable using `get` + fn get(&self, scope_hash: i64) -> Option; +} + + +/// 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. +pub struct Authenticator { + auth_type: AuthenticationType, + storage: S, + // client ref ... +} \ No newline at end of file From 88d4bf8c28ea10db0730e072986303f71bdbfed3 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 27 Feb 2015 20:08:53 +0100 Subject: [PATCH 2/7] fix(refresh): BorrowMut for & and owned Client That way, we are most flexible, at the cost of additional code. --- src/lib.rs | 3 +-- src/refresh.rs | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4dfe23431..6f16aa9f4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,8 +51,7 @@ //! use oauth2::{RefreshFlow, AuthenticationType, RefreshResult}; //! //! # #[test] fn refresh() { -//! let mut c = hyper::Client::new(); -//! let mut f = RefreshFlow::new(&mut c); +//! let mut f = RefreshFlow::new(hyper::Client::new()); //! let new_token = match *f.refresh_token(AuthenticationType::Device, //! "my_client_id", "my_secret", //! "my_refresh_token") { diff --git a/src/refresh.rs b/src/refresh.rs index 83fc37180..07d30310e 100644 --- a/src/refresh.rs +++ b/src/refresh.rs @@ -6,15 +6,19 @@ use hyper::header::ContentType; use rustc_serialize::json; use url::form_urlencoded; use super::Token; +use std::borrow::BorrowMut; +use std::marker::PhantomData; /// 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. -pub struct RefreshFlow<'a, NC> where NC: 'a { - client: &'a mut hyper::Client, +pub struct RefreshFlow + where C: BorrowMut> { + client: C, result: RefreshResult, + _m: PhantomData, } @@ -28,13 +32,15 @@ pub enum RefreshResult { Success(Token), } -impl<'a, NC> RefreshFlow<'a, NC> - where NC: hyper::net::NetworkConnector { +impl RefreshFlow + where NC: hyper::net::NetworkConnector, + C: BorrowMut> { - pub fn new(client: &'a mut hyper::Client) -> RefreshFlow { + pub fn new(client: C) -> RefreshFlow { RefreshFlow { client: client, result: RefreshResult::Error(hyper::HttpError::HttpStatusError), + _m: PhantomData, } } @@ -67,7 +73,7 @@ impl<'a, NC> RefreshFlow<'a, NC> .iter().cloned()); let json_str = - match self.client.post(auth_type.as_slice()) + match self.client.borrow_mut().post(auth_type.as_slice()) .header(ContentType("application/x-www-form-urlencoded".parse().unwrap())) .body(req.as_slice()) .send() { From 3f965c8fea1f341809be97364cbaa570b986f2c4 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 27 Feb 2015 20:15:28 +0100 Subject: [PATCH 3/7] feat(device): BorrowMut for client --- src/device.rs | 22 ++++++++++++++-------- src/refresh.rs | 4 ++-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/device.rs b/src/device.rs index 34d3b212f..26a475bde 100644 --- a/src/device.rs +++ b/src/device.rs @@ -10,6 +10,8 @@ use url::form_urlencoded; use itertools::Itertools; use rustc_serialize::json; use chrono::{DateTime,UTC}; +use std::borrow::BorrowMut; +use std::marker::PhantomData; use common::{Token, AuthenticationType, Flow}; @@ -19,15 +21,17 @@ pub const GOOGLE_TOKEN_URL: &'static str = "https://accounts.google.com/o/oauth2 /// 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 { - client: hyper::Client, +pub struct DeviceFlow { + client: C, device_code: String, state: PollResult, secret: String, id: String, + + _m: PhantomData, } -impl Flow for DeviceFlow { +impl Flow for DeviceFlow { fn type_id() -> AuthenticationType { AuthenticationType::Device } @@ -99,8 +103,9 @@ impl Default for PollResult { } } -impl DeviceFlow - where NC: hyper::net::NetworkConnector { +impl DeviceFlow + where C: BorrowMut>, + NC: hyper::net::NetworkConnector { /// # Examples /// ```test_harness @@ -112,13 +117,14 @@ impl DeviceFlow /// let mut f = DeviceFlow::new(hyper::Client::new()); /// # } /// ``` - pub fn new(client: hyper::Client) -> DeviceFlow { + pub fn new(client: C) -> DeviceFlow { DeviceFlow { client: client, device_code: Default::default(), secret: Default::default(), id: Default::default(), state: Default::default(), + _m: PhantomData, } } @@ -156,7 +162,7 @@ impl DeviceFlow .collect::() .as_slice())].iter().cloned()); - match self.client.post(AuthenticationType::Device.as_slice()) + match self.client.borrow_mut().post(AuthenticationType::Device.as_slice()) .header(ContentType("application/x-www-form-urlencoded".parse().unwrap())) .body(req.as_slice()) .send() { @@ -255,7 +261,7 @@ impl DeviceFlow .iter().cloned()); let json_str = - match self.client.post(GOOGLE_TOKEN_URL) + match self.client.borrow_mut().post(GOOGLE_TOKEN_URL) .header(ContentType("application/x-www-form-urlencoded".parse().unwrap())) .body(req.as_slice()) .send() { diff --git a/src/refresh.rs b/src/refresh.rs index 07d30310e..90ea9b603 100644 --- a/src/refresh.rs +++ b/src/refresh.rs @@ -14,10 +14,10 @@ use std::marker::PhantomData; /// 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 - where C: BorrowMut> { +pub struct RefreshFlow { client: C, result: RefreshResult, + _m: PhantomData, } From 091f1c07592808656735cb8800f0a809329e58d9 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 27 Feb 2015 21:02:41 +0100 Subject: [PATCH 4/7] feat(util): new MemoryStorage and NullStorage Additionally, the Authenticator interface was scetched out. It will replace the DeviceFlowHelper, and become the universal do-it-all tool, as it supports storage as well. --- README.md | 2 -- src/common.rs | 9 +------- src/device.rs | 7 +++--- src/refresh.rs | 2 +- src/util.rs | 59 ++++++++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 61 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index e120def07..7eca37fe5 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,6 @@ It is implemented such that it makes no assumptions about the front-end, allowing more uses than just in yup. -Architecturally, it may never be implementing more than device authentication, yet is set up not to constrain itself. - ### Usage Please have a look at the [API landing page][API-docs] for all the examples you will ever need. diff --git a/src/common.rs b/src/common.rs index 628bd2c6d..d80f7afd7 100644 --- a/src/common.rs +++ b/src/common.rs @@ -17,7 +17,7 @@ pub trait Flow : MarkerTrait { /// for the two fields dealing with expiry - once in relative in and once in /// absolute terms. /// -/// Utility methods make common queries easier, see `invalid()` or `expired()`. +/// Utility methods make common queries easier, see `expired()`. #[derive(Clone, PartialEq, Debug, RustcDecodable, RustcEncodable)] pub struct Token { /// used when authenticating calls to oauth2 enabled services. @@ -36,13 +36,6 @@ pub struct Token { impl Token { - /// Returns true if the access token is expired or unset. - pub fn invalid(&self) -> bool { - self.access_token.len() == 0 - || self.refresh_token.len() == 0 - || self.expired() - } - /// Returns true if we are expired. /// /// # Panics diff --git a/src/device.rs b/src/device.rs index 26a475bde..280d9e8e1 100644 --- a/src/device.rs +++ b/src/device.rs @@ -330,13 +330,14 @@ impl<'a> DeviceFlowHelper<'a> { /// Blocks until a token was retrieved from the server, or the delegate /// decided to abort the attempt, or the user decided not to authorize /// the application. - pub fn retrieve_token<'b, NC, T, I>(&mut self, - client: hyper::Client, + pub fn retrieve_token<'b, C, NC, T, I>(&mut self, + client: C, client_id: &str, client_secret: &str, scopes: I) -> Option where T: Str, I: IntoIterator + Clone, - NC: hyper::net::NetworkConnector { + NC: hyper::net::NetworkConnector, + C: BorrowMut> { let mut flow = DeviceFlow::new(client); // PHASE 1: REQUEST CODE diff --git a/src/refresh.rs b/src/refresh.rs index 90ea9b603..ebb8a5694 100644 --- a/src/refresh.rs +++ b/src/refresh.rs @@ -150,7 +150,7 @@ mod tests { "bogus", "secret", "bogus_refresh_token") { RefreshResult::Success(ref t) => { assert_eq!(t.access_token, "1/fFAGRNJru1FTz70BzhT3Zg"); - assert!(!t.expired() && !t.invalid()); + assert!(!t.expired()); }, _ => unreachable!() } diff --git a/src/util.rs b/src/util.rs index f671bf89d..165dfa9bc 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,4 +1,11 @@ -use common::{Token, AuthenticationType}; +use std::borrow::BorrowMut; +use std::marker::PhantomData; +use std::collections::HashMap; + +use common::{Token, AuthenticationType, ApplicationSecret}; + +use hyper; + /// Implements a specialised storage to set and retrieve `Token` instances. /// The `scope_hash` represents the signature of the scopes for which the given token @@ -7,17 +14,61 @@ pub trait TokenStorage { /// If `token` is None, it is invalid or revoked and should be removed from storage. fn set(&mut self, scope_hash: i64, token: Option); /// A `None` result indicates that there is no token for the given scope_hash. - /// It is assumed that a token previously `set` will be retrievable using `get` fn get(&self, scope_hash: i64) -> Option; } +/// A storage that remembers nothing. +pub struct NullStorage; + +impl TokenStorage for NullStorage { + fn set(&mut self, _: i64, _: Option) {} + fn get(&self, _: i64) -> Option { None } +} + +/// A storage that remembers values for one session only. +pub struct MemoryStorage { + pub tokens: HashMap +} + +impl TokenStorage for MemoryStorage { + fn set(&mut self, scope_hash: i64, token: Option) { + match token { + Some(t) => self.tokens.insert(scope_hash, t), + None => self.tokens.remove(&scope_hash), + }; + } + + fn get(&self, scope_hash: i64) -> Option { + match self.tokens.get(&scope_hash) { + Some(t) => Some(t.clone()), + None => None, + } + } +} /// 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. -pub struct Authenticator { +pub struct Authenticator { auth_type: AuthenticationType, storage: S, - // client ref ... + client: C, + + _m: PhantomData +} + +impl Authenticator + where S: TokenStorage, + NC: hyper::net::NetworkConnector, + C: BorrowMut> { + + // + // fn new() -> Authenticator { + + // } + + // Will retrieve a token, from storage, retrieve a new one, or refresh + // an existing one. + // fn token() -> } \ No newline at end of file From c227c161fd7233d236c1ee5e700dd56298922f08 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 28 Feb 2015 09:37:45 +0100 Subject: [PATCH 5/7] feat(helper): full implementation of Authenticator It's a generalized DeviceFlowHelper, able to operate on all flows. It's also more flexible, as it will automatically refresh token as required. That way, it lends itself to use in libraries which want minimal hassle. --- src/common.rs | 11 +- src/device.rs | 8 +- src/helper.rs | 273 +++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 9 +- src/refresh.rs | 10 +- src/util.rs | 74 -------------- 6 files changed, 292 insertions(+), 93 deletions(-) create mode 100644 src/helper.rs delete mode 100644 src/util.rs diff --git a/src/common.rs b/src/common.rs index d80f7afd7..fbfe082a2 100644 --- a/src/common.rs +++ b/src/common.rs @@ -3,7 +3,7 @@ use std::marker::MarkerTrait; /// A marker trait for all Flows pub trait Flow : MarkerTrait { - fn type_id() -> AuthenticationType; + fn type_id() -> FlowType; } /// Represents a token as returned by OAuth2 servers. @@ -66,23 +66,24 @@ impl Token { } /// All known authentication types, for suitable constants -pub enum AuthenticationType { +#[derive(Copy)] +pub enum FlowType { /// [device authentication](https://developers.google.com/youtube/v3/guides/authentication#devices) Device, } -impl Str for AuthenticationType { +impl Str for FlowType { /// Converts itself into a URL string fn as_slice(&self) -> &'static str { match *self { - AuthenticationType::Device => "https://accounts.google.com/o/oauth2/device/code", + FlowType::Device => "https://accounts.google.com/o/oauth2/device/code", } } } /// Represents either 'installed' or 'web' applications in a json secrets file. /// See `ConsoleApplicationSecret` for more information -#[derive(RustcDecodable, RustcEncodable)] +#[derive(RustcDecodable, RustcEncodable, Clone)] pub struct ApplicationSecret { /// The client ID. pub client_id: String, diff --git a/src/device.rs b/src/device.rs index 280d9e8e1..810abaea0 100644 --- a/src/device.rs +++ b/src/device.rs @@ -13,7 +13,7 @@ use chrono::{DateTime,UTC}; use std::borrow::BorrowMut; use std::marker::PhantomData; -use common::{Token, AuthenticationType, Flow}; +use common::{Token, FlowType, Flow}; pub const GOOGLE_TOKEN_URL: &'static str = "https://accounts.google.com/o/oauth2/token"; @@ -32,8 +32,8 @@ pub struct DeviceFlow { } impl Flow for DeviceFlow { - fn type_id() -> AuthenticationType { - AuthenticationType::Device + fn type_id() -> FlowType { + FlowType::Device } } @@ -162,7 +162,7 @@ impl DeviceFlow .collect::() .as_slice())].iter().cloned()); - match self.client.borrow_mut().post(AuthenticationType::Device.as_slice()) + match self.client.borrow_mut().post(FlowType::Device.as_slice()) .header(ContentType("application/x-www-form-urlencoded".parse().unwrap())) .body(req.as_slice()) .send() { diff --git a/src/helper.rs b/src/helper.rs new file mode 100644 index 000000000..60e21cbe8 --- /dev/null +++ b/src/helper.rs @@ -0,0 +1,273 @@ +use std::iter::IntoIterator; +use std::borrow::{Borrow, BorrowMut}; +use std::marker::PhantomData; +use std::collections::HashMap; +use std::hash::{SipHasher, Hash, Hasher}; +use std::old_io::timer::sleep; +use std::cmp::min; + +use common::{Token, FlowType, ApplicationSecret}; +use device::{PollInformation, RequestResult, DeviceFlow, PollResult}; +use refresh::{RefreshResult, RefreshFlow}; +use chrono::{DateTime, UTC, Duration}; +use hyper; + + +/// Implements a specialised 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. +pub trait TokenStorage { + /// If `token` is None, it is invalid or revoked and should be removed from storage. + fn set(&mut self, scope_hash: u64, token: Option); + /// A `None` result indicates that there is no token for the given scope_hash. + fn get(&self, scope_hash: u64) -> Option; +} + +/// A storage that remembers nothing. +pub struct NullStorage; + +impl TokenStorage for NullStorage { + fn set(&mut self, _: u64, _: Option) {} + fn get(&self, _: u64) -> Option { None } +} + +/// A storage that remembers values for one session only. +pub struct MemoryStorage { + pub tokens: HashMap +} + +impl TokenStorage for MemoryStorage { + fn set(&mut self, scope_hash: u64, token: Option) { + match token { + Some(t) => self.tokens.insert(scope_hash, t), + None => self.tokens.remove(&scope_hash), + }; + } + + fn get(&self, scope_hash: u64) -> Option { + match self.tokens.get(&scope_hash) { + Some(t) => Some(t.clone()), + None => None, + } + } +} + +/// 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. +pub struct Authenticator { + flow_type: FlowType, + delegate: D, + storage: S, + client: C, + secret: ApplicationSecret, + + _m: PhantomData +} + +impl Authenticator + where D: AuthenticatorDelegate, + S: BorrowMut, + NC: hyper::net::NetworkConnector, + C: BorrowMut> { + + + /// 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 + fn new(secret: &ApplicationSecret, + delegate: D, client: C, storage: S, flow_type: Option) + -> Authenticator { + Authenticator { + flow_type: flow_type.unwrap_or(FlowType::Device), + delegate: delegate, + storage: storage, + client: client, + secret: secret.clone(), + _m: PhantomData + } + } + + /// 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 + where T: Str + Ord, + I: IntoIterator { + let (scope_key, scope, scopes) = { + let mut sv: Vec<&str> = scopes.into_iter() + .map(|s|s.as_slice()) + .collect::>(); + sv.sort(); + let s = sv.connect(" "); + + let mut sh = SipHasher::new(); + s.hash(&mut sh); + let sv = sv; + (sh.finish(), s, sv) + }; + + // Get cached token. Yes, let's do an explicit return + return match self.storage.borrow().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), + } + }, + RefreshResult::Refused(_) => { + self.delegate.denied(); + return None + }, + RefreshResult::Success(ref new_t) => { + t = new_t.clone(); + self.storage.borrow_mut().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.borrow_mut().set(scope_key, ot.clone()); + ot + }, + } + } + + fn retrieve_device_token(&mut self, scopes: &Vec<&str>) -> Option { + let mut flow = DeviceFlow::new(self.client.borrow_mut()); + + // PHASE 1: REQUEST CODE + loop { + let res = flow.request_code(&self.secret.client_id, + &self.secret.client_secret, scopes.iter()); + match res { + RequestResult::Error(err) => { + match self.delegate.connection_error(err) { + Retry::Abort => return None, + Retry::After(d) => sleep(d), + } + }, + RequestResult::InvalidClient + |RequestResult::InvalidScope(_) => { + self.delegate.request_failure(res); + return None + } + RequestResult::ProceedWithPolling(pi) => { + self.delegate.present_user_code(pi); + break + } + } + } + + // PHASE 1: POLL TOKEN + loop { + match flow.poll_token() { + PollResult::Error(err) => { + match self.delegate.connection_error(err) { + Retry::Abort => return None, + Retry::After(d) => sleep(d), + } + }, + PollResult::Expired(t) => { + self.delegate.expired(t); + return None + }, + PollResult::AccessDenied => { + self.delegate.denied(); + return None + }, + PollResult::AuthorizationPending(pi) => { + match self.delegate.pending(&pi) { + Retry::Abort => return None, + Retry::After(d) => sleep(min(d, pi.interval)), + } + }, + PollResult::AccessGranted(token) => { + return Some(token) + }, + } + } + } +} + + + +/// 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::HttpError) -> Retry { + 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. + 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. + /// + /// 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::seconds(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, PollInformation); +} + +/// 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) +} diff --git a/src/lib.rs b/src/lib.rs index 6f16aa9f4..f07fb6620 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,11 +48,11 @@ //! ```test_harness,no_run //! extern crate hyper; //! extern crate "yup-oauth2" as oauth2; -//! use oauth2::{RefreshFlow, AuthenticationType, RefreshResult}; +//! use oauth2::{RefreshFlow, FlowType, RefreshResult}; //! //! # #[test] fn refresh() { //! let mut f = RefreshFlow::new(hyper::Client::new()); -//! let new_token = match *f.refresh_token(AuthenticationType::Device, +//! let new_token = match *f.refresh_token(FlowType::Device, //! "my_client_id", "my_secret", //! "my_refresh_token") { //! RefreshResult::Success(ref t) => t, @@ -78,10 +78,9 @@ extern crate "rustc-serialize" as rustc_serialize; mod device; mod refresh; mod common; -mod util; +mod helper; pub use device::{DeviceFlow, PollInformation, PollResult, DeviceFlowHelper, DeviceFlowHelperDelegate, Retry}; pub use refresh::{RefreshFlow, RefreshResult}; -pub use common::{Token, AuthenticationType, ApplicationSecret, ConsoleApplicationSecret}; -pub use util::TokenStorage; +pub use common::{Token, FlowType, ApplicationSecret, ConsoleApplicationSecret}; diff --git a/src/refresh.rs b/src/refresh.rs index ebb8a5694..4eef64cbe 100644 --- a/src/refresh.rs +++ b/src/refresh.rs @@ -1,4 +1,4 @@ -use common::AuthenticationType; +use common::FlowType; use chrono::UTC; use hyper; @@ -58,7 +58,7 @@ impl RefreshFlow /// /// # Examples /// Please see the crate landing page for an example. - pub fn refresh_token(&mut self, auth_type: AuthenticationType, + pub fn refresh_token(&mut self, flow_type: FlowType, client_id: &str, client_secret: &str, refresh_token: &str) -> &RefreshResult { if let RefreshResult::Success(_) = self.result { @@ -73,7 +73,7 @@ impl RefreshFlow .iter().cloned()); let json_str = - match self.client.borrow_mut().post(auth_type.as_slice()) + match self.client.borrow_mut().post(flow_type.as_slice()) .header(ContentType("application/x-www-form-urlencoded".parse().unwrap())) .body(req.as_slice()) .send() { @@ -126,7 +126,7 @@ mod tests { use hyper; use std::default::Default; use super::*; - use super::super::AuthenticationType; + use super::super::FlowType; mock_connector_in_order!(MockGoogleRefresh { "HTTP/1.1 200 OK\r\n\ @@ -146,7 +146,7 @@ mod tests { &mut c); - match *flow.refresh_token(AuthenticationType::Device, + match *flow.refresh_token(FlowType::Device, "bogus", "secret", "bogus_refresh_token") { RefreshResult::Success(ref t) => { assert_eq!(t.access_token, "1/fFAGRNJru1FTz70BzhT3Zg"); diff --git a/src/util.rs b/src/util.rs deleted file mode 100644 index 165dfa9bc..000000000 --- a/src/util.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::borrow::BorrowMut; -use std::marker::PhantomData; -use std::collections::HashMap; - -use common::{Token, AuthenticationType, ApplicationSecret}; - -use hyper; - - -/// Implements a specialised 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. -pub trait TokenStorage { - /// If `token` is None, it is invalid or revoked and should be removed from storage. - fn set(&mut self, scope_hash: i64, token: Option); - /// A `None` result indicates that there is no token for the given scope_hash. - fn get(&self, scope_hash: i64) -> Option; -} - -/// A storage that remembers nothing. -pub struct NullStorage; - -impl TokenStorage for NullStorage { - fn set(&mut self, _: i64, _: Option) {} - fn get(&self, _: i64) -> Option { None } -} - -/// A storage that remembers values for one session only. -pub struct MemoryStorage { - pub tokens: HashMap -} - -impl TokenStorage for MemoryStorage { - fn set(&mut self, scope_hash: i64, token: Option) { - match token { - Some(t) => self.tokens.insert(scope_hash, t), - None => self.tokens.remove(&scope_hash), - }; - } - - fn get(&self, scope_hash: i64) -> Option { - match self.tokens.get(&scope_hash) { - Some(t) => Some(t.clone()), - None => None, - } - } -} - -/// 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. -pub struct Authenticator { - auth_type: AuthenticationType, - storage: S, - client: C, - - _m: PhantomData -} - -impl Authenticator - where S: TokenStorage, - NC: hyper::net::NetworkConnector, - C: BorrowMut> { - - // - // fn new() -> Authenticator { - - // } - - // Will retrieve a token, from storage, retrieve a new one, or refresh - // an existing one. - // fn token() -> -} \ No newline at end of file From 374804331adf4fc263de479a1b0c373a06ad6e2e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 28 Feb 2015 11:09:23 +0100 Subject: [PATCH 6/7] test(auth): Authenticator test Also showed that for some reason, BorrowMut can't really be passed, even though it works for Client struct, which is as simple as well. --- src/common.rs | 8 ++++---- src/device.rs | 22 +--------------------- src/helper.rs | 48 ++++++++++++++++++++++++++++++++++++++++++------ src/lib.rs | 2 +- 4 files changed, 48 insertions(+), 32 deletions(-) diff --git a/src/common.rs b/src/common.rs index fbfe082a2..7876bd9a6 100644 --- a/src/common.rs +++ b/src/common.rs @@ -108,16 +108,16 @@ pub struct ApplicationSecret { /// as returned by the [google developer console](https://code.google.com/apis/console) #[derive(RustcDecodable, RustcEncodable)] pub struct ConsoleApplicationSecret { - web: Option, - installed: Option + pub web: Option, + pub installed: Option } #[cfg(test)] -mod tests { +pub mod tests { use super::*; - 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() { diff --git a/src/device.rs b/src/device.rs index 810abaea0..a713bb8a5 100644 --- a/src/device.rs +++ b/src/device.rs @@ -442,7 +442,7 @@ pub trait DeviceFlowHelperDelegate { #[cfg(test)] -mod tests { +pub mod tests { use super::*; use std::default::Default; use hyper; @@ -505,24 +505,4 @@ mod tests { // As our mock has only 3 items, we would panic on this call flow.poll_token(); } - - #[test] - fn authenticator() { - struct TestHandler; - impl DeviceFlowHelperDelegate for TestHandler { - fn present_user_code(&mut self, pi: PollInformation) { - println!("{:?}", pi); - } - } - if let Some(t) = DeviceFlowHelper::new(&mut TestHandler) - .retrieve_token(hyper::Client::with_connector( - ::default()), - "bogus_client_id", - "bogus_secret", - &["https://www.googleapis.com/auth/youtube.upload"]) { - assert_eq!(t.access_token, "1/fFAGRNJru1FTz70BzhT3Zg") - } else { - panic!("Expected to retrieve token in one go"); - } - } } \ No newline at end of file diff --git a/src/helper.rs b/src/helper.rs index 60e21cbe8..5e5e4f191 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -24,6 +24,7 @@ pub trait TokenStorage { } /// A storage that remembers nothing. +#[derive(Default)] pub struct NullStorage; impl TokenStorage for NullStorage { @@ -32,6 +33,7 @@ impl TokenStorage for NullStorage { } /// A storage that remembers values for one session only. +#[derive(Default)] pub struct MemoryStorage { pub tokens: HashMap } @@ -68,7 +70,7 @@ pub struct Authenticator { impl Authenticator where D: AuthenticatorDelegate, - S: BorrowMut, + S: TokenStorage, NC: hyper::net::NetworkConnector, C: BorrowMut> { @@ -86,7 +88,7 @@ impl Authenticator /// * `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 - fn new(secret: &ApplicationSecret, + pub fn new(secret: &ApplicationSecret, delegate: D, client: C, storage: S, flow_type: Option) -> Authenticator { Authenticator { @@ -103,7 +105,7 @@ impl Authenticator /// 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 + pub fn token<'b, I, T>(&mut self, scopes: I) -> Option where T: Str + Ord, I: IntoIterator { let (scope_key, scope, scopes) = { @@ -120,7 +122,7 @@ impl Authenticator }; // Get cached token. Yes, let's do an explicit return - return match self.storage.borrow().get(scope_key) { + return match self.storage.get(scope_key) { Some(mut t) => { // t needs refresh ? if t.expired() { @@ -142,7 +144,7 @@ impl Authenticator }, RefreshResult::Success(ref new_t) => { t = new_t.clone(); - self.storage.borrow_mut().set(scope_key, Some(t.clone())); + self.storage.set(scope_key, Some(t.clone())); } }// RefreshResult handling }// refresh loop @@ -156,7 +158,7 @@ impl Authenticator }; // store it, no matter what. If tokens have become invalid, it's ok // to indicate that to the storage. - self.storage.borrow_mut().set(scope_key, ot.clone()); + self.storage.set(scope_key, ot.clone()); ot }, } @@ -271,3 +273,37 @@ pub enum Retry { /// Signals you want to retry after the given duration After(Duration) } + + +#[cfg(test)] +mod tests { + use super::*; + use super::super::device::tests::MockGoogleAuth; + use super::super::common::tests::SECRET; + use super::super::common::{ConsoleApplicationSecret, Token}; + use super::super::device::PollInformation; + use std::default::Default; + use hyper; + + #[test] + fn flow() { + use rustc_serialize::json; + + struct TestHandler; + impl AuthenticatorDelegate for TestHandler { + fn present_user_code(&mut self, pi: PollInformation) { + println!("{:?}", pi); + } + } + let secret = json::decode::(SECRET).unwrap().installed.unwrap(); + let res = Authenticator::new(&secret, TestHandler, + hyper::Client::with_connector(::default()), + ::default(), None) + .token(&["https://www.googleapis.com/auth/youtube.upload"]); + + match res { + Some(t) => assert_eq!(t.access_token, "1/fFAGRNJru1FTz70BzhT3Zg"), + _ => panic!("Expected to retrieve token in one go"), + } + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index f07fb6620..e9b68426a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![feature(old_io, std_misc, core)] +#![feature(old_io, std_misc, core, hash)] //! This library can be used to acquire oauth2.0 authentication for services. //! At the time of writing, only one way of doing so is implemented, the [device flow](https://developers.google.com/youtube/v3/guides/authentication#devices), along with a flow //! for [refreshing tokens](https://developers.google.com/youtube/v3/guides/authentication#devices) From dea3e6b3fd6dfaafe795fdaa5b6fbe446955fb09 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 28 Feb 2015 12:03:30 +0100 Subject: [PATCH 7/7] test(auth): removed DeviceFlowHelper It's superseded by the more powerful `Authenticator`. Adjusted library documentation accordingly, as well as `auth` program. --- examples/auth.rs | 21 ++++++-- src/device.rs | 138 ----------------------------------------------- src/helper.rs | 45 ++++++++++------ src/lib.rs | 36 ++++++------- 4 files changed, 64 insertions(+), 176 deletions(-) diff --git a/examples/auth.rs b/examples/auth.rs index 5a4006740..50f808f55 100644 --- a/examples/auth.rs +++ b/examples/auth.rs @@ -9,6 +9,7 @@ extern crate open; use chrono::{Local}; use getopts::{HasArg,Options,Occur,Fail}; use std::env; +use std::default::Default; use std::time::Duration; use std::old_io::timer::sleep; @@ -46,13 +47,21 @@ fn main() { return } - let client_id = m.opt_str("c").unwrap(); - let client_secret = m.opt_str("s").unwrap(); + let secret = oauth2::ApplicationSecret { + client_id: m.opt_str("c").unwrap(), + client_secret: m.opt_str("s").unwrap(), + token_uri: Default::default(), + auth_uri: Default::default(), + redirect_uris: Default::default(), + client_email: None, + auth_provider_x509_cert_url: None, + client_x509_cert_url: None + }; println!("THIS PROGRAM PRINTS ALL COMMUNICATION TO STDERR !!!"); struct StdoutHandler; - impl oauth2::DeviceFlowHelperDelegate for StdoutHandler { + impl oauth2::AuthenticatorDelegate for StdoutHandler { fn present_user_code(&mut self, pi: oauth2::PollInformation) { println!("Please enter '{}' at {} and authenticate the application for the\n\ given scopes. This is not a test !\n\ @@ -70,8 +79,10 @@ fn main() { let client = hyper::Client::with_connector(mock::TeeConnector { connector: hyper::net::HttpConnector(None) }); - if let Some(t) = oauth2::DeviceFlowHelper::new(&mut StdoutHandler) - .retrieve_token(client, &client_id, &client_secret, &m.free) { + + if let Some(t) = oauth2::Authenticator::new(&secret, StdoutHandler, client, + oauth2::NullStorage, None) + .token(&m.free) { println!("Authentication granted !"); println!("You should store the following information for use, or revoke it."); println!("All dates are given in UTC."); diff --git a/src/device.rs b/src/device.rs index a713bb8a5..6a42bd299 100644 --- a/src/device.rs +++ b/src/device.rs @@ -1,8 +1,6 @@ use std::iter::IntoIterator; use std::time::Duration; use std::default::Default; -use std::cmp::min; -use std::old_io::timer; use hyper; use hyper::header::ContentType; @@ -304,142 +302,6 @@ impl DeviceFlow } } -/// A utility type to help executing the `DeviceFlow` correctly. -/// -/// 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 -/// -pub struct DeviceFlowHelper<'a> { - delegate: &'a mut (DeviceFlowHelperDelegate + 'a), -} - -impl<'a> DeviceFlowHelper<'a> { - - /// Initialize a new instance with the given delegate - pub fn new(delegate: &'a mut DeviceFlowHelperDelegate) -> DeviceFlowHelper<'a> { - DeviceFlowHelper { - delegate: delegate, - } - } - - /// Blocks until a token was retrieved from the server, or the delegate - /// decided to abort the attempt, or the user decided not to authorize - /// the application. - pub fn retrieve_token<'b, C, NC, T, I>(&mut self, - client: C, - client_id: &str, client_secret: &str, scopes: I) - -> Option - where T: Str, - I: IntoIterator + Clone, - NC: hyper::net::NetworkConnector, - C: BorrowMut> { - let mut flow = DeviceFlow::new(client); - - // PHASE 1: REQUEST CODE - loop { - let res = flow.request_code(client_id, client_secret, scopes.clone()); - match res { - RequestResult::Error(err) => { - match self.delegate.connection_error(err) { - Retry::Abort => return None, - Retry::After(d) => timer::sleep(d), - } - }, - RequestResult::InvalidClient - |RequestResult::InvalidScope(_) => { - self.delegate.request_failure(res); - return None - } - RequestResult::ProceedWithPolling(pi) => { - self.delegate.present_user_code(pi); - break - } - } - } - - // PHASE 1: POLL TOKEN - loop { - match flow.poll_token() { - PollResult::Error(err) => { - match self.delegate.connection_error(err) { - Retry::Abort => return None, - Retry::After(d) => timer::sleep(d), - } - }, - PollResult::Expired(t) => { - self.delegate.expired(t); - return None - }, - PollResult::AccessDenied => { - self.delegate.denied(); - return None - }, - PollResult::AuthorizationPending(pi) => { - match self.delegate.pending(&pi) { - Retry::Abort => return None, - Retry::After(d) => timer::sleep(min(d, pi.interval)), - } - }, - PollResult::AccessGranted(token) => { - return Some(token) - }, - } - } - } -} - -/// 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) -} - -/// A partially implemented trait to interact with the `DeviceFlowHelper` -/// -/// 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 DeviceFlowHelperDelegate { - - /// Called whenever there is an HttpError, usually if there are network problems. - /// - /// Return retry information. - fn connection_error(&mut self, hyper::HttpError) -> Retry { - Retry::Abort - } - - /// The server denied the attempt to obtain a request code - fn request_failure(&mut self, RequestResult) {} - - /// The server has returned a `user_code` which must be shown to the user, - /// along with the `verification_url`. - /// Will be called exactly once, provided we didn't abort during `request_code` phase. - fn present_user_code(&mut self, PollInformation); - - /// 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. - 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. - /// - /// If the returned `Retry` variant is a duration, it will only be used if it - /// is larger than the interval desired by the server. - fn pending(&mut self, &PollInformation) -> Retry { - Retry::After(Duration::seconds(5)) - } -} - #[cfg(test)] pub mod tests { diff --git a/src/helper.rs b/src/helper.rs index 5e5e4f191..8df69bac3 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -1,5 +1,5 @@ use std::iter::IntoIterator; -use std::borrow::{Borrow, BorrowMut}; +use std::borrow::BorrowMut; use std::marker::PhantomData; use std::collections::HashMap; use std::hash::{SipHasher, Hash, Hasher}; @@ -9,7 +9,7 @@ use std::cmp::min; use common::{Token, FlowType, ApplicationSecret}; use device::{PollInformation, RequestResult, DeviceFlow, PollResult}; use refresh::{RefreshResult, RefreshFlow}; -use chrono::{DateTime, UTC, Duration}; +use chrono::{DateTime, UTC, Duration, Local}; use hyper; @@ -58,6 +58,18 @@ impl TokenStorage for MemoryStorage { /// /// 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, @@ -89,7 +101,7 @@ 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) + delegate: D, client: C, storage: S, flow_type: Option) -> Authenticator { Authenticator { flow_type: flow_type.unwrap_or(FlowType::Device), @@ -108,7 +120,7 @@ impl Authenticator pub fn token<'b, I, T>(&mut self, scopes: I) -> Option where T: Str + Ord, I: IntoIterator { - let (scope_key, scope, scopes) = { + let (scope_key, scopes) = { let mut sv: Vec<&str> = scopes.into_iter() .map(|s|s.as_slice()) .collect::>(); @@ -118,7 +130,7 @@ impl Authenticator let mut sh = SipHasher::new(); s.hash(&mut sh); let sv = sv; - (sh.finish(), s, sv) + (sh.finish(), sv) }; // Get cached token. Yes, let's do an explicit return @@ -263,9 +275,19 @@ pub trait AuthenticatorDelegate { /// # 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, PollInformation); + 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)); + } } +/// 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 @@ -280,8 +302,7 @@ mod tests { use super::*; use super::super::device::tests::MockGoogleAuth; use super::super::common::tests::SECRET; - use super::super::common::{ConsoleApplicationSecret, Token}; - use super::super::device::PollInformation; + use super::super::common::{ConsoleApplicationSecret}; use std::default::Default; use hyper; @@ -289,14 +310,8 @@ mod tests { fn flow() { use rustc_serialize::json; - struct TestHandler; - impl AuthenticatorDelegate for TestHandler { - fn present_user_code(&mut self, pi: PollInformation) { - println!("{:?}", pi); - } - } let secret = json::decode::(SECRET).unwrap().installed.unwrap(); - let res = Authenticator::new(&secret, TestHandler, + let res = Authenticator::new(&secret, DefaultAuthenticatorDelegate, hyper::Client::with_connector(::default()), ::default(), None) .token(&["https://www.googleapis.com/auth/youtube.upload"]); diff --git a/src/lib.rs b/src/lib.rs index e9b68426a..bdc715eac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,27 +16,26 @@ //! ```test_harness,no_run //! extern crate hyper; //! extern crate "yup-oauth2" as oauth2; -//! use oauth2::{DeviceFlowHelper, DeviceFlowHelperDelegate, PollInformation}; +//! extern crate "rustc-serialize" as rustc_serialize; +//! +//! use oauth2::{Authenticator, DefaultAuthenticatorDelegate, PollInformation, ConsoleApplicationSecret, MemoryStorage}; +//! use rustc_serialize::json; +//! use std::default::Default; +//! # 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() { -//! struct PrintHandler; -//! impl DeviceFlowHelperDelegate for PrintHandler { -//! 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"); -//! } -//! } -//! if let Some(t) = DeviceFlowHelper::new(&mut PrintHandler) -//! .retrieve_token(hyper::Client::new(), -//! "your_client_id", -//! "your_secret", -//! &["https://www.googleapis.com/auth/youtube.upload"]) { +//! let secret = json::decode::(SECRET).unwrap().installed.unwrap(); +//! let res = Authenticator::new(&secret, DefaultAuthenticatorDelegate, +//! hyper::Client::new(), +//! ::default(), None) +//! .token(&["https://www.googleapis.com/auth/youtube.upload"]); +//! match res { +//! Some(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` -//! } else { -//! println!("user declined"); +//! }, +//! None => println!("user declined"), //! } //! # } //! ``` @@ -80,7 +79,8 @@ mod refresh; mod common; mod helper; -pub use device::{DeviceFlow, PollInformation, PollResult, DeviceFlowHelper, - DeviceFlowHelperDelegate, Retry}; +pub use device::{DeviceFlow, PollInformation, PollResult}; pub use refresh::{RefreshFlow, RefreshResult}; pub use common::{Token, FlowType, ApplicationSecret, ConsoleApplicationSecret}; +pub use helper::{TokenStorage, NullStorage, MemoryStorage, Authenticator, + AuthenticatorDelegate, Retry, DefaultAuthenticatorDelegate};