mirror of
https://github.com/OMGeeky/yup-oauth2.git
synced 2026-02-23 15:50:00 +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:
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
|
|
||||||
name = "yup-oauth2"
|
name = "yup-oauth2"
|
||||||
version = "0.3.4"
|
version = "0.3.5"
|
||||||
authors = ["Sebastian Thiel <byronimo@gmail.com>"]
|
authors = ["Sebastian Thiel <byronimo@gmail.com>"]
|
||||||
repository = "https://github.com/Byron/yup-oauth2"
|
repository = "https://github.com/Byron/yup-oauth2"
|
||||||
description = "A partial oauth2 implementation, providing the 'device' authorization flow"
|
description = "A partial oauth2 implementation, providing the 'device' authorization flow"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#![feature(collections, old_io, std_misc, exit_status)]
|
#![feature(collections, thread_sleep, std_misc, exit_status)]
|
||||||
#![allow(deprecated)]
|
#![allow(deprecated)]
|
||||||
extern crate yup_oauth2 as oauth2;
|
extern crate yup_oauth2 as oauth2;
|
||||||
extern crate yup_hyper_mock as mock;
|
extern crate yup_hyper_mock as mock;
|
||||||
@@ -13,7 +13,7 @@ use getopts::{HasArg,Options,Occur,Fail};
|
|||||||
use std::env;
|
use std::env;
|
||||||
use std::default::Default;
|
use std::default::Default;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::old_io::timer::sleep;
|
use std::thread::sleep;
|
||||||
|
|
||||||
|
|
||||||
fn usage(program: &str, opts: &Options, err: Option<Fail>) {
|
fn usage(program: &str, opts: &Options, err: Option<Fail>) {
|
||||||
@@ -82,15 +82,17 @@ fn main() {
|
|||||||
connector: hyper::net::HttpConnector(None)
|
connector: hyper::net::HttpConnector(None)
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(t) = oauth2::Authenticator::new(&secret, StdoutHandler, client,
|
match oauth2::Authenticator::new(&secret, StdoutHandler, client,
|
||||||
oauth2::NullStorage, None)
|
oauth2::NullStorage, None).token(&m.free) {
|
||||||
.token(&m.free) {
|
Ok(t) => {
|
||||||
println!("Authentication granted !");
|
println!("Authentication granted !");
|
||||||
println!("You should store the following information for use, or revoke it.");
|
println!("You should store the following information for use, or revoke it.");
|
||||||
println!("All dates are given in UTC.");
|
println!("All dates are given in UTC.");
|
||||||
println!("{:?}", t);
|
println!("{:?}", t);
|
||||||
} else {
|
},
|
||||||
println!("Invalid client id, invalid scope, user denied access or request expired");
|
Err(err) => {
|
||||||
|
println!("Access token wasn't obtained: {}", err);
|
||||||
env::set_exit_status(10);
|
env::set_exit_status(10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,12 @@ pub trait Flow : MarkerTrait {
|
|||||||
fn type_id() -> FlowType;
|
fn type_id() -> FlowType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(RustcDecodable)]
|
||||||
|
pub struct JsonError {
|
||||||
|
pub error: String,
|
||||||
|
pub error_description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Represents all implemented token types
|
/// Represents all implemented token types
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
pub enum TokenType {
|
pub enum TokenType {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use std::iter::IntoIterator;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::default::Default;
|
use std::default::Default;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
use hyper;
|
use hyper;
|
||||||
use hyper::header::ContentType;
|
use hyper::header::ContentType;
|
||||||
@@ -12,7 +13,7 @@ use chrono::{DateTime,UTC};
|
|||||||
use std::borrow::BorrowMut;
|
use std::borrow::BorrowMut;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
||||||
use common::{Token, FlowType, Flow};
|
use common::{Token, FlowType, Flow, JsonError};
|
||||||
|
|
||||||
pub const GOOGLE_TOKEN_URL: &'static str = "https://accounts.google.com/o/oauth2/token";
|
pub const GOOGLE_TOKEN_URL: &'static str = "https://accounts.google.com/o/oauth2/token";
|
||||||
|
|
||||||
@@ -55,6 +56,12 @@ pub struct PollInformation {
|
|||||||
pub server_message: String,
|
pub server_message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for PollInformation {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
|
writeln!(f, "Poll result was: '{}'", self.server_message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Encapsulates all possible results of the `request_token(...)` operation
|
/// Encapsulates all possible results of the `request_token(...)` operation
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum RequestResult {
|
pub enum RequestResult {
|
||||||
@@ -65,22 +72,47 @@ pub enum RequestResult {
|
|||||||
/// Some requested scopes were invalid. String contains the scopes as part of
|
/// Some requested scopes were invalid. String contains the scopes as part of
|
||||||
/// the server error message
|
/// the server error message
|
||||||
InvalidScope(String),
|
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>),
|
||||||
/// Indicates we may enter the next phase
|
/// Indicates we may enter the next phase
|
||||||
ProceedWithPolling(PollInformation),
|
ProceedWithPolling(PollInformation),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RequestResult {
|
impl From<JsonError> for RequestResult {
|
||||||
fn from_server_message(msg: &str, desc: &str) -> RequestResult {
|
fn from(value: JsonError) -> RequestResult {
|
||||||
match msg {
|
match &*value.error {
|
||||||
"invalid_client" => RequestResult::InvalidClient,
|
"invalid_client" => RequestResult::InvalidClient,
|
||||||
"invalid_scope" => RequestResult::InvalidScope(desc.to_string()),
|
"invalid_scope" => RequestResult::InvalidScope(
|
||||||
_ => panic!("'{}' not understood", msg)
|
value.error_description.unwrap_or("no description provided".to_string())
|
||||||
|
),
|
||||||
|
_ => RequestResult::NegativeServerResponse(value.error, value.error_description),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for RequestResult {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
|
match *self {
|
||||||
|
RequestResult::Error(ref err) => err.fmt(f),
|
||||||
|
RequestResult::InvalidClient => "Invalid Client".fmt(f),
|
||||||
|
RequestResult::InvalidScope(ref scope)
|
||||||
|
=> writeln!(f, "Invalid Scope: '{}'", scope),
|
||||||
|
RequestResult::NegativeServerResponse(ref error, ref desc) => {
|
||||||
|
try!(error.fmt(f));
|
||||||
|
if let &Some(ref desc) = desc {
|
||||||
|
try!(write!(f, ": {}", desc));
|
||||||
|
}
|
||||||
|
"\n".fmt(f)
|
||||||
|
},
|
||||||
|
RequestResult::ProceedWithPolling(ref pi)
|
||||||
|
=> write!(f, "Proceed with polling: {}", pi),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encapsulates all possible results of a `poll_token(...)` operation
|
/// Encapsulates all possible results of a `poll_token(...)` operation
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum PollResult {
|
pub enum PollResult {
|
||||||
/// Connection failure - retry if you think it's worth it
|
/// Connection failure - retry if you think it's worth it
|
||||||
Error(Rc<hyper::HttpError>),
|
Error(Rc<hyper::HttpError>),
|
||||||
@@ -94,12 +126,27 @@ pub enum PollResult {
|
|||||||
AccessGranted(Token),
|
AccessGranted(Token),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for PollResult {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
|
match *self {
|
||||||
|
PollResult::Error(ref err) => err.fmt(f),
|
||||||
|
PollResult::AuthorizationPending(ref pi) => pi.fmt(f),
|
||||||
|
PollResult::Expired(ref date)
|
||||||
|
=> writeln!(f, "Authentication expired at {}", date),
|
||||||
|
PollResult::AccessDenied => "Access denied by user".fmt(f),
|
||||||
|
PollResult::AccessGranted(ref token)
|
||||||
|
=> writeln!(f, "Access granted by user, expires at {}", token.expiry_date()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for PollResult {
|
impl Default for PollResult {
|
||||||
fn default() -> PollResult {
|
fn default() -> PollResult {
|
||||||
PollResult::Error(Rc::new(hyper::HttpError::HttpStatusError))
|
PollResult::Error(Rc::new(hyper::HttpError::HttpStatusError))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl<C> DeviceFlow<C>
|
impl<C> DeviceFlow<C>
|
||||||
where C: BorrowMut<hyper::Client> {
|
where C: BorrowMut<hyper::Client> {
|
||||||
|
|
||||||
@@ -176,13 +223,6 @@ impl<C> DeviceFlow<C>
|
|||||||
interval: i64,
|
interval: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(RustcDecodable)]
|
|
||||||
struct JsonError {
|
|
||||||
error: String,
|
|
||||||
error_description: String
|
|
||||||
}
|
|
||||||
|
|
||||||
// This will work once hyper uses std::io::Reader
|
// This will work once hyper uses std::io::Reader
|
||||||
// let decoded: JsonData = rustc_serialize::Decodable::decode(
|
// let decoded: JsonData = rustc_serialize::Decodable::decode(
|
||||||
// &mut json::Decoder::new(
|
// &mut json::Decoder::new(
|
||||||
@@ -196,8 +236,7 @@ impl<C> DeviceFlow<C>
|
|||||||
match json::decode::<JsonError>(&json_str) {
|
match json::decode::<JsonError>(&json_str) {
|
||||||
Err(_) => {}, // ignore, move on
|
Err(_) => {}, // ignore, move on
|
||||||
Ok(res) => {
|
Ok(res) => {
|
||||||
return RequestResult::from_server_message(&res.error,
|
return RequestResult::from(res)
|
||||||
&res.error_description)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
229
src/helper.rs
229
src/helper.rs
@@ -4,6 +4,9 @@ use std::collections::HashMap;
|
|||||||
use std::hash::{SipHasher, Hash, Hasher};
|
use std::hash::{SipHasher, Hash, Hasher};
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
use std::cmp::min;
|
use std::cmp::min;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fmt;
|
||||||
|
use std::convert::From;
|
||||||
|
|
||||||
use common::{Token, FlowType, ApplicationSecret};
|
use common::{Token, FlowType, ApplicationSecret};
|
||||||
use device::{PollInformation, RequestResult, DeviceFlow, PollResult};
|
use device::{PollInformation, RequestResult, DeviceFlow, PollResult};
|
||||||
@@ -12,23 +15,42 @@ use chrono::{DateTime, UTC, Duration, Local};
|
|||||||
use hyper;
|
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
|
/// The `scope_hash` represents the signature of the scopes for which the given token
|
||||||
/// should be stored or retrieved.
|
/// 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 {
|
pub trait TokenStorage {
|
||||||
|
type Error: 'static + Error;
|
||||||
|
|
||||||
/// If `token` is None, it is invalid or revoked and should be removed from storage.
|
/// 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.
|
/// 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.
|
/// A storage that remembers nothing.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct NullStorage;
|
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 {
|
impl TokenStorage for NullStorage {
|
||||||
fn set(&mut self, _: u64, _: Option<Token>) {}
|
type Error = NullError;
|
||||||
fn get(&self, _: u64) -> Option<Token> { None }
|
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.
|
/// A storage that remembers values for one session only.
|
||||||
@@ -38,17 +60,20 @@ pub struct MemoryStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TokenStorage for 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 {
|
match token {
|
||||||
Some(t) => self.tokens.insert(scope_hash, t),
|
Some(t) => self.tokens.insert(scope_hash, t),
|
||||||
None => self.tokens.remove(&scope_hash),
|
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) {
|
match self.tokens.get(&scope_hash) {
|
||||||
Some(t) => Some(t.clone()),
|
Some(t) => Ok(Some(t.clone())),
|
||||||
None => None,
|
None => Ok(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,11 +102,54 @@ pub struct Authenticator<D, S, C> {
|
|||||||
secret: ApplicationSecret,
|
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.
|
/// 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
|
/// The `api_key()` method is an alternative in case there are no scopes or
|
||||||
/// if no user is involved.
|
/// if no user is involved.
|
||||||
pub trait GetToken {
|
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,
|
where T: AsRef<str> + Ord,
|
||||||
I: IntoIterator<Item=&'b T>;
|
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());
|
let mut flow = DeviceFlow::new(self.client.borrow_mut());
|
||||||
|
|
||||||
// PHASE 1: REQUEST CODE
|
// PHASE 1: REQUEST CODE
|
||||||
@@ -129,14 +197,16 @@ impl<D, S, C> Authenticator<D, S, C>
|
|||||||
match res {
|
match res {
|
||||||
RequestResult::Error(err) => {
|
RequestResult::Error(err) => {
|
||||||
match self.delegate.connection_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),
|
Retry::After(d) => sleep(d),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
RequestResult::InvalidClient
|
RequestResult::InvalidClient
|
||||||
|
|RequestResult::NegativeServerResponse(_, _)
|
||||||
|RequestResult::InvalidScope(_) => {
|
|RequestResult::InvalidScope(_) => {
|
||||||
|
let serr = StringError::from(res.to_string());
|
||||||
self.delegate.request_failure(res);
|
self.delegate.request_failure(res);
|
||||||
return None
|
return Err(Box::new(serr))
|
||||||
}
|
}
|
||||||
RequestResult::ProceedWithPolling(pi) => {
|
RequestResult::ProceedWithPolling(pi) => {
|
||||||
self.delegate.present_user_code(pi);
|
self.delegate.present_user_code(pi);
|
||||||
@@ -147,29 +217,31 @@ impl<D, S, C> Authenticator<D, S, C>
|
|||||||
|
|
||||||
// PHASE 1: POLL TOKEN
|
// PHASE 1: POLL TOKEN
|
||||||
loop {
|
loop {
|
||||||
match flow.poll_token() {
|
let pt = flow.poll_token();
|
||||||
|
let pts = pt.to_string();
|
||||||
|
match pt {
|
||||||
PollResult::Error(err) => {
|
PollResult::Error(err) => {
|
||||||
match self.delegate.connection_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),
|
Retry::After(d) => sleep(d),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
PollResult::Expired(t) => {
|
PollResult::Expired(t) => {
|
||||||
self.delegate.expired(t);
|
self.delegate.expired(t);
|
||||||
return None
|
return Err(Box::new(StringError::from(pts)))
|
||||||
},
|
},
|
||||||
PollResult::AccessDenied => {
|
PollResult::AccessDenied => {
|
||||||
self.delegate.denied();
|
self.delegate.denied();
|
||||||
return None
|
return Err(Box::new(StringError::from(pts)))
|
||||||
},
|
},
|
||||||
PollResult::AuthorizationPending(pi) => {
|
PollResult::AuthorizationPending(pi) => {
|
||||||
match self.delegate.pending(&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)),
|
Retry::After(d) => sleep(min(d, pi.interval)),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
PollResult::AccessGranted(token) => {
|
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
|
/// 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.
|
/// 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
|
/// In any failure case, the delegate will be provided with additional information, and
|
||||||
/// valid for the given scopes.
|
/// the caller will be informed about storage related errors.
|
||||||
fn token<'b, I, T>(&mut self, scopes: I) -> Option<Token>
|
/// 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,
|
where T: AsRef<str> + Ord,
|
||||||
I: IntoIterator<Item=&'b T> {
|
I: IntoIterator<Item=&'b T> {
|
||||||
let (scope_key, scopes) = {
|
let (scope_key, scopes) = {
|
||||||
@@ -193,17 +266,16 @@ impl<D, S, C> GetToken for Authenticator<D, S, C>
|
|||||||
.map(|s|s.as_ref())
|
.map(|s|s.as_ref())
|
||||||
.collect::<Vec<&str>>();
|
.collect::<Vec<&str>>();
|
||||||
sv.sort();
|
sv.sort();
|
||||||
let s = sv.connect(" ");
|
|
||||||
|
|
||||||
let mut sh = SipHasher::new();
|
let mut sh = SipHasher::new();
|
||||||
s.hash(&mut sh);
|
&sv.hash(&mut sh);
|
||||||
let sv = sv;
|
let sv = sv;
|
||||||
(sh.finish(), sv)
|
(sh.finish(), sv)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get cached token. Yes, let's do an explicit return
|
// Get cached token. Yes, let's do an explicit return
|
||||||
return match self.storage.get(scope_key) {
|
loop {
|
||||||
Some(mut t) => {
|
return match self.storage.get(scope_key, &scopes) {
|
||||||
|
Ok(Some(mut t)) => {
|
||||||
// t needs refresh ?
|
// t needs refresh ?
|
||||||
if t.expired() {
|
if t.expired() {
|
||||||
let mut rf = RefreshFlow::new(self.client.borrow_mut());
|
let mut rf = RefreshFlow::new(self.client.borrow_mut());
|
||||||
@@ -211,37 +283,82 @@ impl<D, S, C> GetToken for Authenticator<D, S, C>
|
|||||||
match *rf.refresh_token(self.flow_type,
|
match *rf.refresh_token(self.flow_type,
|
||||||
&self.secret.client_id,
|
&self.secret.client_id,
|
||||||
&self.secret.client_secret,
|
&self.secret.client_secret,
|
||||||
&t.refresh_token) {
|
&t.refresh_token,
|
||||||
|
scopes.iter()) {
|
||||||
RefreshResult::Error(ref err) => {
|
RefreshResult::Error(ref err) => {
|
||||||
match self.delegate.connection_error(err.clone()) {
|
match self.delegate.connection_error(err) {
|
||||||
Retry::Abort => return None,
|
Retry::Abort|Retry::Skip =>
|
||||||
|
return Err(Box::new(StringError::new(
|
||||||
|
err.description().to_string(),
|
||||||
|
None))),
|
||||||
Retry::After(d) => sleep(d),
|
Retry::After(d) => sleep(d),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
RefreshResult::Refused(_) => {
|
RefreshResult::RefreshError(ref err_str, ref err_description) => {
|
||||||
self.delegate.denied();
|
self.delegate.token_refresh_failed(&err_str, &err_description);
|
||||||
return None
|
return Err(Box::new(
|
||||||
|
StringError::new(err_str.clone(), err_description.as_ref())))
|
||||||
},
|
},
|
||||||
RefreshResult::Success(ref new_t) => {
|
RefreshResult::Success(ref new_t) => {
|
||||||
t = new_t.clone();
|
t = new_t.clone();
|
||||||
self.storage.set(scope_key, Some(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 handling
|
}// RefreshResult handling
|
||||||
}// refresh loop
|
}// refresh loop
|
||||||
}// handle expiration
|
}// handle expiration
|
||||||
Some(t)
|
Ok(t)
|
||||||
}
|
}
|
||||||
None => {
|
Ok(None) => {
|
||||||
|
// Nothing was in storage - get a new token
|
||||||
// get new token. The respective sub-routine will do all the logic.
|
// get new token. The respective sub-routine will do all the logic.
|
||||||
let ot = match self.flow_type {
|
match
|
||||||
|
match self.flow_type {
|
||||||
FlowType::Device => self.retrieve_device_token(&scopes),
|
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
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
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> {
|
fn api_key(&mut self) -> Option<String> {
|
||||||
@@ -267,17 +384,36 @@ pub trait AuthenticatorDelegate {
|
|||||||
Retry::Abort
|
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
|
/// The server denied the attempt to obtain a request code
|
||||||
fn request_failure(&mut self, RequestResult) {}
|
fn request_failure(&mut self, RequestResult) {}
|
||||||
|
|
||||||
/// Called if the request code is expired. You will have to start over in this case.
|
/// 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.
|
/// This will be the last call the delegate receives.
|
||||||
|
/// Given `DateTime` is the expiration date
|
||||||
fn expired(&mut self, DateTime<UTC>) {}
|
fn expired(&mut self, DateTime<UTC>) {}
|
||||||
|
|
||||||
/// Called if the user denied access. You would have to start over.
|
/// Called if the user denied access. You would have to start over.
|
||||||
/// This will be the last call the delegate receives.
|
/// This will be the last call the delegate receives.
|
||||||
fn denied(&mut self) {}
|
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.
|
/// 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.
|
/// 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
|
/// Signal you don't want to retry
|
||||||
Abort,
|
Abort,
|
||||||
/// Signals you want to retry after the given duration
|
/// 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"]);
|
.token(&["https://www.googleapis.com/auth/youtube.upload"]);
|
||||||
|
|
||||||
match res {
|
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"),
|
_ => panic!("Expected to retrieve token in one go"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,12 +31,12 @@
|
|||||||
//! <MemoryStorage as Default>::default(), None)
|
//! <MemoryStorage as Default>::default(), None)
|
||||||
//! .token(&["https://www.googleapis.com/auth/youtube.upload"]);
|
//! .token(&["https://www.googleapis.com/auth/youtube.upload"]);
|
||||||
//! match res {
|
//! match res {
|
||||||
//! Some(t) => {
|
//! Ok(t) => {
|
||||||
//! // now you can use t.access_token to authenticate API calls within your
|
//! // now you can use t.access_token to authenticate API calls within your
|
||||||
//! // given scopes. It will not be valid forever, which is when you have to
|
//! // given scopes. It will not be valid forever, which is when you have to
|
||||||
//! // refresh it using the `RefreshFlow`
|
//! // refresh it using the `RefreshFlow`
|
||||||
//! },
|
//! },
|
||||||
//! None => println!("user declined"),
|
//! Err(err) => println!("Failed to acquire token: {}", err),
|
||||||
//! }
|
//! }
|
||||||
//! # }
|
//! # }
|
||||||
//! ```
|
//! ```
|
||||||
@@ -54,7 +54,8 @@
|
|||||||
//! let mut f = RefreshFlow::new(hyper::Client::new());
|
//! let mut f = RefreshFlow::new(hyper::Client::new());
|
||||||
//! let new_token = match *f.refresh_token(FlowType::Device,
|
//! let new_token = match *f.refresh_token(FlowType::Device,
|
||||||
//! "my_client_id", "my_secret",
|
//! "my_client_id", "my_secret",
|
||||||
//! "my_refresh_token") {
|
//! "my_refresh_token",
|
||||||
|
//! &["https://scope.url"]) {
|
||||||
//! RefreshResult::Success(ref t) => t,
|
//! RefreshResult::Success(ref t) => t,
|
||||||
//! _ => panic!("bad luck ;)")
|
//! _ => panic!("bad luck ;)")
|
||||||
//! };
|
//! };
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use common::FlowType;
|
use common::{FlowType, JsonError};
|
||||||
|
|
||||||
use chrono::UTC;
|
use chrono::UTC;
|
||||||
use hyper;
|
use hyper;
|
||||||
@@ -6,8 +6,10 @@ use hyper::header::ContentType;
|
|||||||
use rustc_serialize::json;
|
use rustc_serialize::json;
|
||||||
use url::form_urlencoded;
|
use url::form_urlencoded;
|
||||||
use super::Token;
|
use super::Token;
|
||||||
|
use itertools::Itertools;
|
||||||
use std::borrow::BorrowMut;
|
use std::borrow::BorrowMut;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
use std::iter::IntoIterator;
|
||||||
|
|
||||||
/// Implements the [Outh2 Refresh Token Flow](https://developers.google.com/youtube/v3/guides/authentication#devices).
|
/// Implements the [Outh2 Refresh Token Flow](https://developers.google.com/youtube/v3/guides/authentication#devices).
|
||||||
///
|
///
|
||||||
@@ -25,7 +27,7 @@ pub enum RefreshResult {
|
|||||||
/// Indicates connection failure
|
/// Indicates connection failure
|
||||||
Error(hyper::HttpError),
|
Error(hyper::HttpError),
|
||||||
/// The server did not answer with a new token, providing the server message
|
/// The server did not answer with a new token, providing the server message
|
||||||
Refused(String),
|
RefreshError(String, Option<String>),
|
||||||
/// The refresh operation finished successfully, providing a new `Token`
|
/// The refresh operation finished successfully, providing a new `Token`
|
||||||
Success(Token),
|
Success(Token),
|
||||||
}
|
}
|
||||||
@@ -42,7 +44,7 @@ impl<C> RefreshFlow<C>
|
|||||||
|
|
||||||
/// Attempt to refresh the given token, and obtain a new, valid one.
|
/// Attempt to refresh the given token, and obtain a new, valid one.
|
||||||
/// If the `RefreshResult` is `RefreshResult::Error`, you may retry within an interval
|
/// If the `RefreshResult` is `RefreshResult::Error`, you may retry within an interval
|
||||||
/// of your choice. If it is `RefreshResult::Refused`, your refresh token is invalid
|
/// 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`
|
/// and you will have to re-authorize using the `DeviceFlow`
|
||||||
///
|
///
|
||||||
@@ -54,9 +56,15 @@ impl<C> RefreshFlow<C>
|
|||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// Please see the crate landing page for an example.
|
/// Please see the crate landing page for an example.
|
||||||
pub fn refresh_token(&mut self, flow_type: FlowType,
|
pub fn refresh_token<'b, I, T>( &mut self,
|
||||||
client_id: &str, client_secret: &str,
|
flow_type: FlowType,
|
||||||
refresh_token: &str) -> &RefreshResult {
|
client_id: &str,
|
||||||
|
client_secret: &str,
|
||||||
|
refresh_token: &str,
|
||||||
|
scopes: I)
|
||||||
|
-> &RefreshResult
|
||||||
|
where T: AsRef<str> + Ord,
|
||||||
|
I: IntoIterator<Item=&'b T> {
|
||||||
if let RefreshResult::Success(_) = self.result {
|
if let RefreshResult::Success(_) = self.result {
|
||||||
return &self.result;
|
return &self.result;
|
||||||
}
|
}
|
||||||
@@ -65,7 +73,12 @@ impl<C> RefreshFlow<C>
|
|||||||
[("client_id", client_id),
|
[("client_id", client_id),
|
||||||
("client_secret", client_secret),
|
("client_secret", client_secret),
|
||||||
("refresh_token", refresh_token),
|
("refresh_token", refresh_token),
|
||||||
("grant_type", "refresh_token")]
|
("grant_type", "refresh_token"),
|
||||||
|
("scope", scopes.into_iter()
|
||||||
|
.map(|s| s.as_ref())
|
||||||
|
.intersperse(" ")
|
||||||
|
.collect::<String>()
|
||||||
|
.as_ref())]
|
||||||
.iter().cloned());
|
.iter().cloned());
|
||||||
|
|
||||||
let json_str =
|
let json_str =
|
||||||
@@ -84,11 +97,6 @@ impl<C> RefreshFlow<C>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(RustcDecodable)]
|
|
||||||
struct JsonError {
|
|
||||||
error: String
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(RustcDecodable)]
|
#[derive(RustcDecodable)]
|
||||||
struct JsonToken {
|
struct JsonToken {
|
||||||
access_token: String,
|
access_token: String,
|
||||||
@@ -99,7 +107,7 @@ impl<C> RefreshFlow<C>
|
|||||||
match json::decode::<JsonError>(&json_str) {
|
match json::decode::<JsonError>(&json_str) {
|
||||||
Err(_) => {},
|
Err(_) => {},
|
||||||
Ok(res) => {
|
Ok(res) => {
|
||||||
self.result = RefreshResult::Refused(res.error);
|
self.result = RefreshResult::RefreshError(res.error, res.error_description);
|
||||||
return &self.result;
|
return &self.result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,7 +153,7 @@ mod tests {
|
|||||||
|
|
||||||
|
|
||||||
match *flow.refresh_token(FlowType::Device,
|
match *flow.refresh_token(FlowType::Device,
|
||||||
"bogus", "secret", "bogus_refresh_token") {
|
"bogus", "secret", "bogus_refresh_token", &["scope.url"]) {
|
||||||
RefreshResult::Success(ref t) => {
|
RefreshResult::Success(ref t) => {
|
||||||
assert_eq!(t.access_token, "1/fFAGRNJru1FTz70BzhT3Zg");
|
assert_eq!(t.access_token, "1/fFAGRNJru1FTz70BzhT3Zg");
|
||||||
assert!(!t.expired());
|
assert!(!t.expired());
|
||||||
|
|||||||
Reference in New Issue
Block a user