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:
Lewin Bormann
2016-08-31 19:46:11 +02:00
parent 7a907eb318
commit 9e59bf0496
8 changed files with 458 additions and 418 deletions

View File

@@ -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;

View 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 {}

View File

@@ -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());
}

View File

@@ -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

View File

@@ -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());
}
}
_ => {

View File

@@ -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};

View File

@@ -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!(),
}
}
}

View File

@@ -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(|_| ())
}
}