mirror of
https://github.com/OMGeeky/yup-oauth2.git
synced 2026-01-15 23:20:01 +01:00
fix(API): overall improved error handling
* Return `Result<Token, Box<Error>>` type wherever feasible * increment version to 0.3.5 * remove all usages of depreceated std items
This commit is contained in:
271
src/helper.rs
271
src/helper.rs
@@ -4,6 +4,9 @@ use std::collections::HashMap;
|
||||
use std::hash::{SipHasher, Hash, Hasher};
|
||||
use std::thread::sleep;
|
||||
use std::cmp::min;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::convert::From;
|
||||
|
||||
use common::{Token, FlowType, ApplicationSecret};
|
||||
use device::{PollInformation, RequestResult, DeviceFlow, PollResult};
|
||||
@@ -12,23 +15,42 @@ use chrono::{DateTime, UTC, Duration, Local};
|
||||
use hyper;
|
||||
|
||||
|
||||
/// Implements a specialised storage to set and retrieve `Token` instances.
|
||||
/// Implements a specialized storage to set and retrieve `Token` instances.
|
||||
/// The `scope_hash` represents the signature of the scopes for which the given token
|
||||
/// should be stored or retrieved.
|
||||
/// For completeness, the underlying, sorted scopes are provided as well. They might be
|
||||
/// useful for presentation to the user.
|
||||
pub trait TokenStorage {
|
||||
type Error: 'static + Error;
|
||||
|
||||
/// If `token` is None, it is invalid or revoked and should be removed from storage.
|
||||
fn set(&mut self, scope_hash: u64, token: Option<Token>);
|
||||
fn set(&mut self, scope_hash: u64, scopes: &Vec<&str>, token: Option<Token>) -> Option<Self::Error>;
|
||||
/// A `None` result indicates that there is no token for the given scope_hash.
|
||||
fn get(&self, scope_hash: u64) -> Option<Token>;
|
||||
fn get(&self, scope_hash: u64, scopes: &Vec<&str>) -> Result<Option<Token>, Self::Error>;
|
||||
}
|
||||
|
||||
/// A storage that remembers nothing.
|
||||
#[derive(Default)]
|
||||
pub struct NullStorage;
|
||||
#[derive(Debug)]
|
||||
pub struct NullError;
|
||||
|
||||
impl Error for NullError {
|
||||
fn description(&self) -> &str {
|
||||
"NULL"
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for NullError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
"NULL-ERROR".fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl TokenStorage for NullStorage {
|
||||
fn set(&mut self, _: u64, _: Option<Token>) {}
|
||||
fn get(&self, _: u64) -> Option<Token> { None }
|
||||
type Error = NullError;
|
||||
fn set(&mut self, _: u64, _: &Vec<&str>, _: Option<Token>) -> Option<NullError> { None }
|
||||
fn get(&self, _: u64, _: &Vec<&str>) -> Result<Option<Token>, NullError> { Ok(None) }
|
||||
}
|
||||
|
||||
/// A storage that remembers values for one session only.
|
||||
@@ -38,17 +60,20 @@ pub struct MemoryStorage {
|
||||
}
|
||||
|
||||
impl TokenStorage for MemoryStorage {
|
||||
fn set(&mut self, scope_hash: u64, token: Option<Token>) {
|
||||
type Error = NullError;
|
||||
|
||||
fn set(&mut self, scope_hash: u64, _: &Vec<&str>, token: Option<Token>) -> Option<NullError> {
|
||||
match token {
|
||||
Some(t) => self.tokens.insert(scope_hash, t),
|
||||
None => self.tokens.remove(&scope_hash),
|
||||
};
|
||||
None
|
||||
}
|
||||
|
||||
fn get(&self, scope_hash: u64) -> Option<Token> {
|
||||
fn get(&self, scope_hash: u64, _: &Vec<&str>) -> Result<Option<Token>, NullError> {
|
||||
match self.tokens.get(&scope_hash) {
|
||||
Some(t) => Some(t.clone()),
|
||||
None => None,
|
||||
Some(t) => Ok(Some(t.clone())),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,11 +102,54 @@ pub struct Authenticator<D, S, C> {
|
||||
secret: ApplicationSecret,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct StringError {
|
||||
error: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for StringError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
self.description().fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl StringError {
|
||||
fn new(error: String, desc: Option<&String>) -> StringError {
|
||||
let mut error = error;
|
||||
if let Some(d) = desc {
|
||||
error.push_str(": ");
|
||||
error.push_str(&*d);
|
||||
}
|
||||
|
||||
StringError {
|
||||
error: error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Error> for StringError {
|
||||
fn from(err: &'a Error) -> StringError {
|
||||
StringError::new(err.description().to_string(), None)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for StringError {
|
||||
fn from(value: String) -> StringError {
|
||||
StringError::new(value, None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for StringError {
|
||||
fn description(&self) -> &str {
|
||||
&self.error
|
||||
}
|
||||
}
|
||||
|
||||
/// A provider for authorization tokens, yielding tokens valid for a given scope.
|
||||
/// The `api_key()` method is an alternative in case there are no scopes or
|
||||
/// if no user is involved.
|
||||
pub trait GetToken {
|
||||
fn token<'b, I, T>(&mut self, scopes: I) -> Option<Token>
|
||||
fn token<'b, I, T>(&mut self, scopes: I) -> Result<Token, Box<Error>>
|
||||
where T: AsRef<str> + Ord,
|
||||
I: IntoIterator<Item=&'b T>;
|
||||
|
||||
@@ -119,7 +187,7 @@ impl<D, S, C> Authenticator<D, S, C>
|
||||
}
|
||||
}
|
||||
|
||||
fn retrieve_device_token(&mut self, scopes: &Vec<&str>) -> Option<Token> {
|
||||
fn retrieve_device_token(&mut self, scopes: &Vec<&str>) -> Result<Token, Box<Error>> {
|
||||
let mut flow = DeviceFlow::new(self.client.borrow_mut());
|
||||
|
||||
// PHASE 1: REQUEST CODE
|
||||
@@ -129,14 +197,16 @@ impl<D, S, C> Authenticator<D, S, C>
|
||||
match res {
|
||||
RequestResult::Error(err) => {
|
||||
match self.delegate.connection_error(&*err) {
|
||||
Retry::Abort => return None,
|
||||
Retry::Abort|Retry::Skip => return Err(Box::new(StringError::from(&*err as &Error))),
|
||||
Retry::After(d) => sleep(d),
|
||||
}
|
||||
},
|
||||
RequestResult::InvalidClient
|
||||
|RequestResult::NegativeServerResponse(_, _)
|
||||
|RequestResult::InvalidScope(_) => {
|
||||
let serr = StringError::from(res.to_string());
|
||||
self.delegate.request_failure(res);
|
||||
return None
|
||||
return Err(Box::new(serr))
|
||||
}
|
||||
RequestResult::ProceedWithPolling(pi) => {
|
||||
self.delegate.present_user_code(pi);
|
||||
@@ -147,29 +217,31 @@ impl<D, S, C> Authenticator<D, S, C>
|
||||
|
||||
// PHASE 1: POLL TOKEN
|
||||
loop {
|
||||
match flow.poll_token() {
|
||||
let pt = flow.poll_token();
|
||||
let pts = pt.to_string();
|
||||
match pt {
|
||||
PollResult::Error(err) => {
|
||||
match self.delegate.connection_error(&*err) {
|
||||
Retry::Abort => return None,
|
||||
Retry::Abort|Retry::Skip => return Err(Box::new(StringError::from(&*err as &Error))),
|
||||
Retry::After(d) => sleep(d),
|
||||
}
|
||||
},
|
||||
PollResult::Expired(t) => {
|
||||
self.delegate.expired(t);
|
||||
return None
|
||||
return Err(Box::new(StringError::from(pts)))
|
||||
},
|
||||
PollResult::AccessDenied => {
|
||||
self.delegate.denied();
|
||||
return None
|
||||
return Err(Box::new(StringError::from(pts)))
|
||||
},
|
||||
PollResult::AuthorizationPending(pi) => {
|
||||
match self.delegate.pending(&pi) {
|
||||
Retry::Abort => return None,
|
||||
Retry::Abort|Retry::Skip => return Err(Box::new(StringError::new(pts, None))),
|
||||
Retry::After(d) => sleep(min(d, pi.interval)),
|
||||
}
|
||||
},
|
||||
PollResult::AccessGranted(token) => {
|
||||
return Some(token)
|
||||
return Ok(token)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -183,9 +255,10 @@ impl<D, S, C> GetToken for Authenticator<D, S, C>
|
||||
|
||||
/// 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 returned token will be None, otherwise it is guaranteed to be
|
||||
/// valid for the given scopes.
|
||||
fn token<'b, I, T>(&mut self, scopes: I) -> Option<Token>
|
||||
/// 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,
|
||||
I: IntoIterator<Item=&'b T> {
|
||||
let (scope_key, scopes) = {
|
||||
@@ -193,55 +266,99 @@ impl<D, S, C> GetToken for Authenticator<D, S, C>
|
||||
.map(|s|s.as_ref())
|
||||
.collect::<Vec<&str>>();
|
||||
sv.sort();
|
||||
let s = sv.connect(" ");
|
||||
|
||||
let mut sh = SipHasher::new();
|
||||
s.hash(&mut sh);
|
||||
&sv.hash(&mut sh);
|
||||
let sv = sv;
|
||||
(sh.finish(), sv)
|
||||
};
|
||||
|
||||
// Get cached token. Yes, let's do an explicit return
|
||||
return match self.storage.get(scope_key) {
|
||||
Some(mut t) => {
|
||||
// t needs refresh ?
|
||||
if t.expired() {
|
||||
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) {
|
||||
RefreshResult::Error(ref err) => {
|
||||
match self.delegate.connection_error(err.clone()) {
|
||||
Retry::Abort => return None,
|
||||
Retry::After(d) => sleep(d),
|
||||
loop {
|
||||
return match self.storage.get(scope_key, &scopes) {
|
||||
Ok(Some(mut t)) => {
|
||||
// t needs refresh ?
|
||||
if t.expired() {
|
||||
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,
|
||||
scopes.iter()) {
|
||||
RefreshResult::Error(ref err) => {
|
||||
match self.delegate.connection_error(err) {
|
||||
Retry::Abort|Retry::Skip =>
|
||||
return Err(Box::new(StringError::new(
|
||||
err.description().to_string(),
|
||||
None))),
|
||||
Retry::After(d) => sleep(d),
|
||||
}
|
||||
},
|
||||
RefreshResult::RefreshError(ref err_str, ref err_description) => {
|
||||
self.delegate.token_refresh_failed(&err_str, &err_description);
|
||||
return Err(Box::new(
|
||||
StringError::new(err_str.clone(), err_description.as_ref())))
|
||||
},
|
||||
RefreshResult::Success(ref new_t) => {
|
||||
t = new_t.clone();
|
||||
loop {
|
||||
if let Some(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)),
|
||||
Retry::After(d) => {
|
||||
sleep(d);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
break; // .set()
|
||||
}
|
||||
break; // refresh_token loop
|
||||
}
|
||||
},
|
||||
RefreshResult::Refused(_) => {
|
||||
self.delegate.denied();
|
||||
return None
|
||||
},
|
||||
RefreshResult::Success(ref new_t) => {
|
||||
t = new_t.clone();
|
||||
self.storage.set(scope_key, Some(t.clone()));
|
||||
}
|
||||
}// RefreshResult handling
|
||||
}// refresh loop
|
||||
}// handle expiration
|
||||
Some(t)
|
||||
}
|
||||
None => {
|
||||
// get new token. The respective sub-routine will do all the logic.
|
||||
let ot = match self.flow_type {
|
||||
FlowType::Device => self.retrieve_device_token(&scopes),
|
||||
};
|
||||
// store it, no matter what. If tokens have become invalid, it's ok
|
||||
// to indicate that to the storage.
|
||||
self.storage.set(scope_key, ot.clone());
|
||||
ot
|
||||
},
|
||||
}
|
||||
}// RefreshResult handling
|
||||
}// refresh loop
|
||||
}// handle expiration
|
||||
Ok(t)
|
||||
}
|
||||
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),
|
||||
}
|
||||
{
|
||||
Ok(token) => {
|
||||
loop {
|
||||
if let Some(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)),
|
||||
Retry::After(d) => {
|
||||
sleep(d);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
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::After(d) => {
|
||||
sleep(d);
|
||||
continue
|
||||
}
|
||||
}
|
||||
},
|
||||
}// end match
|
||||
}// end loop
|
||||
}
|
||||
|
||||
fn api_key(&mut self) -> Option<String> {
|
||||
@@ -267,17 +384,36 @@ pub trait AuthenticatorDelegate {
|
||||
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, RequestResult) {}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
@@ -312,7 +448,10 @@ pub enum Retry {
|
||||
/// Signal you don't want to retry
|
||||
Abort,
|
||||
/// Signals you want to retry after the given duration
|
||||
After(Duration)
|
||||
After(Duration),
|
||||
/// Instruct the caller to attempt to keep going, or choose an alternate path.
|
||||
/// If this is not supported, it will have the same effect as `Abort`
|
||||
Skip,
|
||||
}
|
||||
|
||||
|
||||
@@ -336,7 +475,7 @@ mod tests {
|
||||
.token(&["https://www.googleapis.com/auth/youtube.upload"]);
|
||||
|
||||
match res {
|
||||
Some(t) => assert_eq!(t.access_token, "1/fFAGRNJru1FTz70BzhT3Zg"),
|
||||
Ok(t) => assert_eq!(t.access_token, "1/fFAGRNJru1FTz70BzhT3Zg"),
|
||||
_ => panic!("Expected to retrieve token in one go"),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user