From c227c161fd7233d236c1ee5e700dd56298922f08 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 28 Feb 2015 09:37:45 +0100 Subject: [PATCH] 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