Skip to content

Commit

Permalink
feat(device): Make the Device flow independent of Google
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dermesser authored and Byron committed Oct 9, 2016
1 parent 08d79de commit a8479b8
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 89 deletions.
21 changes: 9 additions & 12 deletions src/authenticator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -76,7 +76,7 @@ impl<D, S, C> Authenticator<D, S, C>
flow_type: Option<FlowType>)
-> Authenticator<D, S, C> {
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,
Expand All @@ -102,15 +102,13 @@ impl<D, S, C> Authenticator<D, S, C>
flow.obtain_token(&mut self.delegate, &self.secret, scopes.iter())
}

fn retrieve_device_token(&mut self, scopes: &Vec<&str>) -> Result<Token, Box<Error>> {
let mut flow = DeviceFlow::new(self.client.borrow_mut());
fn retrieve_device_token(&mut self, scopes: &Vec<&str>, code_url: String) -> Result<Token, Box<Error>> {
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) => {
Expand Down Expand Up @@ -214,9 +212,8 @@ impl<D, S, C> GetToken for Authenticator<D, S, C>
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) {
Expand Down Expand Up @@ -263,8 +260,8 @@ impl<D, S, C> GetToken for Authenticator<D, S, C>
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),
} {
Expand Down
57 changes: 24 additions & 33 deletions src/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -36,34 +36,25 @@ pub struct DeviceFlow<C> {
device_code: String,
state: Option<DeviceFlowState>,
error: Option<PollError>,
secret: String,
id: String,
application_secret: ApplicationSecret,
device_code_url: String,
}

impl<C> Flow for DeviceFlow<C> {
fn type_id() -> FlowType {
FlowType::Device
FlowType::Device(String::new())
}
}
impl<C> DeviceFlow<C>
where C: BorrowMut<hyper::Client>
{
/// # 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<C> {

pub fn new<S: AsRef<str>>(client: C, secret: &ApplicationSecret, device_code_url: S) -> DeviceFlow<C> {
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,
}
Expand All @@ -85,8 +76,6 @@ impl<C> DeviceFlow<C>
/// # 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<PollInformation, RequestError>
where T: AsRef<str> + 'b,
Expand All @@ -98,19 +87,19 @@ impl<C> DeviceFlow<C>

// 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::<String>()
.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() {
Expand Down Expand Up @@ -149,8 +138,6 @@ impl<C> DeviceFlow<C>
};
self.state = Some(DeviceFlowState::Pending(pi.clone()));

self.secret = client_secret.to_string();
self.id = client_id.to_string();
Ok(pi)
}
};
Expand Down Expand Up @@ -195,15 +182,15 @@ impl<C> DeviceFlow<C>
}

// 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() {
Expand Down Expand Up @@ -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(<MockGoogleAuth as Default>::default()));
hyper::Client::with_connector(<MockGoogleAuth as Default>::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!(),
Expand Down
23 changes: 2 additions & 21 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))]

Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs.in
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
23 changes: 14 additions & 9 deletions src/refresh.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use types::{FlowType, JsonError};
use device::GOOGLE_TOKEN_URL;
use types::{ApplicationSecret, FlowType, JsonError};

use chrono::UTC;
use hyper;
Expand Down Expand Up @@ -57,23 +56,22 @@ impl<C> RefreshFlow<C>
/// 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;
if let RefreshResult::Success(_) = self.result {
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() {
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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(<MockGoogleRefresh as Default>::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());
Expand Down
17 changes: 4 additions & 13 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -242,17 +244,6 @@ pub enum FlowType {
InstalledRedirect(u32),
}

impl AsRef<str> 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)]
Expand Down

0 comments on commit a8479b8

Please sign in to comment.