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
This commit is contained in:
Simonas Kazlauskas
2021-06-28 15:20:35 +03:00
parent ac0e5f606a
commit 4e54fbaeaf
7 changed files with 88 additions and 72 deletions

View File

@@ -16,7 +16,7 @@ where
let request = http::Request::get("https://example.com") let request = http::Request::get("https://example.com")
.header( .header(
http::header::AUTHORIZATION, http::header::AUTHORIZATION,
format!("Bearer {}", token.as_str()), format!("Bearer {}", token.access_token().ok_or("no access token")?),
) )
.body(hyper::body::Body::empty())?; .body(hyper::body::Body::empty())?;
let response = client.request(request).await?; let response = client.request(request).await?;

View File

@@ -6,7 +6,7 @@ use crate::installed::{InstalledFlow, InstalledFlowReturnMethod};
use crate::refresh::RefreshFlow; use crate::refresh::RefreshFlow;
use crate::service_account::{ServiceAccountFlow, ServiceAccountFlowOpts, ServiceAccountKey}; use crate::service_account::{ServiceAccountFlow, ServiceAccountFlowOpts, ServiceAccountKey};
use crate::storage::{self, Storage, TokenStorage}; use crate::storage::{self, Storage, TokenStorage};
use crate::types::{AccessToken, ApplicationSecret, TokenInfo}; use crate::types::{ApplicationSecret, Token, TokenInfo};
use private::AuthFlow; use private::AuthFlow;
use futures::lock::Mutex; use futures::lock::Mutex;
@@ -53,7 +53,7 @@ where
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static, C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
{ {
/// Return the current token for the provided scopes. /// Return the current token for the provided scopes.
pub async fn token<'a, T>(&'a self, scopes: &'a [T]) -> Result<AccessToken, Error> pub async fn token<'a, T>(&'a self, scopes: &'a [T]) -> Result<Token, Error>
where where
T: AsRef<str>, T: AsRef<str>,
{ {
@@ -62,10 +62,7 @@ where
/// Return a token for the provided scopes, but don't reuse cached tokens. Instead, /// Return a token for the provided scopes, but don't reuse cached tokens. Instead,
/// always fetch a new token from the OAuth server. /// always fetch a new token from the OAuth server.
pub async fn force_refreshed_token<'a, T>( pub async fn force_refreshed_token<'a, T>(&'a self, scopes: &'a [T]) -> Result<Token, Error>
&'a self,
scopes: &'a [T],
) -> Result<AccessToken, Error>
where where
T: AsRef<str>, T: AsRef<str>,
{ {
@@ -77,7 +74,7 @@ where
&'a self, &'a self,
scopes: &'a [T], scopes: &'a [T],
force_refresh: bool, force_refresh: bool,
) -> Result<AccessToken, Error> ) -> Result<Token, Error>
where where
T: AsRef<str>, T: AsRef<str>,
{ {

View File

@@ -99,4 +99,4 @@ pub use crate::service_account::ServiceAccountKey;
#[doc(inline)] #[doc(inline)]
pub use crate::error::Error; pub use crate::error::Error;
pub use crate::types::{AccessToken, ApplicationSecret, ConsoleApplicationSecret}; pub use crate::types::{ApplicationSecret, ConsoleApplicationSecret, Token};

View File

@@ -218,7 +218,7 @@ mod tests {
const TEST_PRIVATE_KEY_PATH: &'static str = "examples/Sanguine-69411a0c0eea.json"; const TEST_PRIVATE_KEY_PATH: &'static str = "examples/Sanguine-69411a0c0eea.json";
// Uncomment this test to verify that we can successfully obtain tokens. // Uncomment this test to verify that we can successfully obtain tokens.
//#[tokio::test] // #[tokio::test]
#[allow(dead_code)] #[allow(dead_code)]
async fn test_service_account_e2e() { async fn test_service_account_e2e() {
let key = read_service_account_key(TEST_PRIVATE_KEY_PATH) 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"]) acc.token(&client, &["https://www.googleapis.com/auth/pubsub"])
.await .await
); );
println!(
"{:?}",
acc.token(
&client,
&["https://some.scope/likely-to-hand-out-id-tokens"]
)
.await
);
} }
#[tokio::test] #[tokio::test]

View File

@@ -449,7 +449,8 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_disk_storage() { async fn test_disk_storage() {
let new_token = |access_token: &str| TokenInfo { 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, refresh_token: None,
expires_at: None, expires_at: None,
}; };

View File

@@ -3,49 +3,56 @@ use crate::error::{AuthErrorOr, Error};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Represents an access token returned by oauth2 servers. All access tokens are /// Represents a token returned by oauth2 servers. All tokens are Bearer tokens. Other types of
/// Bearer tokens. Other types of tokens are not supported. /// tokens are not supported.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
pub struct AccessToken { pub struct Token {
value: String, id_token: Option<String>,
access_token: Option<String>,
expires_at: Option<DateTime<Utc>>, expires_at: Option<DateTime<Utc>>,
} }
impl AccessToken { impl Token {
/// A string representation of the access token. /// A string representation of the ID token.
pub fn as_str(&self) -> &str { pub fn id_token(&self) -> Option<&str> {
&self.value 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<DateTime<Utc>> { pub fn expiration_time(&self) -> Option<DateTime<Utc>> {
self.expires_at self.expires_at
} }
/// Determine if the access token is expired. /// 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 /// This will report that the token is expired 1 minute prior to the expiration time to ensure
/// server it's still valid. /// that when the token is actually sent to the server it's still valid.
pub fn is_expired(&self) -> bool { pub fn is_expired(&self) -> bool {
// Consider the token expired if it's within 1 minute of it's expiration // Consider the token expired if it's within 1 minute of it's expiration time.
// time.
self.expires_at self.expires_at
.map(|expiration_time| expiration_time - chrono::Duration::minutes(1) <= Utc::now()) .map(|expiration_time| expiration_time - chrono::Duration::minutes(1) <= Utc::now())
.unwrap_or(false) .unwrap_or(false)
} }
} }
impl AsRef<str> for AccessToken { impl From<TokenInfo> for Token {
fn as_ref(&self) -> &str { fn from(
self.as_str() TokenInfo {
} access_token,
} id_token,
expires_at,
impl From<TokenInfo> for AccessToken { ..
fn from(value: TokenInfo) -> Self { }: TokenInfo,
AccessToken { ) -> Self {
value: value.access_token, Token {
expires_at: value.expires_at, access_token,
id_token,
expires_at,
} }
} }
} }
@@ -53,12 +60,13 @@ impl From<TokenInfo> for AccessToken {
/// Represents a token as returned by OAuth2 servers. /// Represents a token as returned by OAuth2 servers.
/// ///
/// It is produced by all authentication flows. /// It is produced by all authentication flows.
/// It authenticates certain operations, and must be refreshed once /// It authenticates certain operations, and must be refreshed once it reached it's expiry date.
/// it reached it's expiry date.
#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] #[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
pub struct TokenInfo { pub struct TokenInfo {
/// used when authorizing calls to oauth2 enabled services.
pub access_token: Option<String>,
/// used when authenticating calls to oauth2 enabled services. /// used when authenticating calls to oauth2 enabled services.
pub access_token: String, pub id_token: Option<String>,
/// used to refresh an expired access_token. /// used to refresh an expired access_token.
pub refresh_token: Option<String>, pub refresh_token: Option<String>,
/// The time when the token expires. /// The time when the token expires.
@@ -68,38 +76,40 @@ pub struct TokenInfo {
impl TokenInfo { impl TokenInfo {
pub(crate) fn from_json(json_data: &[u8]) -> Result<TokenInfo, Error> { pub(crate) fn from_json(json_data: &[u8]) -> Result<TokenInfo, Error> {
#[derive(Deserialize)] #[derive(Deserialize)]
struct RawToken { struct TokenSchema<'a> {
access_token: String, id_token: Option<&'a str>,
refresh_token: Option<String>, access_token: Option<&'a str>,
token_type: String, refresh_token: Option<&'a str>,
token_type: Option<&'a str>,
expires_in: Option<i64>, expires_in: Option<i64>,
} }
let TokenSchema {
let RawToken { id_token,
access_token, access_token,
refresh_token, refresh_token,
token_type, token_type,
expires_in, expires_in,
} = serde_json::from_slice::<AuthErrorOr<RawToken>>(json_data)?.into_result()?; } = serde_json::from_slice::<AuthErrorOr<_>>(json_data)?.into_result()?;
match token_type {
if token_type.to_lowercase().as_str() != "bearer" { Some(token_ty) if !token_ty.eq_ignore_ascii_case("bearer") => {
use std::io; use std::io;
return Err(io::Error::new( return Err(io::Error::new(
io::ErrorKind::InvalidData, io::ErrorKind::InvalidData,
format!( format!(
r#"unknown token type returned; expected "bearer" found {}"#, r#"unknown token type returned; expected "bearer" found {}"#,
token_type token_ty
), ),
) )
.into()); .into());
}
_ => (),
} }
let expires_at = expires_in let expires_at = expires_in
.map(|seconds_from_now| Utc::now() + chrono::Duration::seconds(seconds_from_now)); .map(|seconds_from_now| Utc::now() + chrono::Duration::seconds(seconds_from_now));
Ok(TokenInfo { Ok(TokenInfo {
access_token, id_token: id_token.map(String::from),
refresh_token, access_token: access_token.map(String::from),
refresh_token: refresh_token.map(String::from),
expires_at, expires_at,
}) })
} }

View File

@@ -92,7 +92,7 @@ async fn test_device_success() {
.token(&["https://www.googleapis.com/scope/1"]) .token(&["https://www.googleapis.com/scope/1"])
.await .await
.expect("token failed"); .expect("token failed");
assert_eq!("accesstoken", token.as_str()); assert_eq!("accesstoken", token.access_token().expect("should have access token"));
} }
#[tokio::test] #[tokio::test]
@@ -252,7 +252,7 @@ async fn test_installed_interactive_success() {
.token(&["https://googleapis.com/some/scope"]) .token(&["https://googleapis.com/some/scope"])
.await .await
.expect("failed to get token"); .expect("failed to get token");
assert_eq!("accesstoken", tok.as_str()); assert_eq!("accesstoken", tok.access_token().expect("should have access token"));
} }
#[tokio::test] #[tokio::test]
@@ -281,7 +281,7 @@ async fn test_installed_redirect_success() {
.token(&["https://googleapis.com/some/scope"]) .token(&["https://googleapis.com/some/scope"])
.await .await
.expect("failed to get token"); .expect("failed to get token");
assert_eq!("accesstoken", tok.as_str()); assert_eq!("accesstoken", tok.access_token().expect("should have access token"));
} }
#[tokio::test] #[tokio::test]
@@ -350,7 +350,7 @@ async fn test_service_account_success() {
.token(&["https://www.googleapis.com/auth/pubsub"]) .token(&["https://www.googleapis.com/auth/pubsub"])
.await .await
.expect("token failed"); .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()); 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"]) .token(&["https://googleapis.com/some/scope"])
.await .await
.expect("failed to get token"); .expect("failed to get token");
assert_eq!("accesstoken", tok.as_str()); assert_eq!("accesstoken", tok.access_token().expect("should have access token"));
server.expect( server.expect(
Expectation::matching(all_of![ Expectation::matching(all_of![
@@ -422,7 +422,7 @@ async fn test_refresh() {
.token(&["https://googleapis.com/some/scope"]) .token(&["https://googleapis.com/some/scope"])
.await .await
.expect("failed to get token"); .expect("failed to get token");
assert_eq!("accesstoken2", tok.as_str()); assert_eq!("accesstoken2", tok.access_token().expect("should have access token"));
server.expect( server.expect(
Expectation::matching(all_of![ Expectation::matching(all_of![
@@ -443,7 +443,7 @@ async fn test_refresh() {
.token(&["https://googleapis.com/some/scope"]) .token(&["https://googleapis.com/some/scope"])
.await .await
.expect("failed to get token"); .expect("failed to get token");
assert_eq!("accesstoken3", tok.as_str()); assert_eq!("accesstoken3", tok.access_token().expect("should have access token"));
server.expect( server.expect(
Expectation::matching(all_of![ Expectation::matching(all_of![
@@ -503,7 +503,7 @@ async fn test_memory_storage() {
.token(&["https://googleapis.com/some/scope"]) .token(&["https://googleapis.com/some/scope"])
.await .await
.expect("failed to get token"); .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); assert_eq!(token1, token2);
// Create a new authenticator. This authenticator does not share a cache // 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"]) .token(&["https://googleapis.com/some/scope"])
.await .await
.expect("failed to get token"); .expect("failed to get token");
assert_eq!(token3.as_str(), "accesstoken2"); assert_eq!(token3.access_token().expect("should have access token"), "accesstoken2");
} }
#[tokio::test] #[tokio::test]
@@ -571,7 +571,7 @@ async fn test_disk_storage() {
.token(&["https://googleapis.com/some/scope"]) .token(&["https://googleapis.com/some/scope"])
.await .await
.expect("failed to get token"); .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); assert_eq!(token1, token2);
} }
@@ -593,6 +593,6 @@ async fn test_disk_storage() {
.token(&["https://googleapis.com/some/scope"]) .token(&["https://googleapis.com/some/scope"])
.await .await
.expect("failed to get token"); .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); assert_eq!(token1, token2);
} }