diff --git a/examples/test-device/src/main.rs b/examples/test-device/src/main.rs index 62bdf10..82a4741 100644 --- a/examples/test-device/src/main.rs +++ b/examples/test-device/src/main.rs @@ -1,13 +1,13 @@ -use yup_oauth2::{self, Authenticator, DeviceFlow, GetToken}; +use yup_oauth2::DeviceFlowAuthenticator; use std::path; use tokio; #[tokio::main] async fn main() { - let creds = yup_oauth2::read_application_secret(path::Path::new("clientsecret.json")) + let app_secret = yup_oauth2::read_application_secret(path::Path::new("clientsecret.json")) .expect("clientsecret"); - let auth = Authenticator::new(DeviceFlow::new(creds)) + let auth = DeviceFlowAuthenticator::builder(app_secret) .persist_tokens_to_disk("tokenstorage.json") .build() .await diff --git a/examples/test-installed/src/main.rs b/examples/test-installed/src/main.rs index 1bdfee0..3febb75 100644 --- a/examples/test-installed/src/main.rs +++ b/examples/test-installed/src/main.rs @@ -1,21 +1,18 @@ -use yup_oauth2::GetToken; -use yup_oauth2::{Authenticator, InstalledFlow}; +use yup_oauth2::{InstalledFlowAuthenticator, InstalledFlowReturnMethod}; use std::path::Path; #[tokio::main] async fn main() { - let secret = yup_oauth2::read_application_secret(Path::new("clientsecret.json")) + let app_secret = yup_oauth2::read_application_secret(Path::new("clientsecret.json")) .expect("clientsecret.json"); - let auth = Authenticator::new(InstalledFlow::new( - secret, - yup_oauth2::InstalledFlowReturnMethod::HTTPRedirect, - )) - .persist_tokens_to_disk("tokencache.json") - .build() - .await - .unwrap(); + let auth = + InstalledFlowAuthenticator::builder(app_secret, InstalledFlowReturnMethod::HTTPRedirect) + .persist_tokens_to_disk("tokencache.json") + .build() + .await + .unwrap(); let scopes = &["https://www.googleapis.com/auth/drive.file"]; match auth.token(scopes).await { diff --git a/examples/test-svc-acct/src/main.rs b/examples/test-svc-acct/src/main.rs index e5ef33c..8ff8db0 100644 --- a/examples/test-svc-acct/src/main.rs +++ b/examples/test-svc-acct/src/main.rs @@ -1,15 +1,10 @@ -use std::path; use tokio; -use yup_oauth2; -use yup_oauth2::GetToken; +use yup_oauth2::ServiceAccountAuthenticator; #[tokio::main] async fn main() { - let creds = - yup_oauth2::service_account_key_from_file(path::Path::new("serviceaccount.json")).unwrap(); - let sa = yup_oauth2::ServiceAccountAccess::new(creds) - .build() - .unwrap(); + let creds = yup_oauth2::service_account_key_from_file("serviceaccount.json").unwrap(); + let sa = ServiceAccountAuthenticator::builder(creds).build().unwrap(); let scopes = &["https://www.googleapis.com/auth/pubsub"]; let tok = sa.token(scopes).await.unwrap(); diff --git a/src/authenticator.rs b/src/authenticator.rs index b332e5a..1b5a2de 100644 --- a/src/authenticator.rs +++ b/src/authenticator.rs @@ -1,32 +1,293 @@ -use crate::authenticator_delegate::{AuthenticatorDelegate, DefaultAuthenticatorDelegate}; +use crate::authenticator_delegate::{ + AuthenticatorDelegate, DefaultAuthenticatorDelegate, FlowDelegate, +}; +use crate::device::DeviceFlow; +use crate::installed::{InstalledFlow, InstalledFlowReturnMethod}; use crate::refresh::RefreshFlow; use crate::storage::{self, Storage}; -use crate::types::{ApplicationSecret, GetToken, RefreshResult, RequestError, Token}; - -use futures::prelude::*; +use crate::types::{ApplicationSecret, RefreshResult, RequestError, Token}; +use private::AuthFlow; +use std::borrow::Cow; use std::error::Error; use std::io; use std::path::PathBuf; -use std::pin::Pin; use std::sync::Mutex; +use std::time::Duration; -/// Authenticator abstracts different `GetToken` implementations behind one type and handles -/// caching received tokens. It's important to use it (instead of the flows directly) because -/// otherwise the user needs to be asked for new authorization every time a token is generated. -/// -/// `ServiceAccountAccess` does not need (and does not work) with `Authenticator`, given that it -/// does not require interaction and implements its own caching. Use it directly. -/// -/// 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. -struct AuthenticatorImpl +pub struct Authenticator { + hyper_client: hyper::Client, + app_secret: ApplicationSecret, + auth_delegate: Box, + storage: Storage, + auth_flow: AuthFlow, +} + +impl Authenticator +where + C: hyper::client::connect::Connect + 'static, { - client: hyper::Client, - inner: T, - store: Storage, - delegate: AD, + pub async fn token<'a, T>(&'a self, scopes: &'a [T]) -> Result + where + T: AsRef, + { + let scope_key = storage::ScopeHash::new(scopes); + match self.storage.get(scope_key, scopes) { + Some(t) if !t.expired() => { + // unexpired token found + Ok(t) + } + Some(Token { + refresh_token: Some(refresh_token), + .. + }) => { + // token is expired but has a refresh token. + let rr = RefreshFlow::refresh_token( + &self.hyper_client, + &self.app_secret, + &refresh_token, + ) + .await?; + match rr { + RefreshResult::Error(ref e) => { + self.auth_delegate.token_refresh_failed( + e.description(), + Some("the request has likely timed out"), + ); + Err(RequestError::Refresh(rr)) + } + RefreshResult::RefreshError(ref s, ref ss) => { + self.auth_delegate.token_refresh_failed( + &format!("{}{}", s, ss.as_ref().map(|s| format!(" ({})", s)).unwrap_or_else(String::new)), + Some("the refresh token is likely invalid and your authorization has been revoked"), + ); + Err(RequestError::Refresh(rr)) + } + RefreshResult::Success(t) => { + self.storage.set(scope_key, scopes, Some(t.clone())).await; + Ok(t) + } + } + } + None + | Some(Token { + refresh_token: None, + .. + }) => { + // no token in the cache or the token returned does not contain a refresh token. + let t = self + .auth_flow + .token(&self.hyper_client, &self.app_secret, scopes) + .await?; + self.storage.set(scope_key, scopes, Some(t.clone())).await; + Ok(t) + } + } + } +} + +pub struct AuthenticatorBuilder { + hyper_client_builder: C, + app_secret: ApplicationSecret, + auth_delegate: Box, + storage_type: StorageType, + auth_flow: F, +} + +pub struct InstalledFlowAuthenticator; +impl InstalledFlowAuthenticator { + pub fn builder( + app_secret: ApplicationSecret, + method: InstalledFlowReturnMethod, + ) -> AuthenticatorBuilder { + AuthenticatorBuilder::::with_auth_flow( + app_secret, + InstalledFlow::new(method), + ) + } +} + +pub struct DeviceFlowAuthenticator; +impl DeviceFlowAuthenticator { + pub fn builder( + app_secret: ApplicationSecret, + ) -> AuthenticatorBuilder { + AuthenticatorBuilder::::with_auth_flow(app_secret, DeviceFlow::new()) + } +} + +impl AuthenticatorBuilder { + fn with_auth_flow( + app_secret: ApplicationSecret, + auth_flow: F, + ) -> AuthenticatorBuilder { + AuthenticatorBuilder { + hyper_client_builder: DefaultHyperClient, + app_secret, + auth_delegate: Box::new(DefaultAuthenticatorDelegate), + storage_type: StorageType::Memory, + auth_flow, + } + } + + /// Use the provided hyper client. + pub fn hyper_client( + self, + hyper_client: hyper::Client, + ) -> AuthenticatorBuilder, F> { + AuthenticatorBuilder { + hyper_client_builder: hyper_client, + app_secret: self.app_secret, + auth_delegate: self.auth_delegate, + storage_type: self.storage_type, + auth_flow: self.auth_flow, + } + } + + /// Persist tokens to disk in the provided filename. + pub fn persist_tokens_to_disk>(self, path: P) -> AuthenticatorBuilder { + AuthenticatorBuilder { + storage_type: StorageType::Disk(path.into()), + ..self + } + } + + /// Use the provided authenticator delegate. + pub fn auth_delegate( + self, + auth_delegate: Box, + ) -> AuthenticatorBuilder { + AuthenticatorBuilder { + auth_delegate, + ..self + } + } + + /// Create the authenticator. + pub async fn build(self) -> io::Result> + where + C: HyperClientBuilder, + F: Into, + { + let hyper_client = self.hyper_client_builder.build_hyper_client(); + let storage = match self.storage_type { + StorageType::Memory => Storage::Memory { + tokens: Mutex::new(storage::JSONTokens::new()), + }, + StorageType::Disk(path) => Storage::Disk(storage::DiskStorage::new(path).await?), + }; + + Ok(Authenticator { + hyper_client, + app_secret: self.app_secret, + storage, + auth_delegate: self.auth_delegate, + auth_flow: self.auth_flow.into(), + }) + } +} + +impl AuthenticatorBuilder { + /// Use the provided device code url. + pub fn device_code_url(self, url: impl Into>) -> Self { + AuthenticatorBuilder { + auth_flow: DeviceFlow { + device_code_url: url.into(), + ..self.auth_flow + }, + ..self + } + } + + /// Use the provided FlowDelegate. + pub fn flow_delegate(self, flow_delegate: Box) -> Self { + AuthenticatorBuilder { + auth_flow: DeviceFlow { + flow_delegate, + ..self.auth_flow + }, + ..self + } + } + + /// Use the provided wait duration. + pub fn wait_duration(self, wait_duration: Duration) -> Self { + AuthenticatorBuilder { + auth_flow: DeviceFlow { + wait_duration, + ..self.auth_flow + }, + ..self + } + } + + /// Use the provided grant type. + pub fn grant_type(self, grant_type: impl Into>) -> Self { + AuthenticatorBuilder { + auth_flow: DeviceFlow { + grant_type: grant_type.into(), + ..self.auth_flow + }, + ..self + } + } +} + +impl AuthenticatorBuilder { + /// Use the provided FlowDelegate. + pub fn flow_delegate(self, flow_delegate: Box) -> Self { + AuthenticatorBuilder { + auth_flow: InstalledFlow { + flow_delegate, + ..self.auth_flow + }, + ..self + } + } +} + +mod private { + use crate::device::DeviceFlow; + use crate::installed::InstalledFlow; + use crate::types::{ApplicationSecret, RequestError, Token}; + pub enum AuthFlow { + DeviceFlow(DeviceFlow), + InstalledFlow(InstalledFlow), + } + + impl From for AuthFlow { + fn from(device_flow: DeviceFlow) -> AuthFlow { + AuthFlow::DeviceFlow(device_flow) + } + } + + impl From for AuthFlow { + fn from(installed_flow: InstalledFlow) -> AuthFlow { + AuthFlow::InstalledFlow(installed_flow) + } + } + + impl AuthFlow { + pub(crate) async fn token<'a, C, T>( + &'a self, + hyper_client: &'a hyper::Client, + app_secret: &'a ApplicationSecret, + scopes: &'a [T], + ) -> Result + where + T: AsRef, + C: hyper::client::connect::Connect + 'static, + { + match self { + AuthFlow::DeviceFlow(device_flow) => { + device_flow.token(hyper_client, app_secret, scopes).await + } + AuthFlow::InstalledFlow(installed_flow) => { + installed_flow.token(hyper_client, app_secret, scopes).await + } + } + } + } } /// A trait implemented for any hyper::Client as well as teh DefaultHyperClient. @@ -59,211 +320,7 @@ where } } -/// An internal trait implemented by flows to be used by an authenticator. -pub trait AuthFlow { - type TokenGetter: GetToken; - - fn build_token_getter(self, client: hyper::Client) -> Self::TokenGetter; -} - enum StorageType { Memory, Disk(PathBuf), } - -/// 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 { - client: C, - token_getter: T, - storage_type: StorageType, - delegate: AD, -} - -impl Authenticator -where - 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 - /// used, and tokens will not be persisted to disk. - /// Accepted flow types are DeviceFlow and InstalledFlow. - /// - /// Examples - /// ``` - /// # #[tokio::main] - /// # async fn main() { - /// use std::path::Path; - /// use yup_oauth2::{ApplicationSecret, Authenticator, DeviceFlow}; - /// let creds = ApplicationSecret::default(); - /// let auth = Authenticator::new(DeviceFlow::new(creds)).build().await.unwrap(); - /// # } - /// ``` - pub fn new(flow: T) -> Authenticator { - Authenticator { - client: DefaultHyperClient, - token_getter: flow, - storage_type: StorageType::Memory, - delegate: DefaultAuthenticatorDelegate, - } - } -} - -impl Authenticator -where - T: AuthFlow, - AD: AuthenticatorDelegate, - C: HyperClientBuilder, -{ - /// Use the provided hyper client. - pub fn hyper_client( - self, - hyper_client: hyper::Client, - ) -> Authenticator> - where - NewC: hyper::client::connect::Connect + 'static, - T: AuthFlow, - { - Authenticator { - client: hyper_client, - token_getter: self.token_getter, - storage_type: self.storage_type, - delegate: self.delegate, - } - } - - /// Persist tokens to disk in the provided filename. - pub fn persist_tokens_to_disk>(self, path: P) -> Authenticator { - Authenticator { - client: self.client, - token_getter: self.token_getter, - storage_type: StorageType::Disk(path.into()), - delegate: self.delegate, - } - } - - /// Use the provided authenticator delegate. - pub fn delegate( - self, - delegate: NewAD, - ) -> Authenticator { - Authenticator { - client: self.client, - token_getter: self.token_getter, - storage_type: self.storage_type, - delegate, - } - } - - /// Create the authenticator. - pub async fn build(self) -> io::Result - where - T::TokenGetter: GetToken, - C::Connector: hyper::client::connect::Connect + 'static, - { - let client = self.client.build_hyper_client(); - let inner = self.token_getter.build_token_getter(client.clone()); - let store = match self.storage_type { - StorageType::Memory => Storage::Memory { - tokens: Mutex::new(storage::JSONTokens::new()), - }, - StorageType::Disk(path) => Storage::Disk(storage::DiskStorage::new(path).await?), - }; - - Ok(AuthenticatorImpl { - client, - inner, - store, - delegate: self.delegate, - }) - } -} - -impl AuthenticatorImpl -where - GT: GetToken, - AD: AuthenticatorDelegate, - C: hyper::client::connect::Connect + 'static, -{ - async fn get_token(&self, scopes: &[T]) -> Result - where - T: AsRef + Sync, - { - let scope_key = storage::ScopeHash::new(scopes); - let store = &self.store; - let delegate = &self.delegate; - let client = &self.client; - let gettoken = &self.inner; - let appsecret = gettoken.application_secret(); - match store.get(scope_key, scopes) { - Some(t) if !t.expired() => { - // unexpired token found - Ok(t) - } - Some(Token { - refresh_token: Some(refresh_token), - .. - }) => { - // token is expired but has a refresh token. - let rr = RefreshFlow::refresh_token(client, appsecret, &refresh_token).await?; - match rr { - RefreshResult::Error(ref e) => { - delegate.token_refresh_failed( - e.description(), - Some("the request has likely timed out"), - ); - Err(RequestError::Refresh(rr)) - } - RefreshResult::RefreshError(ref s, ref ss) => { - delegate.token_refresh_failed( - &format!("{}{}", s, ss.as_ref().map(|s| format!(" ({})", s)).unwrap_or_else(String::new)), - Some("the refresh token is likely invalid and your authorization has been revoked"), - ); - Err(RequestError::Refresh(rr)) - } - RefreshResult::Success(t) => { - store.set(scope_key, scopes, Some(t.clone())).await; - Ok(t) - } - } - } - None - | Some(Token { - refresh_token: None, - .. - }) => { - // no token in the cache or the token returned does not contain a refresh token. - let t = gettoken.token(scopes).await?; - store.set(scope_key, scopes, Some(t.clone())).await; - Ok(t) - } - } - } -} - -impl GetToken for AuthenticatorImpl -where - GT: GetToken, - AD: AuthenticatorDelegate, - C: hyper::client::connect::Connect + 'static, -{ - /// Returns the API Key of the inner flow. - fn api_key(&self) -> Option { - self.inner.api_key() - } - /// Returns the application secret of the inner flow. - fn application_secret(&self) -> &ApplicationSecret { - self.inner.application_secret() - } - - fn token<'a, T>( - &'a self, - scopes: &'a [T], - ) -> Pin> + Send + 'a>> - where - T: AsRef + Sync, - { - Box::pin(self.get_token(scopes)) - } -} diff --git a/src/device.rs b/src/device.rs index 45e117e..373c36a 100644 --- a/src/device.rs +++ b/src/device.rs @@ -1,5 +1,4 @@ use std::borrow::Cow; -use std::pin::Pin; use std::time::Duration; use ::log::error; @@ -11,7 +10,7 @@ use serde_json as json; use url::form_urlencoded; use crate::authenticator_delegate::{DefaultFlowDelegate, FlowDelegate, PollInformation, Retry}; -use crate::types::{ApplicationSecret, GetToken, JsonErrorOr, PollError, RequestError, Token}; +use crate::types::{ApplicationSecret, JsonErrorOr, PollError, RequestError, Token}; pub const GOOGLE_DEVICE_CODE_URL: &str = "https://accounts.google.com/o/oauth2/device/code"; @@ -22,165 +21,77 @@ pub const GOOGLE_GRANT_TYPE: &str = "http://oauth.net/grant_type/device/1.0"; /// It operates in two steps: /// * obtain a code to show to the user // * (repeatedly) poll for the user to authenticate your application -#[derive(Clone)] -pub struct DeviceFlow { - application_secret: ApplicationSecret, - device_code_url: Cow<'static, str>, - flow_delegate: FD, - wait: Duration, - grant_type: Cow<'static, str>, +pub struct DeviceFlow { + pub(crate) device_code_url: Cow<'static, str>, + pub(crate) flow_delegate: Box, + pub(crate) wait_duration: Duration, + pub(crate) grant_type: Cow<'static, str>, } -impl DeviceFlow { +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 { + pub(crate) fn new() -> Self { DeviceFlow { - application_secret: secret, device_code_url: GOOGLE_DEVICE_CODE_URL.into(), - flow_delegate: DefaultFlowDelegate, - wait: Duration::from_secs(120), + flow_delegate: Box::new(DefaultFlowDelegate), + wait_duration: Duration::from_secs(120), grant_type: GOOGLE_GRANT_TYPE.into(), } } -} -impl DeviceFlow { - /// Use the provided device code url. - pub fn device_code_url(self, url: String) -> Self { - DeviceFlow { - device_code_url: url.into(), - ..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, - grant_type: self.grant_type, - } - } - - /// Use the provided wait duration. - pub fn wait_duration(self, duration: Duration) -> Self { - DeviceFlow { - wait: duration, - ..self - } - } - - pub fn grant_type(self, grant_type: String) -> Self { - DeviceFlow { - grant_type: grant_type.into(), - ..self - } - } -} - -impl crate::authenticator::AuthFlow for DeviceFlow -where - FD: FlowDelegate, - 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), - grant_type: self.grant_type, - } - } -} - -/// The DeviceFlow implementation. -pub struct DeviceFlowImpl { - client: hyper::Client, - application_secret: ApplicationSecret, - /// Usually GOOGLE_DEVICE_CODE_URL - device_code_url: Cow<'static, str>, - fd: FD, - wait: Duration, - grant_type: Cow<'static, str>, -} - -impl GetToken for DeviceFlowImpl -where - FD: FlowDelegate, - C: hyper::client::connect::Connect + 'static, -{ - fn token<'a, T>( - &'a self, - scopes: &'a [T], - ) -> Pin> + Send + 'a>> - where - T: AsRef + Sync, - { - Box::pin(self.retrieve_device_token(scopes)) - } - fn api_key(&self) -> Option { - None - } - fn application_secret(&self) -> &ApplicationSecret { - &self.application_secret - } -} - -impl DeviceFlowImpl -where - C: hyper::client::connect::Connect + 'static, - FD: FlowDelegate, -{ - /// Essentially what `GetToken::token` does: Retrieve a token for the given scopes without - /// caching. - pub async fn retrieve_device_token(&self, scopes: &[T]) -> Result + pub(crate) async fn token( + &self, + hyper_client: &hyper::Client, + app_secret: &ApplicationSecret, + scopes: &[T], + ) -> Result where T: AsRef, + C: hyper::client::connect::Connect + 'static, { - let application_secret = &self.application_secret; - let (pollinf, device_code) = Self::request_code( - application_secret, - &self.client, - &self.device_code_url, - scopes, - ) - .await?; - self.fd.present_user_code(&pollinf); + let (pollinf, device_code) = + Self::request_code(app_secret, hyper_client, &self.device_code_url, scopes).await?; + self.flow_delegate.present_user_code(&pollinf); tokio::timer::Timeout::new( - self.wait_for_device_token(&pollinf, &device_code, &self.grant_type), - self.wait, + self.wait_for_device_token( + hyper_client, + app_secret, + &pollinf, + &device_code, + &self.grant_type, + ), + self.wait_duration, ) .await .map_err(|_| RequestError::Poll(PollError::TimedOut))? } - async fn wait_for_device_token( + async fn wait_for_device_token( &self, + hyper_client: &hyper::Client, + app_secret: &ApplicationSecret, pollinf: &PollInformation, device_code: &str, grant_type: &str, - ) -> Result { + ) -> Result + where + C: hyper::client::connect::Connect + 'static, + { let mut interval = pollinf.interval; loop { tokio::timer::delay_for(interval).await; - let r = Self::poll_token( - &self.application_secret, - &self.client, + interval = match Self::poll_token( + &app_secret, + hyper_client, device_code, grant_type, pollinf.expires_at, - &self.fd, + &*self.flow_delegate as &dyn FlowDelegate, ) - .await; - interval = match r { - Ok(None) => match self.fd.pending(&pollinf) { + .await + { + Ok(None) => match self.flow_delegate.pending(&pollinf) { Retry::Abort | Retry::Skip => { return Err(RequestError::Poll(PollError::TimedOut)) } @@ -213,7 +124,7 @@ where /// * If called after a successful result was returned at least once. /// # Examples /// See test-cases in source code for a more complete example. - async fn request_code( + async fn request_code( application_secret: &ApplicationSecret, client: &hyper::Client, device_code_url: &str, @@ -221,6 +132,7 @@ where ) -> Result<(PollInformation, String), RequestError> where T: AsRef, + C: hyper::client::connect::Connect + 'static, { let req = form_urlencoded::Serializer::new(String::new()) .extend_pairs(&[ @@ -288,16 +200,19 @@ where /// /// # Examples /// See test-cases in source code for a more complete example. - async fn poll_token<'a>( + async fn poll_token<'a, C>( application_secret: &ApplicationSecret, client: &hyper::Client, device_code: &str, grant_type: &str, expires_at: DateTime, - fd: &FD, - ) -> Result, PollError> { + flow_delegate: &dyn FlowDelegate, + ) -> Result, PollError> + where + C: hyper::client::connect::Connect + 'static, + { if expires_at <= Utc::now() { - fd.expired(expires_at); + flow_delegate.expired(expires_at); return Err(PollError::Expired(expires_at)); } @@ -334,7 +249,7 @@ where Ok(res) => { match res.error.as_ref() { "access_denied" => { - fd.denied(); + flow_delegate.denied(); return Err(PollError::AccessDenied); } "authorization_pending" => return Ok(None), @@ -364,7 +279,6 @@ mod tests { use tokio; use super::*; - use crate::authenticator::AuthFlow; use crate::helper::parse_application_secret; #[test] @@ -388,10 +302,12 @@ mod tests { .keep_alive(false) .build::<_, hyper::Body>(https); - let flow = DeviceFlow::new(app_secret) - .delegate(FD) - .device_code_url(device_code_url) - .build_token_getter(client); + let flow = DeviceFlow { + device_code_url: device_code_url.into(), + flow_delegate: Box::new(FD), + wait_duration: Duration::from_secs(5), + grant_type: GOOGLE_GRANT_TYPE.into(), + }; let rt = tokio::runtime::Builder::new() .core_threads(1) @@ -420,7 +336,11 @@ mod tests { let fut = async { let token = flow - .token(&["https://www.googleapis.com/scope/1"]) + .token( + &client, + &app_secret, + &["https://www.googleapis.com/scope/1"], + ) .await .unwrap(); assert_eq!("accesstoken", token.access_token); @@ -452,7 +372,13 @@ mod tests { .create(); let fut = async { - let res = flow.token(&["https://www.googleapis.com/scope/1"]).await; + let res = flow + .token( + &client, + &app_secret, + &["https://www.googleapis.com/scope/1"], + ) + .await; assert!(res.is_err()); assert!(format!("{}", res.unwrap_err()).contains("invalid_client_id")); Ok(()) as Result<(), ()> @@ -482,7 +408,13 @@ mod tests { .create(); let fut = async { - let res = flow.token(&["https://www.googleapis.com/scope/1"]).await; + let res = flow + .token( + &client, + &app_secret, + &["https://www.googleapis.com/scope/1"], + ) + .await; assert!(res.is_err()); assert!(format!("{}", res.unwrap_err()).contains("Access denied by user")); Ok(()) as Result<(), ()> diff --git a/src/installed.rs b/src/installed.rs index fa72489..8e1bf3d 100644 --- a/src/installed.rs +++ b/src/installed.rs @@ -17,7 +17,7 @@ use url::form_urlencoded; use url::percent_encoding::{percent_encode, QUERY_ENCODE_SET}; use crate::authenticator_delegate::{DefaultFlowDelegate, FlowDelegate}; -use crate::types::{ApplicationSecret, GetToken, JsonErrorOr, RequestError, Token}; +use crate::types::{ApplicationSecret, JsonErrorOr, RequestError, Token}; const OOB_REDIRECT_URI: &str = "urn:ietf:wg:oauth:2.0:oob"; @@ -51,40 +51,6 @@ where }) } -impl GetToken for InstalledFlowImpl -where - FD: FlowDelegate, - C: hyper::client::connect::Connect + 'static, -{ - fn token<'a, T>( - &'a self, - scopes: &'a [T], - ) -> Pin> + Send + 'a>> - where - T: AsRef + Sync, - { - Box::pin(self.obtain_token(scopes)) - } - fn api_key(&self) -> Option { - None - } - fn application_secret(&self) -> &ApplicationSecret { - &self.appsecret - } -} - -/// The InstalledFlow implementation. -pub struct InstalledFlowImpl -where - FD: FlowDelegate, - C: hyper::client::connect::Connect, -{ - method: InstalledFlowReturnMethod, - client: hyper::client::Client, - fd: FD, - appsecret: ApplicationSecret, -} - /// cf. https://developers.google.com/identity/protocols/OAuth2InstalledApp#choosingredirecturi pub enum InstalledFlowReturnMethod { /// Involves showing a URL to the user and asking to copy a code from their browser @@ -98,151 +64,133 @@ pub enum InstalledFlowReturnMethod { /// 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, +pub struct InstalledFlow { + pub(crate) method: InstalledFlowReturnMethod, + pub(crate) flow_delegate: Box, } -impl InstalledFlow { +impl InstalledFlow { /// Create a new InstalledFlow with the provided secret and method. - pub fn new( - secret: ApplicationSecret, - method: InstalledFlowReturnMethod, - ) -> InstalledFlow { + pub(crate) fn new(method: InstalledFlowReturnMethod) -> InstalledFlow { InstalledFlow { method, - flow_delegate: DefaultFlowDelegate, - appsecret: secret, + flow_delegate: Box::new(DefaultFlowDelegate), } } -} -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, - 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 InstalledFlowImpl -where - FD: FlowDelegate, - C: hyper::client::connect::Connect + 'static, -{ /// 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. /// . Return that token /// /// It's recommended not to use the DefaultFlowDelegate, but a specialized one. - async fn obtain_token(&self, scopes: &[T]) -> Result + pub(crate) async fn token( + &self, + hyper_client: &hyper::Client, + app_secret: &ApplicationSecret, + scopes: &[T], + ) -> Result where T: AsRef, + C: hyper::client::connect::Connect + 'static, { match self.method { - InstalledFlowReturnMethod::HTTPRedirect => self.ask_auth_code_via_http(scopes).await, + InstalledFlowReturnMethod::HTTPRedirect => { + self.ask_auth_code_via_http(hyper_client, app_secret, scopes) + .await + } InstalledFlowReturnMethod::Interactive => { - self.ask_auth_code_interactively(scopes).await + self.ask_auth_code_interactively(hyper_client, app_secret, scopes) + .await } } } - async fn ask_auth_code_interactively(&self, scopes: &[T]) -> Result + async fn ask_auth_code_interactively( + &self, + hyper_client: &hyper::Client, + app_secret: &ApplicationSecret, + scopes: &[T], + ) -> Result where T: AsRef, + C: hyper::client::connect::Connect + 'static, { - let auth_delegate = &self.fd; - let appsecret = &self.appsecret; let url = build_authentication_request_url( - &appsecret.auth_uri, - &appsecret.client_id, + &app_secret.auth_uri, + &app_secret.client_id, scopes, - auth_delegate.redirect_uri(), + self.flow_delegate.redirect_uri(), ); - let authcode = match auth_delegate + let authcode = match self + .flow_delegate .present_user_url(&url, true /* need code */) .await { Ok(mut code) => { // Partial backwards compatibility in case an implementation adds a new line // due to previous behaviour. - let ends_with_newline = code.chars().last().map(|c| c == '\n').unwrap_or(false); - if ends_with_newline { + if code.ends_with('\n') { code.pop(); } code } _ => return Err(RequestError::UserError("couldn't read code".to_string())), }; - self.exchange_auth_code(&authcode, None).await + self.exchange_auth_code(&authcode, hyper_client, app_secret, None) + .await } - async fn ask_auth_code_via_http(&self, scopes: &[T]) -> Result + async fn ask_auth_code_via_http( + &self, + hyper_client: &hyper::Client, + app_secret: &ApplicationSecret, + scopes: &[T], + ) -> Result where T: AsRef, + C: hyper::client::connect::Connect + 'static, { use std::borrow::Cow; - let auth_delegate = &self.fd; - let appsecret = &self.appsecret; let server = InstalledFlowServer::run()?; let server_addr = server.local_addr(); // Present url to user. // The redirect URI must be this very localhost URL, otherwise authorization is refused // by certain providers. - let redirect_uri: Cow = match auth_delegate.redirect_uri() { + let redirect_uri: Cow = match self.flow_delegate.redirect_uri() { Some(uri) => uri.into(), None => format!("http://{}", server_addr).into(), }; let url = build_authentication_request_url( - &appsecret.auth_uri, - &appsecret.client_id, + &app_secret.auth_uri, + &app_secret.client_id, scopes, Some(redirect_uri.as_ref()), ); - let _ = auth_delegate + let _ = self + .flow_delegate .present_user_url(&url, false /* need code */) .await; let auth_code = server.wait_for_auth_code().await; - self.exchange_auth_code(&auth_code, Some(server_addr)).await + self.exchange_auth_code(&auth_code, hyper_client, app_secret, Some(server_addr)) + .await } - async fn exchange_auth_code( + async fn exchange_auth_code( &self, authcode: &str, + hyper_client: &hyper::Client, + app_secret: &ApplicationSecret, server_addr: Option, - ) -> Result { - let appsec = &self.appsecret; - let redirect_uri = self.fd.redirect_uri(); - let request = Self::request_token(appsec, authcode, redirect_uri, server_addr); - let resp = self - .client + ) -> Result + where + C: hyper::client::connect::Connect + 'static, + { + let redirect_uri = self.flow_delegate.redirect_uri(); + let request = Self::request_token(app_secret, authcode, redirect_uri, server_addr); + let resp = hyper_client .request(request) .await .map_err(RequestError::ClientError)?; @@ -283,7 +231,7 @@ where /// Sends the authorization code to the provider in order to obtain access and refresh tokens. fn request_token( - appsecret: &ApplicationSecret, + app_secret: &ApplicationSecret, authcode: &str, custom_redirect_uri: Option<&str>, server_addr: Option, @@ -298,14 +246,14 @@ where let body = form_urlencoded::Serializer::new(String::new()) .extend_pairs(vec![ ("code", authcode), - ("client_id", appsecret.client_id.as_str()), - ("client_secret", appsecret.client_secret.as_str()), + ("client_id", app_secret.client_id.as_str()), + ("client_secret", app_secret.client_secret.as_str()), ("redirect_uri", redirect_uri.as_ref()), ("grant_type", "authorization_code"), ]) .finish(); - hyper::Request::post(&appsecret.token_uri) + hyper::Request::post(&app_secret.token_uri) .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded") .body(hyper::Body::from(body)) .unwrap() // TODO: error check @@ -456,7 +404,6 @@ mod tests { use tokio; use super::*; - use crate::authenticator::AuthFlow; use crate::authenticator_delegate::FlowDelegate; use crate::helper::*; use crate::types::StringError; @@ -523,9 +470,10 @@ mod tests { .build::<_, hyper::Body>(https); let fd = FD("authorizationcode".to_string(), client.clone()); - let inf = InstalledFlow::new(app_secret.clone(), InstalledFlowReturnMethod::Interactive) - .delegate(fd) - .build_token_getter(client.clone()); + let inf = InstalledFlow { + method: InstalledFlowReturnMethod::Interactive, + flow_delegate: Box::new(fd), + }; let rt = tokio::runtime::Builder::new() .core_threads(1) @@ -544,7 +492,7 @@ mod tests { let fut = || { async { let tok = inf - .token(&["https://googleapis.com/some/scope"]) + .token(&client, &app_secret, &["https://googleapis.com/some/scope"]) .await .map_err(|_| ())?; assert_eq!("accesstoken", tok.access_token); @@ -558,12 +506,13 @@ mod tests { } // Successful path with HTTP redirect. { - let inf = InstalledFlow::new(app_secret, InstalledFlowReturnMethod::HTTPRedirect) - .delegate(FD( + let inf = InstalledFlow { + method: InstalledFlowReturnMethod::HTTPRedirect, + flow_delegate: Box::new(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}"#) @@ -572,7 +521,7 @@ mod tests { let fut = async { let tok = inf - .token(&["https://googleapis.com/some/scope"]) + .token(&client, &app_secret, &["https://googleapis.com/some/scope"]) .await .map_err(|_| ())?; assert_eq!("accesstoken", tok.access_token); @@ -595,7 +544,9 @@ mod tests { .create(); let fut = async { - let tokr = inf.token(&["https://googleapis.com/some/scope"]).await; + let tokr = inf + .token(&client, &app_secret, &["https://googleapis.com/some/scope"]) + .await; assert!(tokr.is_err()); assert!(format!("{}", tokr.unwrap_err()).contains("invalid_code")); Ok(()) as Result<(), ()> diff --git a/src/lib.rs b/src/lib.rs index 549ea28..152f5b4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,12 +38,7 @@ //! `examples/test-installed/`, shows the basics of using this crate: //! //! ```test_harness,no_run -//! use futures::prelude::*; -//! use yup_oauth2::GetToken; -//! use yup_oauth2::{Authenticator, InstalledFlow}; -//! -//! use hyper::client::Client; -//! use hyper_rustls::HttpsConnector; +//! use yup_oauth2::{InstalledFlowAuthenticator, InstalledFlowReturnMethod}; //! //! #[tokio::main] //! async fn main() { @@ -53,12 +48,10 @@ //! .expect("clientsecret.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) -//! ) +//! // 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 = InstalledFlowAuthenticator::builder(secret, InstalledFlowReturnMethod::HTTPRedirect) //! .persist_tokens_to_disk("tokencache.json") //! .build() //! .await @@ -88,16 +81,18 @@ mod service_account; mod storage; mod types; -pub use crate::authenticator::{AuthFlow, Authenticator}; +pub use crate::authenticator::{ + Authenticator, AuthenticatorBuilder, DeviceFlowAuthenticator, InstalledFlowAuthenticator, +}; pub use crate::authenticator_delegate::{ AuthenticatorDelegate, DefaultAuthenticatorDelegate, DefaultFlowDelegate, FlowDelegate, PollInformation, }; -pub use crate::device::{DeviceFlow, GOOGLE_DEVICE_CODE_URL}; +pub use crate::device::GOOGLE_DEVICE_CODE_URL; pub use crate::helper::*; -pub use crate::installed::{InstalledFlow, InstalledFlowReturnMethod}; +pub use crate::installed::InstalledFlowReturnMethod; pub use crate::service_account::*; pub use crate::types::{ - ApplicationSecret, ConsoleApplicationSecret, GetToken, PollError, RefreshResult, RequestError, - Scheme, Token, TokenType, + ApplicationSecret, ConsoleApplicationSecret, PollError, RefreshResult, RequestError, Scheme, + Token, TokenType, }; diff --git a/src/service_account.rs b/src/service_account.rs index 1b10de8..eb013e7 100644 --- a/src/service_account.rs +++ b/src/service_account.rs @@ -11,12 +11,11 @@ //! Copyright (c) 2016 Google Inc (lewinb@google.com). //! -use std::pin::Pin; use std::sync::Mutex; use crate::authenticator::{DefaultHyperClient, HyperClientBuilder}; use crate::storage::{self, Storage}; -use crate::types::{ApplicationSecret, GetToken, JsonErrorOr, RequestError, Token}; +use crate::types::{JsonErrorOr, RequestError, Token}; use futures::prelude::*; use hyper::header; @@ -155,20 +154,10 @@ impl JWTSigner { } } -/// A token source (`GetToken`) yielding OAuth tokens for services that use ServiceAccount authorization. -/// This token source caches token and automatically renews expired ones, meaning you do not need -/// (and you also should not) use this with `Authenticator`. Just use it directly. -#[derive(Clone)] -pub struct ServiceAccountAccess { - client: C, - key: ServiceAccountKey, - subject: Option, -} - -impl ServiceAccountAccess { - /// Create a new ServiceAccountAccess with the provided key. - pub fn new(key: ServiceAccountKey) -> Self { - ServiceAccountAccess { +pub struct ServiceAccountAuthenticator; +impl ServiceAccountAuthenticator { + pub fn builder(key: ServiceAccountKey) -> Builder { + Builder { client: DefaultHyperClient, key, subject: None, @@ -176,16 +165,16 @@ impl ServiceAccountAccess { } } -impl ServiceAccountAccess -where - C: HyperClientBuilder, -{ +pub struct Builder { + client: C, + key: ServiceAccountKey, + subject: Option, +} + +impl Builder { /// Use the provided hyper client. - pub fn hyper_client( - self, - hyper_client: NewC, - ) -> ServiceAccountAccess { - ServiceAccountAccess { + pub fn hyper_client(self, hyper_client: NewC) -> Builder { + Builder { client: hyper_client, key: self.key, subject: self.subject, @@ -194,29 +183,32 @@ where /// Use the provided subject. pub fn subject(self, subject: String) -> Self { - ServiceAccountAccess { + Builder { subject: Some(subject), ..self } } /// Build the configured ServiceAccountAccess. - pub fn build(self) -> Result { - ServiceAccountAccessImpl::new(self.client.build_hyper_client(), self.key, self.subject) + pub fn build(self) -> Result, io::Error> + where + C: HyperClientBuilder, + { + ServiceAccountAccess::new(self.client.build_hyper_client(), self.key, self.subject) } } -struct ServiceAccountAccessImpl { - client: hyper::Client, +pub struct ServiceAccountAccess { + client: hyper::Client, key: ServiceAccountKey, cache: Storage, subject: Option, signer: JWTSigner, } -impl ServiceAccountAccessImpl +impl ServiceAccountAccess where - C: hyper::client::connect::Connect, + C: hyper::client::connect::Connect + 'static, { fn new( client: hyper::Client, @@ -224,7 +216,7 @@ where subject: Option, ) -> Result { let signer = JWTSigner::new(&key.private_key)?; - Ok(ServiceAccountAccessImpl { + Ok(ServiceAccountAccess { client, key, cache: Storage::Memory { @@ -234,20 +226,28 @@ where signer, }) } -} -/// This is the schema of the server's response. -#[derive(Deserialize, Debug)] -struct TokenResponse { - access_token: Option, - token_type: Option, - expires_in: Option, -} - -impl ServiceAccountAccessImpl -where - C: hyper::client::connect::Connect + 'static, -{ + pub async fn token(&self, scopes: &[T]) -> Result + where + T: AsRef, + { + let hash = storage::ScopeHash::new(scopes); + let cache = &self.cache; + match cache.get(hash, scopes) { + Some(token) if !token.expired() => return Ok(token), + _ => {} + } + let token = Self::request_token( + &self.client, + &self.signer, + self.subject.as_ref().map(|x| x.as_str()), + &self.key, + scopes, + ) + .await?; + cache.set(hash, scopes, Some(token.clone())).await; + Ok(token) + } /// Send a request for a new Bearer token to the OAuth provider. async fn request_token( client: &hyper::client::Client, @@ -282,6 +282,15 @@ where .try_concat() .await .map_err(RequestError::ClientError)?; + + /// This is the schema of the server's response. + #[derive(Deserialize, Debug)] + struct TokenResponse { + access_token: Option, + token_type: Option, + expires_in: Option, + } + match serde_json::from_slice::>(&body)? { JsonErrorOr::Err(err) => Err(err.into()), JsonErrorOr::Data(TokenResponse { @@ -305,61 +314,12 @@ where ))), } } - - async fn get_token(&self, scopes: &[T]) -> Result - where - T: AsRef, - { - let hash = storage::ScopeHash::new(scopes); - let cache = &self.cache; - match cache.get(hash, scopes) { - Some(token) if !token.expired() => return Ok(token), - _ => {} - } - let token = Self::request_token( - &self.client, - &self.signer, - self.subject.as_ref().map(|x| x.as_str()), - &self.key, - scopes, - ) - .await?; - cache.set(hash, scopes, Some(token.clone())).await; - Ok(token) - } -} - -impl GetToken for ServiceAccountAccessImpl -where - C: hyper::client::connect::Connect + 'static, -{ - fn token<'a, T>( - &'a self, - scopes: &'a [T], - ) -> Pin> + Send + 'a>> - where - T: AsRef + Sync, - { - Box::pin(self.get_token(scopes)) - } - - /// Returns an empty ApplicationSecret as tokens for service accounts don't need to be - /// refreshed (they are simply reissued). - fn application_secret(&self) -> &ApplicationSecret { - static APP_SECRET: ApplicationSecret = ApplicationSecret::empty(); - &APP_SECRET - } - - fn api_key(&self) -> Option { - None - } } #[cfg(test)] mod tests { use super::*; use crate::helper::service_account_key_from_file; - use crate::types::GetToken; use hyper; use hyper_rustls::HttpsConnector; @@ -413,7 +373,7 @@ mod tests { .with_body(json_response) .expect(1) .create(); - let acc = ServiceAccountAccessImpl::new(client.clone(), key.clone(), None).unwrap(); + let acc = ServiceAccountAccess::new(client.clone(), key.clone(), None).unwrap(); let fut = async { let tok = acc .token(&["https://www.googleapis.com/auth/pubsub"]) @@ -453,7 +413,7 @@ mod tests { .with_header("content-type", "text/json") .with_body(bad_json_response) .create(); - let acc = ServiceAccountAccess::new(key.clone()) + let acc = ServiceAccountAuthenticator::builder(key.clone()) .hyper_client(client.clone()) .build() .unwrap(); @@ -478,7 +438,7 @@ mod tests { let key = service_account_key_from_file(&TEST_PRIVATE_KEY_PATH.to_string()).unwrap(); let https = HttpsConnector::new(); let client = hyper::Client::builder().build(https); - let acc = ServiceAccountAccess::new(key) + let acc = ServiceAccountAuthenticator::builder(key) .hyper_client(client) .build() .unwrap(); diff --git a/src/types.rs b/src/types.rs index a9d371c..796cb90 100644 --- a/src/types.rs +++ b/src/types.rs @@ -3,11 +3,8 @@ use hyper; use std::error::Error; use std::fmt; use std::io; -use std::pin::Pin; use std::str::FromStr; -use futures::prelude::*; - #[derive(Deserialize, Debug)] pub struct JsonError { pub error: String, @@ -246,24 +243,6 @@ impl FromStr for Scheme { } } -/// 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: Send + Sync { - fn token<'a, T>( - &'a self, - scopes: &'a [T], - ) -> Pin> + Send + 'a>> - where - T: AsRef + Sync; - - fn api_key(&self) -> Option; - - /// Return an application secret with at least token_uri, client_secret, and client_id filled - /// in. This is used for refreshing tokens without interaction from the flow. - fn application_secret(&self) -> &ApplicationSecret; -} - /// Represents a token as returned by OAuth2 servers. /// /// It is produced by all authentication flows.