Skip to content

Commit

Permalink
feat(helper): full implementation of Authenticator
Browse files Browse the repository at this point in the history
It's a generalized DeviceFlowHelper, able to operate on all flows.
It's also more flexible, as it will automatically refresh token as
required. That way, it lends itself to use in libraries which
want minimal hassle.
  • Loading branch information
Byron committed Feb 28, 2015
1 parent 091f1c0 commit c227c16
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 93 deletions.
11 changes: 6 additions & 5 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::marker::MarkerTrait;

/// A marker trait for all Flows
pub trait Flow : MarkerTrait {
fn type_id() -> AuthenticationType;
fn type_id() -> FlowType;
}

/// Represents a token as returned by OAuth2 servers.
Expand Down Expand Up @@ -66,23 +66,24 @@ impl Token {
}

/// All known authentication types, for suitable constants
pub enum AuthenticationType {
#[derive(Copy)]
pub enum FlowType {
/// [device authentication](https://developers.google.com/youtube/v3/guides/authentication#devices)
Device,
}

impl Str for AuthenticationType {
impl Str for FlowType {
/// Converts itself into a URL string
fn as_slice(&self) -> &'static str {
match *self {
AuthenticationType::Device => "https://accounts.google.com/o/oauth2/device/code",
FlowType::Device => "https://accounts.google.com/o/oauth2/device/code",
}
}
}

/// Represents either 'installed' or 'web' applications in a json secrets file.
/// See `ConsoleApplicationSecret` for more information
#[derive(RustcDecodable, RustcEncodable)]
#[derive(RustcDecodable, RustcEncodable, Clone)]
pub struct ApplicationSecret {
/// The client ID.
pub client_id: String,
Expand Down
8 changes: 4 additions & 4 deletions src/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use chrono::{DateTime,UTC};
use std::borrow::BorrowMut;
use std::marker::PhantomData;

use common::{Token, AuthenticationType, Flow};
use common::{Token, FlowType, Flow};

pub const GOOGLE_TOKEN_URL: &'static str = "https://accounts.google.com/o/oauth2/token";

Expand All @@ -32,8 +32,8 @@ pub struct DeviceFlow<C, NC> {
}

impl<C, NC> Flow for DeviceFlow<C, NC> {
fn type_id() -> AuthenticationType {
AuthenticationType::Device
fn type_id() -> FlowType {
FlowType::Device
}
}

Expand Down Expand Up @@ -162,7 +162,7 @@ impl<C, NC> DeviceFlow<C, NC>
.collect::<String>()
.as_slice())].iter().cloned());

match self.client.borrow_mut().post(AuthenticationType::Device.as_slice())
match self.client.borrow_mut().post(FlowType::Device.as_slice())
.header(ContentType("application/x-www-form-urlencoded".parse().unwrap()))
.body(req.as_slice())
.send() {
Expand Down
273 changes: 273 additions & 0 deletions src/helper.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
use std::iter::IntoIterator;
use std::borrow::{Borrow, BorrowMut};
use std::marker::PhantomData;
use std::collections::HashMap;
use std::hash::{SipHasher, Hash, Hasher};
use std::old_io::timer::sleep;
use std::cmp::min;

use common::{Token, FlowType, ApplicationSecret};
use device::{PollInformation, RequestResult, DeviceFlow, PollResult};
use refresh::{RefreshResult, RefreshFlow};
use chrono::{DateTime, UTC, Duration};
use hyper;


/// Implements a specialised 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.
pub trait TokenStorage {
/// 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>);
/// A `None` result indicates that there is no token for the given scope_hash.
fn get(&self, scope_hash: u64) -> Option<Token>;
}

/// A storage that remembers nothing.
pub struct NullStorage;

impl TokenStorage for NullStorage {
fn set(&mut self, _: u64, _: Option<Token>) {}
fn get(&self, _: u64) -> Option<Token> { None }
}

/// A storage that remembers values for one session only.
pub struct MemoryStorage {
pub tokens: HashMap<u64, Token>
}

impl TokenStorage for MemoryStorage {
fn set(&mut self, scope_hash: u64, token: Option<Token>) {
match token {
Some(t) => self.tokens.insert(scope_hash, t),
None => self.tokens.remove(&scope_hash),
};
}

fn get(&self, scope_hash: u64) -> Option<Token> {
match self.tokens.get(&scope_hash) {
Some(t) => Some(t.clone()),
None => None,
}
}
}

/// A generalized authenticator which will keep tokens valid and store them.
///
/// It is the go-to helper to deal with any kind of supported authentication flow,
/// which will be kept valid and usable.
pub struct Authenticator<D, S, C, NC> {
flow_type: FlowType,
delegate: D,
storage: S,
client: C,
secret: ApplicationSecret,

_m: PhantomData<NC>
}

impl<D, S, C, NC> Authenticator<D, S, C, NC>
where D: AuthenticatorDelegate,
S: BorrowMut<TokenStorage>,
NC: hyper::net::NetworkConnector,
C: BorrowMut<hyper::Client<NC>> {


/// Returns a new `Authenticator` instance
///
/// # Arguments
/// * `secret` - usually obtained from a client secret file produced by the
/// [developer console][dev-con]
/// * `delegate` - Used to further refine the flow of the authentication.
/// * `client` - used for all authentication https requests
/// * `storage` - used to cache authorization tokens tokens permanently. However,
/// the implementation doesn't have any particular semantic requirement, which
/// is why `NullStorage` and `MemoryStorage` can be used as well.
/// * `flow_type` - the kind of authentication to use to obtain a token for the
/// required scopes. If unset, it will be derived from the secret.
/// [dev-con]: https://console.developers.google.com
fn new(secret: &ApplicationSecret,
delegate: D, client: C, storage: S, flow_type: Option<FlowType>)
-> Authenticator<D, S, C, NC> {
Authenticator {
flow_type: flow_type.unwrap_or(FlowType::Device),
delegate: delegate,
storage: storage,
client: client,
secret: secret.clone(),
_m: PhantomData
}
}

/// 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>
where T: Str + Ord,
I: IntoIterator<Item=&'b T> {
let (scope_key, scope, scopes) = {
let mut sv: Vec<&str> = scopes.into_iter()
.map(|s|s.as_slice())
.collect::<Vec<&str>>();
sv.sort();
let s = sv.connect(" ");

let mut sh = SipHasher::new();
s.hash(&mut sh);
let sv = sv;
(sh.finish(), s, sv)
};

// Get cached token. Yes, let's do an explicit return
return match self.storage.borrow().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),
}
},
RefreshResult::Refused(_) => {
self.delegate.denied();
return None
},
RefreshResult::Success(ref new_t) => {
t = new_t.clone();
self.storage.borrow_mut().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.borrow_mut().set(scope_key, ot.clone());
ot
},
}
}

