Format to fustfmt defaults and force lint check in Travis

This commit is contained in:
Guy Taylor
2019-02-21 20:21:18 +00:00
parent b5ff80a541
commit b96cfcd66a
12 changed files with 553 additions and 408 deletions

View File

@@ -44,7 +44,7 @@ jobs:
- rustup component add clippy
- rustup component add rustfmt
script:
- cargo fmt --all -- --check || true
- cargo fmt --all -- --check
- cargo clippy --all-targets --all-features -- -D warnings || true
- stage: coverage

View File

@@ -1,12 +1,11 @@
use yup_oauth2::{self as oauth2, GetToken};
use yup_hyper_mock as mock;
use chrono::{Local};
use getopts::{HasArg,Options,Occur,Fail};
use std::env;
use chrono::Local;
use getopts::{Fail, HasArg, Occur, Options};
use std::default::Default;
use std::time::Duration;
use std::env;
use std::thread::sleep;
use std::time::Duration;
use yup_hyper_mock as mock;
use yup_oauth2::{self as oauth2, GetToken};
fn usage(program: &str, opts: &Options, err: Option<Fail>) -> ! {
if err.is_some() {
@@ -14,9 +13,14 @@ fn usage(program: &str, opts: &Options, err: Option<Fail>) -> ! {
std::process::exit(1);
}
println!("{}", opts.short_usage(program) + " SCOPE [SCOPE ...]");
println!("{}", opts.usage("A program to authenticate against oauthv2 services.\n\
See https://developers.google.com/youtube/registering_an_application\n\
and https://developers.google.com/youtube/v3/guides/authentication#devices"));
println!(
"{}",
opts.usage(
"A program to authenticate against oauthv2 services.\n\
See https://developers.google.com/youtube/registering_an_application\n\
and https://developers.google.com/youtube/v3/guides/authentication#devices"
)
);
std::process::exit(0);
}
@@ -26,8 +30,22 @@ fn main() {
let prog = args[0].clone();
let mut opts = Options::new();
opts.opt("c", "id", "oauthv2 ID of your application", "CLIENT_ID", HasArg::Yes, Occur::Req)
.opt("s", "secret", "oauthv2 secret of your application", "CLIENT_SECRET", HasArg::Yes, Occur::Req);
opts.opt(
"c",
"id",
"oauthv2 ID of your application",
"CLIENT_ID",
HasArg::Yes,
Occur::Req,
)
.opt(
"s",
"secret",
"oauthv2 secret of your application",
"CLIENT_SECRET",
HasArg::Yes,
Occur::Req,
);
let m = match opts.parse(&args[1..]) {
Ok(m) => m,
@@ -37,7 +55,9 @@ fn main() {
};
if m.free.len() == 0 {
let msg = Fail::ArgumentMissing("you must provide one or more authorization scopes as free options".to_string());
let msg = Fail::ArgumentMissing(
"you must provide one or more authorization scopes as free options".to_string(),
);
usage(&prog, &opts, Some(msg));
}
@@ -55,11 +75,15 @@ fn main() {
struct StdoutHandler;
impl oauth2::AuthenticatorDelegate for StdoutHandler {
fn present_user_code(&mut self, pi: &oauth2::PollInformation) {
println!("Please enter '{}' at {} and authenticate the application for the\n\
println!(
"Please enter '{}' at {} and authenticate the application for the\n\
given scopes. This is not a test !\n\
You have time until {} to do that.
Do not terminate the program until you deny or grant access !",
pi.user_code, pi.verification_url, pi.expires_at.with_timezone(&Local));
pi.user_code,
pi.verification_url,
pi.expires_at.with_timezone(&Local)
);
let delay = Duration::from_secs(5);
println!("Browser opens automatically in {:?} seconds", delay);
sleep(delay);
@@ -69,17 +93,18 @@ fn main() {
}
let client = hyper::Client::with_connector(mock::TeeConnector {
connector: hyper::net::HttpConnector
});
connector: hyper::net::HttpConnector,
});
match oauth2::Authenticator::new(&secret, StdoutHandler, client,
oauth2::NullStorage, None).token(&m.free) {
match oauth2::Authenticator::new(&secret, StdoutHandler, client, oauth2::NullStorage, None)
.token(&m.free)
{
Ok(t) => {
println!("Authentication granted !");
println!("You should store the following information for use, or revoke it.");
println!("All dates are given in UTC.");
println!("{:?}", t);
},
}
Err(err) => {
println!("Access token wasn't obtained: {}", err);
std::process::exit(10);

View File

@@ -9,11 +9,11 @@ use std::thread::sleep;
use std::time::Duration;
use crate::authenticator_delegate::{AuthenticatorDelegate, PollError, PollInformation};
use crate::device::{GOOGLE_DEVICE_CODE_URL, DeviceFlow};
use crate::device::{DeviceFlow, GOOGLE_DEVICE_CODE_URL};
use crate::installed::{InstalledFlow, InstalledFlowReturnMethod};
use crate::refresh::{RefreshResult, RefreshFlow};
use crate::refresh::{RefreshFlow, RefreshResult};
use crate::storage::TokenStorage;
use crate::types::{RequestError, StringError, Token, FlowType, ApplicationSecret};
use crate::types::{ApplicationSecret, FlowType, RequestError, StringError, Token};
use hyper;
@@ -46,16 +46,18 @@ pub struct Authenticator<D, S, C> {
/// if no user is involved.
pub trait GetToken {
fn token<'b, I, T>(&mut self, scopes: I) -> Result<Token, Box<Error>>
where T: AsRef<str> + Ord + 'b,
I: IntoIterator<Item = &'b T>;
where
T: AsRef<str> + Ord + 'b,
I: IntoIterator<Item = &'b T>;
fn api_key(&mut self) -> Option<String>;
}
impl<D, S, C> Authenticator<D, S, C>
where D: AuthenticatorDelegate,
S: TokenStorage,
C: BorrowMut<hyper::Client>
where
D: AuthenticatorDelegate,
S: TokenStorage,
C: BorrowMut<hyper::Client>,
{
/// Returns a new `Authenticator` instance
///
@@ -70,12 +72,13 @@ impl<D, S, C> Authenticator<D, S, C>
/// * `flow_type` - the kind of authentication to use to obtain a token for the
/// required scopes. If unset, it will be derived from the secret.
/// [dev-con]: https://console.developers.google.com
pub fn new(secret: &ApplicationSecret,
delegate: D,
client: C,
storage: S,
flow_type: Option<FlowType>)
-> Authenticator<D, S, C> {
pub fn new(
secret: &ApplicationSecret,
delegate: D,
client: C,
storage: S,
flow_type: Option<FlowType>,
) -> Authenticator<D, S, C> {
Authenticator {
flow_type: flow_type.unwrap_or(FlowType::Device(GOOGLE_DEVICE_CODE_URL.to_string())),
delegate: delegate,
@@ -85,7 +88,6 @@ impl<D, S, C> Authenticator<D, S, C>
}
}
fn do_installed_flow(&mut self, scopes: &Vec<&str>) -> Result<Token, Box<Error>> {
let installed_type;
@@ -103,7 +105,11 @@ 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>, code_url: String) -> Result<Token, Box<Error>> {
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
@@ -117,14 +123,14 @@ impl<D, S, C> Authenticator<D, S, C>
RequestError::HttpError(err) => {
match self.delegate.connection_error(&err) {
Retry::Abort | Retry::Skip => {
return Err(Box::new(StringError::from(&err as &Error)))
return Err(Box::new(StringError::from(&err as &Error)));
}
Retry::After(d) => sleep(d),
}
}
RequestError::InvalidClient |
RequestError::NegativeServerResponse(_, _) |
RequestError::InvalidScope(_) => {
RequestError::InvalidClient
| RequestError::NegativeServerResponse(_, _)
| RequestError::InvalidScope(_) => {
let serr = StringError::from(res_err.to_string());
self.delegate.request_failure(res_err);
return Err(Box::new(serr));
@@ -149,7 +155,7 @@ impl<D, S, C> Authenticator<D, S, C>
&&PollError::HttpError(ref err) => {
match self.delegate.connection_error(err) {
Retry::Abort | Retry::Skip => {
return Err(Box::new(StringError::from(err as &Error)))
return Err(Box::new(StringError::from(err as &Error)));
}
Retry::After(d) => sleep(d),
}
@@ -164,16 +170,15 @@ impl<D, S, C> Authenticator<D, S, C>
}
}; // end match poll_err
}
Ok(None) => {
match self.delegate.pending(&pi) {
Retry::Abort | Retry::Skip => {
return Err(Box::new(StringError::new("Pending authentication aborted"
.to_string(),
None)))
}
Retry::After(d) => sleep(min(d, pi.interval)),
Ok(None) => match self.delegate.pending(&pi) {
Retry::Abort | Retry::Skip => {
return Err(Box::new(StringError::new(
"Pending authentication aborted".to_string(),
None,
)));
}
}
Retry::After(d) => sleep(min(d, pi.interval)),
},
Ok(Some(token)) => return Ok(token),
}
}
@@ -181,9 +186,10 @@ impl<D, S, C> Authenticator<D, S, C>
}
impl<D, S, C> GetToken for Authenticator<D, S, C>
where D: AuthenticatorDelegate,
S: TokenStorage,
C: BorrowMut<hyper::Client>
where
D: AuthenticatorDelegate,
S: TokenStorage,
C: BorrowMut<hyper::Client>,
{
/// Blocks until a token was retrieved from storage, from the server, or until the delegate
/// decided to abort the attempt, or the user decided not to authorize the application.
@@ -191,11 +197,13 @@ impl<D, S, C> GetToken for Authenticator<D, S, C>
/// the caller will be informed about storage related errors.
/// Otherwise it is guaranteed to be valid for the given scopes.
fn token<'b, I, T>(&mut self, scopes: I) -> Result<Token, Box<Error>>
where T: AsRef<str> + Ord + 'b,
I: IntoIterator<Item = &'b T>
where
T: AsRef<str> + Ord + 'b,
I: IntoIterator<Item = &'b T>,
{
let (scope_key, scopes) = {
let mut sv: Vec<&str> = scopes.into_iter()
let mut sv: Vec<&str> = scopes
.into_iter()
.map(|s| s.as_ref())
.collect::<Vec<&str>>();
sv.sort();
@@ -213,33 +221,41 @@ 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.clone(),
&self.secret,
&t.refresh_token) {
match *rf.refresh_token(
self.flow_type.clone(),
&self.secret,
&t.refresh_token,
) {
RefreshResult::Error(ref err) => {
match self.delegate.connection_error(err) {
Retry::Abort|Retry::Skip =>
Retry::Abort | Retry::Skip => {
return Err(Box::new(StringError::new(
err.description().to_string(),
None))),
err.description().to_string(),
None,
)));
}
Retry::After(d) => sleep(d),
}
}
RefreshResult::RefreshError(ref err_str, ref err_description) => {
self.delegate.token_refresh_failed(&err_str, &err_description);
let storage_err = match self.storage
.set(scope_key, &scopes, None) {
Ok(_) => String::new(),
Err(err) => err.to_string(),
};
return Err(Box::new(StringError::new(storage_err + err_str,
err_description.as_ref())));
self.delegate
.token_refresh_failed(&err_str, &err_description);
let storage_err =
match self.storage.set(scope_key, &scopes, None) {
Ok(_) => String::new(),
Err(err) => err.to_string(),
};
return Err(Box::new(StringError::new(
storage_err + err_str,
err_description.as_ref(),
)));
}
RefreshResult::Success(ref new_t) => {
t = new_t.clone();
loop {
if let Err(err) = self.storage
.set(scope_key, &scopes, Some(t.clone())) {
if let Err(err) =
self.storage.set(scope_key, &scopes, Some(t.clone()))
{
match self.delegate.token_storage_failure(true, &err) {
Retry::Skip => break,
Retry::Abort => return Err(Box::new(err)),
@@ -253,9 +269,9 @@ impl<D, S, C> GetToken for Authenticator<D, S, C>
}
break; // refresh_token loop
}
}// RefreshResult handling
}// refresh loop
}// handle expiration
} // RefreshResult handling
} // refresh loop
} // handle expiration
Ok(t)
}
Ok(None) => {
@@ -268,8 +284,9 @@ impl<D, S, C> GetToken for Authenticator<D, S, C>
} {
Ok(token) => {
loop {
if let Err(err) = self.storage
.set(scope_key, &scopes, Some(token.clone())) {
if let Err(err) =
self.storage.set(scope_key, &scopes, Some(token.clone()))
{
match self.delegate.token_storage_failure(true, &err) {
Retry::Skip => break,
Retry::Abort => return Err(Box::new(err)),
@@ -280,23 +297,21 @@ impl<D, S, C> GetToken for Authenticator<D, S, C>
}
}
break;
}// end attempt to save
} // end attempt to save
Ok(token)
}
Err(err) => Err(err),
}// end match token retrieve result
} // end match token retrieve result
}
Err(err) => {
match self.delegate.token_storage_failure(false, &err) {
Retry::Abort | Retry::Skip => Err(Box::new(err)),
Retry::After(d) => {
sleep(d);
continue;
}
Err(err) => match self.delegate.token_storage_failure(false, &err) {
Retry::Abort | Retry::Skip => Err(Box::new(err)),
Retry::After(d) => {
sleep(d);
continue;
}
}
};// end match
}// end loop
},
}; // end match
} // end loop
}
fn api_key(&mut self) -> Option<String> {
@@ -307,7 +322,6 @@ impl<D, S, C> GetToken for Authenticator<D, S, C>
}
}
/// A utility type to indicate how operations DeviceFlowHelper operations should be retried
pub enum Retry {
/// Signal you don't want to retry
@@ -319,27 +333,33 @@ pub enum Retry {
Skip,
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::device::tests::MockGoogleAuth;
use super::super::types::tests::SECRET;
use super::super::types::ConsoleApplicationSecret;
use super::*;
use crate::authenticator_delegate::DefaultAuthenticatorDelegate;
use crate::storage::MemoryStorage;
use std::default::Default;
use hyper;
use std::default::Default;
#[test]
fn flow() {
use serde_json as json;
let secret = json::from_str::<ConsoleApplicationSecret>(SECRET).unwrap().installed.unwrap();
let res = Authenticator::new(&secret, DefaultAuthenticatorDelegate,
hyper::Client::with_connector(<MockGoogleAuth as Default>::default()),
<MemoryStorage as Default>::default(), None)
.token(&["https://www.googleapis.com/auth/youtube.upload"]);
let secret = json::from_str::<ConsoleApplicationSecret>(SECRET)
.unwrap()
.installed
.unwrap();
let res = Authenticator::new(
&secret,
DefaultAuthenticatorDelegate,
hyper::Client::with_connector(<MockGoogleAuth as Default>::default()),
<MemoryStorage as Default>::default(),
None,
)
.token(&["https://www.googleapis.com/auth/youtube.upload"]);
match res {
Ok(t) => assert_eq!(t.access_token, "1/fFAGRNJru1FTz70BzhT3Zg"),

View File

@@ -1,8 +1,8 @@
use hyper;
use std::error::Error;
use std::fmt;
use std::io;
use std::error::Error;
use crate::authenticator::Retry;
use crate::types::RequestError;
@@ -53,8 +53,6 @@ impl fmt::Display for PollError {
}
}
/// A partially implemented trait to interact with the `Authenticator`
///
/// The only method that needs to be implemented manually is `present_user_code(...)`,
@@ -123,12 +121,15 @@ pub trait AuthenticatorDelegate {
/// * Will be called exactly once, provided we didn't abort during `request_code` phase.
/// * 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);
println!(
"Please enter {} at {} and grant access to this application",
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)
);
}
/// This method is used by the InstalledFlow.
@@ -137,9 +138,11 @@ pub trait AuthenticatorDelegate {
/// used.
fn present_user_url(&mut self, url: &String, need_code: bool) -> Option<String> {
if need_code {
println!("Please direct your browser to {}, follow the instructions and enter the \
code displayed here: ",
url);
println!(
"Please direct your browser to {}, follow the instructions and enter the \
code displayed here: ",
url
);
let mut code = String::new();
io::stdin().read_line(&mut code).ok().map(|_| {
@@ -148,9 +151,11 @@ pub trait AuthenticatorDelegate {
code
})
} else {
println!("Please direct your browser to {} and follow the instructions displayed \
there.",
url);
println!(
"Please direct your browser to {} and follow the instructions displayed \
there.",
url
);
None
}
}

View File

@@ -1,19 +1,19 @@
use std::default::Default;
use std::iter::IntoIterator;
use std::time::Duration;
use std::default::Default;
use chrono::{self, Utc};
use hyper;
use hyper::header::ContentType;
use url::form_urlencoded;
use itertools::Itertools;
use serde_json as json;
use chrono::{self, Utc};
use std::borrow::BorrowMut;
use std::io::Read;
use std::i64;
use std::io::Read;
use url::form_urlencoded;
use crate::types::{ApplicationSecret, Token, FlowType, Flow, RequestError, JsonError};
use crate::authenticator_delegate::{PollError, PollInformation};
use crate::types::{ApplicationSecret, Flow, FlowType, JsonError, RequestError, Token};
pub const GOOGLE_DEVICE_CODE_URL: &'static str = "https://accounts.google.com/o/oauth2/device/code";
@@ -46,10 +46,14 @@ impl<C> Flow for DeviceFlow<C> {
}
}
impl<C> DeviceFlow<C>
where C: BorrowMut<hyper::Client>
where
C: BorrowMut<hyper::Client>,
{
pub fn new<S: AsRef<str>>(client: C, secret: &ApplicationSecret, device_code_url: S) -> 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(),
@@ -75,11 +79,10 @@ impl<C> DeviceFlow<C>
/// * If called after a successful result was returned at least once.
/// # Examples
/// See test-cases in source code for a more complete example.
pub fn request_code<'b, T, I>(&mut self,
scopes: I)
-> Result<PollInformation, RequestError>
where T: AsRef<str> + 'b,
I: IntoIterator<Item = &'b T>
pub fn request_code<'b, T, I>(&mut self, scopes: I) -> Result<PollInformation, RequestError>
where
T: AsRef<str> + 'b,
I: IntoIterator<Item = &'b T>,
{
if self.state.is_some() {
panic!("Must not be called after we have obtained a token and have no error");
@@ -88,28 +91,35 @@ impl<C> DeviceFlow<C>
// note: cloned() shouldn't be needed, see issue
// https://github.com/servo/rust-url/issues/81
let req = form_urlencoded::Serializer::new(String::new())
.extend_pairs(&[("client_id", &self.application_secret.client_id),
("scope", &scopes
.into_iter()
.map(|s| s.as_ref())
.intersperse(" ")
.collect::<String>())])
.extend_pairs(&[
("client_id", &self.application_secret.client_id),
(
"scope",
&scopes
.into_iter()
.map(|s| s.as_ref())
.intersperse(" ")
.collect::<String>(),
),
])
.finish();
// note: works around bug in rustlang
// https://github.com/rust-lang/rust/issues/22252
let ret = match self.client
let ret = match self
.client
.borrow_mut()
.post(&self.device_code_url)
.header(ContentType("application/x-www-form-urlencoded".parse().unwrap()))
.header(ContentType(
"application/x-www-form-urlencoded".parse().unwrap(),
))
.body(&*req)
.send() {
.send()
{
Err(err) => {
return Err(RequestError::HttpError(err));
}
Ok(mut res) => {
#[derive(Deserialize)]
struct JsonData {
device_code: String,
@@ -166,13 +176,11 @@ impl<C> DeviceFlow<C>
pub fn poll_token(&mut self) -> Result<Option<Token>, &PollError> {
// clone, as we may re-assign our state later
let pi = match self.state {
Some(ref s) => {
match *s {
DeviceFlowState::Pending(ref pi) => pi.clone(),
DeviceFlowState::Error => return Err(self.error.as_ref().unwrap()),
DeviceFlowState::Success(ref t) => return Ok(Some(t.clone())),
}
}
Some(ref s) => match *s {
DeviceFlowState::Pending(ref pi) => pi.clone(),
DeviceFlowState::Error => return Err(self.error.as_ref().unwrap()),
DeviceFlowState::Success(ref t) => return Ok(Some(t.clone())),
},
_ => panic!("You have to call request_code() beforehand"),
};
@@ -184,18 +192,24 @@ impl<C> DeviceFlow<C>
// We should be ready for a new request
let req = form_urlencoded::Serializer::new(String::new())
.extend_pairs(&[("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")])
.extend_pairs(&[
("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"),
])
.finish();
let json_str: String = match self.client
let json_str: String = match self
.client
.borrow_mut()
.post(&self.application_secret.token_uri)
.header(ContentType("application/x-www-form-urlencoded".parse().unwrap()))
.header(ContentType(
"application/x-www-form-urlencoded".parse().unwrap(),
))
.body(&*req)
.send() {
.send()
{
Err(err) => {
self.error = Some(PollError::HttpError(err));
return Err(self.error.as_ref().unwrap());
@@ -237,48 +251,52 @@ impl<C> DeviceFlow<C>
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use hyper;
use std::default::Default;
use std::time::Duration;
use hyper;
use yup_hyper_mock::{SequentialConnector, MockStream};
use yup_hyper_mock::{MockStream, SequentialConnector};
pub struct MockGoogleAuth(SequentialConnector);
impl Default for MockGoogleAuth {
fn default() -> MockGoogleAuth {
let mut c = MockGoogleAuth(Default::default());
c.0.content.push("HTTP/1.1 200 OK\r\n\
Server: BOGUS\r\n\
\r\n\
{\r\n\
\"device_code\" : \"4/L9fTtLrhY96442SEuf1Rl3KLFg3y\",\r\n\
\"user_code\" : \"a9xfwk9c\",\r\n\
\"verification_url\" : \"http://www.google.com/device\",\r\n\
\"expires_in\" : 1800,\r\n\
\"interval\" : 0\r\n\
}"
.to_string());
c.0.content.push(
"HTTP/1.1 200 OK\r\n\
Server: BOGUS\r\n\
\r\n\
{\r\n\
\"device_code\" : \"4/L9fTtLrhY96442SEuf1Rl3KLFg3y\",\r\n\
\"user_code\" : \"a9xfwk9c\",\r\n\
\"verification_url\" : \"http://www.google.com/device\",\r\n\
\"expires_in\" : 1800,\r\n\
\"interval\" : 0\r\n\
}"
.to_string(),
);
c.0.content.push("HTTP/1.1 200 OK\r\n\
Server: BOGUS\r\n\
\r\n\
{\r\n\
\"error\" : \"authorization_pending\"\r\n\
}"
.to_string());
c.0.content.push(
"HTTP/1.1 200 OK\r\n\
Server: BOGUS\r\n\
\r\n\
{\r\n\
\"error\" : \"authorization_pending\"\r\n\
}"
.to_string(),
);
c.0.content.push("HTTP/1.1 200 OK\r\nServer: \
BOGUS\r\n\r\n{\r\n\"access_token\":\"1/fFAGRNJru1FTz70BzhT3Zg\",\
\r\n\"expires_in\":3920,\r\n\"token_type\":\"Bearer\",\
\r\n\"refresh_token\":\
\"1/6BMfW9j53gdGImsixUH6kU5RsR4zwI9lUVX-tqf8JXQ\"\r\n}"
.to_string());
c.0.content.push(
"HTTP/1.1 200 OK\r\nServer: \
BOGUS\r\n\r\n{\r\n\"access_token\":\"1/fFAGRNJru1FTz70BzhT3Zg\",\
\r\n\"expires_in\":3920,\r\n\"token_type\":\"Bearer\",\
\r\n\"refresh_token\":\
\"1/6BMfW9j53gdGImsixUH6kU5RsR4zwI9lUVX-tqf8JXQ\"\r\n}"
.to_string(),
);
c
}
}
@@ -298,10 +316,12 @@ pub mod tests {
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);
hyper::Client::with_connector(<MockGoogleAuth as Default>::default()),
&appsecret,
GOOGLE_DEVICE_CODE_URL,
);
match flow.request_code(
&["https://www.googleapis.com/auth/youtube.upload"]) {
match flow.request_code(&["https://www.googleapis.com/auth/youtube.upload"]) {
Ok(pi) => assert_eq!(pi.interval, Duration::from_secs(0)),
_ => unreachable!(),
}

View File

@@ -9,12 +9,12 @@
use serde_json;
use std::io::{self, Read};
use std::fs;
use std::io::{self, Read};
use std::path::Path;
use crate::service_account::ServiceAccountKey;
use crate::types::{ConsoleApplicationSecret, ApplicationSecret};
use crate::types::{ApplicationSecret, ConsoleApplicationSecret};
/// Read an application secret from a file.
pub fn read_application_secret(path: &Path) -> io::Result<ApplicationSecret> {
@@ -31,18 +31,20 @@ pub fn read_application_secret(path: &Path) -> io::Result<ApplicationSecret> {
pub fn parse_application_secret(secret: &String) -> io::Result<ApplicationSecret> {
let result: serde_json::Result<ConsoleApplicationSecret> = serde_json::from_str(secret);
match result {
Err(e) => {
Err(io::Error::new(io::ErrorKind::InvalidData,
format!("Bad application secret: {}", e)))
}
Err(e) => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Bad application secret: {}", e),
)),
Ok(decoded) => {
if decoded.web.is_some() {
Ok(decoded.web.unwrap())
} else if decoded.installed.is_some() {
Ok(decoded.installed.unwrap())
} else {
Err(io::Error::new(io::ErrorKind::InvalidData,
"Unknown application secret format"))
Err(io::Error::new(
io::ErrorKind::InvalidData,
"Unknown application secret format",
))
}
}
}

View File

@@ -10,8 +10,8 @@ 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 std::sync::Mutex;
use hyper;
use hyper::{client, header, server, status, uri};
@@ -19,21 +19,23 @@ use serde_json::error;
use url::form_urlencoded;
use url::percent_encoding::{percent_encode, QUERY_ENCODE_SET};
use crate::types::{ApplicationSecret, Token};
use crate::authenticator_delegate::AuthenticatorDelegate;
use crate::types::{ApplicationSecret, Token};
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>(auth_uri: &str,
client_id: &str,
scopes: I,
redirect_uri: Option<String>)
-> String
where T: AsRef<str> + 'a,
I: IntoIterator<Item = &'a T>
fn build_authentication_request_url<'a, T, I>(
auth_uri: &str,
client_id: &str,
scopes: I,
redirect_uri: Option<String>,
) -> String
where
T: AsRef<str> + 'a,
I: IntoIterator<Item = &'a T>,
{
let mut url = String::new();
let mut scopes_string = scopes.into_iter().fold(String::new(), |mut acc, sc| {
@@ -45,16 +47,20 @@ fn build_authentication_request_url<'a, T, I>(auth_uri: &str,
scopes_string.pop();
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"),
format!("&client_id={}", client_id)]
.into_iter()
.fold(url, |mut u, param| {
u.push_str(&percent_encode(param.as_ref(), QUERY_ENCODE_SET).to_string());
u
})
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).to_string());
u
})
}
pub struct InstalledFlow<C> {
@@ -77,7 +83,8 @@ pub enum InstalledFlowReturnMethod {
}
impl<C> InstalledFlow<C>
where C: BorrowMut<hyper::Client>
where
C: BorrowMut<hyper::Client>,
{
/// Starts a new Installed App auth flow.
/// If HTTPRedirect is chosen as method and the server can't be started, the flow falls
@@ -100,19 +107,18 @@ impl<C> InstalledFlow<C>
Result::Err(_) => default,
Result::Ok(server) => {
let (tx, rx) = channel();
let listening =
server.handle(InstalledFlowHandler { auth_code_snd: Mutex::new(tx) });
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),
}
}
Result::Ok(listening) => InstalledFlow {
client: default.client,
server: Some(listening),
port: Some(port),
auth_code_rcv: Some(rx),
},
}
}
}
@@ -126,13 +132,15 @@ impl<C> InstalledFlow<C>
/// . 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,
appsecret: &ApplicationSecret,
scopes: S)
-> Result<Token, Box<Error>>
where T: AsRef<str> + 'a,
S: Iterator<Item = &'a T>
pub fn obtain_token<'a, AD: AuthenticatorDelegate, S, T>(
&mut self,
auth_delegate: &mut AD,
appsecret: &ApplicationSecret,
scopes: S,
) -> Result<Token, Box<Error>>
where
T: AsRef<str> + 'a,
S: Iterator<Item = &'a T>,
{
let authcode = self.get_authorization_code(auth_delegate, &appsecret, scopes)?;
let tokens = self.request_token(&appsecret, &authcode, auth_delegate.redirect_uri())?;
@@ -150,37 +158,44 @@ impl<C> InstalledFlow<C>
token.set_expiry_absolute();
Result::Ok(token)
} else {
let err = io::Error::new(io::ErrorKind::Other,
format!("Token API error: {} {}",
tokens.error.unwrap_or("<unknown err>".to_string()),
tokens.error_description
.unwrap_or("".to_string()))
.as_str());
let err = io::Error::new(
io::ErrorKind::Other,
format!(
"Token API error: {} {}",
tokens.error.unwrap_or("<unknown err>".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).
fn get_authorization_code<'a, AD: AuthenticatorDelegate, S, T>(&mut self,
auth_delegate: &mut AD,
appsecret: &ApplicationSecret,
scopes: S)
-> Result<String, Box<Error>>
where T: AsRef<str> + 'a,
S: Iterator<Item = &'a T>
fn get_authorization_code<'a, AD: AuthenticatorDelegate, S, T>(
&mut self,
auth_delegate: &mut AD,
appsecret: &ApplicationSecret,
scopes: S,
) -> Result<String, Box<Error>>
where
T: AsRef<str> + 'a,
S: Iterator<Item = &'a T>,
{
let result: Result<String, Box<Error>> = match self.server {
None => {
let url = build_authentication_request_url(&appsecret.auth_uri,
&appsecret.client_id,
scopes,
auth_delegate.redirect_uri());
let url = build_authentication_request_url(
&appsecret.auth_uri,
&appsecret.client_id,
scopes,
auth_delegate.redirect_uri(),
);
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")))
}
None => Result::Err(Box::new(io::Error::new(
io::ErrorKind::UnexpectedEof,
"couldn't read code",
))),
Some(mut code) => {
// Partial backwards compatibilty in case an implementation adds a new line
// due to previous behaviour.
@@ -196,12 +211,14 @@ impl<C> InstalledFlow<C>
Some(_) => {
// The redirect URI must be this very localhost URL, otherwise Google refuses
// authorization.
let url = build_authentication_request_url(&appsecret.auth_uri,
&appsecret.client_id,
scopes,
auth_delegate.redirect_uri().or_else(|| {
Some(format!("http://localhost:{}", self.port.unwrap_or(8080)))
}));
let url = build_authentication_request_url(
&appsecret.auth_uri,
&appsecret.client_id,
scopes,
auth_delegate.redirect_uri().or_else(|| {
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() {
@@ -215,31 +232,35 @@ impl<C> InstalledFlow<C>
}
/// Sends the authorization code to the provider in order to obtain access and refresh tokens.
fn request_token(&mut self,
appsecret: &ApplicationSecret,
authcode: &str,
custom_redirect_uri: Option<String>)
-> Result<JSONTokenResponse, Box<Error>> {
let redirect_uri = custom_redirect_uri.unwrap_or_else(|| {
match self.port {
None => OOB_REDIRECT_URI.to_string(),
Some(p) => format!("http://localhost:{}", p),
}
fn request_token(
&mut self,
appsecret: &ApplicationSecret,
authcode: &str,
custom_redirect_uri: Option<String>,
) -> Result<JSONTokenResponse, Box<Error>> {
let redirect_uri = custom_redirect_uri.unwrap_or_else(|| match self.port {
None => OOB_REDIRECT_URI.to_string(),
Some(p) => format!("http://localhost:{}", p),
});
let body = form_urlencoded::Serializer::new(String::new())
.extend_pairs(vec![("code".to_string(), authcode.to_string()),
("client_id".to_string(), appsecret.client_id.clone()),
("client_secret".to_string(), appsecret.client_secret.clone()),
("redirect_uri".to_string(), redirect_uri),
("grant_type".to_string(), "authorization_code".to_string())])
.extend_pairs(vec![
("code".to_string(), authcode.to_string()),
("client_id".to_string(), appsecret.client_id.clone()),
("client_secret".to_string(), appsecret.client_secret.clone()),
("redirect_uri".to_string(), redirect_uri),
("grant_type".to_string(), "authorization_code".to_string()),
])
.finish();
let result: Result<client::Response, hyper::Error> = self.client
let result: Result<client::Response, hyper::Error> = self
.client
.borrow_mut()
.post(&appsecret.token_uri)
.body(&body)
.header(header::ContentType("application/x-www-form-urlencoded".parse().unwrap()))
.header(header::ContentType(
"application/x-www-form-urlencoded".parse().unwrap(),
))
.send();
let mut resp = String::new();
@@ -296,10 +317,11 @@ impl server::Handler for InstalledFlowHandler {
} else {
self.handle_url(url.unwrap());
*rp.status_mut() = status::StatusCode::Ok;
let _ =
rp.send("<html><head><title>Success</title></head><body>You may now \
close this window.</body></html>"
.as_ref());
let _ = rp.send(
"<html><head><title>Success</title></head><body>You may now \
close this window.</body></html>"
.as_ref(),
);
}
}
_ => {
@@ -321,7 +343,6 @@ impl InstalledFlowHandler {
let _ = self.auth_code_snd.lock().unwrap().send(val);
}
}
}
}
@@ -330,29 +351,34 @@ mod tests {
use super::build_authentication_request_url;
use super::InstalledFlowHandler;
use std::sync::Mutex;
use std::sync::mpsc::channel;
use std::sync::Mutex;
use hyper::Url;
#[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-h38jh0j4fv0ce1krdkiq0hfvt6n5amr\
f.apps.googleusercontent.com",
build_authentication_request_url("https://accounts.google.com/o/oauth2/auth",
"812741506391-h38jh0j4fv0ce1krdkiq0hfvt6n5am\
rf.apps.googleusercontent.com",
vec![&"email".to_string(),
&"profile".to_string()],
None));
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-h38jh0j4fv0ce1krdkiq0hfvt6n5amr\
f.apps.googleusercontent.com",
build_authentication_request_url(
"https://accounts.google.com/o/oauth2/auth",
"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) };
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);

View File

@@ -37,7 +37,7 @@
//! ```test_harness,no_run
//! #[macro_use]
//! extern crate serde_derive;
//!
//!
//! use yup_oauth2::{Authenticator, DefaultAuthenticatorDelegate, PollInformation, ConsoleApplicationSecret, MemoryStorage, GetToken};
//! use serde_json as json;
//! use std::default::Default;
@@ -73,12 +73,12 @@ extern crate chrono;
extern crate hyper;
extern crate hyper_native_tls;
extern crate itertools;
#[cfg(test)]
extern crate log;
extern crate url;
#[cfg(test)]
extern crate yup_hyper_mock;
extern crate url;
extern crate itertools;
mod authenticator;
mod authenticator_delegate;
@@ -90,14 +90,16 @@ mod service_account;
mod storage;
mod types;
pub use crate::device::{GOOGLE_DEVICE_CODE_URL, DeviceFlow};
pub use crate::refresh::{RefreshFlow, RefreshResult};
pub use crate::types::{Token, FlowType, ApplicationSecret, ConsoleApplicationSecret, Scheme, TokenType};
pub use crate::installed::{InstalledFlow, InstalledFlowReturnMethod};
pub use crate::storage::{TokenStorage, NullStorage, MemoryStorage, DiskTokenStorage};
pub use crate::authenticator::{Authenticator, Retry, GetToken};
pub use crate::authenticator_delegate::{AuthenticatorDelegate, DefaultAuthenticatorDelegate, PollError,
PollInformation};
pub use crate::authenticator::{Authenticator, GetToken, Retry};
pub use crate::authenticator_delegate::{
AuthenticatorDelegate, DefaultAuthenticatorDelegate, PollError, PollInformation,
};
pub use crate::device::{DeviceFlow, GOOGLE_DEVICE_CODE_URL};
pub use crate::helper::*;
pub use crate::installed::{InstalledFlow, InstalledFlowReturnMethod};
pub use crate::refresh::{RefreshFlow, RefreshResult};
pub use crate::service_account::*;
pub use crate::storage::{DiskTokenStorage, MemoryStorage, NullStorage, TokenStorage};
pub use crate::types::{
ApplicationSecret, ConsoleApplicationSecret, FlowType, Scheme, Token, TokenType,
};

View File

@@ -1,13 +1,13 @@
use crate::types::{ApplicationSecret, FlowType, JsonError};
use super::Token;
use chrono::Utc;
use hyper;
use hyper::header::ContentType;
use serde_json as json;
use url::form_urlencoded;
use super::Token;
use std::borrow::BorrowMut;
use std::io::Read;
use url::form_urlencoded;
/// Implements the [Outh2 Refresh Token Flow](https://developers.google.com/youtube/v3/guides/authentication#devices).
///
@@ -19,7 +19,6 @@ pub struct RefreshFlow<C> {
result: RefreshResult,
}
/// All possible outcomes of the refresh flow
pub enum RefreshResult {
/// Indicates connection failure
@@ -31,7 +30,8 @@ pub enum RefreshResult {
}
impl<C> RefreshFlow<C>
where C: BorrowMut<hyper::Client>
where
C: BorrowMut<hyper::Client>,
{
pub fn new(client: C) -> RefreshFlow<C> {
RefreshFlow {
@@ -54,29 +54,36 @@ impl<C> RefreshFlow<C>
///
/// # Examples
/// Please see the crate landing page for an example.
pub fn refresh_token(&mut self,
flow_type: FlowType,
client_secret: &ApplicationSecret,
refresh_token: &str)
-> &RefreshResult {
pub fn refresh_token(
&mut self,
flow_type: FlowType,
client_secret: &ApplicationSecret,
refresh_token: &str,
) -> &RefreshResult {
let _ = flow_type;
if let RefreshResult::Success(_) = self.result {
return &self.result;
}
let req = form_urlencoded::Serializer::new(String::new())
.extend_pairs(&[("client_id", client_secret.client_id.as_ref()),
("client_secret", client_secret.client_secret.as_ref()),
("refresh_token", refresh_token),
("grant_type", "refresh_token")])
.extend_pairs(&[
("client_id", client_secret.client_id.as_ref()),
("client_secret", client_secret.client_secret.as_ref()),
("refresh_token", refresh_token),
("grant_type", "refresh_token"),
])
.finish();
let json_str: String = match self.client
let json_str: String = match self
.client
.borrow_mut()
.post(&client_secret.token_uri)
.header(ContentType("application/x-www-form-urlencoded".parse().unwrap()))
.header(ContentType(
"application/x-www-form-urlencoded".parse().unwrap(),
))
.body(&*req)
.send() {
.send()
{
Err(err) => {
self.result = RefreshResult::Error(err);
return &self.result;
@@ -116,32 +123,32 @@ impl<C> RefreshFlow<C>
}
}
#[cfg(test)]
mod tests {
use super::super::FlowType;
use super::*;
use crate::device::GOOGLE_DEVICE_CODE_URL;
use crate::helper::parse_application_secret;
use hyper;
use std::default::Default;
use super::*;
use super::super::FlowType;
use yup_hyper_mock::{MockStream, SequentialConnector};
use crate::helper::parse_application_secret;
use crate::device::GOOGLE_DEVICE_CODE_URL;
struct MockGoogleRefresh(SequentialConnector);
impl Default for MockGoogleRefresh {
fn default() -> MockGoogleRefresh {
let mut c = MockGoogleRefresh(Default::default());
c.0.content.push("HTTP/1.1 200 OK\r\n\
Server: BOGUS\r\n\
\r\n\
{\r\n\
\"access_token\":\"1/fFAGRNJru1FTz70BzhT3Zg\",\r\n\
\"expires_in\":3920,\r\n\
\"token_type\":\"Bearer\"\r\n\
}"
.to_string());
c.0.content.push(
"HTTP/1.1 200 OK\r\n\
Server: BOGUS\r\n\
\r\n\
{\r\n\
\"access_token\":\"1/fFAGRNJru1FTz70BzhT3Zg\",\r\n\
\"expires_in\":3920,\r\n\
\"token_type\":\"Bearer\"\r\n\
}"
.to_string(),
);
c
}
@@ -159,14 +166,16 @@ mod tests {
#[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(GOOGLE_DEVICE_CODE_URL.to_string()), &appsecret, "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

@@ -27,18 +27,18 @@ use url::form_urlencoded;
#[cfg(not(feature = "no-openssl"))]
use openssl::{
sign::Signer,
hash::MessageDigest,
pkey::{PKey, Private},
rsa::Padding,
sign::Signer,
};
#[cfg(feature = "no-openssl")]
use rustls::{
self,
PrivateKey,
sign::{self, SigningKey},
internal::pemfile,
sign::{self, SigningKey},
PrivateKey,
};
#[cfg(feature = "no-openssl")]
use std::io;
@@ -72,11 +72,16 @@ fn decode_rsa_key(pem_pkcs8: &str) -> Result<PrivateKey, Box<error::Error>> {
if pk.len() > 0 {
Ok(pk[0].clone())
} else {
Err(Box::new(io::Error::new(io::ErrorKind::InvalidInput,
"Not enough private keys in PEM")))
Err(Box::new(io::Error::new(
io::ErrorKind::InvalidInput,
"Not enough private keys in PEM",
)))
}
} else {
Err(Box::new(io::Error::new(io::ErrorKind::InvalidInput, "Error reading key from PEM")))
Err(Box::new(io::Error::new(
io::ErrorKind::InvalidInput,
"Error reading key from PEM",
)))
}
}
@@ -87,7 +92,7 @@ fn decode_rsa_key(pem_pkcs8: &str) -> Result<PrivateKey, Box<error::Error>> {
/// secret into a ServiceAccountKey.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ServiceAccountKey {
#[serde(rename="type")]
#[serde(rename = "type")]
pub key_type: Option<String>,
pub project_id: Option<String>,
pub private_key_id: Option<String>,
@@ -155,8 +160,12 @@ impl JWT {
let key = decode_rsa_key(private_key)?;
let signing_key = sign::RSASigningKey::new(&key)
.map_err(|_| io::Error::new(io::ErrorKind::Other, "Couldn't initialize signer"))?;
let signer = signing_key.choose_scheme(&[rustls::SignatureScheme::RSA_PKCS1_SHA256])
.ok_or(io::Error::new(io::ErrorKind::Other, "Couldn't choose signing scheme"))?;
let signer = signing_key
.choose_scheme(&[rustls::SignatureScheme::RSA_PKCS1_SHA256])
.ok_or(io::Error::new(
io::ErrorKind::Other,
"Couldn't choose signing scheme",
))?;
let signature = signer.sign(jwt_head.as_bytes())?;
let signature_b64 = encode_base64(signature);
@@ -168,8 +177,9 @@ impl JWT {
}
fn init_claims_from_key<'a, I, T>(key: &ServiceAccountKey, scopes: I) -> Claims
where T: AsRef<str> + 'a,
I: IntoIterator<Item = &'a T>
where
T: AsRef<str> + 'a,
I: IntoIterator<Item = &'a T>,
{
let iat = chrono::Utc::now().timestamp();
let expiry = iat + 3600 - 5; // Max validity is 1h.
@@ -230,7 +240,8 @@ impl TokenResponse {
}
impl<'a, C> ServiceAccountAccess<C>
where C: BorrowMut<hyper::Client>
where
C: BorrowMut<hyper::Client>,
{
/// Returns a new `ServiceAccountAccess` token source.
#[allow(dead_code)]
@@ -255,20 +266,24 @@ impl<'a, C> ServiceAccountAccess<C>
fn request_token(&mut self, scopes: &Vec<&str>) -> result::Result<Token, Box<error::Error>> {
let mut claims = init_claims_from_key(&self.key, scopes);
claims.sub = self.sub.clone();
let signed = JWT::new(claims)
.sign(self.key.private_key.as_ref().unwrap())?;
let signed = JWT::new(claims).sign(self.key.private_key.as_ref().unwrap())?;
let body = form_urlencoded::Serializer::new(String::new())
.extend_pairs(vec![("grant_type".to_string(), GRANT_TYPE.to_string()),
("assertion".to_string(), signed)])
.extend_pairs(vec![
("grant_type".to_string(), GRANT_TYPE.to_string()),
("assertion".to_string(), signed),
])
.finish();
let mut response = String::new();
let mut result = self.client
let mut result = self
.client
.borrow_mut()
.post(self.key.token_uri.as_ref().unwrap())
.body(&body)
.header(header::ContentType("application/x-www-form-urlencoded".parse().unwrap()))
.header(header::ContentType(
"application/x-www-form-urlencoded".parse().unwrap(),
))
.send()?;
result.read_to_string(&mut response)?;
@@ -279,10 +294,14 @@ impl<'a, C> ServiceAccountAccess<C>
match token {
Err(e) => return Err(Box::new(e)),
Ok(token) => {
if token.access_token.is_none() || token.token_type.is_none() ||
token.expires_in.is_none() {
Err(Box::new(StringError::new("Token response lacks fields".to_string(),
Some(&format!("{:?}", token)))))
if token.access_token.is_none()
|| token.token_type.is_none()
|| token.expires_in.is_none()
{
Err(Box::new(StringError::new(
"Token response lacks fields".to_string(),
Some(&format!("{:?}", token)),
)))
} else {
Ok(token.to_oauth_token())
}
@@ -293,8 +312,9 @@ impl<'a, C> ServiceAccountAccess<C>
impl<C: BorrowMut<hyper::Client>> GetToken for ServiceAccountAccess<C> {
fn token<'b, I, T>(&mut self, scopes: I) -> result::Result<Token, Box<error::Error>>
where T: AsRef<str> + Ord + 'b,
I: IntoIterator<Item = &'b T>
where
T: AsRef<str> + Ord + 'b,
I: IntoIterator<Item = &'b T>,
{
let (hash, scps) = hash_scopes(scopes);
@@ -318,11 +338,11 @@ impl<C: BorrowMut<hyper::Client>> GetToken for ServiceAccountAccess<C> {
#[cfg(test)]
mod tests {
use super::*;
use crate::authenticator::GetToken;
use crate::helper::service_account_key_from_file;
use hyper;
use hyper::net::HttpsConnector;
use hyper_native_tls::NativeTlsClient;
use crate::authenticator::GetToken;
// This is a valid but deactivated key.
const TEST_PRIVATE_KEY_PATH: &'static str = "examples/Sanguine-69411a0c0eea.json";
@@ -332,10 +352,14 @@ mod tests {
#[allow(dead_code)]
fn test_service_account_e2e() {
let key = service_account_key_from_file(&TEST_PRIVATE_KEY_PATH.to_string()).unwrap();
let client = hyper::Client::with_connector(HttpsConnector::new(NativeTlsClient::new().unwrap()));
let client =
hyper::Client::with_connector(HttpsConnector::new(NativeTlsClient::new().unwrap()));
let mut acc = ServiceAccountAccess::new(key, client);
println!("{:?}",
acc.token(vec![&"https://www.googleapis.com/auth/pubsub"]).unwrap());
println!(
"{:?}",
acc.token(vec![&"https://www.googleapis.com/auth/pubsub"])
.unwrap()
);
}
#[test]
@@ -344,11 +368,15 @@ mod tests {
let scopes = vec!["scope1", "scope2", "scope3"];
let claims = super::init_claims_from_key(&key, &scopes);
assert_eq!(claims.iss,
"oauth2-public-test@sanguine-rhythm-105020.iam.gserviceaccount.com".to_string());
assert_eq!(
claims.iss,
"oauth2-public-test@sanguine-rhythm-105020.iam.gserviceaccount.com".to_string()
);
assert_eq!(claims.scope, "scope1 scope2 scope3".to_string());
assert_eq!(claims.aud,
"https://accounts.google.com/o/oauth2/token".to_string());
assert_eq!(
claims.aud,
"https://accounts.google.com/o/oauth2/token".to_string()
);
assert!(claims.exp > 1000000000);
assert!(claims.iat < claims.exp);
assert_eq!(claims.exp - claims.iat, 3595);
@@ -365,7 +393,9 @@ mod tests {
assert!(signature.is_ok());
let signature = signature.unwrap();
assert_eq!(signature.split(".").nth(0).unwrap(),
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9");
assert_eq!(
signature.split(".").nth(0).unwrap(),
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"
);
}
}

View File

@@ -5,14 +5,14 @@
extern crate serde_json;
use std::collections::HashMap;
use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::error::Error;
use std::fmt;
use std::fs;
use std::hash::{Hash, Hasher};
use std::io::{Read, Write};
use std::io;
use std::io::{Read, Write};
use crate::types::Token;
@@ -26,21 +26,24 @@ pub trait TokenStorage {
/// If `token` is None, it is invalid or revoked and should be removed from storage.
/// Otherwise, it should be saved.
fn set(&mut self,
scope_hash: u64,
scopes: &Vec<&str>,
token: Option<Token>)
-> Result<(), Self::Error>;
fn set(
&mut self,
scope_hash: u64,
scopes: &Vec<&str>,
token: Option<Token>,
) -> Result<(), Self::Error>;
/// A `None` result indicates that there is no token for the given scope_hash.
fn get(&self, scope_hash: u64, scopes: &Vec<&str>) -> Result<Option<Token>, Self::Error>;
}
/// Calculate a hash value describing the scopes, and return a sorted Vec of the scopes.
pub fn hash_scopes<'a, I, T>(scopes: I) -> (u64, Vec<&'a str>)
where T: AsRef<str> + Ord + 'a,
I: IntoIterator<Item = &'a T>
where
T: AsRef<str> + Ord + 'a,
I: IntoIterator<Item = &'a T>,
{
let mut sv: Vec<&str> = scopes.into_iter()
let mut sv: Vec<&str> = scopes
.into_iter()
.map(|s| s.as_ref())
.collect::<Vec<&str>>();
sv.sort();
@@ -87,11 +90,12 @@ pub struct MemoryStorage {
impl TokenStorage for MemoryStorage {
type Error = NullError;
fn set(&mut self,
scope_hash: u64,
_: &Vec<&str>,
token: Option<Token>)
-> Result<(), NullError> {
fn set(
&mut self,
scope_hash: u64,
_: &Vec<&str>,
token: Option<Token>,
) -> Result<(), NullError> {
match token {
Some(t) => self.tokens.insert(scope_hash, t),
None => self.tokens.remove(&scope_hash),
@@ -142,7 +146,7 @@ impl DiskTokenStorage {
Result::Err(e) => {
match e.kind() {
io::ErrorKind::NotFound => Result::Ok(dts), // File not found; ignore and create new one
_ => Result::Err(e), // e.g. PermissionDenied
_ => Result::Err(e), // e.g. PermissionDenied
}
}
}
@@ -198,11 +202,12 @@ impl DiskTokenStorage {
impl TokenStorage for DiskTokenStorage {
type Error = io::Error;
fn set(&mut self,
scope_hash: u64,
_: &Vec<&str>,
token: Option<Token>)
-> Result<(), Self::Error> {
fn set(
&mut self,
scope_hash: u64,
_: &Vec<&str>,
token: Option<Token>,
) -> Result<(), Self::Error> {
match token {
None => {
self.tokens.remove(&scope_hash);

View File

@@ -1,8 +1,8 @@
use chrono::{DateTime, Utc, TimeZone};
use chrono::{DateTime, TimeZone, Utc};
use hyper;
use std::error::Error;
use std::fmt;
use std::str::FromStr;
use hyper;
/// A marker trait for all Flows
pub trait Flow {
@@ -34,10 +34,11 @@ impl From<JsonError> for RequestError {
fn from(value: JsonError) -> RequestError {
match &*value.error {
"invalid_client" => RequestError::InvalidClient,
"invalid_scope" => {
RequestError::InvalidScope(value.error_description
.unwrap_or("no description provided".to_string()))
}
"invalid_scope" => RequestError::InvalidScope(
value
.error_description
.unwrap_or("no description provided".to_string()),
),
_ => RequestError::NegativeServerResponse(value.error, value.error_description),
}
}
@@ -101,7 +102,6 @@ impl Error for StringError {
}
}
/// Represents all implemented token types
#[derive(Clone, PartialEq, Debug)]
pub enum TokenType {
@@ -127,7 +127,6 @@ impl FromStr for TokenType {
}
}
/// A scheme for use in `hyper::header::Authorization`
#[derive(Clone, PartialEq, Debug)]
pub struct Scheme {
@@ -155,12 +154,10 @@ impl FromStr for Scheme {
return Err("Expected two parts: <token_type> <token>");
}
match <TokenType as FromStr>::from_str(parts[0]) {
Ok(t) => {
Ok(Scheme {
token_type: t,
access_token: parts[1].to_string(),
})
}
Ok(t) => Ok(Scheme {
token_type: t,
access_token: parts[1].to_string(),
}),
Err(_) => Err("Couldn't parse token type"),
}
}
@@ -208,9 +205,11 @@ impl Token {
/// Returns a DateTime object representing our expiry date.
pub fn expiry_date(&self) -> DateTime<Utc> {
Utc.timestamp(self.expires_in_timestamp
.expect("Tokens without an absolute expiry are invalid"),
0)
Utc.timestamp(
self.expires_in_timestamp
.expect("Tokens without an absolute expiry are invalid"),
0,
)
}
/// Adjust our stored expiry format to be absolute, using the current time.
@@ -307,8 +306,10 @@ pub mod tests {
};
let mut headers = hyper::header::Headers::new();
headers.set(hyper::header::Authorization(s));
assert_eq!(headers.to_string(),
"Authorization: Bearer foo\r\n".to_string());
assert_eq!(
headers.to_string(),
"Authorization: Bearer foo\r\n".to_string()
);
}
#[test]