Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support id tokens from metadata service #209

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]

name = "yup-oauth2"
version = "8.3.0"
version = "9.0.0"
authors = ["Sebastian Thiel <[email protected]>", "Lewin Bormann <[email protected]>"]
repository = "https://github.com/dermesser/yup-oauth2"
description = "An oauth2 implementation, providing the 'device', 'service account' and 'installed' authorization flows"
Expand Down
38 changes: 33 additions & 5 deletions src/application_default_credentials.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use crate::error::Error;
use crate::types::TokenInfo;
use http::Uri;
use hyper::client::connect::Connection;
use std::error::Error as StdError;
use http::Uri;
use tokio::io::{AsyncRead, AsyncWrite};
use tower_service::Service;

Expand All @@ -11,16 +11,28 @@ use tower_service::Service;
pub struct ApplicationDefaultCredentialsFlowOpts {
/// Used as base to build the url during token request from GCP metadata server
pub metadata_url: Option<String>,
/// If true, asks for an ID token instead of an OAuth access token.
pub id_token: bool,
}

pub struct ApplicationDefaultCredentialsFlow {
metadata_url: String,
id_token: bool,
}

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 }
let id_token = opts.id_token;
let metadata_url = opts.metadata_url
.unwrap_or_else(|| if id_token {
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity".to_string()
} else {
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token".to_string()
});
ApplicationDefaultCredentialsFlow {
metadata_url,
id_token,
}
}

pub(crate) async fn token<S, T>(
Expand All @@ -36,7 +48,14 @@ impl ApplicationDefaultCredentialsFlow {
S::Error: Into<Box<dyn StdError + Send + Sync>>,
{
let scope = crate::helper::join(scopes, ",");
let token_uri = format!("{}?scopes={}", self.metadata_url, scope);
let token_uri = format!(
"{}?{}={}",
self.metadata_url,
// For ID tokens we use the scope arguments to pass the audience. Bit of a hack, since
// this was originally designed only for the access tokens.
if self.id_token { "audience" } else { "scopes" },
scope
);
let request = hyper::Request::get(token_uri)
.header("Metadata-Flavor", "Google")
.body(hyper::Body::from(String::new())) // why body is needed?
Expand All @@ -45,7 +64,16 @@ impl ApplicationDefaultCredentialsFlow {
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)
if self.id_token {
Ok(TokenInfo {
id_token: Some(String::from_utf8(body.to_vec())?),
access_token: None,
refresh_token: None,
expires_at: None,
})
} else {
TokenInfo::from_json(&body)
}
}
}

Expand Down
9 changes: 9 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ pub enum Error {
AuthError(AuthError),
/// Error while decoding a JSON response.
JSONError(serde_json::Error),
/// Error while decoding a text response.
Utf8Error(std::string::FromUtf8Error),
/// Error within user input.
UserError(String),
/// A lower level IO error.
Expand All @@ -160,6 +162,12 @@ pub enum Error {
OtherError(anyhow::Error),
}

impl From<std::string::FromUtf8Error> for Error {
fn from(v: std::string::FromUtf8Error) -> Self {
Self::Utf8Error(v)
}
}

impl From<hyper::Error> for Error {
fn from(error: hyper::Error) -> Error {
Error::HttpError(error)
Expand Down Expand Up @@ -215,6 +223,7 @@ impl fmt::Display for Error {
)?;
Ok(())
}
Error::Utf8Error(ref err) => err.fmt(f),
Error::OtherError(ref e) => e.fmt(f),
}
}
Expand Down
100 changes: 73 additions & 27 deletions tests/tests.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use yup_oauth2::{
authenticator::{DefaultAuthenticator, DefaultHyperClient, HyperClientBuilder},
authenticator_delegate::{DeviceAuthResponse, DeviceFlowDelegate, InstalledFlowDelegate},
ApplicationDefaultCredentialsAuthenticator, ApplicationDefaultCredentialsFlowOpts,
ApplicationSecret, DeviceFlowAuthenticator, InstalledFlowAuthenticator,
InstalledFlowReturnMethod, ServiceAccountAuthenticator, ServiceAccountKey,
AccessTokenAuthenticator,
AccessTokenAuthenticator, ApplicationDefaultCredentialsAuthenticator,
ApplicationDefaultCredentialsFlowOpts, ApplicationSecret, DeviceFlowAuthenticator,
InstalledFlowAuthenticator, InstalledFlowReturnMethod, ServiceAccountAuthenticator,
ServiceAccountKey,
};

use std::future::Future;
Expand Down Expand Up @@ -93,7 +93,10 @@ async fn test_device_success() {
.token(&["https://www.googleapis.com/scope/1"])
.await
.expect("token failed");
assert_eq!("accesstoken", token.token().expect("should have access token"));
assert_eq!(
"accesstoken",
token.token().expect("should have access token")
);
}

#[tokio::test]
Expand Down Expand Up @@ -215,7 +218,7 @@ async fn create_installed_flow_auth(
}
}

let client = DefaultHyperClient.build_test_hyper_client();
let client = DefaultHyperClient.build_test_hyper_client();
let mut builder = InstalledFlowAuthenticator::with_client(app_secret, method, client.clone())
.flow_delegate(Box::new(FD(client)));

Expand Down Expand Up @@ -254,7 +257,10 @@ async fn test_installed_interactive_success() {
.token(&["https://googleapis.com/some/scope"])
.await
.expect("failed to get token");
assert_eq!("accesstoken", tok.token().expect("should have access token"));
assert_eq!(
"accesstoken",
tok.token().expect("should have access token")
);
}

