mirror of
https://github.com/OMGeeky/yup-oauth2.git
synced 2026-01-07 03:31:31 +01:00
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:
committed by
Sebastian Thiel
parent
08d79de313
commit
a8479b8ddb
@@ -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),
|
||||
} {
|
||||
|
||||
@@ -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!(),
|
||||
|
||||
23
src/lib.rs
23
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))]
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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());
|
||||
|
||||
17
src/types.rs
17
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<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)]
|
||||
|
||||
Reference in New Issue
Block a user