From 4e54fbaeaf7b0082e7146568936a22993207c7dd Mon Sep 17 00:00:00 2001 From: Simonas Kazlauskas Date: Mon, 28 Jun 2021 15:20:35 +0300 Subject: [PATCH] Implement support for ID tokens For google stuff these are relevant when trying to invoke e.g. Cloud Run services. I'm not at all knowledgeable enough with OAuth to be able to tell if what I'm doing here is correct. This is a breaking change. `AccessToken` got renamed to just `Token` (since it now encompasses more than just `access_token` and there are some changes to the `TokenInfo` type too. Sponsored by: standard.ai --- examples/custom_client.rs | 2 +- src/authenticator.rs | 11 ++-- src/lib.rs | 2 +- src/service_account.rs | 10 +++- src/storage.rs | 3 +- src/types.rs | 110 +++++++++++++++++++++----------------- tests/tests.rs | 22 ++++---- 7 files changed, 88 insertions(+), 72 deletions(-) diff --git a/examples/custom_client.rs b/examples/custom_client.rs index 3294cc2..5847d6d 100644 --- a/examples/custom_client.rs +++ b/examples/custom_client.rs @@ -16,7 +16,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/authenticator.rs b/src/authenticator.rs index d3d72e4..fa51c52 100644 --- a/src/authenticator.rs +++ b/src/authenticator.rs @@ -6,7 +6,7 @@ use crate::installed::{InstalledFlow, InstalledFlowReturnMethod}; use crate::refresh::RefreshFlow; use crate::service_account::{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 futures::lock::Mutex; @@ -53,7 +53,7 @@ where C: hyper::client::connect::Connect + Clone + Send + Sync + 'static, { /// 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, { @@ -62,10 +62,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, { @@ -77,7 +74,7 @@ where &'a self, scopes: &'a [T], force_refresh: bool, - ) -> Result + ) -> Result where T: AsRef, { diff --git a/src/lib.rs b/src/lib.rs index 78d7491..2e55171 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -99,4 +99,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 1c11863..7fd0a18 100644 --- a/src/service_account.rs +++ b/src/service_account.rs @@ -218,7 +218,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 key = read_service_account_key(TEST_PRIVATE_KEY_PATH) @@ -232,6 +232,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 0dceb3b..40b5582 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, }; diff --git a/src/types.rs b/src/types.rs index f29286f..11344af 100644 --- a/src/types.rs +++ b/src/types.rs @@ -3,49 +3,56 @@ use crate::error::{AuthErrorOr, Error}; use chrono::{DateTime, Utc}; 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 - chrono::Duration::minutes(1) <= Utc::now()) .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,13 @@ 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 authorizing calls to oauth2 enabled services. + pub access_token: Option, /// used when authenticating calls to oauth2 enabled services. - pub access_token: String, + pub id_token: Option, /// used to refresh an expired access_token. pub refresh_token: Option, /// The time when the token expires. @@ -68,38 +76,40 @@ pub struct TokenInfo { impl TokenInfo { pub(crate) fn from_json(json_data: &[u8]) -> Result { #[derive(Deserialize)] - struct RawToken { - access_token: String, - refresh_token: Option, - token_type: String, + struct TokenSchema<'a> { + id_token: Option<&'a str>, + access_token: Option<&'a str>, + refresh_token: Option<&'a str>, + token_type: Option<&'a str>, expires_in: Option, } - - let RawToken { + let TokenSchema { + id_token, access_token, refresh_token, token_type, expires_in, - } = serde_json::from_slice::>(json_data)?.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()); + } = serde_json::from_slice::>(json_data)?.into_result()?; + 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| Utc::now() + chrono::Duration::seconds(seconds_from_now)); - Ok(TokenInfo { - access_token, - refresh_token, + id_token: id_token.map(String::from), + access_token: access_token.map(String::from), + refresh_token: refresh_token.map(String::from), expires_at, }) } diff --git a/tests/tests.rs b/tests/tests.rs index 75f0ab5..45d6ca5 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -92,7 +92,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] @@ -252,7 +252,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] @@ -281,7 +281,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] @@ -350,7 +350,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!(Utc::now() + chrono::Duration::seconds(3600) >= tok.expiration_time().unwrap()); } @@ -401,7 +401,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![ @@ -422,7 +422,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![ @@ -443,7 +443,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")); server.expect( Expectation::matching(all_of![ @@ -503,7 +503,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 @@ -529,7 +529,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] @@ -571,7 +571,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); } @@ -593,6 +593,6 @@ 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); }