From ccc6601ff3598b65a200af713dba391852b6d8f9 Mon Sep 17 00:00:00 2001 From: Glenn Griffin Date: Mon, 12 Aug 2019 15:19:42 -0700 Subject: [PATCH 1/2] Use the builder pattern to create authenticators. Beyond simply moving to the builder pattern for intialization this has a few other effects. The DeviceFlow and InstalledFlow can no longer be used without an associated Authenticator. This is becaus they no longer have any publicly accessible constructor. All initialization goes through the Authenticator. This also means that the flows are always initialized with a clone of the hyper client used by the Authenticator. The authenticator uses the builder pattern which allows omitting optional fields. This means that if users simply want a default hyper client, they don't need to create one explicitly. One will be created automatically. If users want to specify a hyper client (maybe to allow sharing a single client between different libraries) they can still do so by using the hyper_client method on the builder. Additionally for both AuthenticatorDelegate's and FlowDelegate's if the user does not specify an override the default ones will be used. The builders are now exposed publicly with the names of Authenicator, InstalledFlow, and DeviceFlow. The structs that actually implement those behaviors are now hidden and only expose the GetToken trait. This means some methods that were previously publicly accessible are no longer available, but the methods appeared to be implementation details that probably shouldn't have been exposed anyway. --- examples/service_account/src/main.rs | 6 +- examples/test-device/src/main.rs | 30 ++--- examples/test-installed/src/main.rs | 19 ++- examples/test-svc-acct/src/main.rs | 13 +- src/authenticator.rs | 172 ++++++++++++++++++++++----- src/device.rs | 111 ++++++++++++----- src/installed.rs | 71 +++++------ src/lib.rs | 37 ++---- src/service_account.rs | 105 ++++++++++------ 9 files changed, 357 insertions(+), 207 deletions(-) diff --git a/examples/service_account/src/main.rs b/examples/service_account/src/main.rs index 314372e..6e4c219 100644 --- a/examples/service_account/src/main.rs +++ b/examples/service_account/src/main.rs @@ -178,15 +178,13 @@ fn publish_stuff(methods: &PubsubMethods, message: &str) { fn main() { let client_secret = oauth::service_account_key_from_file(&"pubsub-auth.json".to_string()).unwrap(); - let client = - hyper::Client::with_connector(HttpsConnector::new(NativeTlsClient::new().unwrap())); - let mut access = oauth::ServiceAccountAccess::new(client_secret, client); + let mut access = oauth::ServiceAccountAccess::new(client_secret).build(); use yup_oauth2::GetToken; println!( "{:?}", access - .token(&vec!["https://www.googleapis.com/auth/pubsub"]) + .token(vec!["https://www.googleapis.com/auth/pubsub"]) .unwrap() ); diff --git a/examples/test-device/src/main.rs b/examples/test-device/src/main.rs index 7edb91c..278fd78 100644 --- a/examples/test-device/src/main.rs +++ b/examples/test-device/src/main.rs @@ -1,36 +1,20 @@ use futures::prelude::*; -use yup_oauth2::{self, Authenticator, GetToken}; +use yup_oauth2::{self, Authenticator, DeviceFlow, GetToken}; -use hyper::client::Client; -use hyper_rustls::HttpsConnector; use std::path; -use std::time::Duration; use tokio; fn main() { let creds = yup_oauth2::read_application_secret(path::Path::new("clientsecret.json")) .expect("clientsecret"); - let https = HttpsConnector::new(1); - let client = Client::builder() - .keep_alive(false) - .build::<_, hyper::Body>(https); - let scopes = &["https://www.googleapis.com/auth/youtube.readonly".to_string()]; - - let ad = yup_oauth2::DefaultFlowDelegate; - let mut df = yup_oauth2::DeviceFlow::new::(client.clone(), creds, ad, None); - df.set_wait_duration(Duration::from_secs(120)); - let mut auth = Authenticator::new_disk( - client, - df, - yup_oauth2::DefaultAuthenticatorDelegate, - "tokenstorage.json", - ) - .expect("authenticator"); + let mut auth = Authenticator::new(DeviceFlow::new(creds)) + .persist_tokens_to_disk("tokenstorage.json") + .build() + .expect("authenticator"); + let scopes = vec!["https://www.googleapis.com/auth/youtube.readonly"]; let mut rt = tokio::runtime::Runtime::new().unwrap(); - let fut = auth - .token(scopes.iter()) - .and_then(|tok| Ok(println!("{:?}", tok))); + let fut = auth.token(scopes).and_then(|tok| Ok(println!("{:?}", tok))); println!("{:?}", rt.block_on(fut)); } diff --git a/examples/test-installed/src/main.rs b/examples/test-installed/src/main.rs index b57ec82..3aa29ca 100644 --- a/examples/test-installed/src/main.rs +++ b/examples/test-installed/src/main.rs @@ -15,23 +15,18 @@ fn main() { let ad = yup_oauth2::DefaultFlowDelegate; let secret = yup_oauth2::read_application_secret(Path::new("clientsecret.json")) .expect("clientsecret.json"); - let inf = InstalledFlow::new( - client.clone(), - ad, + + let mut auth = Authenticator::new(InstalledFlow::new( secret, - yup_oauth2::InstalledFlowReturnMethod::HTTPRedirectEphemeral, - ); - let mut auth = Authenticator::new_disk( - client, - inf, - yup_oauth2::DefaultAuthenticatorDelegate, - "tokencache.json", - ) + yup_oauth2::InstalledFlowReturnMethod::HTTPRedirect(8081), + )) + .persist_tokens_to_disk("tokencache.json") + .build() .unwrap(); let s = "https://www.googleapis.com/auth/drive.file".to_string(); let scopes = vec![s]; - let tok = auth.token(scopes.iter()); + let tok = auth.token(scopes); let fut = tok.map_err(|e| println!("error: {:?}", e)).and_then(|t| { println!("The token is {:?}", t); Ok(()) diff --git a/examples/test-svc-acct/src/main.rs b/examples/test-svc-acct/src/main.rs index cf44e3d..ebaaac1 100644 --- a/examples/test-svc-acct/src/main.rs +++ b/examples/test-svc-acct/src/main.rs @@ -3,8 +3,6 @@ use yup_oauth2; use futures::prelude::*; use yup_oauth2::GetToken; -use hyper::client::Client; -use hyper_rustls::HttpsConnector; use tokio; use std::path; @@ -12,21 +10,16 @@ use std::path; fn main() { let creds = yup_oauth2::service_account_key_from_file(path::Path::new("serviceaccount.json")).unwrap(); - let https = HttpsConnector::new(1); - let client = Client::builder() - .keep_alive(false) - .build::<_, hyper::Body>(https); - - let mut sa = yup_oauth2::ServiceAccountAccess::new(creds, client); + let mut sa = yup_oauth2::ServiceAccountAccess::new(creds).build(); let fut = sa - .token(["https://www.googleapis.com/auth/pubsub"].iter()) + .token(vec!["https://www.googleapis.com/auth/pubsub"]) .and_then(|tok| { println!("token is: {:?}", tok); Ok(()) }); let fut2 = sa - .token(["https://www.googleapis.com/auth/pubsub"].iter()) + .token(vec!["https://www.googleapis.com/auth/pubsub"]) .and_then(|tok| { println!("cached token is {:?} and should be identical", tok); Ok(()) diff --git a/src/authenticator.rs b/src/authenticator.rs index 6953a06..4600040 100644 --- a/src/authenticator.rs +++ b/src/authenticator.rs @@ -1,4 +1,4 @@ -use crate::authenticator_delegate::{AuthenticatorDelegate, Retry}; +use crate::authenticator_delegate::{AuthenticatorDelegate, DefaultAuthenticatorDelegate, Retry}; use crate::refresh::RefreshFlow; use crate::storage::{hash_scopes, DiskTokenStorage, MemoryStorage, TokenStorage}; use crate::types::{ApplicationSecret, GetToken, RefreshResult, RequestError, Token}; @@ -8,6 +8,7 @@ use tokio_timer; use std::error::Error; use std::io; +use std::path::Path; use std::sync::{Arc, Mutex}; /// Authenticator abstracts different `GetToken` implementations behind one type and handles @@ -20,7 +21,7 @@ use std::sync::{Arc, Mutex}; /// NOTE: It is recommended to use a client constructed like this in order to prevent functions /// like `hyper::run()` from hanging: `let client = hyper::Client::builder().keep_alive(false);`. /// Due to token requests being rare, this should not result in a too bad performance problem. -pub struct Authenticator< +struct AuthenticatorImpl< T: GetToken, S: TokenStorage, AD: AuthenticatorDelegate, @@ -32,39 +33,156 @@ pub struct Authenticator< delegate: AD, } -impl - Authenticator +/// A trait implemented for any hyper::Client as well as teh DefaultHyperClient. +pub trait HyperClientBuilder { + type Connector: hyper::client::connect::Connect; + + fn build_hyper_client(self) -> hyper::Client; +} + +/// The builder value used when the default hyper client should be used. +pub struct DefaultHyperClient; +impl HyperClientBuilder for DefaultHyperClient { + type Connector = hyper_rustls::HttpsConnector; + + fn build_hyper_client(self) -> hyper::Client { + hyper::Client::builder() + .keep_alive(false) + .build::<_, hyper::Body>(hyper_rustls::HttpsConnector::new(1)) + } +} + +impl HyperClientBuilder for hyper::Client +where + C: hyper::client::connect::Connect, { - /// Create an Authenticator caching tokens for the duration of this authenticator. + type Connector = C; + + fn build_hyper_client(self) -> hyper::Client { + self + } +} + +/// An internal trait implemented by flows to be used by an authenticator. +pub trait TokenGetterBuilder { + type TokenGetter: GetToken; + + fn build_token_getter(self, client: hyper::Client) -> Self::TokenGetter; +} + +/// An authenticator can be used with `InstalledFlow`'s or `DeviceFlow`'s and +/// will refresh tokens as they expire as well as optionally persist tokens to +/// disk. +pub struct Authenticator< + T: TokenGetterBuilder, + S: TokenStorage, + AD: AuthenticatorDelegate, + C: HyperClientBuilder, +> { + client: C, + token_getter: T, + store: io::Result, + delegate: AD, +} + +impl Authenticator +where + T: TokenGetterBuilder<::Connector>, +{ + /// Create a new authenticator with the provided flow. By default a new + /// hyper::Client will be created the default authenticator delegate will be + /// used, and tokens will not be persisted to disk. + /// Accepted flow types are DeviceFlow and InstalledFlow. + /// + /// Examples + /// ``` + /// use std::path::Path; + /// use yup_oauth2::{ApplicationSecret, Authenticator, DeviceFlow}; + /// let creds = ApplicationSecret::default(); + /// let auth = Authenticator::new(DeviceFlow::new(creds)).build().unwrap(); + /// ``` pub fn new( - client: hyper::Client, - inner: T, - delegate: AD, - ) -> Authenticator { + flow: T, + ) -> Authenticator { Authenticator { - client: client, - inner: Arc::new(Mutex::new(inner)), - store: Arc::new(Mutex::new(MemoryStorage::new())), - delegate: delegate, + client: DefaultHyperClient, + token_getter: flow, + store: Ok(MemoryStorage::new()), + delegate: DefaultAuthenticatorDelegate, } } } -impl - Authenticator +impl Authenticator +where + T: TokenGetterBuilder, + S: TokenStorage, + AD: AuthenticatorDelegate, + C: HyperClientBuilder, { - /// Create an Authenticator using the store at `path`. - pub fn new_disk>( - client: hyper::Client, - inner: T, - delegate: AD, - token_storage_path: P, - ) -> io::Result> { - Ok(Authenticator { - client: client, - inner: Arc::new(Mutex::new(inner)), - store: Arc::new(Mutex::new(DiskTokenStorage::new(token_storage_path)?)), + /// Use the provided hyper client. + pub fn hyper_client( + self, + hyper_client: hyper::Client, + ) -> Authenticator> + where + NewC: hyper::client::connect::Connect, + T: TokenGetterBuilder, + { + Authenticator { + client: hyper_client, + token_getter: self.token_getter, + store: self.store, + delegate: self.delegate, + } + } + + /// Persist tokens to disk in the provided filename. + pub fn persist_tokens_to_disk>( + self, + path: P, + ) -> Authenticator { + let disk_storage = DiskTokenStorage::new(path.as_ref().to_str().unwrap()); + Authenticator { + client: self.client, + token_getter: self.token_getter, + store: disk_storage, + delegate: self.delegate, + } + } + + /// Use the provided authenticator delegate. + pub fn delegate( + self, + delegate: NewAD, + ) -> Authenticator { + Authenticator { + client: self.client, + token_getter: self.token_getter, + store: self.store, delegate: delegate, + } + } + + /// Create the authenticator. + pub fn build(self) -> io::Result + where + T::TokenGetter: 'static + GetToken + Send, + S: 'static + Send, + AD: 'static + Send, + C::Connector: 'static + Clone + Send, + { + let client = self.client.build_hyper_client(); + let store = Arc::new(Mutex::new(self.store?)); + let inner = Arc::new(Mutex::new( + self.token_getter.build_token_getter(client.clone()), + )); + + Ok(AuthenticatorImpl { + client, + inner, + store, + delegate: self.delegate, }) } } @@ -74,7 +192,7 @@ impl< S: 'static + TokenStorage + Send, AD: 'static + AuthenticatorDelegate + Send, C: 'static + hyper::client::connect::Connect + Clone + Send, - > GetToken for Authenticator + > GetToken for AuthenticatorImpl { /// Returns the API Key of the inner flow. fn api_key(&mut self) -> Option { diff --git a/src/device.rs b/src/device.rs index 67d7048..684db7e 100644 --- a/src/device.rs +++ b/src/device.rs @@ -13,7 +13,7 @@ use serde_json as json; use tokio_timer; use url::form_urlencoded; -use crate::authenticator_delegate::{FlowDelegate, PollInformation, Retry}; +use crate::authenticator_delegate::{DefaultFlowDelegate, FlowDelegate, PollInformation, Retry}; use crate::types::{ ApplicationSecret, Flow, FlowType, GetToken, JsonError, PollError, RequestError, Token, }; @@ -23,8 +23,76 @@ pub const GOOGLE_DEVICE_CODE_URL: &'static str = "https://accounts.google.com/o/ /// Implements the [Oauth2 Device Flow](https://developers.google.com/youtube/v3/guides/authentication#devices) /// It operates in two steps: /// * obtain a code to show to the user -/// * (repeatedly) poll for the user to authenticate your application -pub struct DeviceFlow { +// * (repeatedly) poll for the user to authenticate your application +#[derive(Clone)] +pub struct DeviceFlow { + application_secret: ApplicationSecret, + device_code_url: String, + flow_delegate: FD, + wait: Duration, +} + +impl DeviceFlow { + /// Create a new DeviceFlow. The default FlowDelegate will be used and the + /// default wait time is 120 seconds. + pub fn new(secret: ApplicationSecret) -> DeviceFlow { + DeviceFlow { + application_secret: secret, + device_code_url: GOOGLE_DEVICE_CODE_URL.to_string(), + flow_delegate: DefaultFlowDelegate, + wait: Duration::from_secs(120), + } + } +} + +impl DeviceFlow { + /// Use the provided device code url. + pub fn device_code_url(self, url: String) -> Self { + DeviceFlow { + device_code_url: url, + ..self + } + } + + /// Use the provided FlowDelegate. + pub fn delegate(self, delegate: NewFD) -> DeviceFlow { + DeviceFlow { + application_secret: self.application_secret, + device_code_url: self.device_code_url, + flow_delegate: delegate, + wait: self.wait, + } + } + + /// Use the provided wait duration. + pub fn wait_duration(self, duration: Duration) -> Self { + DeviceFlow { + wait: duration, + ..self + } + } +} + +impl crate::authenticator::TokenGetterBuilder for DeviceFlow +where + FD: FlowDelegate + Send + 'static, + C: hyper::client::connect::Connect + 'static, +{ + type TokenGetter = DeviceFlowImpl; + + fn build_token_getter(self, client: hyper::Client) -> Self::TokenGetter { + DeviceFlowImpl { + client, + application_secret: self.application_secret, + device_code_url: self.device_code_url, + fd: self.flow_delegate, + wait: Duration::from_secs(1200), + } + } +} + +/// The DeviceFlow implementation. +pub struct DeviceFlowImpl { client: hyper::Client, application_secret: ApplicationSecret, /// Usually GOOGLE_DEVICE_CODE_URL @@ -33,7 +101,7 @@ pub struct DeviceFlow { wait: Duration, } -impl Flow for DeviceFlow { +impl Flow for DeviceFlowImpl { fn type_id() -> FlowType { FlowType::Device(String::new()) } @@ -42,7 +110,7 @@ impl Flow for DeviceFlow { impl< FD: FlowDelegate + Clone + Send + 'static, C: hyper::client::connect::Connect + Sync + 'static, - > GetToken for DeviceFlow + > GetToken for DeviceFlowImpl { fn token( &mut self, @@ -62,39 +130,16 @@ impl< } } -impl DeviceFlow +impl DeviceFlowImpl where C: hyper::client::connect::Connect + Sync + 'static, C::Transport: 'static, C::Future: 'static, FD: FlowDelegate + Clone + Send + 'static, { - pub fn new>( - client: hyper::Client, - secret: ApplicationSecret, - fd: FD, - device_code_url: Option, - ) -> DeviceFlow { - DeviceFlow { - client: client, - application_secret: secret, - device_code_url: device_code_url - .as_ref() - .map(|s| s.as_ref().to_string()) - .unwrap_or(GOOGLE_DEVICE_CODE_URL.to_string()), - fd: fd, - wait: Duration::from_secs(1200), - } - } - - /// Set the time to wait for the user to authorize us. The default is 120 seconds. - pub fn set_wait_duration(&mut self, wait: Duration) { - self.wait = wait; - } - /// Essentially what `GetToken::token` does: Retrieve a token for the given scopes without /// caching. - pub fn retrieve_device_token<'a>( + fn retrieve_device_token<'a>( &mut self, scopes: Vec, ) -> Box + Send> { @@ -362,6 +407,7 @@ mod tests { use tokio; use super::*; + use crate::authenticator::TokenGetterBuilder; use crate::helper::parse_application_secret; #[test] @@ -385,7 +431,10 @@ mod tests { .keep_alive(false) .build::<_, hyper::Body>(https); - let mut flow = DeviceFlow::new(client.clone(), app_secret, FD, Some(device_code_url)); + let mut flow = DeviceFlow::new(app_secret) + .delegate(FD) + .device_code_url(device_code_url) + .build_token_getter(client); let mut rt = tokio::runtime::Builder::new() .core_threads(1) diff --git a/src/installed.rs b/src/installed.rs index 8372247..523bfea 100644 --- a/src/installed.rs +++ b/src/installed.rs @@ -13,7 +13,7 @@ use hyper::{header, StatusCode, Uri}; use url::form_urlencoded; use url::percent_encoding::{percent_encode, QUERY_ENCODE_SET}; -use crate::authenticator_delegate::FlowDelegate; +use crate::authenticator_delegate::{DefaultFlowDelegate, FlowDelegate}; use crate::types::{ApplicationSecret, GetToken, RequestError, Token}; const OOB_REDIRECT_URI: &'static str = "urn:ietf:wg:oauth:2.0:oob"; @@ -59,7 +59,7 @@ where } impl - GetToken for InstalledFlow + GetToken for InstalledFlowImpl { fn token( &mut self, @@ -79,12 +79,8 @@ impl { +/// The InstalledFlow implementation. +pub struct InstalledFlowImpl { method: InstalledFlowReturnMethod, client: hyper::client::Client, fd: FD, @@ -101,30 +97,32 @@ pub enum InstalledFlowReturnMethod { HTTPRedirectEphemeral, /// Involves spinning up a local HTTP server and Google redirecting the browser to /// the server with a URL containing the code (preferred, but not as reliable). The - /// parameter is the port to listen on. Users should typically prefer - /// HTTPRedirectEphemeral unless they need to specify the port to listen on. + /// parameter is the port to listen on. HTTPRedirect(u16), } -impl<'c, FD: 'static + FlowDelegate + Clone + Send, C: 'c + hyper::client::connect::Connect> - InstalledFlow -{ - /// Starts a new Installed App auth flow. - /// If HTTPRedirect is chosen as method and the server can't be started, the flow falls - /// back to Interactive. +/// InstalledFlowImpl provides tokens for services that follow the "Installed" OAuth flow. (See +/// https://www.oauth.com/oauth2-servers/authorization/, +/// https://developers.google.com/identity/protocols/OAuth2InstalledApp). +pub struct InstalledFlow { + method: InstalledFlowReturnMethod, + flow_delegate: FD, + appsecret: ApplicationSecret, +} + +impl InstalledFlow { + /// Create a new InstalledFlow with the provided secret and method. pub fn new( - client: hyper::client::Client, - fd: FD, secret: ApplicationSecret, method: InstalledFlowReturnMethod, - ) -> InstalledFlow { + ) -> InstalledFlow { InstalledFlow { - method: method, - fd: fd, + method, + flow_delegate: DefaultFlowDelegate, appsecret: secret, - client: client, } } +} /// Handles the token request flow; it consists of the following steps: /// . Obtain a authorization code with user cooperation or internal redirect. @@ -132,7 +130,7 @@ impl<'c, FD: 'static + FlowDelegate + Clone + Send, C: 'c + hyper::client::conne /// . Return that token /// /// It's recommended not to use the DefaultFlowDelegate, but a specialized one. - pub fn obtain_token<'a>( + fn obtain_token<'a>( &mut self, scopes: Vec, // Note: I haven't found a better way to give a list of strings here, due to ownership issues with futures. ) -> impl 'a + Future + Send { @@ -545,6 +543,7 @@ mod tests { use tokio; use super::*; + use crate::authenticator::TokenGetterBuilder; use crate::authenticator_delegate::FlowDelegate; use crate::helper::*; use crate::types::StringError; @@ -612,12 +611,10 @@ mod tests { .build::<_, hyper::Body>(https); let fd = FD("authorizationcode".to_string(), client.clone()); - let mut inf = InstalledFlow::new( - client.clone(), - fd, - app_secret.clone(), - InstalledFlowReturnMethod::Interactive, - ); + let mut inf = + InstalledFlow::new(app_secret.clone(), InstalledFlowReturnMethod::Interactive) + .delegate(fd) + .build_token_getter(client.clone()); let mut rt = tokio::runtime::Builder::new() .core_threads(1) @@ -646,15 +643,13 @@ mod tests { } // Successful path with HTTP redirect. { - let mut inf = InstalledFlow::new( - client.clone(), - FD( - "authorizationcodefromlocalserver".to_string(), - client.clone(), - ), - app_secret, - InstalledFlowReturnMethod::HTTPRedirectEphemeral, - ); + let mut inf = + InstalledFlow::new(app_secret, InstalledFlowReturnMethod::HTTPRedirect(8081)) + .delegate(FD( + "authorizationcodefromlocalserver".to_string(), + client.clone(), + )) + .build_token_getter(client.clone()); let _m = mock("POST", "/token") .match_body(mockito::Matcher::Regex(".*code=authorizationcodefromlocalserver.*client_id=9022167.*".to_string())) .with_body(r#"{"access_token": "accesstoken", "refresh_token": "refreshtoken", "token_type": "Bearer", "expires_in": 12345678}"#) diff --git a/src/lib.rs b/src/lib.rs index 761f663..f9ad5ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,41 +48,22 @@ //! use std::path::Path; //! //! fn main() { -//! // Boilerplate: Set up hyper HTTP client and TLS. -//! let https = HttpsConnector::new(1); -//! let client = Client::builder() -//! .keep_alive(false) -//! .build::<_, hyper::Body>(https); -//! //! // Read application secret from a file. Sometimes it's easier to compile it directly into //! // the binary. The clientsecret file contains JSON like `{"installed":{"client_id": ... }}` //! let secret = yup_oauth2::read_application_secret(Path::new("clientsecret.json")) //! .expect("clientsecret.json"); //! -//! // There are two types of delegates; FlowDelegate and AuthenticatorDelegate. See the -//! // respective documentation; all you need to know here is that they determine how the user -//! // is asked to visit the OAuth flow URL or how to read back the provided code. -//! let ad = yup_oauth2::DefaultFlowDelegate; -//! -//! // InstalledFlow handles OAuth flows of that type. They are usually the ones where a user -//! // grants access to their personal account (think Google Drive, Github API, etc.). -//! let inf = InstalledFlow::new( -//! client.clone(), -//! ad, -//! secret, -//! yup_oauth2::InstalledFlowReturnMethod::HTTPRedirectEphemeral, -//! ); -//! // You could already use InstalledFlow by itself, but usually you want to cache tokens and -//! // refresh them, rather than ask the user every time to log in again. Authenticator wraps -//! // other flows and handles these. -//! // This type of authenticator caches tokens in a JSON file on disk. -//! let mut auth = Authenticator::new_disk( -//! client, -//! inf, -//! yup_oauth2::DefaultAuthenticatorDelegate, -//! "tokencache.json", +//! // Create an authenticator that uses an InstalledFlow to authenticate. The +//! // authentication tokens are persisted to a file named tokencache.json. The +//! // authenticator takes care of caching tokens to disk and refreshing tokens once +//! // they've expired. +//! let mut auth = Authenticator::new( +//! InstalledFlow::new(secret, yup_oauth2::InstalledFlowReturnMethod::HTTPRedirect(0)) //! ) +//! .persist_tokens_to_disk("tokencache.json") +//! .build() //! .unwrap(); +//! //! let s = "https://www.googleapis.com/auth/drive.file".to_string(); //! let scopes = vec![s]; //! diff --git a/src/service_account.rs b/src/service_account.rs index 598722a..c67fb94 100644 --- a/src/service_account.rs +++ b/src/service_account.rs @@ -14,6 +14,7 @@ use std::default::Default; use std::sync::{Arc, Mutex}; +use crate::authenticator::{DefaultHyperClient, HyperClientBuilder}; use crate::storage::{hash_scopes, MemoryStorage, TokenStorage}; use crate::types::{ApplicationSecret, GetToken, JsonError, RequestError, StringError, Token}; @@ -188,12 +189,75 @@ where /// (and you also should not) use this with `Authenticator`. Just use it directly. #[derive(Clone)] pub struct ServiceAccountAccess { + client: C, + key: ServiceAccountKey, + sub: Option, +} + +impl ServiceAccountAccess { + /// Create a new ServiceAccountAccess with the provided key. + pub fn new(key: ServiceAccountKey) -> Self { + ServiceAccountAccess { + client: DefaultHyperClient, + key, + sub: None, + } + } +} + +impl ServiceAccountAccess +where + C: HyperClientBuilder, + C::Connector: 'static, +{ + /// Use the provided hyper client. + pub fn hyper_client( + self, + hyper_client: NewC, + ) -> ServiceAccountAccess { + ServiceAccountAccess { + client: hyper_client, + key: self.key, + sub: self.sub, + } + } + + /// Use the provided sub. + pub fn sub(self, sub: String) -> Self { + ServiceAccountAccess { + sub: Some(sub), + ..self + } + } + + /// Build the configured ServiceAccountAccess. + pub fn build(self) -> impl GetToken { + ServiceAccountAccessImpl::new(self.client.build_hyper_client(), self.key, self.sub) + } +} + +#[derive(Clone)] +struct ServiceAccountAccessImpl { client: hyper::Client, key: ServiceAccountKey, cache: Arc>, sub: Option, } +impl ServiceAccountAccessImpl +where + C: hyper::client::connect::Connect, +{ + fn new(client: hyper::Client, key: ServiceAccountKey, sub: Option) -> Self { + ServiceAccountAccessImpl { + client, + key, + cache: Arc::new(Mutex::new(MemoryStorage::default())), + sub, + } + } +} + /// This is the schema of the server's response. #[derive(Deserialize, Debug)] struct TokenResponse { @@ -216,36 +280,7 @@ impl TokenResponse { } } -impl<'a, C: 'static + hyper::client::connect::Connect> ServiceAccountAccess { - /// Returns a new `ServiceAccountAccess` token source. - #[allow(dead_code)] - pub fn new( - key: ServiceAccountKey, - client: hyper::Client, - ) -> ServiceAccountAccess { - ServiceAccountAccess { - client: client, - key: key, - cache: Arc::new(Mutex::new(MemoryStorage::default())), - sub: None, - } - } - - /// Set `sub` claim in new `ServiceAccountKey` (see - /// https://developers.google.com/identity/protocols/OAuth2ServiceAccount#authorizingrequests). - pub fn with_sub( - key: ServiceAccountKey, - client: hyper::Client, - sub: String, - ) -> ServiceAccountAccess { - ServiceAccountAccess { - client: client, - key: key, - cache: Arc::new(Mutex::new(MemoryStorage::default())), - sub: Some(sub), - } - } - +impl<'a, C: 'static + hyper::client::connect::Connect> ServiceAccountAccessImpl { /// Send a request for a new Bearer token to the OAuth provider. fn request_token( client: hyper::client::Client, @@ -311,7 +346,7 @@ impl<'a, C: 'static + hyper::client::connect::Connect> ServiceAccountAccess { } } -impl GetToken for ServiceAccountAccess +impl GetToken for ServiceAccountAccessImpl where C: hyper::client::connect::Connect, { @@ -441,7 +476,7 @@ mod tests { .with_body(json_response) .expect(1) .create(); - let mut acc = ServiceAccountAccess::new(key.clone(), client.clone()); + let mut acc = ServiceAccountAccessImpl::new(client.clone(), key.clone(), None); let fut = acc .token(vec!["https://www.googleapis.com/auth/pubsub"]) .and_then(|tok| { @@ -480,7 +515,9 @@ mod tests { .with_header("content-type", "text/json") .with_body(bad_json_response) .create(); - let mut acc = ServiceAccountAccess::new(key.clone(), client.clone()); + let mut acc = ServiceAccountAccess::new(key.clone()) + .hyper_client(client.clone()) + .build(); let fut = acc .token(vec!["https://www.googleapis.com/auth/pubsub"]) .then(|result| { @@ -506,7 +543,7 @@ mod tests { let client = hyper::Client::builder() .executor(runtime.executor()) .build(https); - let mut acc = ServiceAccountAccess::new(key, client); + let mut acc = ServiceAccountAccess::new(key).hyper_client(client).build(); println!( "{:?}", acc.token(vec!["https://www.googleapis.com/auth/pubsub"]) From fbb8c69efbb2edefb870dc635a070c0ac1ebeb9f Mon Sep 17 00:00:00 2001 From: Glenn Griffin Date: Thu, 29 Aug 2019 11:29:42 -0700 Subject: [PATCH 2/2] Change the name of TokenGetterBuilder to AuthFlow. I believe AuthFlow more succinctly describes the purpose of the type to users reading documentation. --- src/authenticator.rs | 10 +++++----- src/device.rs | 4 ++-- src/installed.rs | 36 +++++++++++++++++++++++++++++++++++- src/lib.rs | 2 +- 4 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/authenticator.rs b/src/authenticator.rs index 4600040..cf866ec 100644 --- a/src/authenticator.rs +++ b/src/authenticator.rs @@ -64,7 +64,7 @@ where } /// An internal trait implemented by flows to be used by an authenticator. -pub trait TokenGetterBuilder { +pub trait AuthFlow { type TokenGetter: GetToken; fn build_token_getter(self, client: hyper::Client) -> Self::TokenGetter; @@ -74,7 +74,7 @@ pub trait TokenGetterBuilder { /// will refresh tokens as they expire as well as optionally persist tokens to /// disk. pub struct Authenticator< - T: TokenGetterBuilder, + T: AuthFlow, S: TokenStorage, AD: AuthenticatorDelegate, C: HyperClientBuilder, @@ -87,7 +87,7 @@ pub struct Authenticator< impl Authenticator where - T: TokenGetterBuilder<::Connector>, + T: AuthFlow<::Connector>, { /// Create a new authenticator with the provided flow. By default a new /// hyper::Client will be created the default authenticator delegate will be @@ -115,7 +115,7 @@ where impl Authenticator where - T: TokenGetterBuilder, + T: AuthFlow, S: TokenStorage, AD: AuthenticatorDelegate, C: HyperClientBuilder, @@ -127,7 +127,7 @@ where ) -> Authenticator> where NewC: hyper::client::connect::Connect, - T: TokenGetterBuilder, + T: AuthFlow, { Authenticator { client: hyper_client, diff --git a/src/device.rs b/src/device.rs index 684db7e..78cce65 100644 --- a/src/device.rs +++ b/src/device.rs @@ -73,7 +73,7 @@ impl DeviceFlow { } } -impl crate::authenticator::TokenGetterBuilder for DeviceFlow +impl crate::authenticator::AuthFlow for DeviceFlow where FD: FlowDelegate + Send + 'static, C: hyper::client::connect::Connect + 'static, @@ -407,7 +407,7 @@ mod tests { use tokio; use super::*; - use crate::authenticator::TokenGetterBuilder; + use crate::authenticator::AuthFlow; use crate::helper::parse_application_secret; #[test] diff --git a/src/installed.rs b/src/installed.rs index 523bfea..0ae8ae0 100644 --- a/src/installed.rs +++ b/src/installed.rs @@ -124,6 +124,40 @@ impl InstalledFlow { } } +impl InstalledFlow +where + FD: FlowDelegate, +{ + /// Use the provided FlowDelegate. + pub fn delegate(self, delegate: NewFD) -> InstalledFlow { + InstalledFlow { + method: self.method, + flow_delegate: delegate, + appsecret: self.appsecret, + } + } +} + +impl crate::authenticator::AuthFlow for InstalledFlow +where + FD: FlowDelegate + Send + 'static, + C: hyper::client::connect::Connect + 'static, +{ + type TokenGetter = InstalledFlowImpl; + + fn build_token_getter(self, client: hyper::Client) -> Self::TokenGetter { + InstalledFlowImpl { + method: self.method, + fd: self.flow_delegate, + appsecret: self.appsecret, + client, + } + } +} + +impl<'c, FD: 'static + FlowDelegate + Clone + Send, C: 'c + hyper::client::connect::Connect> + InstalledFlowImpl +{ /// Handles the token request flow; it consists of the following steps: /// . Obtain a authorization code with user cooperation or internal redirect. /// . Obtain a token and refresh token using that code. @@ -543,7 +577,7 @@ mod tests { use tokio; use super::*; - use crate::authenticator::TokenGetterBuilder; + use crate::authenticator::AuthFlow; use crate::authenticator_delegate::FlowDelegate; use crate::helper::*; use crate::types::StringError; diff --git a/src/lib.rs b/src/lib.rs index f9ad5ca..48e3b28 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -93,7 +93,7 @@ mod service_account; mod storage; mod types; -pub use crate::authenticator::Authenticator; +pub use crate::authenticator::{AuthFlow, Authenticator}; pub use crate::authenticator_delegate::{ AuthenticatorDelegate, DefaultAuthenticatorDelegate, DefaultFlowDelegate, FlowDelegate, PollInformation,