From 77355888f2d1f0db09b10d81a374d8d254dd13fb Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Fri, 15 Apr 2016 19:01:30 +0000 Subject: [PATCH 1/5] feat(flows): Implement flow for installed apps The "Installed App Flow" requires user interaction; we first generate a URL that the user has to navigate to. Then, the user either pastes a displayed code into the app being authorized, or the OAuth provider redirects the user's browser to a webserver that is running on localhost. This webserver is provided by the library and the flow should work automatically. Extensive documentation can be found here: https://developers.google.com/identity/protocols/OAuth2InstalledApp An example for the InstalledFlow with the Drive API is here: https://gist.github.com/dermesser/8c915ec4c88ee8e8927e7d40b276ca52 --- src/common.rs | 13 +- src/helper.rs | 55 +++++++- src/installed.rs | 354 +++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs.in | 8 +- 4 files changed, 424 insertions(+), 6 deletions(-) create mode 100644 src/installed.rs diff --git a/src/common.rs b/src/common.rs index 22bb6a0..1dee61a 100644 --- a/src/common.rs +++ b/src/common.rs @@ -136,8 +136,17 @@ impl Token { /// All known authentication types, for suitable constants #[derive(Clone, Copy)] pub enum FlowType { - /// [device authentication](https://developers.google.com/youtube/v3/guides/authentication#devices) + /// [device authentication](https://developers.google.com/youtube/v3/guides/authentication#devices). Only works + /// for certain scopes. Device, + /// [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, + /// Same as InstalledInteractive, but uses a redirect: The OAuth provider redirects the user's + /// browser to a web server that is running on localhost. This may not work as well with the + /// Windows Firewall, but is more comfortable otherwise. The integer describes which port to + /// bind to (default: 8080) + InstalledRedirect(u32), } impl AsRef for FlowType { @@ -145,6 +154,8 @@ impl AsRef for FlowType { 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", } } } diff --git a/src/helper.rs b/src/helper.rs index 83f232e..ed7a59d 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -7,9 +7,11 @@ use std::cmp::min; use std::error::Error; use std::fmt; use std::convert::From; +use std::io; use common::{Token, FlowType, ApplicationSecret}; use device::{PollInformation, RequestError, DeviceFlow, PollError}; +use installed::{InstalledFlow, InstalledFlowReturnMethod}; use refresh::{RefreshResult, RefreshFlow}; use chrono::{DateTime, UTC, Local}; use std::time::Duration; @@ -189,6 +191,27 @@ impl Authenticator } } + + fn do_installed_flow(&mut self, scopes: &Vec<&str>) -> Result> { + let installed_type; + + match self.flow_type { + FlowType::InstalledInteractive => { + installed_type = Some(InstalledFlowReturnMethod::Interactive) + } + FlowType::InstalledRedirect(port) => { + installed_type = Some(InstalledFlowReturnMethod::HTTPRedirect(port)) + } + _ => installed_type = None, + } + + let mut flow = InstalledFlow::new(self.client.borrow_mut(), installed_type); + flow.obtain_token(&mut self.delegate, + &self.secret.client_id, + &self.secret.client_secret, + scopes.iter()) + } + fn retrieve_device_token(&mut self, scopes: &Vec<&str>) -> Result> { let mut flow = DeviceFlow::new(self.client.borrow_mut()); @@ -448,9 +471,37 @@ pub trait AuthenticatorDelegate { /// * Will only be called if the Authenticator's flow_type is `FlowType::Device`. fn present_user_code(&mut self, pi: &PollInformation) { println!("Please enter {} at {} and grant access to this application", - pi.user_code, pi.verification_url); + 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)); + println!("You have time until {}.", + pi.expires_at.with_timezone(&Local)); + } + + /// Only method currently used by the InstalledFlow. + /// We need the user to navigate to a URL using their browser and potentially paste back a code + /// (or maybe not). Whether they have to enter a code depends on the InstalledFlowReturnMethod + /// used. + fn present_user_url(&mut self, url: &String, need_code: bool) -> Option { + if need_code { + println!("Please direct your browser to {}, follow the instructions and enter the \ + code displayed here: ", + url); + + let mut code = String::new(); + let _ = io::stdin().read_line(&mut code); + + if !code.is_empty() { + Some(code) + } else { + None + } + } else { + println!("Please direct your browser to {} and follow the instructions displayed \ + there.", + url); + None + } } } diff --git a/src/installed.rs b/src/installed.rs new file mode 100644 index 0000000..7e95fd0 --- /dev/null +++ b/src/installed.rs @@ -0,0 +1,354 @@ +/* + * Copyright (c) 2016 Google Inc (lewinb@google.com). + * + * Refer to the project root for licensing information. + */ +extern crate serde_json; +extern crate url; + +use std::borrow::BorrowMut; +use std::convert::AsRef; +use std::error::Error; +use std::io; +use std::io::Read; +use std::sync::Mutex; +use std::sync::mpsc::{channel, Receiver, Sender}; + +use hyper; +use hyper::{client, header, server, status, uri}; +use serde_json::error; +use url::form_urlencoded; +use url::percent_encoding::{percent_encode, QUERY_ENCODE_SET}; + +use common::Token; +use helper::AuthenticatorDelegate; + +const OOB_REDIRECT_URI: &'static str = "urn:ietf:wg:oauth:2.0:oob"; + +/// Assembles a URL to request an authorization token (with user interaction). +/// Note that the redirect_uri here has to be either None or some variation of +/// http://localhost:{port}, or the authorization won't work (error "redirect_uri_mismatch") +fn build_authentication_request_url<'a, T, I>(client_id: &str, + scopes: I, + redirect_uri: Option) + -> String + where T: AsRef + 'a, + I: IntoIterator +{ + let mut url = String::new(); + let mut scopes_string = scopes.into_iter().fold(String::new(), |mut acc, sc| { + acc.push_str(sc.as_ref()); + acc.push_str(" "); + acc + }); + // Remove last space + scopes_string.pop(); + + url.push_str("https://accounts.google.com/o/oauth2/v2/auth?"); + vec![format!("scope={}", scopes_string), + format!("&redirect_uri={}", + redirect_uri.unwrap_or(OOB_REDIRECT_URI.to_string())), + format!("&response_type=code"), + format!("&client_id={}", client_id)] + .into_iter() + .fold(url, |mut u, param| { + u.push_str(&percent_encode(param.as_ref(), QUERY_ENCODE_SET)); + u + }) +} + +pub struct InstalledFlow { + client: C, + server: Option, + port: Option, + + auth_code_rcv: Option>, +} + +/// cf. https://developers.google.com/identity/protocols/OAuth2InstalledApp#choosingredirecturi +pub enum InstalledFlowReturnMethod { + /// Involves showing a URL to the user and asking to copy a code from their browser + /// (default) + Interactive, + /// Involves spinning up a local HTTP server and Google redirecting the browser to + /// the server with a URL containing the code (preferred, but not as reliable). The + /// parameter is the port to listen on. + HTTPRedirect(u32), +} + +impl InstalledFlow where C: BorrowMut +{ + /// Starts a new Installed App auth flow. + /// If HTTPRedirect is chosen as method and the server can't be started, the flow falls + /// back to Interactive. + pub fn new(client: C, method: Option) -> InstalledFlow { + let default = InstalledFlow { + client: client, + server: None, + port: None, + auth_code_rcv: None, + }; + match method { + None => default, + Some(InstalledFlowReturnMethod::Interactive) => default, + // Start server on localhost to accept auth code. + Some(InstalledFlowReturnMethod::HTTPRedirect(port)) => { + let server = server::Server::http(format!("127.0.0.1:{}", port).as_str()); + + match server { + Result::Err(_) => default, + Result::Ok(server) => { + let (tx, rx) = channel(); + let listening = server.handle(InstalledFlowHandler { + auth_code_snd: Mutex::new(tx), + }); + + match listening { + Result::Err(_) => default, + Result::Ok(listening) => { + InstalledFlow { + client: default.client, + server: Some(listening), + port: Some(port), + auth_code_rcv: Some(rx), + } + } + } + } + } + } + } + } + + /// Handles the token request flow; it consists of the following steps: + /// . Obtain a auhorization code with user cooperation or internal redirect. + /// . Obtain a token and refresh token using that code. + /// . Return that token + /// + /// It's recommended not to use the DefaultAuthenticatorDelegate, but a specialized one. + pub fn obtain_token<'a, AD: AuthenticatorDelegate, S, T>(&mut self, + auth_delegate: &mut AD, + client_id: &str, + client_secret: &str, + scopes: S) + -> Result> + where T: AsRef + 'a, + S: Iterator + { + use std::error::Error; + let authcode = try!(self.get_authorization_code(auth_delegate, client_id, scopes)); + let tokens = try!(self.request_token(client_secret, client_id, &authcode)); + + // Successful response + if tokens.access_token.is_some() { + let token = Token { + access_token: tokens.access_token.unwrap(), + refresh_token: tokens.refresh_token.unwrap(), + token_type: tokens.token_type.unwrap(), + expires_in: tokens.expires_in, + expires_in_timestamp: None, + }; + + Result::Ok(token) + } else { + let err = io::Error::new(io::ErrorKind::Other, + format!("Token API error: {} {}", + tokens.error.unwrap_or("".to_string()), + tokens.error_description + .unwrap_or("".to_string())) + .as_str()); + Result::Err(Box::new(err)) + } + } + + /// Obtains an authorization code either interactively or via HTTP redirect (see + /// InstalledFlowReturnMethod). + /// TODO(dermesser): Add timeout configurability! + fn get_authorization_code<'a, AD: AuthenticatorDelegate, S, T>(&mut self, + auth_delegate: &mut AD, + client_id: &str, + scopes: S) + -> Result> + where T: AsRef + 'a, + S: Iterator + { + let result: Result> = match self.server { + None => { + let url = build_authentication_request_url(client_id, scopes, None); + match auth_delegate.present_user_url(&url, true /* need_code */) { + None => { + Result::Err(Box::new(io::Error::new(io::ErrorKind::UnexpectedEof, + "couldn't read code"))) + } + // Remove newline + Some(mut code) => { + code.pop(); + Result::Ok(code) + } + } + } + Some(_) => { + // The redirect URI must be this very localhost URL, otherwise Google refuses + // authorization. + let url = build_authentication_request_url(client_id, + scopes, + Some(format!("http://localhost:{}", + self.port + .unwrap_or(8080)))); + auth_delegate.present_user_url(&url, false /* need_code */); + + match self.auth_code_rcv.as_ref().unwrap().recv() { + Result::Err(e) => Result::Err(Box::new(e)), + Result::Ok(s) => Result::Ok(s), + } + } + }; + let _ = self.server.as_mut().map(|l| l.close()); + result + } + + /// Sends the authorization code to the provider in order to obtain access and refresh tokens. + fn request_token(&mut self, + client_secret: &str, + client_id: &str, + authcode: &str) + -> Result> { + let redirect_uri; + + match self.port { + None => redirect_uri = OOB_REDIRECT_URI.to_string(), + Some(p) => redirect_uri = format!("http://localhost:{}", p), + } + + let body = form_urlencoded::serialize(vec![("code".to_string(), authcode.to_string()), + ("client_id".to_string(), + client_id.to_string()), + ("client_secret".to_string(), + client_secret.to_string()), + ("redirect_uri".to_string(), redirect_uri), + ("grant_type".to_string(), + "authorization_code".to_string())]); + + let result: Result = + self.client + .borrow_mut() + .post("https://www.googleapis.com/oauth2/v4/token") + .body(&body) + .header(header::ContentType("application/x-www-form-urlencoded".parse().unwrap())) + .send(); + + let mut resp = String::new(); + + match result { + Result::Err(e) => return Result::Err(Box::new(e)), + Result::Ok(mut response) => { + let result = response.read_to_string(&mut resp); + + match result { + Result::Err(e) => return Result::Err(Box::new(e)), + Result::Ok(_) => (), + } + } + } + + let token_resp: Result = serde_json::from_str(&resp); + + match token_resp { + Result::Err(e) => return Result::Err(Box::new(e)), + Result::Ok(tok) => Result::Ok(tok) as Result>, + } + } +} + +#[derive(Deserialize)] +struct JSONTokenResponse { + access_token: Option, + refresh_token: Option, + token_type: Option, + expires_in: Option, + + error: Option, + error_description: Option, +} + +/// HTTP handler handling the redirect from the provider. +struct InstalledFlowHandler { + auth_code_snd: Mutex>, +} + +impl server::Handler for InstalledFlowHandler { + fn handle(&self, rq: server::Request, mut rp: server::Response) { + match rq.uri { + uri::RequestUri::AbsolutePath(path) => { + // We use a fake URL because the redirect goes to a URL, meaning we + // can't use the url form decode (because there's slashes and hashes and stuff in + // it). + let url = hyper::Url::parse(&format!("http://example.com{}", path)); + + if url.is_err() { + *rp.status_mut() = status::StatusCode::BadRequest; + let _ = rp.send("Unparseable URL".as_ref()); + } else { + self.handle_url(url.unwrap()); + *rp.status_mut() = status::StatusCode::Ok; + let _ = rp.send("SuccessYou may now \ + close this window." + .as_ref()); + } + } + _ => { + *rp.status_mut() = status::StatusCode::BadRequest; + let _ = rp.send("Invalid Request!".as_ref()); + } + } + } +} + +impl InstalledFlowHandler { + fn handle_url(&self, url: hyper::Url) { + // Google redirects to the specified localhost URL, appending the authorization + // code, like this: http://localhost:8080/xyz/?code=4/731fJ3BheyCouCniPufAd280GHNV5Ju35yYcGs + // We take that code and send it to the get_authorization_code() function that + // waits for it. + for (param, val) in url.query_pairs().unwrap_or(Vec::new()) { + if param == "code".to_string() { + let _ = self.auth_code_snd.lock().unwrap().send(val); + } + } + + } +} + +#[cfg(test)] +mod tests { + use super::build_authentication_request_url; + use super::InstalledFlowHandler; + + use std::sync::Mutex; + use std::sync::mpsc::channel; + + use hyper::Url; + + #[test] + fn test_request_url_builder() { + assert_eq!("https://accounts.google.\ + com/o/oauth2/v2/auth?scope=email%20profile&redirect_uri=urn:ietf:wg:oauth:2.\ + 0:oob&response_type=code&client_id=812741506391-h38jh0j4fv0ce1krdkiq0hfvt6n5a\ + mrf.apps.googleusercontent.com", + build_authentication_request_url("812741506391-h38jh0j4fv0ce1krdkiq0hfvt6n5am\ + rf.apps.googleusercontent.com", + vec![&"email".to_string(), + &"profile".to_string()], + None)); + } + + #[test] + fn test_http_handle_url() { + let (tx, rx) = channel(); + let handler = InstalledFlowHandler { auth_code_snd: Mutex::new(tx) }; + // URLs are usually a bit botched + let url = Url::parse("http://example.com:1234/?code=ab/c%2Fd#").unwrap(); + handler.handle_url(url); + assert_eq!(rx.recv().unwrap(), "ab/c/d".to_string()); + } +} diff --git a/src/lib.rs.in b/src/lib.rs.in index 17d5103..cdf1d22 100644 --- a/src/lib.rs.in +++ b/src/lib.rs.in @@ -13,12 +13,14 @@ extern crate url; extern crate itertools; mod device; +mod installed; +mod helper; mod refresh; mod common; -mod helper; pub use device::{DeviceFlow, PollInformation, PollError}; pub use refresh::{RefreshFlow, RefreshResult}; pub use common::{Token, FlowType, ApplicationSecret, ConsoleApplicationSecret, Scheme, TokenType}; -pub use helper::{TokenStorage, NullStorage, MemoryStorage, Authenticator, - AuthenticatorDelegate, Retry, DefaultAuthenticatorDelegate, GetToken}; +pub use installed::{InstalledFlow, InstalledFlowReturnMethod}; +pub use helper::{TokenStorage, NullStorage, MemoryStorage, Authenticator, AuthenticatorDelegate, + Retry, DefaultAuthenticatorDelegate, GetToken}; From 9b31070edbac3ba9e42dc08c81a48ec3fee8a728 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Sat, 16 Apr 2016 20:54:11 +0200 Subject: [PATCH 2/5] fix(installed): Genericize token/auth URI This means that in theory other providers besides Google could be used. --- src/helper.rs | 4 ++++ src/installed.rs | 35 ++++++++++++++++++++--------------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/helper.rs b/src/helper.rs index ed7a59d..5cb71a8 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -207,6 +207,8 @@ impl Authenticator let mut flow = InstalledFlow::new(self.client.borrow_mut(), installed_type); flow.obtain_token(&mut self.delegate, + &self.secret.auth_uri, + &self.secret.token_uri, &self.secret.client_id, &self.secret.client_secret, scopes.iter()) @@ -366,6 +368,8 @@ impl GetToken for Authenticator match match self.flow_type { FlowType::Device => self.retrieve_device_token(&scopes), + FlowType::InstalledInteractive => self.do_installed_flow(&scopes), + FlowType::InstalledRedirect(_) => self.do_installed_flow(&scopes), } { Ok(token) => { diff --git a/src/installed.rs b/src/installed.rs index 7e95fd0..49da82f 100644 --- a/src/installed.rs +++ b/src/installed.rs @@ -1,8 +1,7 @@ -/* - * Copyright (c) 2016 Google Inc (lewinb@google.com). - * - * Refer to the project root for licensing information. - */ +// Copyright (c) 2016 Google Inc (lewinb@google.com). +// +// Refer to the project root for licensing information. +// extern crate serde_json; extern crate url; @@ -28,7 +27,8 @@ const OOB_REDIRECT_URI: &'static str = "urn:ietf:wg:oauth:2.0:oob"; /// Assembles a URL to request an authorization token (with user interaction). /// Note that the redirect_uri here has to be either None or some variation of /// http://localhost:{port}, or the authorization won't work (error "redirect_uri_mismatch") -fn build_authentication_request_url<'a, T, I>(client_id: &str, +fn build_authentication_request_url<'a, T, I>(auth_uri: &str, + client_id: &str, scopes: I, redirect_uri: Option) -> String @@ -44,8 +44,8 @@ fn build_authentication_request_url<'a, T, I>(client_id: &str, // Remove last space scopes_string.pop(); - url.push_str("https://accounts.google.com/o/oauth2/v2/auth?"); - vec![format!("scope={}", scopes_string), + url.push_str(auth_uri); + vec![format!("?scope={}", scopes_string), format!("&redirect_uri={}", redirect_uri.unwrap_or(OOB_REDIRECT_URI.to_string())), format!("&response_type=code"), @@ -128,6 +128,8 @@ impl InstalledFlow where C: BorrowMut /// It's recommended not to use the DefaultAuthenticatorDelegate, but a specialized one. pub fn obtain_token<'a, AD: AuthenticatorDelegate, S, T>(&mut self, auth_delegate: &mut AD, + auth_uri: &str, + token_uri: &str, client_id: &str, client_secret: &str, scopes: S) @@ -136,8 +138,8 @@ impl InstalledFlow where C: BorrowMut S: Iterator { use std::error::Error; - let authcode = try!(self.get_authorization_code(auth_delegate, client_id, scopes)); - let tokens = try!(self.request_token(client_secret, client_id, &authcode)); + let authcode = try!(self.get_authorization_code(auth_delegate, auth_uri, client_id, scopes)); + let tokens = try!(self.request_token(token_uri, client_secret, client_id, &authcode)); // Successful response if tokens.access_token.is_some() { @@ -166,6 +168,7 @@ impl InstalledFlow where C: BorrowMut /// TODO(dermesser): Add timeout configurability! fn get_authorization_code<'a, AD: AuthenticatorDelegate, S, T>(&mut self, auth_delegate: &mut AD, + auth_uri: &str, client_id: &str, scopes: S) -> Result> @@ -174,7 +177,7 @@ impl InstalledFlow where C: BorrowMut { let result: Result> = match self.server { None => { - let url = build_authentication_request_url(client_id, scopes, None); + let url = build_authentication_request_url(auth_uri, client_id, scopes, None); match auth_delegate.present_user_url(&url, true /* need_code */) { None => { Result::Err(Box::new(io::Error::new(io::ErrorKind::UnexpectedEof, @@ -190,7 +193,7 @@ impl InstalledFlow where C: BorrowMut Some(_) => { // The redirect URI must be this very localhost URL, otherwise Google refuses // authorization. - let url = build_authentication_request_url(client_id, + let url = build_authentication_request_url(auth_uri, client_id, scopes, Some(format!("http://localhost:{}", self.port @@ -209,6 +212,7 @@ impl InstalledFlow where C: BorrowMut /// Sends the authorization code to the provider in order to obtain access and refresh tokens. fn request_token(&mut self, + token_uri: &str, client_secret: &str, client_id: &str, authcode: &str) @@ -232,7 +236,7 @@ impl InstalledFlow where C: BorrowMut let result: Result = self.client .borrow_mut() - .post("https://www.googleapis.com/oauth2/v4/token") + .post(token_uri) .body(&body) .header(header::ContentType("application/x-www-form-urlencoded".parse().unwrap())) .send(); @@ -332,10 +336,11 @@ mod tests { #[test] fn test_request_url_builder() { assert_eq!("https://accounts.google.\ - com/o/oauth2/v2/auth?scope=email%20profile&redirect_uri=urn:ietf:wg:oauth:2.\ + com/o/oauth2/auth?scope=email%20profile&redirect_uri=urn:ietf:wg:oauth:2.\ 0:oob&response_type=code&client_id=812741506391-h38jh0j4fv0ce1krdkiq0hfvt6n5a\ mrf.apps.googleusercontent.com", - build_authentication_request_url("812741506391-h38jh0j4fv0ce1krdkiq0hfvt6n5am\ + build_authentication_request_url("https://accounts.google.com/o/oauth2/auth", + "812741506391-h38jh0j4fv0ce1krdkiq0hfvt6n5am\ rf.apps.googleusercontent.com", vec![&"email".to_string(), &"profile".to_string()], From b039dc0cdc29bc92b3cdcdf19502f2589dc92327 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Sat, 16 Apr 2016 21:02:57 +0200 Subject: [PATCH 3/5] refactor(installedflow): use app secret instead of single params --- src/helper.rs | 5 +---- src/installed.rs | 38 ++++++++++++++++++-------------------- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/src/helper.rs b/src/helper.rs index 5cb71a8..d8f255c 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -207,10 +207,7 @@ impl Authenticator let mut flow = InstalledFlow::new(self.client.borrow_mut(), installed_type); flow.obtain_token(&mut self.delegate, - &self.secret.auth_uri, - &self.secret.token_uri, - &self.secret.client_id, - &self.secret.client_secret, + &self.secret, scopes.iter()) } diff --git a/src/installed.rs b/src/installed.rs index 49da82f..d50ef03 100644 --- a/src/installed.rs +++ b/src/installed.rs @@ -19,7 +19,7 @@ use serde_json::error; use url::form_urlencoded; use url::percent_encoding::{percent_encode, QUERY_ENCODE_SET}; -use common::Token; +use common::{ApplicationSecret, Token}; use helper::AuthenticatorDelegate; const OOB_REDIRECT_URI: &'static str = "urn:ietf:wg:oauth:2.0:oob"; @@ -128,18 +128,15 @@ impl InstalledFlow where C: BorrowMut /// It's recommended not to use the DefaultAuthenticatorDelegate, but a specialized one. pub fn obtain_token<'a, AD: AuthenticatorDelegate, S, T>(&mut self, auth_delegate: &mut AD, - auth_uri: &str, - token_uri: &str, - client_id: &str, - client_secret: &str, + appsecret: &ApplicationSecret, scopes: S) -> Result> where T: AsRef + 'a, S: Iterator { use std::error::Error; - let authcode = try!(self.get_authorization_code(auth_delegate, auth_uri, client_id, scopes)); - let tokens = try!(self.request_token(token_uri, client_secret, client_id, &authcode)); + let authcode = try!(self.get_authorization_code(auth_delegate, &appsecret, scopes)); + let tokens = try!(self.request_token(&appsecret, &authcode)); // Successful response if tokens.access_token.is_some() { @@ -168,8 +165,7 @@ impl InstalledFlow where C: BorrowMut /// TODO(dermesser): Add timeout configurability! fn get_authorization_code<'a, AD: AuthenticatorDelegate, S, T>(&mut self, auth_delegate: &mut AD, - auth_uri: &str, - client_id: &str, + appsecret: &ApplicationSecret, scopes: S) -> Result> where T: AsRef + 'a, @@ -177,7 +173,10 @@ impl InstalledFlow where C: BorrowMut { let result: Result> = match self.server { None => { - let url = build_authentication_request_url(auth_uri, client_id, scopes, None); + let url = build_authentication_request_url(&appsecret.auth_uri, + &appsecret.client_id, + scopes, + None); match auth_delegate.present_user_url(&url, true /* need_code */) { None => { Result::Err(Box::new(io::Error::new(io::ErrorKind::UnexpectedEof, @@ -193,7 +192,8 @@ impl InstalledFlow where C: BorrowMut Some(_) => { // The redirect URI must be this very localhost URL, otherwise Google refuses // authorization. - let url = build_authentication_request_url(auth_uri, client_id, + let url = build_authentication_request_url(&appsecret.auth_uri, + &appsecret.client_id, scopes, Some(format!("http://localhost:{}", self.port @@ -212,9 +212,7 @@ impl InstalledFlow where C: BorrowMut /// Sends the authorization code to the provider in order to obtain access and refresh tokens. fn request_token(&mut self, - token_uri: &str, - client_secret: &str, - client_id: &str, + appsecret: &ApplicationSecret, authcode: &str) -> Result> { let redirect_uri; @@ -226,9 +224,9 @@ impl InstalledFlow where C: BorrowMut let body = form_urlencoded::serialize(vec![("code".to_string(), authcode.to_string()), ("client_id".to_string(), - client_id.to_string()), + appsecret.client_id.clone()), ("client_secret".to_string(), - client_secret.to_string()), + appsecret.client_secret.clone()), ("redirect_uri".to_string(), redirect_uri), ("grant_type".to_string(), "authorization_code".to_string())]); @@ -236,7 +234,7 @@ impl InstalledFlow where C: BorrowMut let result: Result = self.client .borrow_mut() - .post(token_uri) + .post(&appsecret.token_uri) .body(&body) .header(header::ContentType("application/x-www-form-urlencoded".parse().unwrap())) .send(); @@ -336,9 +334,9 @@ mod tests { #[test] fn test_request_url_builder() { assert_eq!("https://accounts.google.\ - com/o/oauth2/auth?scope=email%20profile&redirect_uri=urn:ietf:wg:oauth:2.\ - 0:oob&response_type=code&client_id=812741506391-h38jh0j4fv0ce1krdkiq0hfvt6n5a\ - mrf.apps.googleusercontent.com", + com/o/oauth2/auth?scope=email%20profile&redirect_uri=urn:ietf:wg:oauth:2.0:\ + oob&response_type=code&client_id=812741506391-h38jh0j4fv0ce1krdkiq0hfvt6n5amr\ + f.apps.googleusercontent.com", build_authentication_request_url("https://accounts.google.com/o/oauth2/auth", "812741506391-h38jh0j4fv0ce1krdkiq0hfvt6n5am\ rf.apps.googleusercontent.com", From 4e1d9bd75091583ff9d8f49e6e8cd46458677457 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Wed, 27 Apr 2016 19:19:20 +0200 Subject: [PATCH 4/5] fix(dependencies): hyper also broke us The Url::query_pairs() function returns a different type now. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c5d362a..a083a4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,8 +14,8 @@ build = "src/build.rs" chrono = ">= 0.2" log = ">= 0.3" mime = ">= 0.1" -url = ">= 0.5" -hyper = ">= 0.8.0" +url = "= 0.5" +hyper = "0.8.0" itertools = ">= 0.4" serde = "0.6.0" serde_json = "0.6.0" From a7e8423fe0caa6d119d9ab439b6fe58aac1382e3 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Wed, 27 Apr 2016 19:24:28 +0200 Subject: [PATCH 5/5] refactor(installedflow): Apply suggestions from PR --- src/helper.rs | 8 +------- src/installed.rs | 3 +-- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/helper.rs b/src/helper.rs index d8f255c..917b072 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -490,13 +490,7 @@ pub trait AuthenticatorDelegate { url); let mut code = String::new(); - let _ = io::stdin().read_line(&mut code); - - if !code.is_empty() { - Some(code) - } else { - None - } + io::stdin().read_line(&mut code).ok().map(|_| code) } else { println!("Please direct your browser to {} and follow the instructions displayed \ there.", diff --git a/src/installed.rs b/src/installed.rs index d50ef03..19f8701 100644 --- a/src/installed.rs +++ b/src/installed.rs @@ -162,7 +162,6 @@ impl InstalledFlow where C: BorrowMut /// Obtains an authorization code either interactively or via HTTP redirect (see /// InstalledFlowReturnMethod). - /// TODO(dermesser): Add timeout configurability! fn get_authorization_code<'a, AD: AuthenticatorDelegate, S, T>(&mut self, auth_delegate: &mut AD, appsecret: &ApplicationSecret, @@ -206,7 +205,7 @@ impl InstalledFlow where C: BorrowMut } } }; - let _ = self.server.as_mut().map(|l| l.close()); + self.server.as_mut().map(|l| l.close()).is_some(); result }