diff --git a/Cargo.toml b/Cargo.toml index b0d7cd8..fd1f99d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ webbrowser = "0.5" hyper-rustls = "0.22.1" [workspace] -members = ["examples/test-installed/", "examples/test-svc-acct/", "examples/test-device/", "examples/service_account", "examples/drive_example"] +members = ["examples/test-installed/", "examples/test-svc-acct/", "examples/test-device/", "examples/service_account", "examples/drive_example", "examples/test-adc"] [package.metadata.docs.rs] all-features = true diff --git a/examples/test-adc/Cargo.toml b/examples/test-adc/Cargo.toml new file mode 100644 index 0000000..e2fa1f0 --- /dev/null +++ b/examples/test-adc/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "test-adc" +version = "0.1.0" +authors = ["Antti Peltonen ", "Lukas Winkler "] +edition = "2018" + +[dependencies] +yup-oauth2 = { path = "../../" } +tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } diff --git a/examples/test-adc/src/main.rs b/examples/test-adc/src/main.rs new file mode 100644 index 0000000..0b63f4f --- /dev/null +++ b/examples/test-adc/src/main.rs @@ -0,0 +1,22 @@ +use yup_oauth2::authenticator::ApplicationDefaultCredentialsTypes; +use yup_oauth2::ApplicationDefaultCredentialsAuthenticator; +use yup_oauth2::ApplicationDefaultCredentialsFlowOpts; + +#[tokio::main] +async fn main() { + let opts = ApplicationDefaultCredentialsFlowOpts::default(); + let auth = match ApplicationDefaultCredentialsAuthenticator::builder(opts).await { + ApplicationDefaultCredentialsTypes::InstanceMetadata(auth) => auth + .build() + .await + .expect("Unable to create instance metadata authenticator"), + ApplicationDefaultCredentialsTypes::ServiceAccount(auth) => auth + .build() + .await + .expect("Unable to create service account authenticator"), + }; + let scopes = &["https://www.googleapis.com/auth/pubsub"]; + + let tok = auth.token(scopes).await.unwrap(); + println!("token is: {:?}", tok); +} diff --git a/src/application_default_credentials.rs b/src/application_default_credentials.rs new file mode 100644 index 0000000..5b5f2f5 --- /dev/null +++ b/src/application_default_credentials.rs @@ -0,0 +1,48 @@ +use crate::error::Error; +use crate::types::TokenInfo; + +/// Provide options for the Application Default Credential Flow, mostly used for testing +pub struct ApplicationDefaultCredentialsFlowOpts { + /// Used as base to build the url during token request from GCP metadata server + pub metadata_url: Option, +} +impl Default for ApplicationDefaultCredentialsFlowOpts { + fn default() -> Self { + Self { metadata_url: None } + } +} + +pub struct ApplicationDefaultCredentialsFlow { + metadata_url: String, +} + +impl ApplicationDefaultCredentialsFlow { + pub(crate) fn new(opts: ApplicationDefaultCredentialsFlowOpts) -> Self { + let metadata_url = opts.metadata_url.unwrap_or_else(|| "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token".to_string()); + ApplicationDefaultCredentialsFlow { metadata_url } + } + + pub(crate) async fn token( + &self, + hyper_client: &hyper::Client, + scopes: &[T], + ) -> Result + where + T: AsRef, + C: hyper::client::connect::Connect + Clone + Send + Sync + 'static, + { + let scope = crate::helper::join(scopes, ","); + let token_uri = format!("{}?scopes={}", self.metadata_url, scope); + let request = hyper::Request::get(token_uri) + .header("Metadata-Flavor", "Google") + .body(hyper::Body::from(String::new())) // why body is needed? + .unwrap(); + log::debug!("requesting token from metadata server: {:?}", request); + let (head, body) = hyper_client.request(request).await?.into_parts(); + let body = hyper::body::to_bytes(body).await?; + log::debug!("received response; head: {:?}, body: {:?}", head, body); + TokenInfo::from_json(&body) + } +} + +// eof diff --git a/src/authenticator.rs b/src/authenticator.rs index d3d72e4..cc9cc1c 100644 --- a/src/authenticator.rs +++ b/src/authenticator.rs @@ -1,4 +1,7 @@ //! Module contianing the core functionality for OAuth2 Authentication. +use crate::application_default_credentials::{ + ApplicationDefaultCredentialsFlow, ApplicationDefaultCredentialsFlowOpts, +}; use crate::authenticator_delegate::{DeviceFlowDelegate, InstalledFlowDelegate}; use crate::device::DeviceFlow; use crate::error::Error; @@ -254,6 +257,86 @@ impl ServiceAccountAuthenticator { } } +/// Create an authenticator that uses a application default credentials. +/// ``` +/// # #[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))] +/// # async fn foo() { +/// # use yup_oauth2::ApplicationDefaultCredentialsAuthenticator; +/// # use yup_oauth2::ApplicationDefaultCredentialsFlowOpts; +/// # use yup_oauth2::authenticator::ApplicationDefaultCredentialsTypes; +/// +/// let opts = ApplicationDefaultCredentialsFlowOpts::default(); +/// let authenticator = match ApplicationDefaultCredentialsAuthenticator::builder(opts).await { +/// ApplicationDefaultCredentialsTypes::InstanceMetadata(auth) => auth +/// .build() +/// .await +/// .expect("Unable to create instance metadata authenticator"), +/// ApplicationDefaultCredentialsTypes::ServiceAccount(auth) => auth +/// .build() +/// .await +/// .expect("Unable to create service account authenticator"), +/// }; +/// # } +/// ``` +pub struct ApplicationDefaultCredentialsAuthenticator; +impl ApplicationDefaultCredentialsAuthenticator { + /// Try to build ServiceAccountFlowOpts from the environment + pub async fn from_environment() -> Result { + let service_account_key = + crate::read_service_account_key(std::env::var("GOOGLE_APPLICATION_CREDENTIALS")?) + .await + .unwrap(); + + Ok(ServiceAccountFlowOpts { + key: service_account_key, + subject: None, + }) + } + + /// Use the builder pattern to deduce which model of authenticator should be used: + /// Service account one or GCE instance metadata kind + #[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))] + #[cfg_attr( + yup_oauth2_docsrs, + doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))) + )] + pub async fn builder( + opts: ApplicationDefaultCredentialsFlowOpts, + ) -> ApplicationDefaultCredentialsTypes { + Self::with_client(DefaultHyperClient, opts).await + } + + /// Use the builder pattern to deduce which model of authenticator should be used and allow providing a hyper client + pub async fn with_client( + client: C, + opts: ApplicationDefaultCredentialsFlowOpts, + ) -> ApplicationDefaultCredentialsTypes + where + C: HyperClientBuilder, + { + match ApplicationDefaultCredentialsAuthenticator::from_environment().await { + Ok(flow_opts) => { + let builder = AuthenticatorBuilder::new(flow_opts, client); + + ApplicationDefaultCredentialsTypes::ServiceAccount(builder) + } + Err(_) => ApplicationDefaultCredentialsTypes::InstanceMetadata( + AuthenticatorBuilder::new(opts, client), + ), + } + } +} +/// Types of authenticators provided by ApplicationDefaultCredentialsAuthenticator +pub enum ApplicationDefaultCredentialsTypes +where + C: HyperClientBuilder, +{ + /// Service account based authenticator signature + ServiceAccount(AuthenticatorBuilder), + /// GCE Instance Metadata based authenticator signature + InstanceMetadata(AuthenticatorBuilder), +} + /// ## Methods available when building any Authenticator. /// ``` /// # #[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))] @@ -479,7 +562,25 @@ impl AuthenticatorBuilder { } } +impl AuthenticatorBuilder { + /// Create the authenticator. + pub async fn build(self) -> io::Result> + where + C: HyperClientBuilder, + { + let application_default_credential_flow = + ApplicationDefaultCredentialsFlow::new(self.auth_flow); + Self::common_build( + self.hyper_client_builder, + self.storage_type, + AuthFlow::ApplicationDefaultCredentialsFlow(application_default_credential_flow), + ) + .await + } +} + mod private { + use crate::application_default_credentials::ApplicationDefaultCredentialsFlow; use crate::device::DeviceFlow; use crate::error::Error; use crate::installed::InstalledFlow; @@ -490,6 +591,7 @@ mod private { DeviceFlow(DeviceFlow), InstalledFlow(InstalledFlow), ServiceAccountFlow(ServiceAccountFlow), + ApplicationDefaultCredentialsFlow(ApplicationDefaultCredentialsFlow), } impl AuthFlow { @@ -498,6 +600,7 @@ mod private { AuthFlow::DeviceFlow(device_flow) => Some(&device_flow.app_secret), AuthFlow::InstalledFlow(installed_flow) => Some(&installed_flow.app_secret), AuthFlow::ServiceAccountFlow(_) => None, + AuthFlow::ApplicationDefaultCredentialsFlow(_) => None, } } @@ -518,6 +621,9 @@ mod private { AuthFlow::ServiceAccountFlow(service_account_flow) => { service_account_flow.token(hyper_client, scopes).await } + AuthFlow::ApplicationDefaultCredentialsFlow(service_account_flow) => { + service_account_flow.token(hyper_client, scopes).await + } } } } diff --git a/src/lib.rs b/src/lib.rs index 78d7491..f4c298a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -72,6 +72,7 @@ #![deny(missing_docs)] #![cfg_attr(yup_oauth2_docsrs, feature(doc_cfg))] +mod application_default_credentials; pub mod authenticator; pub mod authenticator_delegate; mod device; @@ -89,12 +90,14 @@ mod types; #[doc(inline)] pub use crate::authenticator::{ - DeviceFlowAuthenticator, InstalledFlowAuthenticator, ServiceAccountAuthenticator, + ApplicationDefaultCredentialsAuthenticator, DeviceFlowAuthenticator, + InstalledFlowAuthenticator, ServiceAccountAuthenticator, }; pub use crate::helper::*; pub use crate::installed::InstalledFlowReturnMethod; +pub use crate::application_default_credentials::ApplicationDefaultCredentialsFlowOpts; pub use crate::service_account::ServiceAccountKey; #[doc(inline)] diff --git a/tests/tests.rs b/tests/tests.rs index 75f0ab5..846e51e 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -2,6 +2,7 @@ use yup_oauth2::{ authenticator::{DefaultAuthenticator, DefaultHyperClient, HyperClientBuilder}, authenticator_delegate::{DeviceAuthResponse, DeviceFlowDelegate, InstalledFlowDelegate}, error::{AuthError, AuthErrorCode}, + ApplicationDefaultCredentialsAuthenticator, ApplicationDefaultCredentialsFlowOpts, ApplicationSecret, DeviceFlowAuthenticator, Error, InstalledFlowAuthenticator, InstalledFlowReturnMethod, ServiceAccountAuthenticator, ServiceAccountKey, }; @@ -596,3 +597,38 @@ async fn test_disk_storage() { assert_eq!(token1.as_str(), "accesstoken"); assert_eq!(token1, token2); } + +#[tokio::test] +async fn test_default_application_credentials_from_metadata_server() { + use yup_oauth2::authenticator::ApplicationDefaultCredentialsTypes; + let _ = env_logger::try_init(); + let server = Server::run(); + server.expect( + Expectation::matching(all_of![ + request::method_path("GET", "/token"), + request::query(url_decoded(all_of![contains(( + "scopes", + "https://googleapis.com/some/scope" + ))])) + ]) + .respond_with(json_encoded(serde_json::json!({ + "access_token": "accesstoken", + "refresh_token": "refreshtoken", + "token_type": "Bearer", + "expires_in": 12345678, + }))), + ); + + let opts = ApplicationDefaultCredentialsFlowOpts { + metadata_url: Some(server.url("/token").to_string()), + }; + let authenticator = match ApplicationDefaultCredentialsAuthenticator::builder(opts).await { + ApplicationDefaultCredentialsTypes::InstanceMetadata(auth) => auth.build().await.unwrap(), + _ => panic!("We are not testing service account adc model"), + }; + let token = authenticator + .token(&["https://googleapis.com/some/scope"]) + .await + .unwrap(); + assert_eq!(token.as_str(), "accesstoken"); +}