fn retrieve_device_token(&mut self, scopes: &Vec<&str>) -> Option<Token> {
let mut flow = DeviceFlow::new(self.client.borrow_mut());

// PHASE 1: REQUEST CODE
loop {
let res = flow.request_code(&self.secret.client_id,
&self.secret.client_secret, scopes.iter());
match res {
RequestResult::Error(err) => {
match self.delegate.connection_error(err) {
Retry::Abort => return None,
Retry::After(d) => sleep(d),
}
},
RequestResult::InvalidClient
|RequestResult::InvalidScope(_) => {
self.delegate.request_failure(res);
return None
}
RequestResult::ProceedWithPolling(pi) => {
self.delegate.present_user_code(pi);
break
}
}
}

// PHASE 1: POLL TOKEN
loop {
match flow.poll_token() {
PollResult::Error(err) => {
match self.delegate.connection_error(err) {
Retry::Abort => return None,
Retry::After(d) => sleep(d),
}
},
PollResult::Expired(t) => {
self.delegate.expired(t);
return None
},
PollResult::AccessDenied => {
self.delegate.denied();
return None
},
PollResult::AuthorizationPending(pi) => {
match self.delegate.pending(&pi) {
Retry::Abort => return None,
Retry::After(d) => sleep(min(d, pi.interval)),
}
},
PollResult::AccessGranted(token) => {
return Some(token)
},
}
}
}
}



/// 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::HttpError) -> Retry {
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.
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 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::seconds(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, PollInformation);
}

/// A utility type to indicate how operations DeviceFlowHelper operations should be retried
pub enum Retry {
/// Signal you don't want to retry
Abort,
/// Signals you want to retry after the given duration
After(Duration)
}
9 changes: 4 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@
//! ```test_harness,no_run
//! extern crate hyper;
//! extern crate "yup-oauth2" as oauth2;
//! use oauth2::{RefreshFlow, AuthenticationType, RefreshResult};
//! use oauth2::{RefreshFlow, FlowType, RefreshResult};
//!
//! # #[test] fn refresh() {
//! let mut f = RefreshFlow::new(hyper::Client::new());
//! let new_token = match *f.refresh_token(AuthenticationType::Device,
//! let new_token = match *f.refresh_token(FlowType::Device,
//! "my_client_id", "my_secret",
//! "my_refresh_token") {
//! RefreshResult::Success(ref t) => t,
Expand All @@ -78,10 +78,9 @@ extern crate "rustc-serialize" as rustc_serialize;
mod device;
mod refresh;
mod common;
mod util;
mod helper;

pub use device::{DeviceFlow, PollInformation, PollResult, DeviceFlowHelper,
DeviceFlowHelperDelegate, Retry};
pub use refresh::{RefreshFlow, RefreshResult};
pub use common::{Token, AuthenticationType, ApplicationSecret, ConsoleApplicationSecret};
pub use util::TokenStorage;
pub use common::{Token, FlowType, ApplicationSecret, ConsoleApplicationSecret};
Loading

0 comments on commit c227c16

Please sign in to comment.