From a8479b8ddb70c32d01f907b64e0a2f61ec5b3370 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Sun, 2 Oct 2016 18:09:21 +0200 Subject: [PATCH] feat(device): Make the Device flow independent of Google This is a breaking change; it's supposed to fix #1. Also, it's a proposal -- not sure if the benefits outweigh the cost of this. The example/auth.rs binary is not broken by this, as it doesn't use the API that changed. The tests have been updated accordingly. --- src/authenticator.rs | 21 +++++++--------- src/device.rs | 57 +++++++++++++++++++------------------------- src/lib.rs | 23 ++---------------- src/lib.rs.in | 2 +- src/refresh.rs | 23 +++++++++++------- src/types.rs | 17 ++++--------- 6 files changed, 54 insertions(+), 89 deletions(-) diff --git a/src/authenticator.rs b/src/authenticator.rs index 66be97be9..52f53a628 100644 --- a/src/authenticator.rs +++ b/src/authenticator.rs @@ -8,7 +8,7 @@ use std::convert::From; use authenticator_delegate::{AuthenticatorDelegate, PollError, PollInformation}; use types::{RequestError, StringError, Token, FlowType, ApplicationSecret}; -use device::DeviceFlow; +use device::{GOOGLE_DEVICE_CODE_URL, DeviceFlow}; use installed::{InstalledFlow, InstalledFlowReturnMethod}; use refresh::{RefreshResult, RefreshFlow}; use storage::TokenStorage; @@ -76,7 +76,7 @@ impl Authenticator flow_type: Option) -> Authenticator { Authenticator { - flow_type: flow_type.unwrap_or(FlowType::Device), + flow_type: flow_type.unwrap_or(FlowType::Device(GOOGLE_DEVICE_CODE_URL.to_string())), delegate: delegate, storage: storage, client: client, @@ -102,15 +102,13 @@ impl Authenticator flow.obtain_token(&mut self.delegate, &self.secret, scopes.iter()) } - fn retrieve_device_token(&mut self, scopes: &Vec<&str>) -> Result> { - let mut flow = DeviceFlow::new(self.client.borrow_mut()); + fn retrieve_device_token(&mut self, scopes: &Vec<&str>, code_url: String) -> Result> { + let mut flow = DeviceFlow::new(self.client.borrow_mut(), &self.secret, &code_url); // PHASE 1: REQUEST CODE let pi: PollInformation; loop { - let res = flow.request_code(&self.secret.client_id, - &self.secret.client_secret, - scopes.iter()); + let res = flow.request_code(scopes.iter()); pi = match res { Err(res_err) => { @@ -214,9 +212,8 @@ impl GetToken for Authenticator 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, + match *rf.refresh_token(self.flow_type.clone(), + &self.secret, &t.refresh_token) { RefreshResult::Error(ref err) => { match self.delegate.connection_error(err) { @@ -263,8 +260,8 @@ impl GetToken for Authenticator Ok(None) => { // Nothing was in storage - get a new token // get new token. The respective sub-routine will do all the logic. - match match self.flow_type { - FlowType::Device => self.retrieve_device_token(&scopes), + match match self.flow_type.clone() { + FlowType::Device(url) => self.retrieve_device_token(&scopes, url), FlowType::InstalledInteractive => self.do_installed_flow(&scopes), FlowType::InstalledRedirect(_) => self.do_installed_flow(&scopes), } { diff --git a/src/device.rs b/src/device.rs index de86258b0..c89cd474c 100644 --- a/src/device.rs +++ b/src/device.rs @@ -12,10 +12,10 @@ use std::borrow::BorrowMut; use std::io::Read; use std::i64; -use types::{Token, FlowType, Flow, RequestError, JsonError}; +use types::{ApplicationSecret, Token, FlowType, Flow, RequestError, JsonError}; use authenticator_delegate::{PollError, PollInformation}; -pub const GOOGLE_TOKEN_URL: &'static str = "https://accounts.google.com/o/oauth2/token"; +pub const GOOGLE_DEVICE_CODE_URL: &'static str = "https://accounts.google.com/o/oauth2/device/code"; /// Encapsulates all possible states of the Device Flow enum DeviceFlowState { @@ -36,34 +36,25 @@ pub struct DeviceFlow { device_code: String, state: Option, error: Option, - secret: String, - id: String, + application_secret: ApplicationSecret, + device_code_url: String, } impl Flow for DeviceFlow { fn type_id() -> FlowType { - FlowType::Device + FlowType::Device(String::new()) } } impl DeviceFlow where C: BorrowMut { - /// # Examples - /// ```test_harness - /// extern crate hyper; - /// extern crate yup_oauth2 as oauth2; - /// use oauth2::DeviceFlow; - /// - /// # #[test] fn new() { - /// let mut f = DeviceFlow::new(hyper::Client::new()); - /// # } - /// ``` - pub fn new(client: C) -> DeviceFlow { + + pub fn new>(client: C, secret: &ApplicationSecret, device_code_url: S) -> DeviceFlow { DeviceFlow { client: client, device_code: Default::default(), - secret: Default::default(), - id: Default::default(), + application_secret: secret.clone(), + device_code_url: device_code_url.as_ref().to_string(), state: None, error: None, } @@ -85,8 +76,6 @@ impl DeviceFlow /// # Examples /// See test-cases in source code for a more complete example. pub fn request_code<'b, T, I>(&mut self, - client_id: &str, - client_secret: &str, scopes: I) -> Result where T: AsRef + 'b, @@ -98,19 +87,19 @@ impl DeviceFlow // note: cloned() shouldn't be needed, see issue // https://github.com/servo/rust-url/issues/81 - let req = form_urlencoded::serialize(&[("client_id", client_id), + let req = form_urlencoded::serialize(&[("client_id", &self.application_secret.client_id), ("scope", - scopes.into_iter() + &scopes.into_iter() .map(|s| s.as_ref()) .intersperse(" ") .collect::() - .as_ref())]); + )]); // note: works around bug in rustlang // https://github.com/rust-lang/rust/issues/22252 let ret = match self.client .borrow_mut() - .post(FlowType::Device.as_ref()) + .post(&self.device_code_url) .header(ContentType("application/x-www-form-urlencoded".parse().unwrap())) .body(&*req) .send() { @@ -149,8 +138,6 @@ impl DeviceFlow }; self.state = Some(DeviceFlowState::Pending(pi.clone())); - self.secret = client_secret.to_string(); - self.id = client_id.to_string(); Ok(pi) } }; @@ -195,15 +182,15 @@ impl DeviceFlow } // We should be ready for a new request - let req = form_urlencoded::serialize(&[("client_id", &self.id[..]), - ("client_secret", &self.secret), + let req = form_urlencoded::serialize(&[("client_id", &self.application_secret.client_id[..]), + ("client_secret", &self.application_secret.client_secret), ("code", &self.device_code), ("grant_type", "http://oauth.net/grant_type/device/1.0")]); - let json_str = match self.client + let json_str: String = match self.client .borrow_mut() - .post(GOOGLE_TOKEN_URL) + .post(&self.application_secret.token_uri) .header(ContentType("application/x-www-form-urlencoded".parse().unwrap())) .body(&*req) .send() { @@ -301,13 +288,17 @@ pub mod tests { } } + const TEST_APP_SECRET: &'static str = r#"{"installed":{"client_id":"384278056379-tr5pbot1mil66749n639jo54i4840u77.apps.googleusercontent.com","project_id":"sanguine-rhythm-105020","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://accounts.google.com/o/oauth2/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"QeQUnhzsiO4t--ZGmj9muUAu","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}}"#; + #[test] fn working_flow() { + use helper::parse_application_secret; + + let appsecret = parse_application_secret(&TEST_APP_SECRET.to_string()).unwrap(); let mut flow = DeviceFlow::new( - hyper::Client::with_connector(::default())); + hyper::Client::with_connector(::default()), &appsecret, GOOGLE_DEVICE_CODE_URL); - match flow.request_code("bogus_client_id", - "bogus_secret", + match flow.request_code( &["https://www.googleapis.com/auth/youtube.upload"]) { Ok(pi) => assert_eq!(pi.interval, Duration::from_secs(0)), _ => unreachable!(), diff --git a/src/lib.rs b/src/lib.rs index 6b44c59dc..a64625977 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,33 +56,14 @@ //! match res { //! 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` +//! // given scopes. It will not be valid forever, but Authenticator will automatically +//! // refresh the token for you. //! }, //! Err(err) => println!("Failed to acquire token: {}", err), //! } //! # } //! ``` //! -//! # Refresh Flow Usage -//! As the `Token` you retrieved previously will only be valid for a certain time, you will have -//! to use the information from the `Token.refresh_token` field to get a new `access_token`. -//! -//! ```test_harness,no_run -//! extern crate hyper; -//! extern crate yup_oauth2 as oauth2; -//! use oauth2::{RefreshFlow, FlowType, RefreshResult}; -//! -//! # #[test] fn refresh() { -//! 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") { -//! RefreshResult::Success(ref t) => t, -//! _ => panic!("bad luck ;)") -//! }; -//! # } -//! ``` #![cfg_attr(feature = "nightly", feature(custom_derive, custom_attribute, plugin))] #![cfg_attr(feature = "nightly", plugin(serde_macros))] diff --git a/src/lib.rs.in b/src/lib.rs.in index 1a51f557e..3c24bc31e 100644 --- a/src/lib.rs.in +++ b/src/lib.rs.in @@ -22,7 +22,7 @@ mod service_account; mod storage; mod types; -pub use device::DeviceFlow; +pub use device::{GOOGLE_DEVICE_CODE_URL, DeviceFlow}; pub use refresh::{RefreshFlow, RefreshResult}; pub use types::{Token, FlowType, ApplicationSecret, ConsoleApplicationSecret, Scheme, TokenType}; pub use installed::{InstalledFlow, InstalledFlowReturnMethod}; diff --git a/src/refresh.rs b/src/refresh.rs index 2460b0275..b98002126 100644 --- a/src/refresh.rs +++ b/src/refresh.rs @@ -1,5 +1,4 @@ -use types::{FlowType, JsonError}; -use device::GOOGLE_TOKEN_URL; +use types::{ApplicationSecret, FlowType, JsonError}; use chrono::UTC; use hyper; @@ -57,8 +56,7 @@ impl RefreshFlow /// Please see the crate landing page for an example. pub fn refresh_token(&mut self, flow_type: FlowType, - client_id: &str, - client_secret: &str, + client_secret: &ApplicationSecret, refresh_token: &str) -> &RefreshResult { let _ = flow_type; @@ -66,14 +64,14 @@ impl RefreshFlow return &self.result; } - let req = form_urlencoded::serialize(&[("client_id", client_id), - ("client_secret", client_secret), + let req = form_urlencoded::serialize(&[("client_id", client_secret.client_id.as_ref()), + ("client_secret", client_secret.client_secret.as_ref()), ("refresh_token", refresh_token), ("grant_type", "refresh_token")]); - let json_str = match self.client + let json_str: String = match self.client .borrow_mut() - .post(GOOGLE_TOKEN_URL) + .post(&client_secret.token_uri) .header(ContentType("application/x-www-form-urlencoded".parse().unwrap())) .body(&*req) .send() { @@ -125,6 +123,8 @@ mod tests { use super::*; use super::super::FlowType; use yup_hyper_mock::{MockStream, SequentialConnector}; + use helper::parse_application_secret; + use device::GOOGLE_DEVICE_CODE_URL; struct MockGoogleRefresh(SequentialConnector); @@ -153,13 +153,18 @@ mod tests { } } + const TEST_APP_SECRET: &'static str = r#"{"installed":{"client_id":"384278056379-tr5pbot1mil66749n639jo54i4840u77.apps.googleusercontent.com","project_id":"sanguine-rhythm-105020","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://accounts.google.com/o/oauth2/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"QeQUnhzsiO4t--ZGmj9muUAu","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}}"#; + #[test] fn refresh_flow() { + + let appsecret = parse_application_secret(&TEST_APP_SECRET.to_string()).unwrap(); + let mut c = hyper::Client::with_connector(::default()); let mut flow = RefreshFlow::new(&mut c); - match *flow.refresh_token(FlowType::Device, "bogus", "secret", "bogus_refresh_token") { + match *flow.refresh_token(FlowType::Device(GOOGLE_DEVICE_CODE_URL.to_string()), &appsecret, "bogus_refresh_token") { RefreshResult::Success(ref t) => { assert_eq!(t.access_token, "1/fFAGRNJru1FTz70BzhT3Zg"); assert!(!t.expired()); diff --git a/src/types.rs b/src/types.rs index 7e8392486..e924d698f 100644 --- a/src/types.rs +++ b/src/types.rs @@ -227,11 +227,13 @@ impl Token { } /// All known authentication types, for suitable constants -#[derive(Clone, Copy)] +#[derive(Clone)] pub enum FlowType { /// [device authentication](https://developers.google.com/youtube/v3/guides/authentication#devices). Only works /// for certain scopes. - Device, + /// Contains the device token URL; for google, that is + /// https://accounts.google.com/o/oauth2/device/code (exported as `GOOGLE_DEVICE_CODE_URL`) + Device(String), /// [installed app flow](https://developers.google.com/identity/protocols/OAuth2InstalledApp). Required /// for Drive, Calendar, Gmail...; Requires user to paste a code from the browser. InstalledInteractive, @@ -242,17 +244,6 @@ pub enum FlowType { InstalledRedirect(u32), } -impl AsRef for FlowType { - /// Converts itself into a URL string - fn as_ref(&self) -> &'static str { - match *self { - FlowType::Device => "https://accounts.google.com/o/oauth2/device/code", - FlowType::InstalledInteractive => "https://accounts.google.com/o/oauth2/v2/auth", - FlowType::InstalledRedirect(_) => "https://accounts.google.com/o/oauth2/v2/auth", - } - } -} - /// Represents either 'installed' or 'web' applications in a json secrets file. /// See `ConsoleApplicationSecret` for more information #[derive(Deserialize, Serialize, Clone, Default)]