diff --git a/examples/custom_client.rs b/examples/custom_client.rs index 6d9c1d2..102aa46 100644 --- a/examples/custom_client.rs +++ b/examples/custom_client.rs @@ -26,7 +26,7 @@ where let request = http::Request::get("https://example.com") .header( http::header::AUTHORIZATION, - format!("Bearer {}", token.as_str()), + format!("Bearer {}", token.access_token().ok_or("no access token")?), ) .body(hyper::body::Body::empty())?; let response = client.request(request).await?; diff --git a/src/access_token.rs b/src/access_token.rs index af83b5d..6512b7c 100644 --- a/src/access_token.rs +++ b/src/access_token.rs @@ -33,7 +33,7 @@ impl AccessTokenFlow { S::Error: Into>, { Ok(TokenInfo { - access_token: self.access_token.clone(), + access_token: Some(self.access_token.clone()), refresh_token: None, expires_at: None, id_token: None, diff --git a/src/authenticator.rs b/src/authenticator.rs index 8325905..efd2e01 100644 --- a/src/authenticator.rs +++ b/src/authenticator.rs @@ -12,7 +12,7 @@ use crate::refresh::RefreshFlow; #[cfg(feature = "service_account")] use crate::service_account::{self, ServiceAccountFlow, ServiceAccountFlowOpts, ServiceAccountKey}; use crate::storage::{self, Storage, TokenStorage}; -use crate::types::{AccessToken, ApplicationSecret, TokenInfo}; +use crate::types::{ApplicationSecret, Token, TokenInfo}; use private::AuthFlow; use crate::access_token::{AccessTokenFlow}; @@ -69,7 +69,7 @@ where S::Error: Into>, { /// Return the current token for the provided scopes. - pub async fn token<'a, T>(&'a self, scopes: &'a [T]) -> Result + pub async fn token<'a, T>(&'a self, scopes: &'a [T]) -> Result where T: AsRef, { @@ -80,10 +80,7 @@ where /// Return a token for the provided scopes, but don't reuse cached tokens. Instead, /// always fetch a new token from the OAuth server. - pub async fn force_refreshed_token<'a, T>( - &'a self, - scopes: &'a [T], - ) -> Result + pub async fn force_refreshed_token<'a, T>(&'a self, scopes: &'a [T]) -> Result where T: AsRef, { diff --git a/src/lib.rs b/src/lib.rs index b274277..405a9fe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -111,4 +111,4 @@ pub use crate::service_account::ServiceAccountKey; #[doc(inline)] pub use crate::error::Error; -pub use crate::types::{AccessToken, ApplicationSecret, ConsoleApplicationSecret}; +pub use crate::types::{ApplicationSecret, ConsoleApplicationSecret, Token}; diff --git a/src/service_account.rs b/src/service_account.rs index dd30d57..e888f85 100644 --- a/src/service_account.rs +++ b/src/service_account.rs @@ -240,7 +240,7 @@ mod tests { const TEST_PRIVATE_KEY_PATH: &'static str = "examples/Sanguine-69411a0c0eea.json"; // Uncomment this test to verify that we can successfully obtain tokens. - //#[tokio::test] + // #[tokio::test] #[allow(dead_code)] async fn test_service_account_e2e() { let acc = ServiceAccountFlow::new(ServiceAccountFlowOpts { @@ -262,6 +262,14 @@ mod tests { acc.token(&client, &["https://www.googleapis.com/auth/pubsub"]) .await ); + println!( + "{:?}", + acc.token( + &client, + &["https://some.scope/likely-to-hand-out-id-tokens"] + ) + .await + ); } #[tokio::test] diff --git a/src/storage.rs b/src/storage.rs index deee7cc..d46a945 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -449,7 +449,8 @@ mod tests { #[tokio::test] async fn test_disk_storage() { let new_token = |access_token: &str| TokenInfo { - access_token: access_token.to_owned(), + id_token: None, + access_token: Some(access_token.to_owned()), refresh_token: None, expires_at: None, id_token: None, diff --git a/src/types.rs b/src/types.rs index a62d8f4..485e9a4 100644 --- a/src/types.rs +++ b/src/types.rs @@ -3,49 +3,56 @@ use crate::error::{AuthErrorOr, Error}; use time::OffsetDateTime; use serde::{Deserialize, Serialize}; -/// Represents an access token returned by oauth2 servers. All access tokens are -/// Bearer tokens. Other types of tokens are not supported. +/// Represents a token returned by oauth2 servers. All tokens are Bearer tokens. Other types of +/// tokens are not supported. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)] -pub struct AccessToken { - value: String, +pub struct Token { + id_token: Option, + access_token: Option, expires_at: Option, } -impl AccessToken { - /// A string representation of the access token. - pub fn as_str(&self) -> &str { - &self.value +impl Token { + /// A string representation of the ID token. + pub fn id_token(&self) -> Option<&str> { + self.id_token.as_deref() } - /// The time the access token will expire, if any. + /// A string representation of the access token. + pub fn access_token(&self) -> Option<&str> { + self.access_token.as_deref() + } + + /// The time at which the tokens will expire, if any. pub fn expiration_time(&self) -> Option { self.expires_at } /// Determine if the access token is expired. - /// This will report that the token is expired 1 minute prior to the - /// expiration time to ensure that when the token is actually sent to the - /// server it's still valid. + /// + /// This will report that the token is expired 1 minute prior to the expiration time to ensure + /// that when the token is actually sent to the server it's still valid. pub fn is_expired(&self) -> bool { - // Consider the token expired if it's within 1 minute of it's expiration - // time. + // Consider the token expired if it's within 1 minute of it's expiration time. self.expires_at .map(|expiration_time| expiration_time - time::Duration::minutes(1) <= OffsetDateTime::now_utc()) .unwrap_or(false) } } -impl AsRef for AccessToken { - fn as_ref(&self) -> &str { - self.as_str() - } -} - -impl From for AccessToken { - fn from(value: TokenInfo) -> Self { - AccessToken { - value: value.access_token, - expires_at: value.expires_at, +impl From for Token { + fn from( + TokenInfo { + access_token, + id_token, + expires_at, + .. + }: TokenInfo, + ) -> Self { + Token { + access_token, + id_token, + expires_at, } } } @@ -53,12 +60,11 @@ impl From for AccessToken { /// Represents a token as returned by OAuth2 servers. /// /// It is produced by all authentication flows. -/// It authenticates certain operations, and must be refreshed once -/// it reached it's expiry date. +/// It authenticates certain operations, and must be refreshed once it reached it's expiry date. #[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] pub struct TokenInfo { - /// used when authenticating calls to oauth2 enabled services. - pub access_token: String, + /// used when authorizing calls to oauth2 enabled services. + pub access_token: Option, /// used to refresh an expired access_token. pub refresh_token: Option, /// The time when the token expires. @@ -74,9 +80,9 @@ impl TokenInfo { pub(crate) fn from_json(json_data: &[u8]) -> Result { #[derive(Deserialize)] struct RawToken { - access_token: String, + access_token: Option, refresh_token: Option, - token_type: String, + token_type: Option, expires_in: Option, id_token: Option, } @@ -92,26 +98,29 @@ impl TokenInfo { id_token, } = >::deserialize(raw_token)?.into_result()?; - if token_type.to_lowercase().as_str() != "bearer" { - use std::io; - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!( - r#"unknown token type returned; expected "bearer" found {}"#, - token_type - ), - ) - .into()); + match token_type { + Some(token_ty) if !token_ty.eq_ignore_ascii_case("bearer") => { + use std::io; + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + r#"unknown token type returned; expected "bearer" found {}"#, + token_ty + ), + ) + .into()); + } + _ => (), } let expires_at = expires_in .map(|seconds_from_now| OffsetDateTime::now_utc() + time::Duration::seconds(seconds_from_now)); Ok(TokenInfo { + id_token, access_token, refresh_token, expires_at, - id_token, }) } diff --git a/tests/tests.rs b/tests/tests.rs index aa4756d..aedc76f 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -94,7 +94,7 @@ async fn test_device_success() { .token(&["https://www.googleapis.com/scope/1"]) .await .expect("token failed"); - assert_eq!("accesstoken", token.as_str()); + assert_eq!("accesstoken", token.access_token().expect("should have access token")); } #[tokio::test] @@ -255,7 +255,7 @@ async fn test_installed_interactive_success() { .token(&["https://googleapis.com/some/scope"]) .await .expect("failed to get token"); - assert_eq!("accesstoken", tok.as_str()); + assert_eq!("accesstoken", tok.access_token().expect("should have access token")); } #[tokio::test] @@ -284,7 +284,7 @@ async fn test_installed_redirect_success() { .token(&["https://googleapis.com/some/scope"]) .await .expect("failed to get token"); - assert_eq!("accesstoken", tok.as_str()); + assert_eq!("accesstoken", tok.access_token().expect("should have access token")); } #[tokio::test] @@ -353,7 +353,7 @@ async fn test_service_account_success() { .token(&["https://www.googleapis.com/auth/pubsub"]) .await .expect("token failed"); - assert!(tok.as_str().contains("ya29.c.ElouBywiys0Ly")); + assert!(tok.access_token().expect("should have access token").contains("ya29.c.ElouBywiys0Ly")); assert!(OffsetDateTime::now_utc() + time::Duration::seconds(3600) >= tok.expiration_time().unwrap()); } @@ -404,7 +404,7 @@ async fn test_refresh() { .token(&["https://googleapis.com/some/scope"]) .await .expect("failed to get token"); - assert_eq!("accesstoken", tok.as_str()); + assert_eq!("accesstoken", tok.access_token().expect("should have access token")); server.expect( Expectation::matching(all_of![ @@ -425,7 +425,7 @@ async fn test_refresh() { .token(&["https://googleapis.com/some/scope"]) .await .expect("failed to get token"); - assert_eq!("accesstoken2", tok.as_str()); + assert_eq!("accesstoken2", tok.access_token().expect("should have access token")); server.expect( Expectation::matching(all_of![ @@ -446,7 +446,7 @@ async fn test_refresh() { .token(&["https://googleapis.com/some/scope"]) .await .expect("failed to get token"); - assert_eq!("accesstoken3", tok.as_str()); + assert_eq!("accesstoken3", tok.access_token().expect("should have access token")); // Refresh fails, but renewing the token succeeds. // PR #165 @@ -516,7 +516,7 @@ async fn test_memory_storage() { .token(&["https://googleapis.com/some/scope"]) .await .expect("failed to get token"); - assert_eq!(token1.as_str(), "accesstoken"); + assert_eq!(token1.access_token().expect("should have access token"), "accesstoken"); assert_eq!(token1, token2); // Create a new authenticator. This authenticator does not share a cache @@ -542,7 +542,7 @@ async fn test_memory_storage() { .token(&["https://googleapis.com/some/scope"]) .await .expect("failed to get token"); - assert_eq!(token3.as_str(), "accesstoken2"); + assert_eq!(token3.access_token().expect("should have access token"), "accesstoken2"); } #[tokio::test] @@ -584,7 +584,7 @@ async fn test_disk_storage() { .token(&["https://googleapis.com/some/scope"]) .await .expect("failed to get token"); - assert_eq!(token1.as_str(), "accesstoken"); + assert_eq!(token1.access_token().expect("should have access token"), "accesstoken"); assert_eq!(token1, token2); } @@ -606,7 +606,7 @@ async fn test_disk_storage() { .token(&["https://googleapis.com/some/scope"]) .await .expect("failed to get token"); - assert_eq!(token1.as_str(), "accesstoken"); + assert_eq!(token1.access_token().expect("should have access token"), "accesstoken"); assert_eq!(token1, token2); }