diff --git a/examples/auth.rs b/examples/auth.rs index 5a40067..50f808f 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 a713bb8..6a42bd2 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 5e5e4f1..8df69ba 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 e9b6842..bdc715e 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};