feat(Authenticator client): Accept custom connectors

Update Authenticator to accept clients with custom connectors, rather
than depending on the sealed hyper::client::connect::Connect trait, as recommended by hyper: https://docs.rs/hyper/0.13.8/src/hyper/client/connect/mod.rs.html#256-258

Closes #177.
This commit is contained in:
Kyle Gentle
2022-05-21 00:00:13 -04:00
committed by Kyle Gentle
parent 253528a1fe
commit c76ae18224
9 changed files with 146 additions and 55 deletions

View File

@@ -49,6 +49,7 @@ serde = {version = "1.0", features = ["derive"]}
serde_json = "1.0"
time = { version = "0.3.7", features = ["local-offset", "serde"] }
tokio = { version = "1.0", features = ["fs", "macros", "io-std", "io-util", "time", "sync", "rt"] }
tower-service = "^0.3.1"
url = "2"
[dev-dependencies]

View File

@@ -5,12 +5,22 @@
//!
//! It is also a better use of resources (memory, sockets, etc.)
async fn r#use<C>(
client: hyper::Client<C>,
authenticator: yup_oauth2::authenticator::Authenticator<C>,
use std::error::Error as StdError;
use hyper::client::connect::Connection;
use http::Uri;
use tokio::io::{AsyncRead, AsyncWrite};
use tower_service::Service;
async fn r#use<S>(
client: hyper::Client<S>,
authenticator: yup_oauth2::authenticator::Authenticator<S>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
where
C: Clone + Send + Sync + hyper::client::connect::Connect + 'static,
S: Service<Uri> + Clone + Send + Sync + 'static,
S::Response: Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static,
S::Future: Send + Unpin + 'static,
S::Error: Into<Box<dyn StdError + Send + Sync>>,
{
let token = authenticator.token(&["email"]).await?;
let request = http::Request::get("https://example.com")

View File

@@ -1,5 +1,10 @@
use crate::error::Error;
use crate::types::TokenInfo;
use hyper::client::connect::Connection;
use std::error::Error as StdError;
use http::Uri;
use tokio::io::{AsyncRead, AsyncWrite};
use tower_service::Service;
/// Provide options for the Application Default Credential Flow, mostly used for testing
#[derive(Default, Clone, Debug)]
@@ -18,14 +23,17 @@ impl ApplicationDefaultCredentialsFlow {
ApplicationDefaultCredentialsFlow { metadata_url }
}
pub(crate) async fn token<C, T>(
pub(crate) async fn token<S, T>(
&self,
hyper_client: &hyper::Client<C>,
hyper_client: &hyper::Client<S>,
scopes: &[T],
) -> Result<TokenInfo, Error>
where
T: AsRef<str>,
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
S: Service<Uri> + Clone + Send + Sync + 'static,
S::Response: Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static,
S::Future: Send + Unpin + 'static,
S::Error: Into<Box<dyn StdError + Send + Sync>>,
{
let scope = crate::helper::join(scopes, ",");
let token_uri = format!("{}?scopes={}", self.metadata_url, scope);

View File

@@ -16,14 +16,19 @@ use crate::types::{AccessToken, ApplicationSecret, TokenInfo};
use private::AuthFlow;
use futures::lock::Mutex;
use http::{Uri};
use hyper::client::connect::Connection;
use std::borrow::Cow;
use std::error::Error as StdError;
use std::fmt;
use std::io;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::io::{AsyncRead, AsyncWrite};
use tower_service::Service;
struct InnerAuthenticator<C> {
hyper_client: hyper::Client<C>,
struct InnerAuthenticator<S> {
hyper_client: hyper::Client<S>,
storage: Storage,
auth_flow: AuthFlow,
}
@@ -31,8 +36,8 @@ struct InnerAuthenticator<C> {
/// Authenticator is responsible for fetching tokens, handling refreshing tokens,
/// and optionally persisting tokens to disk.
#[derive(Clone)]
pub struct Authenticator<C> {
inner: Arc<InnerAuthenticator<C>>,
pub struct Authenticator<S> {
inner: Arc<InnerAuthenticator<S>>,
}
struct DisplayScopes<'a, T>(&'a [T]);
@@ -54,9 +59,12 @@ where
}
}
impl<C> Authenticator<C>
impl<S> Authenticator<S>
where
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
S: Service<Uri> + Clone + Send + Sync + 'static,
S::Response: Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static,
S::Future: Send + Unpin + 'static,
S::Error: Into<Box<dyn StdError + Send + Sync>>,
{
/// Return the current token for the provided scopes.
pub async fn token<'a, T>(&'a self, scopes: &'a [T]) -> Result<AccessToken, Error>
@@ -669,6 +677,7 @@ impl<C> AuthenticatorBuilder<C, AuthorizedUserFlow> {
mod private {
use crate::application_default_credentials::ApplicationDefaultCredentialsFlow;
use crate::authorized_user::AuthorizedUserFlow;
use crate::authenticator::{AsyncRead, AsyncWrite, Connection, Service, StdError, Uri};
use crate::device::DeviceFlow;
use crate::error::Error;
use crate::installed::InstalledFlow;
@@ -697,14 +706,17 @@ mod private {
}
}
pub(crate) async fn token<'a, C, T>(
pub(crate) async fn token<'a, S, T>(
&'a self,
hyper_client: &'a hyper::Client<C>,
hyper_client: &'a hyper::Client<S>,
scopes: &'a [T],
) -> Result<TokenInfo, Error>
where
T: AsRef<str>,
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
S: Service<Uri> + Clone + Send + Sync + 'static,
S::Response: Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static,
S::Future: Send + Unpin + 'static,
S::Error: Into<Box<dyn StdError + Send + Sync>>,
{
match self {
AuthFlow::DeviceFlow(device_flow) => device_flow.token(hyper_client, scopes).await,
@@ -729,7 +741,7 @@ mod private {
/// A trait implemented for any hyper::Client as well as the DefaultHyperClient.
pub trait HyperClientBuilder {
/// The hyper connector that the resulting hyper client will use.
type Connector: hyper::client::connect::Connect + Clone + Send + Sync + 'static;
type Connector: Service<Uri> + Clone + Send + Sync + 'static;
/// Create a hyper::Client
fn build_hyper_client(self) -> hyper::Client<Self::Connector>;
@@ -809,13 +821,16 @@ impl HyperClientBuilder for DefaultHyperClient {
}
}
impl<C> HyperClientBuilder for hyper::Client<C>
impl<S> HyperClientBuilder for hyper::Client<S>
where
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
S: Service<Uri> + Clone + Send + Sync + 'static,
S::Response: Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static,
S::Future: Send + Unpin + 'static,
S::Error: Into<Box<dyn StdError + Send + Sync>>,
{
type Connector = C;
type Connector = S;
fn build_hyper_client(self) -> hyper::Client<C> {
fn build_hyper_client(self) -> hyper::Client<S> {
self
}

View File

@@ -6,8 +6,13 @@
//!
use crate::error::Error;
use crate::types::TokenInfo;
use hyper::client::connect::Connection;
use hyper::header;
use http::Uri;
use serde::{Deserialize, Serialize};
use std::error::Error as StdError;
use tokio::io::{AsyncRead, AsyncWrite};
use tower_service::Service;
use url::form_urlencoded;
const TOKEN_URI: &str = "https://accounts.google.com/o/oauth2/token";
@@ -37,14 +42,17 @@ pub struct AuthorizedUserFlow {
impl AuthorizedUserFlow {
/// Send a request for a new Bearer token to the OAuth provider.
pub(crate) async fn token<C, T>(
pub(crate) async fn token<S, T>(
&self,
hyper_client: &hyper::Client<C>,
hyper_client: &hyper::Client<S>,
_scopes: &[T],
) -> Result<TokenInfo, Error>
where
T: AsRef<str>,
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
S: Service<Uri> + Clone + Send + Sync + 'static,
S::Response: Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static,
S::Future: Send + Unpin + 'static,
S::Error: Into<Box<dyn StdError + Send + Sync>>,
{
let req = form_urlencoded::Serializer::new(String::new())
.extend_pairs(&[

View File

@@ -5,10 +5,15 @@ use crate::error::{AuthError, Error};
use crate::types::{ApplicationSecret, TokenInfo};
use std::borrow::Cow;
use std::error::Error as StdError;
use std::time::Duration;
use hyper::client::connect::Connection;
use hyper::header;
use http::Uri;
use url::form_urlencoded;
use tokio::io::{AsyncRead, AsyncWrite};
use tower_service::Service;
pub const GOOGLE_DEVICE_CODE_URL: &str = "https://accounts.google.com/o/oauth2/device/code";
@@ -38,14 +43,17 @@ impl DeviceFlow {
}
}
pub(crate) async fn token<C, T>(
pub(crate) async fn token<S, T>(
&self,
hyper_client: &hyper::Client<C>,
hyper_client: &hyper::Client<S>,
scopes: &[T],
) -> Result<TokenInfo, Error>
where
T: AsRef<str>,
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
S: Service<Uri> + Clone + Send + Sync + 'static,
S::Response: Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static,
S::Future: Send + Unpin + 'static,
S::Error: Into<Box<dyn StdError + Send + Sync>>,
{
let device_auth_resp = Self::request_code(
&self.app_secret,
@@ -67,15 +75,18 @@ impl DeviceFlow {
.await
}
async fn wait_for_device_token<C>(
async fn wait_for_device_token<S>(
&self,
hyper_client: &hyper::Client<C>,
hyper_client: &hyper::Client<S>,
app_secret: &ApplicationSecret,
device_auth_resp: &DeviceAuthResponse,
grant_type: &str,
) -> Result<TokenInfo, Error>
where
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
S: Service<Uri> + Clone + Send + Sync + 'static,
S::Response: Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static,
S::Future: Send + Unpin + 'static,
S::Error: Into<Box<dyn StdError + Send + Sync>>,
{
let mut interval = device_auth_resp.interval;
log::debug!("Polling every {:?} for device token", interval);
@@ -124,15 +135,18 @@ impl DeviceFlow {
/// * If called after a successful result was returned at least once.
/// # Examples
/// See test-cases in source code for a more complete example.
async fn request_code<C, T>(
async fn request_code<S, T>(
application_secret: &ApplicationSecret,
client: &hyper::Client<C>,
client: &hyper::Client<S>,
device_code_url: &str,
scopes: &[T],
) -> Result<DeviceAuthResponse, Error>
where
T: AsRef<str>,
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
S: Service<Uri> + Clone + Send + Sync + 'static,
S::Response: Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static,
S::Future: Send + Unpin + 'static,
S::Error: Into<Box<dyn StdError + Send + Sync>>,
{
let req = form_urlencoded::Serializer::new(String::new())
.extend_pairs(&[
@@ -172,14 +186,17 @@ impl DeviceFlow {
///
/// # Examples
/// See test-cases in source code for a more complete example.
async fn poll_token<'a, C>(
async fn poll_token<'a, S>(
application_secret: &ApplicationSecret,
client: &hyper::Client<C>,
client: &hyper::Client<S>,
device_code: &str,
grant_type: &str,
) -> Result<TokenInfo, Error>
where
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
S: Service<Uri> + Clone + Send + Sync + 'static,
S::Response: Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static,
S::Future: Send + Unpin + 'static,
S::Error: Into<Box<dyn StdError + Send + Sync>>,
{
// We should be ready for a new request
let req = form_urlencoded::Serializer::new(String::new())

View File

@@ -8,12 +8,17 @@ use crate::types::{ApplicationSecret, TokenInfo};
use futures::lock::Mutex;
use std::convert::AsRef;
use std::error::Error as StdError;
use std::net::SocketAddr;
use std::sync::Arc;
use hyper::client::connect::Connection;
use hyper::header;
use http::Uri;
use percent_encoding::{percent_encode, AsciiSet, CONTROLS};
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::sync::oneshot;
use tower_service::Service;
use url::form_urlencoded;
const QUERY_SET: AsciiSet = CONTROLS.add(b' ').add(b'"').add(b'#').add(b'<').add(b'>');
@@ -100,14 +105,17 @@ impl InstalledFlow {
/// . Return that token
///
/// It's recommended not to use the DefaultInstalledFlowDelegate, but a specialized one.
pub(crate) async fn token<C, T>(
pub(crate) async fn token<S, T>(
&self,
hyper_client: &hyper::Client<C>,
hyper_client: &hyper::Client<S>,
scopes: &[T],
) -> Result<TokenInfo, Error>
where
T: AsRef<str>,
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
S: Service<Uri> + Clone + Send + Sync + 'static,
S::Response: Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static,
S::Future: Send + Unpin + 'static,
S::Error: Into<Box<dyn StdError + Send + Sync>>,
{
match self.method {
InstalledFlowReturnMethod::HTTPRedirect => {
@@ -121,15 +129,18 @@ impl InstalledFlow {
}
}
async fn ask_auth_code_interactively<C, T>(
async fn ask_auth_code_interactively<S, T>(
&self,
hyper_client: &hyper::Client<C>,
hyper_client: &hyper::Client<S>,
app_secret: &ApplicationSecret,
scopes: &[T],
) -> Result<TokenInfo, Error>
where
T: AsRef<str>,
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
S: Service<Uri> + Clone + Send + Sync + 'static,
S::Response: Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static,
S::Future: Send + Unpin + 'static,
S::Error: Into<Box<dyn StdError + Send + Sync>>,
{
let url = build_authentication_request_url(
&app_secret.auth_uri,
@@ -148,15 +159,18 @@ impl InstalledFlow {
.await
}
async fn ask_auth_code_via_http<C, T>(
async fn ask_auth_code_via_http<S, T>(
&self,
hyper_client: &hyper::Client<C>,
hyper_client: &hyper::Client<S>,
app_secret: &ApplicationSecret,
scopes: &[T],
) -> Result<TokenInfo, Error>
where
T: AsRef<str>,
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
S: Service<Uri> + Clone + Send + Sync + 'static,
S::Response: Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static,
S::Future: Send + Unpin + 'static,
S::Error: Into<Box<dyn StdError + Send + Sync>>,
{
use std::borrow::Cow;
let server = InstalledFlowServer::run()?;
@@ -185,15 +199,18 @@ impl InstalledFlow {
.await
}
async fn exchange_auth_code<C>(
async fn exchange_auth_code<S>(
&self,
authcode: &str,
hyper_client: &hyper::Client<C>,
hyper_client: &hyper::Client<S>,
app_secret: &ApplicationSecret,
server_addr: Option<SocketAddr>,
) -> Result<TokenInfo, Error>
where
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
S: Service<Uri> + Clone + Send + Sync + 'static,
S::Response: Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static,
S::Future: Send + Unpin + 'static,
S::Error: Into<Box<dyn StdError + Send + Sync>>,
{
let redirect_uri = self.flow_delegate.redirect_uri();
let request = Self::request_token(app_secret, authcode, redirect_uri, server_addr);

View File

@@ -1,7 +1,12 @@
use crate::error::Error;
use crate::types::{ApplicationSecret, TokenInfo};
use http::{Uri};
use hyper::client::connect::Connection;
use hyper::header;
use std::error::Error as StdError;
use tokio::io::{AsyncRead, AsyncWrite};
use tower_service::Service;
use url::form_urlencoded;
/// Implements the [OAuth2 Refresh Token Flow](https://developers.google.com/youtube/v3/guides/authentication#devices).
@@ -26,13 +31,16 @@ impl RefreshFlow {
///
/// # Examples
/// Please see the crate landing page for an example.
pub(crate) async fn refresh_token<C>(
client: &hyper::Client<C>,
pub(crate) async fn refresh_token<S>(
client: &hyper::Client<S>,
client_secret: &ApplicationSecret,
refresh_token: &str,
) -> Result<TokenInfo, Error>
where
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
S: Service<Uri> + Clone + Send + Sync + 'static,
S::Response: Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static,
S::Future: Send + Unpin + 'static,
S::Error: Into<Box<dyn StdError + Send + Sync>>,
{
log::debug!(
"refreshing access token with refresh token: {}",

View File

@@ -16,15 +16,19 @@
use crate::error::Error;
use crate::types::TokenInfo;
use std::{io, path::PathBuf};
use std::{io, path::PathBuf, error::Error as StdError};
use hyper::client::connect::Connection;
use hyper::header;
use http::Uri;
use rustls::{
self,
sign::{self, SigningKey},
PrivateKey,
};
use serde::{Deserialize, Serialize};
use tokio::io::{AsyncRead, AsyncWrite};
use tower_service::Service;
use time::OffsetDateTime;
use url::form_urlencoded;
@@ -193,14 +197,17 @@ impl ServiceAccountFlow {
}
/// Send a request for a new Bearer token to the OAuth provider.
pub(crate) async fn token<C, T>(
pub(crate) async fn token<S, T>(
&self,
hyper_client: &hyper::Client<C>,
hyper_client: &hyper::Client<S>,
scopes: &[T],
) -> Result<TokenInfo, Error>
where
T: AsRef<str>,
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
S: Service<Uri> + Clone + Send + Sync + 'static,
S::Response: Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static,
S::Future: Send + Unpin + 'static,
S::Error: Into<Box<dyn StdError + Send + Sync>>,
{
let claims = Claims::new(&self.key, scopes, self.subject.as_deref());
let signed = self.signer.sign_claims(&claims).map_err(|_| {