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.
This commit is contained in:
Lewin Bormann
2016-10-02 18:09:21 +02:00
committed by Sebastian Thiel
parent 08d79de313
commit a8479b8ddb
6 changed files with 55 additions and 90 deletions

View File

@@ -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<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,
@@ -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) => {
@@ -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) {
@@ -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),
} {

View File

@@ -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<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,
}
@@ -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,
@@ -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() {
@@ -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)
}
};
@@ -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() {
@@ -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() {
let mut flow = DeviceFlow::new(
hyper::Client::with_connector(<MockGoogleAuth as Default>::default()));
use helper::parse_application_secret;
match flow.request_code("bogus_client_id",
"bogus_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()), &appsecret, GOOGLE_DEVICE_CODE_URL);
match flow.request_code(
&["https://www.googleapis.com/auth/youtube.upload"]) {
Ok(pi) => assert_eq!(pi.interval, Duration::from_secs(0)),
_ => unreachable!(),

View File

@@ -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))]

View File

@@ -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};

View File

@@ -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<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;
@@ -66,14 +64,14 @@ impl<C> RefreshFlow<C>
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(<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());

View File

@@ -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<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)]