#[tokio::test]
Expand Down Expand Up @@ -283,7 +289,10 @@ async fn test_installed_redirect_success() {
.token(&["https://googleapis.com/some/scope"])
.await
.expect("failed to get token");
assert_eq!("accesstoken", tok.token().expect("should have access token"));
assert_eq!(
"accesstoken",
tok.token().expect("should have access token")
);
}

#[tokio::test]
Expand Down Expand Up @@ -352,8 +361,13 @@ async fn test_service_account_success() {
.token(&["https://www.googleapis.com/auth/pubsub"])
.await
.expect("token failed");
assert!(tok.token().expect("should have access token").contains("ya29.c.ElouBywiys0Ly"));
assert!(OffsetDateTime::now_utc() + time::Duration::seconds(3600) >= tok.expiration_time().unwrap());
assert!(tok
.token()
.expect("should have access token")
.contains("ya29.c.ElouBywiys0Ly"));
assert!(
OffsetDateTime::now_utc() + time::Duration::seconds(3600) >= tok.expiration_time().unwrap()
);
}

#[tokio::test]
Expand Down Expand Up @@ -403,7 +417,10 @@ async fn test_refresh() {
.token(&["https://googleapis.com/some/scope"])
.await
.expect("failed to get token");
assert_eq!("accesstoken", tok.token().expect("should have access token"));
assert_eq!(
"accesstoken",
tok.token().expect("should have access token")
);

server.expect(
Expectation::matching(all_of![
Expand All @@ -424,7 +441,10 @@ async fn test_refresh() {
.token(&["https://googleapis.com/some/scope"])
.await
.expect("failed to get token");
assert_eq!("accesstoken2", tok.token().expect("should have access token"));
assert_eq!(
"accesstoken2",
tok.token().expect("should have access token")
);

server.expect(
Expectation::matching(all_of![
Expand All @@ -445,7 +465,10 @@ async fn test_refresh() {
.token(&["https://googleapis.com/some/scope"])
.await
.expect("failed to get token");
assert_eq!("accesstoken3", tok.token().expect("should have access token"));
assert_eq!(
"accesstoken3",
tok.token().expect("should have access token")
);

// Refresh fails, but renewing the token succeeds.
// PR #165
Expand Down Expand Up @@ -477,9 +500,7 @@ async fn test_refresh() {
}))),
);

let tok_err = auth
.token(&["https://googleapis.com/some/scope"])
.await;
let tok_err = auth.token(&["https://googleapis.com/some/scope"]).await;
assert!(tok_err.is_ok());
}

Expand Down Expand Up @@ -515,7 +536,10 @@ async fn test_memory_storage() {
.token(&["https://googleapis.com/some/scope"])
.await
.expect("failed to get token");
assert_eq!(token1.token().expect("should have access token"), "accesstoken");
assert_eq!(
token1.token().expect("should have access token"),
"accesstoken"
);
assert_eq!(token1, token2);

// Create a new authenticator. This authenticator does not share a cache
Expand All @@ -541,7 +565,10 @@ async fn test_memory_storage() {
.token(&["https://googleapis.com/some/scope"])
.await
.expect("failed to get token");
assert_eq!(token3.token().expect("should have access token"), "accesstoken2");
assert_eq!(
token3.token().expect("should have access token"),
"accesstoken2"
);
}

#[tokio::test]
Expand Down Expand Up @@ -583,7 +610,10 @@ async fn test_disk_storage() {
.token(&["https://googleapis.com/some/scope"])
.await
.expect("failed to get token");
assert_eq!(token1.token().expect("should have access token"), "accesstoken");
assert_eq!(
token1.token().expect("should have access token"),
"accesstoken"
);
assert_eq!(token1, token2);
}

Expand All @@ -605,7 +635,10 @@ async fn test_disk_storage() {
.token(&["https://googleapis.com/some/scope"])
.await
.expect("failed to get token");
assert_eq!(token1.token().expect("should have access token"), "accesstoken");
assert_eq!(
token1.token().expect("should have access token"),
"accesstoken"
);
assert_eq!(token1, token2);
}

Expand All @@ -632,27 +665,40 @@ async fn test_default_application_credentials_from_metadata_server() {

let opts = ApplicationDefaultCredentialsFlowOpts {
metadata_url: Some(server.url("/token").to_string()),
..Default::default()
};
let authenticator = match ApplicationDefaultCredentialsAuthenticator::with_client(opts, DefaultHyperClient.build_test_hyper_client()).await {
let authenticator = match ApplicationDefaultCredentialsAuthenticator::with_client(
opts,
DefaultHyperClient.build_test_hyper_client(),
)
.await
{
ApplicationDefaultCredentialsTypes::InstanceMetadata(auth) => auth.build().await.unwrap(),
_ => panic!("We are not testing service account adc model"),
};
let access_token = authenticator
.token(&["https://googleapis.com/some/scope"])
.await
.unwrap();
assert_eq!(access_token.token().expect("should have access token"), "accesstoken");
assert_eq!(
access_token.token().expect("should have access token"),
"accesstoken"
);
}

#[tokio::test]
async fn test_token() {
let authenticator = AccessTokenAuthenticator::with_client("0815".to_string(), DefaultHyperClient)
.build()
.await
.unwrap();
let authenticator =
AccessTokenAuthenticator::with_client("0815".to_string(), DefaultHyperClient)
.build()
.await
.unwrap();
let access_token = authenticator
.token(&["https://googleapis.com/some/scope"])
.await
.unwrap();
assert_eq!(access_token.token().expect("should have access token"), "0815".to_string());
assert_eq!(
access_token.token().expect("should have access token"),
"0815".to_string()
);
}
Loading