mirror of
https://github.com/OMGeeky/yup-oauth2.git
synced 2026-01-02 01:16:15 +01:00
refactor(all): More flows demand for a different structure
I mainly resolved some circular dependencies that had crept in, and moved code around. I renamed helper.rs because that was not really an appropriate name anymore, and moved the delegate code into a new module.
This commit is contained in:
@@ -6,14 +6,13 @@ use std::cmp::min;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::convert::From;
|
||||
use std::io;
|
||||
|
||||
use common::{Token, FlowType, ApplicationSecret};
|
||||
use device::{PollInformation, RequestError, DeviceFlow, PollError};
|
||||
use authenticator_delegate::{AuthenticatorDelegate, PollError, PollInformation};
|
||||
use common::{RequestError, Token, FlowType, ApplicationSecret};
|
||||
use device::DeviceFlow;
|
||||
use installed::{InstalledFlow, InstalledFlowReturnMethod};
|
||||
use refresh::{RefreshResult, RefreshFlow};
|
||||
use storage::{TokenStorage};
|
||||
use chrono::{DateTime, UTC, Local};
|
||||
use storage::TokenStorage;
|
||||
use std::time::Duration;
|
||||
use hyper;
|
||||
|
||||
@@ -61,9 +60,7 @@ impl StringError {
|
||||
error.push_str(&*d);
|
||||
}
|
||||
|
||||
StringError {
|
||||
error: error,
|
||||
}
|
||||
StringError { error: error }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,18 +87,17 @@ impl Error for StringError {
|
||||
/// 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
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -116,8 +112,11 @@ impl<D, S, C> Authenticator<D, S, C>
|
||||
/// 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> {
|
||||
delegate: D,
|
||||
client: C,
|
||||
storage: S,
|
||||
flow_type: Option<FlowType>)
|
||||
-> Authenticator<D, S, C> {
|
||||
Authenticator {
|
||||
flow_type: flow_type.unwrap_or(FlowType::Device),
|
||||
delegate: delegate,
|
||||
@@ -142,9 +141,7 @@ impl<D, S, C> Authenticator<D, S, C>
|
||||
}
|
||||
|
||||
let mut flow = InstalledFlow::new(self.client.borrow_mut(), installed_type);
|
||||
flow.obtain_token(&mut self.delegate,
|
||||
&self.secret,
|
||||
scopes.iter())
|
||||
flow.obtain_token(&mut self.delegate, &self.secret, scopes.iter())
|
||||
}
|
||||
|
||||
fn retrieve_device_token(&mut self, scopes: &Vec<&str>) -> Result<Token, Box<Error>> {
|
||||
@@ -154,33 +151,36 @@ impl<D, S, C> Authenticator<D, S, C>
|
||||
let pi: PollInformation;
|
||||
loop {
|
||||
let res = flow.request_code(&self.secret.client_id,
|
||||
&self.secret.client_secret, scopes.iter());
|
||||
&self.secret.client_secret,
|
||||
scopes.iter());
|
||||
|
||||
pi = match res {
|
||||
Err(res_err) => {
|
||||
match res_err {
|
||||
RequestError::HttpError(err) => {
|
||||
match self.delegate.connection_error(&err) {
|
||||
Retry::Abort|Retry::Skip => return Err(Box::new(StringError::from(&err as &Error))),
|
||||
Retry::After(d) => sleep(d),
|
||||
Err(res_err) => {
|
||||
match res_err {
|
||||
RequestError::HttpError(err) => {
|
||||
match self.delegate.connection_error(&err) {
|
||||
Retry::Abort | Retry::Skip => {
|
||||
return Err(Box::new(StringError::from(&err as &Error)))
|
||||
}
|
||||
},
|
||||
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))
|
||||
Retry::After(d) => sleep(d),
|
||||
}
|
||||
};
|
||||
continue
|
||||
},
|
||||
Ok(pi) => {
|
||||
self.delegate.present_user_code(&pi);
|
||||
pi
|
||||
}
|
||||
};
|
||||
break
|
||||
}
|
||||
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));
|
||||
}
|
||||
};
|
||||
continue;
|
||||
}
|
||||
Ok(pi) => {
|
||||
self.delegate.present_user_code(&pi);
|
||||
pi
|
||||
}
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
// PHASE 1: POLL TOKEN
|
||||
@@ -191,50 +191,56 @@ impl<D, S, C> Authenticator<D, S, C>
|
||||
match poll_err {
|
||||
&&PollError::HttpError(ref err) => {
|
||||
match self.delegate.connection_error(err) {
|
||||
Retry::Abort|Retry::Skip
|
||||
=> return Err(Box::new(StringError::from(err as &Error))),
|
||||
Retry::Abort | Retry::Skip => {
|
||||
return Err(Box::new(StringError::from(err as &Error)))
|
||||
}
|
||||
Retry::After(d) => sleep(d),
|
||||
}
|
||||
},
|
||||
}
|
||||
&&PollError::Expired(ref t) => {
|
||||
self.delegate.expired(t);
|
||||
return Err(Box::new(StringError::from(pts)))
|
||||
},
|
||||
return Err(Box::new(StringError::from(pts)));
|
||||
}
|
||||
&&PollError::AccessDenied => {
|
||||
self.delegate.denied();
|
||||
return Err(Box::new(StringError::from(pts)))
|
||||
},
|
||||
return Err(Box::new(StringError::from(pts)));
|
||||
}
|
||||
}; // 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(Some(token)) => return Ok(token)
|
||||
}
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
/// In any failure case, the delegate will be provided with additional information, and
|
||||
/// 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()
|
||||
.map(|s|s.as_ref())
|
||||
.collect::<Vec<&str>>();
|
||||
.map(|s| s.as_ref())
|
||||
.collect::<Vec<&str>>();
|
||||
sv.sort();
|
||||
let mut sh = SipHasher::new();
|
||||
&sv.hash(&mut sh);
|
||||
@@ -251,9 +257,9 @@ impl<D, S, C> GetToken for Authenticator<D, S, C>
|
||||
let mut rf = RefreshFlow::new(self.client.borrow_mut());
|
||||
loop {
|
||||
match *rf.refresh_token(self.flow_type,
|
||||
&self.secret.client_id,
|
||||
&self.secret.client_secret,
|
||||
&t.refresh_token) {
|
||||
&self.secret.client_id,
|
||||
&self.secret.client_secret,
|
||||
&t.refresh_token) {
|
||||
RefreshResult::Error(ref err) => {
|
||||
match self.delegate.connection_error(err) {
|
||||
Retry::Abort|Retry::Skip =>
|
||||
@@ -262,21 +268,22 @@ impl<D, S, C> GetToken for Authenticator<D, S, C>
|
||||
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())))
|
||||
},
|
||||
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)),
|
||||
@@ -298,16 +305,15 @@ impl<D, S, C> GetToken for Authenticator<D, S, C>
|
||||
Ok(None) => {
|
||||
// Nothing was in storage - get a new token
|
||||
// get new token. The respective sub-routine will do all the logic.
|
||||
match
|
||||
match self.flow_type {
|
||||
FlowType::Device => self.retrieve_device_token(&scopes),
|
||||
FlowType::InstalledInteractive => self.do_installed_flow(&scopes),
|
||||
FlowType::InstalledRedirect(_) => self.do_installed_flow(&scopes),
|
||||
}
|
||||
{
|
||||
match match self.flow_type {
|
||||
FlowType::Device => self.retrieve_device_token(&scopes),
|
||||
FlowType::InstalledInteractive => self.do_installed_flow(&scopes),
|
||||
FlowType::InstalledRedirect(_) => self.do_installed_flow(&scopes),
|
||||
} {
|
||||
Ok(token) => {
|
||||
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)),
|
||||
@@ -320,127 +326,32 @@ impl<D, S, C> GetToken for Authenticator<D, S, C>
|
||||
break;
|
||||
}// end attempt to save
|
||||
Ok(token)
|
||||
},
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}// end match token retrieve result
|
||||
},
|
||||
}
|
||||
Err(err) => {
|
||||
match self.delegate.token_storage_failure(false, &err) {
|
||||
Retry::Abort|Retry::Skip => Err(Box::new(err)),
|
||||
Retry::Abort | Retry::Skip => Err(Box::new(err)),
|
||||
Retry::After(d) => {
|
||||
sleep(d);
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
}
|
||||
},
|
||||
}// end match
|
||||
}
|
||||
};// end match
|
||||
}// end loop
|
||||
}
|
||||
|
||||
fn api_key(&mut self) -> Option<String> {
|
||||
if self.secret.client_id.len() == 0 {
|
||||
return None
|
||||
return None;
|
||||
}
|
||||
Some(self.secret.client_id.clone())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// A partially implemented trait to interact with the `Authenticator`
|
||||
///
|
||||
/// The only method that needs to be implemented manually is `present_user_code(...)`,
|
||||
/// as no assumptions are made on how this presentation should happen.
|
||||
pub trait AuthenticatorDelegate {
|
||||
|
||||
/// Called whenever there is an HttpError, usually if there are network problems.
|
||||
///
|
||||
/// Return retry information.
|
||||
fn connection_error(&mut self, &hyper::Error) -> Retry {
|
||||
Retry::Abort
|
||||
}
|
||||
|
||||
/// Called whenever we failed to retrieve a token or set a token due to a storage error.
|
||||
/// You may use it to either ignore the incident or retry.
|
||||
/// This can be useful if the underlying `TokenStorage` may fail occasionally.
|
||||
/// if `is_set` is true, the failure resulted from `TokenStorage.set(...)`. Otherwise,
|
||||
/// it was `TokenStorage.get(...)`
|
||||
fn token_storage_failure(&mut self, is_set: bool, _: &Error) -> Retry {
|
||||
let _ = is_set;
|
||||
Retry::Abort
|
||||
}
|
||||
|
||||
/// The server denied the attempt to obtain a request code
|
||||
fn request_failure(&mut self, RequestError) {}
|
||||
|
||||
/// Called if the request code is expired. You will have to start over in this case.
|
||||
/// This will be the last call the delegate receives.
|
||||
/// Given `DateTime` is the expiration date
|
||||
fn expired(&mut self, &DateTime<UTC>) {}
|
||||
|
||||
/// Called if the user denied access. You would have to start over.
|
||||
/// This will be the last call the delegate receives.
|
||||
fn denied(&mut self) {}
|
||||
|
||||
/// Called if we could not acquire a refresh token for a reason possibly specified
|
||||
/// by the server.
|
||||
/// This call is made for the delegate's information only.
|
||||
fn token_refresh_failed(&mut self, error: &String, error_description: &Option<String>) {
|
||||
{ let _ = error; }
|
||||
{ let _ = error_description; }
|
||||
}
|
||||
|
||||
/// Called as long as we are waiting for the user to authorize us.
|
||||
/// Can be used to print progress information, or decide to time-out.
|
||||
///
|
||||
/// If the returned `Retry` variant is a duration.
|
||||
/// # Notes
|
||||
/// * Only used in `DeviceFlow`. Return value will only be used if it
|
||||
/// is larger than the interval desired by the server.
|
||||
fn pending(&mut self, &PollInformation) -> Retry {
|
||||
Retry::After(Duration::from_secs(5))
|
||||
}
|
||||
|
||||
/// The server has returned a `user_code` which must be shown to the user,
|
||||
/// along with the `verification_url`.
|
||||
/// # Notes
|
||||
/// * 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!("Do not close this application until you either denied or granted access.");
|
||||
println!("You have time until {}.",
|
||||
pi.expires_at.with_timezone(&Local));
|
||||
}
|
||||
|
||||
/// Only method currently used by the InstalledFlow.
|
||||
/// We need the user to navigate to a URL using their browser and potentially paste back a code
|
||||
/// (or maybe not). Whether they have to enter a code depends on the InstalledFlowReturnMethod
|
||||
/// used.
|
||||
fn present_user_url(&mut self, url: &String, need_code: bool) -> Option<String> {
|
||||
if need_code {
|
||||
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(|_| code)
|
||||
} else {
|
||||
println!("Please direct your browser to {} and follow the instructions displayed \
|
||||
there.",
|
||||
url);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Uses all default implementations by AuthenticatorDelegate, and makes the trait's
|
||||
/// implementation usable in the first place.
|
||||
pub struct DefaultAuthenticatorDelegate;
|
||||
impl AuthenticatorDelegate for DefaultAuthenticatorDelegate {}
|
||||
|
||||
/// A utility type to indicate how operations DeviceFlowHelper operations should be retried
|
||||
pub enum Retry {
|
||||
/// Signal you don't want to retry
|
||||
@@ -458,7 +369,7 @@ mod tests {
|
||||
use super::*;
|
||||
use super::super::device::tests::MockGoogleAuth;
|
||||
use super::super::common::tests::SECRET;
|
||||
use super::super::common::{ConsoleApplicationSecret};
|
||||
use super::super::common::ConsoleApplicationSecret;
|
||||
use storage::MemoryStorage;
|
||||
use std::default::Default;
|
||||
use hyper;
|
||||
153
src/authenticator_delegate.rs
Normal file
153
src/authenticator_delegate.rs
Normal file
@@ -0,0 +1,153 @@
|
||||
use hyper;
|
||||
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::error::Error;
|
||||
|
||||
use authenticator::Retry;
|
||||
use common::RequestError;
|
||||
|
||||
use chrono::{DateTime, Local, UTC};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Contains state of pending authentication requests
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PollInformation {
|
||||
/// Code the user must enter ...
|
||||
pub user_code: String,
|
||||
/// ... at the verification URL
|
||||
pub verification_url: String,
|
||||
|
||||
/// The `user_code` expires at the given time
|
||||
/// It's the time the user has left to authenticate your application
|
||||
pub expires_at: DateTime<UTC>,
|
||||
/// The interval in which we may poll for a status change
|
||||
/// The server responds with errors of we poll too fast.
|
||||
pub interval: Duration,
|
||||
}
|
||||
|
||||
impl fmt::Display for PollInformation {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
writeln!(f, "Proceed with polling until {}", self.expires_at)
|
||||
}
|
||||
}
|
||||
|
||||
/// Encapsulates all possible results of a `poll_token(...)` operation
|
||||
#[derive(Debug)]
|
||||
pub enum PollError {
|
||||
/// Connection failure - retry if you think it's worth it
|
||||
HttpError(hyper::Error),
|
||||
/// indicates we are expired, including the expiration date
|
||||
Expired(DateTime<UTC>),
|
||||
/// Indicates that the user declined access. String is server response
|
||||
AccessDenied,
|
||||
}
|
||||
|
||||
impl fmt::Display for PollError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
match *self {
|
||||
PollError::HttpError(ref err) => err.fmt(f),
|
||||
PollError::Expired(ref date) => writeln!(f, "Authentication expired at {}", date),
|
||||
PollError::AccessDenied => "Access denied by user".fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// A partially implemented trait to interact with the `Authenticator`
|
||||
///
|
||||
/// The only method that needs to be implemented manually is `present_user_code(...)`,
|
||||
/// as no assumptions are made on how this presentation should happen.
|
||||
pub trait AuthenticatorDelegate {
|
||||
/// Called whenever there is an HttpError, usually if there are network problems.
|
||||
///
|
||||
/// Return retry information.
|
||||
fn connection_error(&mut self, &hyper::Error) -> Retry {
|
||||
Retry::Abort
|
||||
}
|
||||
|
||||
/// Called whenever we failed to retrieve a token or set a token due to a storage error.
|
||||
/// You may use it to either ignore the incident or retry.
|
||||
/// This can be useful if the underlying `TokenStorage` may fail occasionally.
|
||||
/// if `is_set` is true, the failure resulted from `TokenStorage.set(...)`. Otherwise,
|
||||
/// it was `TokenStorage.get(...)`
|
||||
fn token_storage_failure(&mut self, is_set: bool, _: &Error) -> Retry {
|
||||
let _ = is_set;
|
||||
Retry::Abort
|
||||
}
|
||||
|
||||
/// The server denied the attempt to obtain a request code
|
||||
fn request_failure(&mut self, RequestError) {}
|
||||
|
||||
/// Called if the request code is expired. You will have to start over in this case.
|
||||
/// This will be the last call the delegate receives.
|
||||
/// Given `DateTime` is the expiration date
|
||||
fn expired(&mut self, &DateTime<UTC>) {}
|
||||
|
||||
/// Called if the user denied access. You would have to start over.
|
||||
/// This will be the last call the delegate receives.
|
||||
fn denied(&mut self) {}
|
||||
|
||||
/// Called if we could not acquire a refresh token for a reason possibly specified
|
||||
/// by the server.
|
||||
/// This call is made for the delegate's information only.
|
||||
fn token_refresh_failed(&mut self, error: &String, error_description: &Option<String>) {
|
||||
{
|
||||
let _ = error;
|
||||
}
|
||||
{
|
||||
let _ = error_description;
|
||||
}
|
||||
}
|
||||
|
||||
/// Called as long as we are waiting for the user to authorize us.
|
||||
/// Can be used to print progress information, or decide to time-out.
|
||||
///
|
||||
/// If the returned `Retry` variant is a duration.
|
||||
/// # Notes
|
||||
/// * Only used in `DeviceFlow`. Return value will only be used if it
|
||||
/// is larger than the interval desired by the server.
|
||||
fn pending(&mut self, &PollInformation) -> Retry {
|
||||
Retry::After(Duration::from_secs(5))
|
||||
}
|
||||
|
||||
/// The server has returned a `user_code` which must be shown to the user,
|
||||
/// along with the `verification_url`.
|
||||
/// # Notes
|
||||
/// * 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!("Do not close this application until you either denied or granted access.");
|
||||
println!("You have time until {}.",
|
||||
pi.expires_at.with_timezone(&Local));
|
||||
}
|
||||
|
||||
/// Only method currently used by the InstalledFlow.
|
||||
/// We need the user to navigate to a URL using their browser and potentially paste back a code
|
||||
/// (or maybe not). Whether they have to enter a code depends on the InstalledFlowReturnMethod
|
||||
/// used.
|
||||
fn present_user_url(&mut self, url: &String, need_code: bool) -> Option<String> {
|
||||
if need_code {
|
||||
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(|_| code)
|
||||
} else {
|
||||
println!("Please direct your browser to {} and follow the instructions displayed \
|
||||
there.",
|
||||
url);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Uses all default implementations by AuthenticatorDelegate, and makes the trait's
|
||||
/// implementation usable in the first place.
|
||||
pub struct DefaultAuthenticatorDelegate;
|
||||
impl AuthenticatorDelegate for DefaultAuthenticatorDelegate {}
|
||||
@@ -15,6 +15,51 @@ pub struct JsonError {
|
||||
pub error_uri: Option<String>,
|
||||
}
|
||||
|
||||
/// Encapsulates all possible results of the `request_token(...)` operation
|
||||
pub enum RequestError {
|
||||
/// Indicates connection failure
|
||||
HttpError(hyper::Error),
|
||||
/// The OAuth client was not found
|
||||
InvalidClient,
|
||||
/// Some requested scopes were invalid. String contains the scopes as part of
|
||||
/// the server error message
|
||||
InvalidScope(String),
|
||||
/// A 'catch-all' variant containing the server error and description
|
||||
/// First string is the error code, the second may be a more detailed description
|
||||
NegativeServerResponse(String, Option<String>),
|
||||
}
|
||||
|
||||
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()))
|
||||
}
|
||||
_ => RequestError::NegativeServerResponse(value.error, value.error_description),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for RequestError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
match *self {
|
||||
RequestError::HttpError(ref err) => err.fmt(f),
|
||||
RequestError::InvalidClient => "Invalid Client".fmt(f),
|
||||
RequestError::InvalidScope(ref scope) => writeln!(f, "Invalid Scope: '{}'", scope),
|
||||
RequestError::NegativeServerResponse(ref error, ref desc) => {
|
||||
try!(error.fmt(f));
|
||||
if let &Some(ref desc) = desc {
|
||||
try!(write!(f, ": {}", desc));
|
||||
}
|
||||
"\n".fmt(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Represents all implemented token types
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum TokenType {
|
||||
@@ -25,7 +70,7 @@ pub enum TokenType {
|
||||
impl AsRef<str> for TokenType {
|
||||
fn as_ref(&self) -> &'static str {
|
||||
match *self {
|
||||
TokenType::Bearer => "Bearer"
|
||||
TokenType::Bearer => "Bearer",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +80,7 @@ impl FromStr for TokenType {
|
||||
fn from_str(s: &str) -> Result<TokenType, ()> {
|
||||
match s {
|
||||
"Bearer" => Ok(TokenType::Bearer),
|
||||
_ => Err(())
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,7 +92,7 @@ pub struct Scheme {
|
||||
/// The type of our access token
|
||||
pub token_type: TokenType,
|
||||
/// The token returned by one of the Authorization Flows
|
||||
pub access_token: String
|
||||
pub access_token: String,
|
||||
}
|
||||
|
||||
impl hyper::header::Scheme for Scheme {
|
||||
@@ -65,11 +110,16 @@ impl FromStr for Scheme {
|
||||
fn from_str(s: &str) -> Result<Scheme, &'static str> {
|
||||
let parts: Vec<&str> = s.split(' ').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err("Expected two parts: <token_type> <token>")
|
||||
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() }),
|
||||
Err(_) => Err("Couldn't parse token type")
|
||||
Ok(t) => {
|
||||
Ok(Scheme {
|
||||
token_type: t,
|
||||
access_token: parts[1].to_string(),
|
||||
})
|
||||
}
|
||||
Err(_) => Err("Couldn't parse token type"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,7 +153,6 @@ pub struct Token {
|
||||
}
|
||||
|
||||
impl Token {
|
||||
|
||||
/// Returns true if we are expired.
|
||||
///
|
||||
/// # Panics
|
||||
@@ -117,14 +166,16 @@ 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.
|
||||
pub fn set_expiry_absolute(&mut self) -> &mut Token {
|
||||
if self.expires_in_timestamp.is_some() {
|
||||
assert!(self.expires_in.is_none());
|
||||
return self
|
||||
return self;
|
||||
}
|
||||
|
||||
self.expires_in_timestamp = Some(UTC::now().timestamp() + self.expires_in.unwrap());
|
||||
@@ -182,7 +233,7 @@ pub struct ApplicationSecret {
|
||||
/// as ID tokens, signed by the authentication provider.
|
||||
pub auth_provider_x509_cert_url: Option<String>,
|
||||
/// The URL of the public x509 certificate, used to verify JWTs signed by the client.
|
||||
pub client_x509_cert_url: Option<String>
|
||||
pub client_x509_cert_url: Option<String>,
|
||||
}
|
||||
|
||||
/// A type to facilitate reading and writing the json secret file
|
||||
@@ -190,7 +241,7 @@ pub struct ApplicationSecret {
|
||||
#[derive(Deserialize, Serialize, Default)]
|
||||
pub struct ConsoleApplicationSecret {
|
||||
pub web: Option<ApplicationSecret>,
|
||||
pub installed: Option<ApplicationSecret>
|
||||
pub installed: Option<ApplicationSecret>,
|
||||
}
|
||||
|
||||
|
||||
@@ -199,7 +250,13 @@ pub mod tests {
|
||||
use super::*;
|
||||
use hyper;
|
||||
|
||||
pub const SECRET: &'static str = "{\"installed\":{\"auth_uri\":\"https://accounts.google.com/o/oauth2/auth\",\"client_secret\":\"UqkDJd5RFwnHoiG5x5Rub8SI\",\"token_uri\":\"https://accounts.google.com/o/oauth2/token\",\"client_email\":\"\",\"redirect_uris\":[\"urn:ietf:wg:oauth:2.0:oob\",\"oob\"],\"client_x509_cert_url\":\"\",\"client_id\":\"14070749909-vgip2f1okm7bkvajhi9jugan6126io9v.apps.googleusercontent.com\",\"auth_provider_x509_cert_url\":\"https://www.googleapis.com/oauth2/v1/certs\"}}";
|
||||
pub const SECRET: &'static str =
|
||||
"{\"installed\":{\"auth_uri\":\"https://accounts.google.com/o/oauth2/auth\",\
|
||||
\"client_secret\":\"UqkDJd5RFwnHoiG5x5Rub8SI\",\"token_uri\":\"https://accounts.google.\
|
||||
com/o/oauth2/token\",\"client_email\":\"\",\"redirect_uris\":[\"urn:ietf:wg:oauth:2.0:\
|
||||
oob\",\"oob\"],\"client_x509_cert_url\":\"\",\"client_id\":\
|
||||
\"14070749909-vgip2f1okm7bkvajhi9jugan6126io9v.apps.googleusercontent.com\",\
|
||||
\"auth_provider_x509_cert_url\":\"https://www.googleapis.com/oauth2/v1/certs\"}}";
|
||||
|
||||
#[test]
|
||||
fn console_secret() {
|
||||
@@ -212,15 +269,20 @@ pub mod tests {
|
||||
|
||||
#[test]
|
||||
fn schema() {
|
||||
let s = Scheme {token_type: TokenType::Bearer, access_token: "foo".to_string() };
|
||||
let s = Scheme {
|
||||
token_type: TokenType::Bearer,
|
||||
access_token: "foo".to_string(),
|
||||
};
|
||||
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]
|
||||
fn parse_schema() {
|
||||
let auth: hyper::header::Authorization<Scheme> = hyper::header::Header::parse_header(&[b"Bearer foo".to_vec()]).unwrap();
|
||||
let auth: hyper::header::Authorization<Scheme> =
|
||||
hyper::header::Header::parse_header(&[b"Bearer foo".to_vec()]).unwrap();
|
||||
assert_eq!(auth.0.token_type, TokenType::Bearer);
|
||||
assert_eq!(auth.0.access_token, "foo".to_string());
|
||||
}
|
||||
|
||||
221
src/device.rs
221
src/device.rs
@@ -1,19 +1,19 @@
|
||||
use std::iter::IntoIterator;
|
||||
use std::time::Duration;
|
||||
use std::default::Default;
|
||||
use std::fmt;
|
||||
|
||||
use hyper;
|
||||
use hyper::header::ContentType;
|
||||
use url::form_urlencoded;
|
||||
use itertools::Itertools;
|
||||
use serde_json as json;
|
||||
use chrono::{DateTime,UTC, self};
|
||||
use chrono::{self, UTC};
|
||||
use std::borrow::BorrowMut;
|
||||
use std::io::Read;
|
||||
use std::i64;
|
||||
|
||||
use common::{Token, FlowType, Flow, JsonError};
|
||||
use common::{Token, FlowType, Flow, RequestError, JsonError};
|
||||
use authenticator_delegate::{PollError, PollInformation};
|
||||
|
||||
pub const GOOGLE_TOKEN_URL: &'static str = "https://accounts.google.com/o/oauth2/token";
|
||||
|
||||
@@ -45,100 +45,9 @@ impl<C> Flow for DeviceFlow<C> {
|
||||
FlowType::Device
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Contains state of pending authentication requests
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PollInformation {
|
||||
/// Code the user must enter ...
|
||||
pub user_code: String,
|
||||
/// ... at the verification URL
|
||||
pub verification_url: String,
|
||||
|
||||
/// The `user_code` expires at the given time
|
||||
/// It's the time the user has left to authenticate your application
|
||||
pub expires_at: DateTime<UTC>,
|
||||
/// The interval in which we may poll for a status change
|
||||
/// The server responds with errors of we poll too fast.
|
||||
pub interval: Duration,
|
||||
}
|
||||
|
||||
impl fmt::Display for PollInformation {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
writeln!(f, "Proceed with polling until {}", self.expires_at)
|
||||
}
|
||||
}
|
||||
|
||||
/// Encapsulates all possible results of the `request_token(...)` operation
|
||||
pub enum RequestError {
|
||||
/// Indicates connection failure
|
||||
HttpError(hyper::Error),
|
||||
/// The OAuth client was not found
|
||||
InvalidClient,
|
||||
/// Some requested scopes were invalid. String contains the scopes as part of
|
||||
/// the server error message
|
||||
InvalidScope(String),
|
||||
/// A 'catch-all' variant containing the server error and description
|
||||
/// First string is the error code, the second may be a more detailed description
|
||||
NegativeServerResponse(String, Option<String>),
|
||||
}
|
||||
|
||||
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())
|
||||
),
|
||||
_ => RequestError::NegativeServerResponse(value.error, value.error_description),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for RequestError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
match *self {
|
||||
RequestError::HttpError(ref err) => err.fmt(f),
|
||||
RequestError::InvalidClient => "Invalid Client".fmt(f),
|
||||
RequestError::InvalidScope(ref scope)
|
||||
=> writeln!(f, "Invalid Scope: '{}'", scope),
|
||||
RequestError::NegativeServerResponse(ref error, ref desc) => {
|
||||
try!(error.fmt(f));
|
||||
if let &Some(ref desc) = desc {
|
||||
try!(write!(f, ": {}", desc));
|
||||
}
|
||||
"\n".fmt(f)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encapsulates all possible results of a `poll_token(...)` operation
|
||||
#[derive(Debug)]
|
||||
pub enum PollError {
|
||||
/// Connection failure - retry if you think it's worth it
|
||||
HttpError(hyper::Error),
|
||||
/// indicates we are expired, including the expiration date
|
||||
Expired(DateTime<UTC>),
|
||||
/// Indicates that the user declined access. String is server response
|
||||
AccessDenied,
|
||||
}
|
||||
|
||||
impl fmt::Display for PollError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
match *self {
|
||||
PollError::HttpError(ref err) => err.fmt(f),
|
||||
PollError::Expired(ref date)
|
||||
=> writeln!(f, "Authentication expired at {}", date),
|
||||
PollError::AccessDenied => "Access denied by user".fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl<C> DeviceFlow<C>
|
||||
where C: BorrowMut<hyper::Client> {
|
||||
|
||||
where C: BorrowMut<hyper::Client>
|
||||
{
|
||||
/// # Examples
|
||||
/// ```test_harness
|
||||
/// extern crate hyper;
|
||||
@@ -175,30 +84,36 @@ 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, client_id: &str, client_secret: &str, scopes: I)
|
||||
-> Result<PollInformation, RequestError>
|
||||
where T: AsRef<str> + 'b,
|
||||
I: IntoIterator<Item=&'b T> {
|
||||
pub fn request_code<'b, T, I>(&mut self,
|
||||
client_id: &str,
|
||||
client_secret: &str,
|
||||
scopes: I)
|
||||
-> Result<PollInformation, RequestError>
|
||||
where T: AsRef<str> + 'b,
|
||||
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");
|
||||
}
|
||||
|
||||
// note: cloned() shouldn't be needed, see issue
|
||||
// https://github.com/servo/rust-url/issues/81
|
||||
let req = form_urlencoded::serialize(
|
||||
&[("client_id", client_id),
|
||||
("scope", scopes.into_iter()
|
||||
.map(|s| s.as_ref())
|
||||
.intersperse(" ")
|
||||
.collect::<String>()
|
||||
.as_ref())]);
|
||||
let req = form_urlencoded::serialize(&[("client_id", client_id),
|
||||
("scope",
|
||||
scopes.into_iter()
|
||||
.map(|s| s.as_ref())
|
||||
.intersperse(" ")
|
||||
.collect::<String>()
|
||||
.as_ref())]);
|
||||
|
||||
// note: works around bug in rustlang
|
||||
// https://github.com/rust-lang/rust/issues/22252
|
||||
let ret = match self.client.borrow_mut().post(FlowType::Device.as_ref())
|
||||
.header(ContentType("application/x-www-form-urlencoded".parse().unwrap()))
|
||||
.body(&*req)
|
||||
.send() {
|
||||
let ret = match self.client
|
||||
.borrow_mut()
|
||||
.post(FlowType::Device.as_ref())
|
||||
.header(ContentType("application/x-www-form-urlencoded".parse().unwrap()))
|
||||
.body(&*req)
|
||||
.send() {
|
||||
Err(err) => {
|
||||
return Err(RequestError::HttpError(err));
|
||||
}
|
||||
@@ -219,10 +134,8 @@ impl<C> DeviceFlow<C>
|
||||
|
||||
// check for error
|
||||
match json::from_str::<JsonError>(&json_str) {
|
||||
Err(_) => {}, // ignore, move on
|
||||
Ok(res) => {
|
||||
return Err(RequestError::from(res))
|
||||
}
|
||||
Err(_) => {} // ignore, move on
|
||||
Ok(res) => return Err(RequestError::from(res)),
|
||||
}
|
||||
|
||||
let decoded: JsonData = json::from_str(&json_str).unwrap();
|
||||
@@ -231,7 +144,7 @@ impl<C> DeviceFlow<C>
|
||||
let pi = PollInformation {
|
||||
user_code: decoded.user_code,
|
||||
verification_url: decoded.verification_url,
|
||||
expires_at: UTC::now() + chrono::Duration::seconds(decoded.expires_in),
|
||||
expires_at: UTC::now() + chrono::Duration::seconds(decoded.expires_in),
|
||||
interval: Duration::from_secs(i64::abs(decoded.interval) as u64),
|
||||
};
|
||||
self.state = Some(DeviceFlowState::Pending(pi.clone()));
|
||||
@@ -265,33 +178,35 @@ 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) =>
|
||||
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"),
|
||||
};
|
||||
|
||||
if pi.expires_at <= UTC::now() {
|
||||
self.error = Some(PollError::Expired(pi.expires_at));
|
||||
self.state = Some(DeviceFlowState::Error);
|
||||
return Err(&self.error.as_ref().unwrap())
|
||||
return Err(&self.error.as_ref().unwrap());
|
||||
}
|
||||
|
||||
// We should be ready for a new request
|
||||
let req = form_urlencoded::serialize(
|
||||
&[("client_id", &self.id[..]),
|
||||
("client_secret", &self.secret),
|
||||
("code", &self.device_code),
|
||||
("grant_type", "http://oauth.net/grant_type/device/1.0")]);
|
||||
let req = form_urlencoded::serialize(&[("client_id", &self.id[..]),
|
||||
("client_secret", &self.secret),
|
||||
("code", &self.device_code),
|
||||
("grant_type",
|
||||
"http://oauth.net/grant_type/device/1.0")]);
|
||||
|
||||
let json_str =
|
||||
match self.client.borrow_mut().post(GOOGLE_TOKEN_URL)
|
||||
.header(ContentType("application/x-www-form-urlencoded".parse().unwrap()))
|
||||
.body(&*req)
|
||||
.send() {
|
||||
let json_str = match self.client
|
||||
.borrow_mut()
|
||||
.post(GOOGLE_TOKEN_URL)
|
||||
.header(ContentType("application/x-www-form-urlencoded".parse().unwrap()))
|
||||
.body(&*req)
|
||||
.send() {
|
||||
Err(err) => {
|
||||
self.error = Some(PollError::HttpError(err));
|
||||
return Err(self.error.as_ref().unwrap());
|
||||
@@ -305,18 +220,18 @@ impl<C> DeviceFlow<C>
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct JsonError {
|
||||
error: String
|
||||
error: String,
|
||||
}
|
||||
|
||||
match json::from_str::<JsonError>(&json_str) {
|
||||
Err(_) => {}, // ignore, move on, it's not an error
|
||||
Err(_) => {} // ignore, move on, it's not an error
|
||||
Ok(res) => {
|
||||
match res.error.as_ref() {
|
||||
"access_denied" => {
|
||||
self.error = Some(PollError::AccessDenied);
|
||||
self.state = Some(DeviceFlowState::Error);
|
||||
return Err(self.error.as_ref().unwrap())
|
||||
},
|
||||
return Err(self.error.as_ref().unwrap());
|
||||
}
|
||||
"authorization_pending" => return Ok(None),
|
||||
_ => panic!("server message '{}' not understood", res.error),
|
||||
};
|
||||
@@ -329,7 +244,7 @@ impl<C> DeviceFlow<C>
|
||||
|
||||
let res = Ok(Some(t.clone()));
|
||||
self.state = Some(DeviceFlowState::Success(t));
|
||||
return res
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,24 +271,23 @@ pub mod tests {
|
||||
\"verification_url\" : \"http://www.google.com/device\",\r\n\
|
||||
\"expires_in\" : 1800,\r\n\
|
||||
\"interval\" : 0\r\n\
|
||||
}".to_string());
|
||||
}"
|
||||
.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());
|
||||
}"
|
||||
.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\
|
||||
\"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
|
||||
|
||||
}
|
||||
@@ -400,18 +314,17 @@ pub mod tests {
|
||||
}
|
||||
|
||||
match flow.poll_token() {
|
||||
Ok(None) => {},
|
||||
Ok(None) => {}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
let t =
|
||||
match flow.poll_token() {
|
||||
Ok(Some(t)) => {
|
||||
assert_eq!(t.access_token, "1/fFAGRNJru1FTz70BzhT3Zg");
|
||||
t
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let t = match flow.poll_token() {
|
||||
Ok(Some(t)) => {
|
||||
assert_eq!(t.access_token, "1/fFAGRNJru1FTz70BzhT3Zg");
|
||||
t
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
// from now on, all calls will yield the same result
|
||||
// As our mock has only 3 items, we would panic on this call
|
||||
|
||||
@@ -20,7 +20,7 @@ use url::form_urlencoded;
|
||||
use url::percent_encoding::{percent_encode, QUERY_ENCODE_SET};
|
||||
|
||||
use common::{ApplicationSecret, Token};
|
||||
use helper::AuthenticatorDelegate;
|
||||
use authenticator_delegate::AuthenticatorDelegate;
|
||||
|
||||
const OOB_REDIRECT_URI: &'static str = "urn:ietf:wg:oauth:2.0:oob";
|
||||
|
||||
@@ -76,7 +76,8 @@ pub enum InstalledFlowReturnMethod {
|
||||
HTTPRedirect(u32),
|
||||
}
|
||||
|
||||
impl<C> InstalledFlow<C> where C: BorrowMut<hyper::Client>
|
||||
impl<C> InstalledFlow<C>
|
||||
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
|
||||
@@ -99,9 +100,8 @@ impl<C> InstalledFlow<C> where C: BorrowMut<hyper::Client>
|
||||
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,
|
||||
@@ -146,7 +146,7 @@ impl<C> InstalledFlow<C> where C: BorrowMut<hyper::Client>
|
||||
expires_in: tokens.expires_in,
|
||||
expires_in_timestamp: None,
|
||||
};
|
||||
|
||||
|
||||
token.set_expiry_absolute();
|
||||
Result::Ok(token)
|
||||
} else {
|
||||
@@ -154,7 +154,7 @@ impl<C> InstalledFlow<C> where C: BorrowMut<hyper::Client>
|
||||
format!("Token API error: {} {}",
|
||||
tokens.error.unwrap_or("<unknown err>".to_string()),
|
||||
tokens.error_description
|
||||
.unwrap_or("".to_string()))
|
||||
.unwrap_or("".to_string()))
|
||||
.as_str());
|
||||
Result::Err(Box::new(err))
|
||||
}
|
||||
@@ -230,13 +230,12 @@ impl<C> InstalledFlow<C> where C: BorrowMut<hyper::Client>
|
||||
("grant_type".to_string(),
|
||||
"authorization_code".to_string())]);
|
||||
|
||||
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()))
|
||||
.send();
|
||||
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()))
|
||||
.send();
|
||||
|
||||
let mut resp = String::new();
|
||||
|
||||
@@ -292,9 +291,10 @@ 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());
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
|
||||
@@ -12,17 +12,19 @@ extern crate mime;
|
||||
extern crate url;
|
||||
extern crate itertools;
|
||||
|
||||
mod authenticator_delegate;
|
||||
mod authenticator;
|
||||
mod device;
|
||||
mod storage;
|
||||
mod installed;
|
||||
mod helper;
|
||||
mod refresh;
|
||||
mod common;
|
||||
|
||||
pub use device::{DeviceFlow, PollInformation, PollError};
|
||||
pub use device::DeviceFlow;
|
||||
pub use refresh::{RefreshFlow, RefreshResult};
|
||||
pub use common::{Token, FlowType, ApplicationSecret, ConsoleApplicationSecret, Scheme, TokenType};
|
||||
pub use installed::{InstalledFlow, InstalledFlowReturnMethod};
|
||||
pub use storage::{TokenStorage, NullStorage, MemoryStorage, DiskTokenStorage};
|
||||
pub use helper::{Authenticator, AuthenticatorDelegate,
|
||||
Retry, DefaultAuthenticatorDelegate, GetToken};
|
||||
pub use authenticator::{Authenticator, Retry, GetToken};
|
||||
pub use authenticator_delegate::{AuthenticatorDelegate, DefaultAuthenticatorDelegate, PollError,
|
||||
PollInformation};
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::borrow::BorrowMut;
|
||||
use std::io::Read;
|
||||
|
||||
/// Implements the [Outh2 Refresh Token Flow](https://developers.google.com/youtube/v3/guides/authentication#devices).
|
||||
///
|
||||
///
|
||||
/// Refresh an expired access token, as obtained by any other authentication flow.
|
||||
/// This flow is useful when your `Token` is expired and allows to obtain a new
|
||||
/// and valid access token.
|
||||
@@ -32,8 +32,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 {
|
||||
client: client,
|
||||
@@ -44,7 +44,7 @@ impl<C> RefreshFlow<C>
|
||||
/// Attempt to refresh the given token, and obtain a new, valid one.
|
||||
/// If the `RefreshResult` is `RefreshResult::Error`, you may retry within an interval
|
||||
/// of your choice. If it is `RefreshResult:RefreshError`, your refresh token is invalid
|
||||
/// or your authorization was revoked. Therefore no further attempt shall be made,
|
||||
/// or your authorization was revoked. Therefore no further attempt shall be made,
|
||||
/// and you will have to re-authorize using the `DeviceFlow`
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -52,31 +52,32 @@ impl<C> RefreshFlow<C>
|
||||
/// your refresh_token in the first place.
|
||||
/// * `client_id` & `client_secret` - as obtained when [registering your application](https://developers.google.com/youtube/registering_an_application)
|
||||
/// * `refresh_token` - obtained during previous call to `DeviceFlow::poll_token()` or equivalent
|
||||
///
|
||||
///
|
||||
/// # Examples
|
||||
/// Please see the crate landing page for an example.
|
||||
pub fn refresh_token(&mut self,
|
||||
flow_type: FlowType,
|
||||
client_id: &str,
|
||||
client_secret: &str,
|
||||
refresh_token: &str) -> &RefreshResult {
|
||||
pub fn refresh_token(&mut self,
|
||||
flow_type: FlowType,
|
||||
client_id: &str,
|
||||
client_secret: &str,
|
||||
refresh_token: &str)
|
||||
-> &RefreshResult {
|
||||
let _ = flow_type;
|
||||
if let RefreshResult::Success(_) = self.result {
|
||||
return &self.result;
|
||||
}
|
||||
|
||||
let req = form_urlencoded::serialize(
|
||||
&[("client_id", client_id),
|
||||
("client_secret", client_secret),
|
||||
("refresh_token", refresh_token),
|
||||
("grant_type", "refresh_token")]);
|
||||
let req = form_urlencoded::serialize(&[("client_id", client_id),
|
||||
("client_secret", client_secret),
|
||||
("refresh_token", refresh_token),
|
||||
("grant_type", "refresh_token")]);
|
||||
|
||||
let json_str =
|
||||
match self.client.borrow_mut().post(GOOGLE_TOKEN_URL)
|
||||
.header(ContentType("application/x-www-form-urlencoded".parse().unwrap()))
|
||||
.body(&*req)
|
||||
.send() {
|
||||
Err(err) => {
|
||||
let json_str = match self.client
|
||||
.borrow_mut()
|
||||
.post(GOOGLE_TOKEN_URL)
|
||||
.header(ContentType("application/x-www-form-urlencoded".parse().unwrap()))
|
||||
.body(&*req)
|
||||
.send() {
|
||||
Err(err) => {
|
||||
self.result = RefreshResult::Error(err);
|
||||
return &self.result;
|
||||
}
|
||||
@@ -95,7 +96,7 @@ impl<C> RefreshFlow<C>
|
||||
}
|
||||
|
||||
match json::from_str::<JsonError>(&json_str) {
|
||||
Err(_) => {},
|
||||
Err(_) => {}
|
||||
Ok(res) => {
|
||||
self.result = RefreshResult::RefreshError(res.error, res.error_description);
|
||||
return &self.result;
|
||||
@@ -137,7 +138,8 @@ mod tests {
|
||||
\"access_token\":\"1/fFAGRNJru1FTz70BzhT3Zg\",\r\n\
|
||||
\"expires_in\":3920,\r\n\
|
||||
\"token_type\":\"Bearer\"\r\n\
|
||||
}".to_string());
|
||||
}"
|
||||
.to_string());
|
||||
|
||||
c
|
||||
}
|
||||
@@ -154,17 +156,15 @@ mod tests {
|
||||
#[test]
|
||||
fn refresh_flow() {
|
||||
let mut c = hyper::Client::with_connector(<MockGoogleRefresh as Default>::default());
|
||||
let mut flow = RefreshFlow::new(
|
||||
&mut c);
|
||||
let mut flow = RefreshFlow::new(&mut c);
|
||||
|
||||
|
||||
match *flow.refresh_token(FlowType::Device,
|
||||
"bogus", "secret", "bogus_refresh_token") {
|
||||
match *flow.refresh_token(FlowType::Device, "bogus", "secret", "bogus_refresh_token") {
|
||||
RefreshResult::Success(ref t) => {
|
||||
assert_eq!(t.access_token, "1/fFAGRNJru1FTz70BzhT3Zg");
|
||||
assert!(!t.expired());
|
||||
},
|
||||
_ => unreachable!()
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/*
|
||||
* partially (c) 2016 Google Inc. (Lewin Bormann, lewinb@google.com)
|
||||
*
|
||||
* See project root for licensing information.
|
||||
*/
|
||||
// partially (c) 2016 Google Inc. (Lewin Bormann, lewinb@google.com)
|
||||
//
|
||||
// See project root for licensing information.
|
||||
//
|
||||
|
||||
extern crate serde_json;
|
||||
|
||||
@@ -172,10 +171,10 @@ impl DiskTokenStorage {
|
||||
}
|
||||
|
||||
let mut f = try!(fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(&self.location));
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(&self.location));
|
||||
f.write(serialized.as_ref()).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user