mirror of
https://github.com/OMGeeky/yup-oauth2.git
synced 2026-01-04 10:20:26 +01:00
Format to fustfmt defaults and force lint check in Travis
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
158
src/device.rs
158
src/device.rs
@@ -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!(),
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
222
src/installed.rs
222
src/installed.rs
@@ -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);
|
||||
|
||||
26
src/lib.rs
26
src/lib.rs
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
39
src/types.rs
39
src/types.rs
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user