Merge remote-tracking branch 'upstream/master' into two-steps-docfix

This commit is contained in:
Steven 'Steve' Kendall
2020-01-17 14:45:16 -05:00
19 changed files with 2606 additions and 2949 deletions

View File

@@ -12,28 +12,22 @@ edition = "2018"
[dependencies]
base64 = "0.10"
chrono = "0.4"
http = "0.1"
hyper = {version = "0.12", default-features = false}
hyper-rustls = "0.17"
itertools = "0.8"
log = "0.3"
chrono = { version = "0.4", features = ["serde"] }
http = "0.2"
hyper = "0.13.1"
hyper-rustls = "0.19"
log = "0.4"
rustls = "0.16"
serde = "1.0"
seahash = "3.0.6"
serde = {version = "1.0", features = ["derive"]}
serde_json = "1.0"
serde_derive = "1.0"
tokio = { version = "0.2", features = ["fs", "macros", "io-std", "time"] }
url = "1"
futures = "0.1"
tokio-threadpool = "0.1"
tokio = "0.1"
tokio-timer = "0.2"
[dev-dependencies]
getopts = "0.2"
open = "1.1"
yup-hyper-mock = "3.14"
mockito = "0.17"
httptest = "0.5"
env_logger = "0.6"
tempfile = "3.1"
[workspace]
members = ["examples/test-installed/", "examples/test-svc-acct/", "examples/test-device/"]

View File

@@ -6,7 +6,4 @@ edition = "2018"
[dependencies]
yup-oauth2 = { path = "../../" }
hyper = "0.12"
hyper-rustls = "0.17"
futures = "0.1"
tokio = "0.1"
tokio = { version = "0.2", features = ["macros"] }

View File

@@ -1,20 +1,19 @@
use futures::prelude::*;
use yup_oauth2::{self, Authenticator, DeviceFlow, GetToken};
use yup_oauth2::DeviceFlowAuthenticator;
use std::path;
use tokio;
fn main() {
let creds = yup_oauth2::read_application_secret(path::Path::new("clientsecret.json"))
#[tokio::main]
async fn main() {
let app_secret = yup_oauth2::read_application_secret("clientsecret.json")
.await
.expect("clientsecret");
let mut auth = Authenticator::new(DeviceFlow::new(creds))
let auth = DeviceFlowAuthenticator::builder(app_secret)
.persist_tokens_to_disk("tokenstorage.json")
.build()
.await
.expect("authenticator");
let scopes = vec!["https://www.googleapis.com/auth/youtube.readonly"];
let mut rt = tokio::runtime::Runtime::new().unwrap();
let fut = auth.token(scopes).and_then(|tok| Ok(println!("{:?}", tok)));
println!("{:?}", rt.block_on(fut));
let scopes = &["https://www.googleapis.com/auth/youtube.readonly"];
match auth.token(scopes).await {
Err(e) => println!("error: {:?}", e),
Ok(t) => println!("token: {:?}", t),
}
}

View File

@@ -6,7 +6,4 @@ edition = "2018"
[dependencies]
yup-oauth2 = { path = "../../" }
hyper = "0.12"
hyper-rustls = "0.17"
futures = "0.1"
tokio = "0.1"
tokio = { version = "0.2", features = ["macros"] }

View File

@@ -1,36 +1,21 @@
use futures::prelude::*;
use yup_oauth2::GetToken;
use yup_oauth2::{Authenticator, InstalledFlow};
use yup_oauth2::{InstalledFlowAuthenticator, InstalledFlowReturnMethod};
use hyper::client::Client;
use hyper_rustls::HttpsConnector;
use std::path::Path;
fn main() {
let https = HttpsConnector::new(1);
let client = Client::builder()
.keep_alive(false)
.build::<_, hyper::Body>(https);
let ad = yup_oauth2::DefaultFlowDelegate;
let secret = yup_oauth2::read_application_secret(Path::new("clientsecret.json"))
#[tokio::main]
async fn main() {
let app_secret = yup_oauth2::read_application_secret("clientsecret.json")
.await
.expect("clientsecret.json");
let mut auth = Authenticator::new(InstalledFlow::new(
secret,
yup_oauth2::InstalledFlowReturnMethod::HTTPRedirect(8081),
))
.persist_tokens_to_disk("tokencache.json")
.build()
.unwrap();
let s = "https://www.googleapis.com/auth/drive.file".to_string();
let scopes = vec![s];
let auth =
InstalledFlowAuthenticator::builder(app_secret, InstalledFlowReturnMethod::HTTPRedirect)
.persist_tokens_to_disk("tokencache.json")
.build()
.await
.unwrap();
let scopes = &["https://www.googleapis.com/auth/drive.file"];
let tok = auth.token(scopes);
let fut = tok.map_err(|e| println!("error: {:?}", e)).and_then(|t| {
println!("The token is {:?}", t);
Ok(())
});
tokio::run(fut)
match auth.token(scopes).await {
Err(e) => println!("error: {:?}", e),
Ok(t) => println!("The token is {:?}", t),
}
}

View File

@@ -6,7 +6,4 @@ edition = "2018"
[dependencies]
yup-oauth2 = { path = "../../" }
hyper = "0.12"
hyper-rustls = "0.17"
futures = "0.1"
tokio = "0.1"
tokio = { version = "0.2", features = ["macros"] }

View File

@@ -1,29 +1,18 @@
use yup_oauth2;
use yup_oauth2::ServiceAccountAuthenticator;
use futures::prelude::*;
use yup_oauth2::GetToken;
#[tokio::main]
async fn main() {
let creds = yup_oauth2::read_service_account_key("serviceaccount.json")
.await
.unwrap();
let sa = ServiceAccountAuthenticator::builder(creds)
.build()
.await
.unwrap();
let scopes = &["https://www.googleapis.com/auth/pubsub"];
use tokio;
use std::path;
fn main() {
let creds =
yup_oauth2::service_account_key_from_file(path::Path::new("serviceaccount.json")).unwrap();
let mut sa = yup_oauth2::ServiceAccountAccess::new(creds).build();
let fut = sa
.token(vec!["https://www.googleapis.com/auth/pubsub"])
.and_then(|tok| {
println!("token is: {:?}", tok);
Ok(())
});
let fut2 = sa
.token(vec!["https://www.googleapis.com/auth/pubsub"])
.and_then(|tok| {
println!("cached token is {:?} and should be identical", tok);
Ok(())
});
let all = fut.join(fut2).then(|_| Ok(()));
tokio::run(all)
let tok = sa.token(scopes).await.unwrap();
println!("token is: {:?}", tok);
let tok = sa.token(scopes).await.unwrap();
println!("cached token is {:?} and should be identical", tok);
}

View File

@@ -1,42 +1,429 @@
use crate::authenticator_delegate::{AuthenticatorDelegate, DefaultAuthenticatorDelegate, Retry};
//! Module contianing the core functionality for OAuth2 Authentication.
use crate::authenticator_delegate::{DeviceFlowDelegate, InstalledFlowDelegate};
use crate::device::DeviceFlow;
use crate::error::Error;
use crate::installed::{InstalledFlow, InstalledFlowReturnMethod};
use crate::refresh::RefreshFlow;
use crate::storage::{hash_scopes, DiskTokenStorage, MemoryStorage, TokenStorage};
use crate::types::{ApplicationSecret, GetToken, RefreshResult, RequestError, Token};
use crate::service_account::{ServiceAccountFlow, ServiceAccountFlowOpts, ServiceAccountKey};
use crate::storage::{self, Storage};
use crate::types::{AccessToken, ApplicationSecret, TokenInfo};
use private::AuthFlow;
use futures::{future, prelude::*};
use tokio_timer;
use std::error::Error;
use std::borrow::Cow;
use std::fmt;
use std::io;
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::path::PathBuf;
use std::sync::Mutex;
/// Authenticator abstracts different `GetToken` implementations behind one type and handles
/// caching received tokens. It's important to use it (instead of the flows directly) because
/// otherwise the user needs to be asked for new authorization every time a token is generated.
///
/// `ServiceAccountAccess` does not need (and does not work) with `Authenticator`, given that it
/// does not require interaction and implements its own caching. Use it directly.
///
/// NOTE: It is recommended to use a client constructed like this in order to prevent functions
/// like `hyper::run()` from hanging: `let client = hyper::Client::builder().keep_alive(false);`.
/// Due to token requests being rare, this should not result in a too bad performance problem.
struct AuthenticatorImpl<
T: GetToken,
S: TokenStorage,
AD: AuthenticatorDelegate,
C: hyper::client::connect::Connect,
> {
client: hyper::Client<C>,
inner: Arc<Mutex<T>>,
store: Arc<Mutex<S>>,
delegate: AD,
/// Authenticator is responsible for fetching tokens, handling refreshing tokens,
/// and optionally persisting tokens to disk.
pub struct Authenticator<C> {
hyper_client: hyper::Client<C>,
storage: Storage,
auth_flow: AuthFlow,
}
/// A trait implemented for any hyper::Client as well as teh DefaultHyperClient.
pub trait HyperClientBuilder {
type Connector: hyper::client::connect::Connect;
struct DisplayScopes<'a, T>(&'a [T]);
impl<'a, T> fmt::Display for DisplayScopes<'a, T>
where
T: AsRef<str>,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("[")?;
let mut iter = self.0.iter();
if let Some(first) = iter.next() {
f.write_str(first.as_ref())?;
for scope in iter {
f.write_str(", ")?;
f.write_str(scope.as_ref())?;
}
}
f.write_str("]")
}
}
impl<C> Authenticator<C>
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<AccessToken, Error>
where
T: AsRef<str>,
{
log::debug!(
"access token requested for scopes: {}",
DisplayScopes(scopes)
);
let hashed_scopes = storage::ScopeSet::from(scopes);
match (self.storage.get(hashed_scopes), self.auth_flow.app_secret()) {
(Some(t), _) if !t.is_expired() => {
// unexpired token found
log::debug!("found valid token in cache: {:?}", t);
Ok(t.into())
}
(
Some(TokenInfo {
refresh_token: Some(refresh_token),
..
}),
Some(app_secret),
) => {
// token is expired but has a refresh token.
let token_info =
RefreshFlow::refresh_token(&self.hyper_client, app_secret, &refresh_token)
.await?;
self.storage.set(hashed_scopes, token_info.clone()).await?;
Ok(token_info.into())
}
_ => {
// no token in the cache or the token returned can't be refreshed.
let token_info = self.auth_flow.token(&self.hyper_client, scopes).await?;
self.storage.set(hashed_scopes, token_info.clone()).await?;
Ok(token_info.into())
}
}
}
}
/// Configure an Authenticator using the builder pattern.
pub struct AuthenticatorBuilder<C, F> {
hyper_client_builder: C,
storage_type: StorageType,
auth_flow: F,
}
/// Create an authenticator that uses the installed flow.
/// ```
/// # async fn foo() {
/// # use yup_oauth2::InstalledFlowReturnMethod;
/// # let custom_flow_delegate = yup_oauth2::authenticator_delegate::DefaultInstalledFlowDelegate;
/// # let app_secret = yup_oauth2::read_application_secret("/tmp/foo").await.unwrap();
/// let authenticator = yup_oauth2::InstalledFlowAuthenticator::builder(
/// app_secret,
/// InstalledFlowReturnMethod::HTTPRedirect,
/// )
/// .build()
/// .await
/// .expect("failed to create authenticator");
/// # }
/// ```
pub struct InstalledFlowAuthenticator;
impl InstalledFlowAuthenticator {
/// Use the builder pattern to create an Authenticator that uses the installed flow.
pub fn builder(
app_secret: ApplicationSecret,
method: InstalledFlowReturnMethod,
) -> AuthenticatorBuilder<DefaultHyperClient, InstalledFlow> {
AuthenticatorBuilder::<DefaultHyperClient, _>::with_auth_flow(InstalledFlow::new(
app_secret, method,
))
}
}
/// Create an authenticator that uses the device flow.
/// ```
/// # async fn foo() {
/// # let app_secret = yup_oauth2::read_application_secret("/tmp/foo").await.unwrap();
/// let authenticator = yup_oauth2::DeviceFlowAuthenticator::builder(app_secret)
/// .build()
/// .await
/// .expect("failed to create authenticator");
/// # }
/// ```
pub struct DeviceFlowAuthenticator;
impl DeviceFlowAuthenticator {
/// Use the builder pattern to create an Authenticator that uses the device flow.
pub fn builder(
app_secret: ApplicationSecret,
) -> AuthenticatorBuilder<DefaultHyperClient, DeviceFlow> {
AuthenticatorBuilder::<DefaultHyperClient, _>::with_auth_flow(DeviceFlow::new(app_secret))
}
}
/// Create an authenticator that uses a service account.
/// ```
/// # async fn foo() {
/// # let service_account_key = yup_oauth2::read_service_account_key("/tmp/foo").await.unwrap();
/// let authenticator = yup_oauth2::ServiceAccountAuthenticator::builder(service_account_key)
/// .build()
/// .await
/// .expect("failed to create authenticator");
/// # }
/// ```
pub struct ServiceAccountAuthenticator;
impl ServiceAccountAuthenticator {
/// Use the builder pattern to create an Authenticator that uses a service account.
pub fn builder(
service_account_key: ServiceAccountKey,
) -> AuthenticatorBuilder<DefaultHyperClient, ServiceAccountFlowOpts> {
AuthenticatorBuilder::<DefaultHyperClient, _>::with_auth_flow(ServiceAccountFlowOpts {
key: service_account_key,
subject: None,
})
}
}
/// ## Methods available when building any Authenticator.
/// ```
/// # async fn foo() {
/// # let custom_hyper_client = hyper::Client::new();
/// # let app_secret = yup_oauth2::read_application_secret("/tmp/foo").await.unwrap();
/// let authenticator = yup_oauth2::DeviceFlowAuthenticator::builder(app_secret)
/// .hyper_client(custom_hyper_client)
/// .persist_tokens_to_disk("/tmp/tokenfile.json")
/// .build()
/// .await
/// .expect("failed to create authenticator");
/// # }
/// ```
impl<C, F> AuthenticatorBuilder<C, F> {
async fn common_build(
hyper_client_builder: C,
storage_type: StorageType,
auth_flow: AuthFlow,
) -> io::Result<Authenticator<C::Connector>>
where
C: HyperClientBuilder,
{
let hyper_client = hyper_client_builder.build_hyper_client();
let storage = match storage_type {
StorageType::Memory => Storage::Memory {
tokens: Mutex::new(storage::JSONTokens::new()),
},
StorageType::Disk(path) => Storage::Disk(storage::DiskStorage::new(path).await?),
};
Ok(Authenticator {
hyper_client,
storage,
auth_flow,
})
}
fn with_auth_flow(auth_flow: F) -> AuthenticatorBuilder<DefaultHyperClient, F> {
AuthenticatorBuilder {
hyper_client_builder: DefaultHyperClient,
storage_type: StorageType::Memory,
auth_flow,
}
}
/// Use the provided hyper client.
pub fn hyper_client<NewC>(
self,
hyper_client: hyper::Client<NewC>,
) -> AuthenticatorBuilder<hyper::Client<NewC>, F> {
AuthenticatorBuilder {
hyper_client_builder: hyper_client,
storage_type: self.storage_type,
auth_flow: self.auth_flow,
}
}
/// Persist tokens to disk in the provided filename.
pub fn persist_tokens_to_disk<P: Into<PathBuf>>(self, path: P) -> AuthenticatorBuilder<C, F> {
AuthenticatorBuilder {
storage_type: StorageType::Disk(path.into()),
..self
}
}
}
/// ## Methods available when building a device flow Authenticator.
/// ```
/// # async fn foo() {
/// # let custom_flow_delegate = yup_oauth2::authenticator_delegate::DefaultDeviceFlowDelegate;
/// # let app_secret = yup_oauth2::read_application_secret("/tmp/foo").await.unwrap();
/// let authenticator = yup_oauth2::DeviceFlowAuthenticator::builder(app_secret)
/// .device_code_url("foo")
/// .flow_delegate(Box::new(custom_flow_delegate))
/// .grant_type("foo")
/// .build()
/// .await
/// .expect("failed to create authenticator");
/// # }
/// ```
impl<C> AuthenticatorBuilder<C, DeviceFlow> {
/// Use the provided device code url.
pub fn device_code_url(self, url: impl Into<Cow<'static, str>>) -> Self {
AuthenticatorBuilder {
auth_flow: DeviceFlow {
device_code_url: url.into(),
..self.auth_flow
},
..self
}
}
/// Use the provided DeviceFlowDelegate.
pub fn flow_delegate(self, flow_delegate: Box<dyn DeviceFlowDelegate>) -> Self {
AuthenticatorBuilder {
auth_flow: DeviceFlow {
flow_delegate,
..self.auth_flow
},
..self
}
}
/// Use the provided grant type.
pub fn grant_type(self, grant_type: impl Into<Cow<'static, str>>) -> Self {
AuthenticatorBuilder {
auth_flow: DeviceFlow {
grant_type: grant_type.into(),
..self.auth_flow
},
..self
}
}
/// Create the authenticator.
pub async fn build(self) -> io::Result<Authenticator<C::Connector>>
where
C: HyperClientBuilder,
{
Self::common_build(
self.hyper_client_builder,
self.storage_type,
AuthFlow::DeviceFlow(self.auth_flow),
)
.await
}
}
/// ## Methods available when building an installed flow Authenticator.
/// ```
/// # async fn foo() {
/// # use yup_oauth2::InstalledFlowReturnMethod;
/// # let custom_flow_delegate = yup_oauth2::authenticator_delegate::DefaultInstalledFlowDelegate;
/// # let app_secret = yup_oauth2::read_application_secret("/tmp/foo").await.unwrap();
/// let authenticator = yup_oauth2::InstalledFlowAuthenticator::builder(
/// app_secret,
/// InstalledFlowReturnMethod::HTTPRedirect,
/// )
/// .flow_delegate(Box::new(custom_flow_delegate))
/// .build()
/// .await
/// .expect("failed to create authenticator");
/// # }
/// ```
impl<C> AuthenticatorBuilder<C, InstalledFlow> {
/// Use the provided InstalledFlowDelegate.
pub fn flow_delegate(self, flow_delegate: Box<dyn InstalledFlowDelegate>) -> Self {
AuthenticatorBuilder {
auth_flow: InstalledFlow {
flow_delegate,
..self.auth_flow
},
..self
}
}
/// Create the authenticator.
pub async fn build(self) -> io::Result<Authenticator<C::Connector>>
where
C: HyperClientBuilder,
{
Self::common_build(
self.hyper_client_builder,
self.storage_type,
AuthFlow::InstalledFlow(self.auth_flow),
)
.await
}
}
/// ## Methods available when building a service account authenticator.
/// ```
/// # async fn foo() {
/// # let service_account_key = yup_oauth2::read_service_account_key("/tmp/foo").await.unwrap();
/// let authenticator = yup_oauth2::ServiceAccountAuthenticator::builder(
/// service_account_key,
/// )
/// .subject("mysubject")
/// .build()
/// .await
/// .expect("failed to create authenticator");
/// # }
/// ```
impl<C> AuthenticatorBuilder<C, ServiceAccountFlowOpts> {
/// Use the provided subject.
pub fn subject(self, subject: impl Into<String>) -> Self {
AuthenticatorBuilder {
auth_flow: ServiceAccountFlowOpts {
subject: Some(subject.into()),
..self.auth_flow
},
..self
}
}
/// Create the authenticator.
pub async fn build(self) -> io::Result<Authenticator<C::Connector>>
where
C: HyperClientBuilder,
{
let service_account_auth_flow = ServiceAccountFlow::new(self.auth_flow)?;
Self::common_build(
self.hyper_client_builder,
self.storage_type,
AuthFlow::ServiceAccountFlow(service_account_auth_flow),
)
.await
}
}
mod private {
use crate::device::DeviceFlow;
use crate::error::Error;
use crate::installed::InstalledFlow;
use crate::service_account::ServiceAccountFlow;
use crate::types::{ApplicationSecret, TokenInfo};
pub enum AuthFlow {
DeviceFlow(DeviceFlow),
InstalledFlow(InstalledFlow),
ServiceAccountFlow(ServiceAccountFlow),
}
impl AuthFlow {
pub(crate) fn app_secret(&self) -> Option<&ApplicationSecret> {
match self {
AuthFlow::DeviceFlow(device_flow) => Some(&device_flow.app_secret),
AuthFlow::InstalledFlow(installed_flow) => Some(&installed_flow.app_secret),
AuthFlow::ServiceAccountFlow(_) => None,
}
}
pub(crate) async fn token<'a, C, T>(
&'a self,
hyper_client: &'a hyper::Client<C>,
scopes: &'a [T],
) -> Result<TokenInfo, Error>
where
T: AsRef<str>,
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
{
match self {
AuthFlow::DeviceFlow(device_flow) => device_flow.token(hyper_client, scopes).await,
AuthFlow::InstalledFlow(installed_flow) => {
installed_flow.token(hyper_client, scopes).await
}
AuthFlow::ServiceAccountFlow(service_account_flow) => {
service_account_flow.token(hyper_client, scopes).await
}
}
}
}
}
/// 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;
/// Create a hyper::Client
fn build_hyper_client(self) -> hyper::Client<Self::Connector>;
}
@@ -48,13 +435,13 @@ impl HyperClientBuilder for DefaultHyperClient {
fn build_hyper_client(self) -> hyper::Client<Self::Connector> {
hyper::Client::builder()
.keep_alive(false)
.build::<_, hyper::Body>(hyper_rustls::HttpsConnector::new(1))
.build::<_, hyper::Body>(hyper_rustls::HttpsConnector::new())
}
}
impl<C> HyperClientBuilder for hyper::Client<C>
where
C: hyper::client::connect::Connect,
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
{
type Connector = C;
@@ -63,271 +450,18 @@ where
}
}
/// An internal trait implemented by flows to be used by an authenticator.
pub trait AuthFlow<C> {
type TokenGetter: GetToken;
fn build_token_getter(self, client: hyper::Client<C>) -> Self::TokenGetter;
enum StorageType {
Memory,
Disk(PathBuf),
}
/// An authenticator can be used with `InstalledFlow`'s or `DeviceFlow`'s and
/// will refresh tokens as they expire as well as optionally persist tokens to
/// disk.
pub struct Authenticator<
T: AuthFlow<C::Connector>,
S: TokenStorage,
AD: AuthenticatorDelegate,
C: HyperClientBuilder,
> {
client: C,
token_getter: T,
store: io::Result<S>,
delegate: AD,
}
#[cfg(test)]
mod tests {
use super::*;
impl<T> Authenticator<T, MemoryStorage, DefaultAuthenticatorDelegate, DefaultHyperClient>
where
T: AuthFlow<<DefaultHyperClient as HyperClientBuilder>::Connector>,
{
/// Create a new authenticator with the provided flow. By default a new
/// hyper::Client will be created the default authenticator delegate will be
/// used, and tokens will not be persisted to disk.
/// Accepted flow types are DeviceFlow and InstalledFlow.
///
/// Examples
/// ```
/// use std::path::Path;
/// use yup_oauth2::{ApplicationSecret, Authenticator, DeviceFlow};
/// let creds = ApplicationSecret::default();
/// let auth = Authenticator::new(DeviceFlow::new(creds)).build().unwrap();
/// ```
pub fn new(
flow: T,
) -> Authenticator<T, MemoryStorage, DefaultAuthenticatorDelegate, DefaultHyperClient> {
Authenticator {
client: DefaultHyperClient,
token_getter: flow,
store: Ok(MemoryStorage::new()),
delegate: DefaultAuthenticatorDelegate,
}
}
}
impl<T, S, AD, C> Authenticator<T, S, AD, C>
where
T: AuthFlow<C::Connector>,
S: TokenStorage,
AD: AuthenticatorDelegate,
C: HyperClientBuilder,
{
/// Use the provided hyper client.
pub fn hyper_client<NewC>(
self,
hyper_client: hyper::Client<NewC>,
) -> Authenticator<T, S, AD, hyper::Client<NewC>>
where
NewC: hyper::client::connect::Connect,
T: AuthFlow<NewC>,
{
Authenticator {
client: hyper_client,
token_getter: self.token_getter,
store: self.store,
delegate: self.delegate,
}
}
/// Persist tokens to disk in the provided filename.
pub fn persist_tokens_to_disk<P: AsRef<Path>>(
self,
path: P,
) -> Authenticator<T, DiskTokenStorage, AD, C> {
let disk_storage = DiskTokenStorage::new(path.as_ref().to_str().unwrap());
Authenticator {
client: self.client,
token_getter: self.token_getter,
store: disk_storage,
delegate: self.delegate,
}
}
/// Use the provided authenticator delegate.
pub fn delegate<NewAD: AuthenticatorDelegate>(
self,
delegate: NewAD,
) -> Authenticator<T, S, NewAD, C> {
Authenticator {
client: self.client,
token_getter: self.token_getter,
store: self.store,
delegate: delegate,
}
}
/// Create the authenticator.
pub fn build(self) -> io::Result<impl GetToken>
where
T::TokenGetter: 'static + GetToken + Send,
S: 'static + Send,
AD: 'static + Send,
C::Connector: 'static + Clone + Send,
{
let client = self.client.build_hyper_client();
let store = Arc::new(Mutex::new(self.store?));
let inner = Arc::new(Mutex::new(
self.token_getter.build_token_getter(client.clone()),
));
Ok(AuthenticatorImpl {
client,
inner,
store,
delegate: self.delegate,
})
}
}
impl<
GT: 'static + GetToken + Send,
S: 'static + TokenStorage + Send,
AD: 'static + AuthenticatorDelegate + Send,
C: 'static + hyper::client::connect::Connect + Clone + Send,
> GetToken for AuthenticatorImpl<GT, S, AD, C>
{
/// Returns the API Key of the inner flow.
fn api_key(&mut self) -> Option<String> {
self.inner.lock().unwrap().api_key()
}
/// Returns the application secret of the inner flow.
fn application_secret(&self) -> ApplicationSecret {
self.inner.lock().unwrap().application_secret()
}
fn token<I, T>(
&mut self,
scopes: I,
) -> Box<dyn Future<Item = Token, Error = RequestError> + Send>
where
T: Into<String>,
I: IntoIterator<Item = T>,
{
let (scope_key, scopes) = hash_scopes(scopes);
let store = self.store.clone();
let mut delegate = self.delegate.clone();
let client = self.client.clone();
let appsecret = self.inner.lock().unwrap().application_secret();
let gettoken = self.inner.clone();
let loopfn = move |()| -> Box<
dyn Future<Item = future::Loop<Token, ()>, Error = RequestError> + Send,
> {
// How well does this work with tokio?
match store.lock().unwrap().get(
scope_key.clone(),
&scopes.iter().map(|s| s.as_str()).collect(),
) {
Ok(Some(t)) => {
if !t.expired() {
return Box::new(Ok(future::Loop::Break(t)).into_future());
}
// Implement refresh flow.
let refresh_token = t.refresh_token.clone();
let mut delegate = delegate.clone();
let store = store.clone();
let scopes = scopes.clone();
let refresh_fut = RefreshFlow::refresh_token(
client.clone(),
appsecret.clone(),
refresh_token.unwrap(),
)
.and_then(move |rr| -> Box<dyn Future<Item=future::Loop<Token, ()>, Error=RequestError> + Send> {
match rr {
RefreshResult::Error(ref e) => {
delegate.token_refresh_failed(
format!("{}", e.description().to_string()),
&Some("the request has likely timed out".to_string()),
);
Box::new(Err(RequestError::Refresh(rr)).into_future())
}
RefreshResult::RefreshError(ref s, ref ss) => {
delegate.token_refresh_failed(
format!("{} {}", s, ss.clone().map(|s| format!("({})", s)).unwrap_or("".to_string())),
&Some("the refresh token is likely invalid and your authorization has been revoked".to_string()),
);
Box::new(Err(RequestError::Refresh(rr)).into_future())
}
RefreshResult::Success(t) => {
if let Err(e) = store.lock().unwrap().set(scope_key, &scopes.iter().map(|s| s.as_str()).collect(), Some(t.clone())) {
match delegate.token_storage_failure(true, &e) {
Retry::Skip => Box::new(Ok(future::Loop::Break(t)).into_future()),
Retry::Abort => Box::new(Err(RequestError::Cache(Box::new(e))).into_future()),
Retry::After(d) => Box::new(
tokio_timer::sleep(d)
.then(|_| Ok(future::Loop::Continue(()))),
)
as Box<
dyn Future<
Item = future::Loop<Token, ()>,
Error = RequestError> + Send>,
}
} else {
Box::new(Ok(future::Loop::Break(t)).into_future())
}
},
}
});
Box::new(refresh_fut)
}
Ok(None) => {
let store = store.clone();
let scopes = scopes.clone();
let mut delegate = delegate.clone();
Box::new(
gettoken
.lock()
.unwrap()
.token(scopes.clone())
.and_then(move |t| {
if let Err(e) = store.lock().unwrap().set(
scope_key,
&scopes.iter().map(|s| s.as_str()).collect(),
Some(t.clone()),
) {
match delegate.token_storage_failure(true, &e) {
Retry::Skip => {
Box::new(Ok(future::Loop::Break(t)).into_future())
}
Retry::Abort => Box::new(
Err(RequestError::Cache(Box::new(e))).into_future(),
),
Retry::After(d) => Box::new(
tokio_timer::sleep(d)
.then(|_| Ok(future::Loop::Continue(()))),
)
as Box<
dyn Future<
Item = future::Loop<Token, ()>,
Error = RequestError,
> + Send,
>,
}
} else {
Box::new(Ok(future::Loop::Break(t)).into_future())
}
}),
)
}
Err(err) => match delegate.token_storage_failure(false, &err) {
Retry::Abort | Retry::Skip => {
return Box::new(Err(RequestError::Cache(Box::new(err))).into_future())
}
Retry::After(d) => {
return Box::new(
tokio_timer::sleep(d).then(|_| Ok(future::Loop::Continue(()))),
)
}
},
}
};
Box::new(future::loop_fn((), loopfn))
#[test]
fn ensure_send_sync() {
fn is_send_sync<T: Send + Sync>() {}
is_send_sync::<Authenticator<<DefaultHyperClient as HyperClientBuilder>::Connector>>()
}
}

View File

@@ -1,36 +1,21 @@
use hyper;
//! Module containing types related to delegates.
use crate::error::{AuthErrorOr, Error};
use std::error::Error;
use std::fmt;
use std::io;
use crate::types::{PollError, RequestError};
use chrono::{DateTime, Local, Utc};
use std::pin::Pin;
use std::time::Duration;
use futures::{future, prelude::*};
use tokio::io as tio;
/// A utility type to indicate how operations DeviceFlowHelper operations should be retried
pub enum Retry {
/// Signal you don't want to retry
Abort,
/// Signals you want to retry after the given duration
After(Duration),
/// Instruct the caller to attempt to keep going, or choose an alternate path.
/// If this is not supported, it will have the same effect as `Abort`
Skip,
}
use chrono::{DateTime, Local, Utc};
use std::future::Future;
/// Contains state of pending authentication requests
#[derive(Clone, Debug, PartialEq)]
pub struct PollInformation {
pub struct DeviceAuthResponse {
/// The device verification code.
pub device_code: String,
/// Code the user must enter ...
pub user_code: String,
/// ... at the verification URL
pub verification_url: String,
/// ... at the verification URI
pub verification_uri: String,
/// The `user_code` expires at the given time
/// It's the time the user has left to authenticate your application
pub expires_at: DateTime<Utc>,
@@ -39,162 +24,134 @@ pub struct PollInformation {
pub interval: Duration,
}
impl fmt::Display for PollInformation {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
writeln!(f, "Proceed with polling until {}", self.expires_at)
impl DeviceAuthResponse {
pub(crate) fn from_json(json_data: &[u8]) -> Result<Self, Error> {
Ok(serde_json::from_slice::<AuthErrorOr<_>>(json_data)?.into_result()?)
}
}
impl fmt::Display for PollError {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
PollError::HttpError(ref err) => err.fmt(f),
PollError::Expired(ref date) => writeln!(f, "Authentication expired at {}", date),
PollError::AccessDenied => "Access denied by user".fmt(f),
PollError::TimedOut => "Timed out waiting for token".fmt(f),
PollError::Other(ref s) => format!("Unknown server error: {}", s).fmt(f),
impl<'de> serde::Deserialize<'de> for DeviceAuthResponse {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
struct RawDeviceAuthResponse {
device_code: String,
user_code: String,
// The standard dictates that verification_uri is required, but
// sadly google uses verification_url currently. One of these two
// fields need to be set and verification_uri takes precedence if
// they both are set.
verification_uri: Option<String>,
verification_url: Option<String>,
expires_in: i64,
interval: Option<u64>,
}
let RawDeviceAuthResponse {
device_code,
user_code,
verification_uri,
verification_url,
expires_in,
interval,
} = RawDeviceAuthResponse::deserialize(deserializer)?;
let verification_uri = verification_uri.or(verification_url).ok_or_else(|| {
serde::de::Error::custom("neither verification_uri nor verification_url specified")
})?;
let expires_at = Utc::now() + chrono::Duration::seconds(expires_in);
let interval = Duration::from_secs(interval.unwrap_or(5));
Ok(DeviceAuthResponse {
device_code,
user_code,
verification_uri,
expires_at,
interval,
})
}
}
impl Error for PollError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match *self {
PollError::HttpError(ref e) => Some(e),
_ => None,
}
}
}
/// A partially implemented trait to interact with the `Authenticator`
///
/// The only method that needs to be implemented manually is `present_user_code(...)`,
/// as no assumptions are made on how this presentation should happen.
pub trait AuthenticatorDelegate: Clone {
/// Called whenever there is an client, usually if there are network problems.
///
/// Return retry information.
fn client_error(&mut self, _: &hyper::Error) -> Retry {
Retry::Abort
}
/// Called whenever we failed to retrieve a token or set a token due to a storage error.
/// You may use it to either ignore the incident or retry.
/// This can be useful if the underlying `TokenStorage` may fail occasionally.
/// if `is_set` is true, the failure resulted from `TokenStorage.set(...)`. Otherwise,
/// it was `TokenStorage.get(...)`
fn token_storage_failure(&mut self, is_set: bool, _: &dyn Error) -> Retry {
let _ = is_set;
Retry::Abort
}
/// The server denied the attempt to obtain a request code
fn request_failure(&mut self, _: RequestError) {}
/// Called if we could not acquire a refresh token for a reason possibly specified
/// by the server.
/// This call is made for the delegate's information only.
fn token_refresh_failed<S: AsRef<str>>(
&mut self,
error: S,
error_description: &Option<String>,
) {
{
let _ = error;
}
{
let _ = error_description;
}
}
}
/// FlowDelegate methods are called when an OAuth flow needs to ask the application what to do in
/// certain cases.
pub trait FlowDelegate: Clone {
/// Called if the request code is expired. You will have to start over in this case.
/// This will be the last call the delegate receives.
/// Given `DateTime` is the expiration date
fn expired(&mut self, _: &DateTime<Utc>) {}
/// Called if the user denied access. You would have to start over.
/// This will be the last call the delegate receives.
fn denied(&mut self) {}
/// Called as long as we are waiting for the user to authorize us.
/// Can be used to print progress information, or decide to time-out.
///
/// If the returned `Retry` variant is a duration.
/// # Notes
/// * Only used in `DeviceFlow`. Return value will only be used if it
/// is larger than the interval desired by the server.
fn pending(&mut self, _: &PollInformation) -> Retry {
Retry::After(Duration::from_secs(5))
}
/// Configure a custom redirect uri if needed.
fn redirect_uri(&self) -> Option<String> {
None
}
/// DeviceFlowDelegate methods are called when a device flow needs to ask the
/// application what to do in certain cases.
pub trait DeviceFlowDelegate: Send + Sync {
/// The server has returned a `user_code` which must be shown to the user,
/// along with the `verification_url`.
/// along with the `verification_uri`.
/// # Notes
/// * Will be called exactly once, provided we didn't abort during `request_code` phase.
/// * Will only be called if the Authenticator's flow_type is `FlowType::Device`.
fn present_user_code(&mut self, pi: &PollInformation) {
println!(
"Please enter {} at {} and grant access to this application",
pi.user_code, pi.verification_url
);
println!("Do not close this application until you either denied or granted access.");
println!(
"You have time until {}.",
pi.expires_at.with_timezone(&Local)
);
fn present_user_code<'a>(
&'a self,
device_auth_resp: &'a DeviceAuthResponse,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
Box::pin(present_user_code(device_auth_resp))
}
}
async fn present_user_code(device_auth_resp: &DeviceAuthResponse) {
println!(
"Please enter {} at {} and grant access to this application",
device_auth_resp.user_code, device_auth_resp.verification_uri
);
println!("Do not close this application until you either denied or granted access.");
println!(
"You have time until {}.",
device_auth_resp.expires_at.with_timezone(&Local)
);
}
/// InstalledFlowDelegate methods are called when an installed flow needs to ask
/// the application what to do in certain cases.
pub trait InstalledFlowDelegate: Send + Sync {
/// Configure a custom redirect uri if needed.
fn redirect_uri(&self) -> Option<&str> {
None
}
/// This method is used by the InstalledFlow.
/// We need the user to navigate to a URL using their browser and potentially paste back a code
/// (or maybe not). Whether they have to enter a code depends on the InstalledFlowReturnMethod
/// used.
fn present_user_url<S: AsRef<str> + fmt::Display>(
&mut self,
url: S,
fn present_user_url<'a>(
&'a self,
url: &'a str,
need_code: bool,
) -> Box<dyn Future<Item = Option<String>, Error = Box<dyn Error + Send>> + Send> {
if need_code {
println!(
"Please direct your browser to {}, follow the instructions and enter the \
code displayed here: ",
url
);
Box::new(
tio::lines(io::BufReader::new(tio::stdin()))
.into_future()
.map_err(|(e, _)| {
println!("{:?}", e);
Box::new(e) as Box<dyn Error + Send>
})
.and_then(|(l, _)| Ok(l)),
)
} else {
println!(
"Please direct your browser to {} and follow the instructions displayed \
there.",
url
);
Box::new(future::ok(None))
}
) -> Pin<Box<dyn Future<Output = Result<String, String>> + Send + 'a>> {
Box::pin(present_user_url(url, need_code))
}
}
/// Uses all default implementations by AuthenticatorDelegate, and makes the trait's
/// implementation usable in the first place.
#[derive(Clone)]
pub struct DefaultAuthenticatorDelegate;
impl AuthenticatorDelegate for DefaultAuthenticatorDelegate {}
async fn present_user_url(url: &str, need_code: bool) -> Result<String, String> {
use tokio::io::AsyncBufReadExt;
if need_code {
println!(
"Please direct your browser to {}, follow the instructions and enter the \
code displayed here: ",
url
);
let mut user_input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut user_input)
.await
.map_err(|e| format!("couldn't read code: {}", e))?;
// remove trailing whitespace.
user_input.truncate(user_input.trim_end().len());
Ok(user_input)
} else {
println!(
"Please direct your browser to {} and follow the instructions displayed \
there.",
url
);
Ok(String::new())
}
}
/// Uses all default implementations in the FlowDelegate trait.
#[derive(Clone)]
pub struct DefaultFlowDelegate;
impl FlowDelegate for DefaultFlowDelegate {}
/// Uses all default implementations in the DeviceFlowDelegate trait.
#[derive(Copy, Clone)]
pub struct DefaultDeviceFlowDelegate;
impl DeviceFlowDelegate for DefaultDeviceFlowDelegate {}
/// Uses all default implementations in the DeviceFlowDelegate trait.
#[derive(Copy, Clone)]
pub struct DefaultInstalledFlowDelegate;
impl InstalledFlowDelegate for DefaultInstalledFlowDelegate {}

View File

@@ -1,213 +1,112 @@
use std::iter::{FromIterator, IntoIterator};
use crate::authenticator_delegate::{
DefaultDeviceFlowDelegate, DeviceAuthResponse, DeviceFlowDelegate,
};
use crate::error::{AuthError, Error};
use crate::types::{ApplicationSecret, TokenInfo};
use std::borrow::Cow;
use std::time::Duration;
use ::log::{error, log};
use chrono::{self, Utc};
use futures::stream::Stream;
use futures::{future, prelude::*};
use http;
use hyper;
use hyper::header;
use itertools::Itertools;
use serde_json as json;
use tokio_timer;
use url::form_urlencoded;
use crate::authenticator_delegate::{DefaultFlowDelegate, FlowDelegate, PollInformation, Retry};
use crate::types::{
ApplicationSecret, Flow, FlowType, GetToken, JsonError, PollError, RequestError, Token,
};
pub const GOOGLE_DEVICE_CODE_URL: &str = "https://accounts.google.com/o/oauth2/device/code";
pub const GOOGLE_DEVICE_CODE_URL: &'static str = "https://accounts.google.com/o/oauth2/device/code";
// https://developers.google.com/identity/protocols/OAuth2ForDevices#step-4:-poll-googles-authorization-server
pub const GOOGLE_GRANT_TYPE: &str = "http://oauth.net/grant_type/device/1.0";
/// Implements the [Oauth2 Device Flow](https://developers.google.com/youtube/v3/guides/authentication#devices)
/// It operates in two steps:
/// * obtain a code to show to the user
/// * (repeatedly) poll for the user to authenticate your application
#[derive(Clone)]
pub struct DeviceFlow<FD> {
application_secret: ApplicationSecret,
device_code_url: String,
flow_delegate: FD,
wait: Duration,
pub struct DeviceFlow {
pub(crate) app_secret: ApplicationSecret,
pub(crate) device_code_url: Cow<'static, str>,
pub(crate) flow_delegate: Box<dyn DeviceFlowDelegate>,
pub(crate) grant_type: Cow<'static, str>,
}
impl DeviceFlow<DefaultFlowDelegate> {
impl DeviceFlow {
/// Create a new DeviceFlow. The default FlowDelegate will be used and the
/// default wait time is 120 seconds.
pub fn new(secret: ApplicationSecret) -> DeviceFlow<DefaultFlowDelegate> {
pub(crate) fn new(app_secret: ApplicationSecret) -> Self {
DeviceFlow {
application_secret: secret,
device_code_url: GOOGLE_DEVICE_CODE_URL.to_string(),
flow_delegate: DefaultFlowDelegate,
wait: Duration::from_secs(120),
}
}
}
impl<FD> DeviceFlow<FD> {
/// Use the provided device code url.
pub fn device_code_url(self, url: String) -> Self {
DeviceFlow {
device_code_url: url,
..self
app_secret,
device_code_url: GOOGLE_DEVICE_CODE_URL.into(),
flow_delegate: Box::new(DefaultDeviceFlowDelegate),
grant_type: GOOGLE_GRANT_TYPE.into(),
}
}
/// Use the provided FlowDelegate.
pub fn delegate<NewFD>(self, delegate: NewFD) -> DeviceFlow<NewFD> {
DeviceFlow {
application_secret: self.application_secret,
device_code_url: self.device_code_url,
flow_delegate: delegate,
wait: self.wait,
}
}
/// Use the provided wait duration.
pub fn wait_duration(self, duration: Duration) -> Self {
DeviceFlow {
wait: duration,
..self
}
}
}
impl<FD, C> crate::authenticator::AuthFlow<C> for DeviceFlow<FD>
where
FD: FlowDelegate + Send + 'static,
C: hyper::client::connect::Connect + 'static,
{
type TokenGetter = DeviceFlowImpl<FD, C>;
fn build_token_getter(self, client: hyper::Client<C>) -> Self::TokenGetter {
DeviceFlowImpl {
client,
application_secret: self.application_secret,
device_code_url: self.device_code_url,
fd: self.flow_delegate,
wait: Duration::from_secs(1200),
}
}
}
/// The DeviceFlow implementation.
pub struct DeviceFlowImpl<FD, C> {
client: hyper::Client<C, hyper::Body>,
application_secret: ApplicationSecret,
/// Usually GOOGLE_DEVICE_CODE_URL
device_code_url: String,
fd: FD,
wait: Duration,
}
impl<FD, C> Flow for DeviceFlowImpl<FD, C> {
fn type_id() -> FlowType {
FlowType::Device(String::new())
}
}
impl<
FD: FlowDelegate + Clone + Send + 'static,
C: hyper::client::connect::Connect + Sync + 'static,
> GetToken for DeviceFlowImpl<FD, C>
{
fn token<I, T>(
&mut self,
scopes: I,
) -> Box<dyn Future<Item = Token, Error = RequestError> + Send>
pub(crate) async fn token<C, T>(
&self,
hyper_client: &hyper::Client<C>,
scopes: &[T],
) -> Result<TokenInfo, Error>
where
T: Into<String>,
I: IntoIterator<Item = T>,
T: AsRef<str>,
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
{
self.retrieve_device_token(Vec::from_iter(scopes.into_iter().map(Into::into)))
}
fn api_key(&mut self) -> Option<String> {
None
}
fn application_secret(&self) -> ApplicationSecret {
self.application_secret.clone()
}
}
impl<FD, C> DeviceFlowImpl<FD, C>
where
C: hyper::client::connect::Connect + Sync + 'static,
C::Transport: 'static,
C::Future: 'static,
FD: FlowDelegate + Clone + Send + 'static,
{
/// Essentially what `GetToken::token` does: Retrieve a token for the given scopes without
/// caching.
fn retrieve_device_token<'a>(
&mut self,
scopes: Vec<String>,
) -> Box<dyn Future<Item = Token, Error = RequestError> + Send> {
let application_secret = self.application_secret.clone();
let client = self.client.clone();
let wait = self.wait;
let mut fd = self.fd.clone();
let request_code = Self::request_code(
application_secret.clone(),
client.clone(),
self.device_code_url.clone(),
let device_auth_resp = Self::request_code(
&self.app_secret,
hyper_client,
&self.device_code_url,
scopes,
)
.and_then(move |(pollinf, device_code)| {
fd.present_user_code(&pollinf);
Ok((pollinf, device_code))
});
let fd = self.fd.clone();
Box::new(request_code.and_then(move |(pollinf, device_code)| {
future::loop_fn(0, move |i| {
// Make a copy of everything every time, because the loop function needs to be
// repeatable, i.e. we can't move anything out.
let pt = Self::poll_token(
application_secret.clone(),
client.clone(),
device_code.clone(),
pollinf.clone(),
fd.clone(),
);
let maxn = wait.as_secs() / pollinf.interval.as_secs();
let mut fd = fd.clone();
let pollinf = pollinf.clone();
tokio_timer::sleep(pollinf.interval)
.then(|_| pt)
.then(move |r| match r {
Ok(None) if i < maxn => match fd.pending(&pollinf) {
Retry::Abort | Retry::Skip => {
Box::new(Err(RequestError::Poll(PollError::TimedOut)).into_future())
}
Retry::After(d) => Box::new(
tokio_timer::sleep(d)
.then(move |_| Ok(future::Loop::Continue(i + 1))),
)
as Box<
dyn Future<
Item = future::Loop<Token, u64>,
Error = RequestError,
> + Send,
>,
},
Ok(Some(tok)) => Box::new(Ok(future::Loop::Break(tok)).into_future()),
Err(e @ PollError::AccessDenied)
| Err(e @ PollError::TimedOut)
| Err(e @ PollError::Expired(_)) => {
Box::new(Err(RequestError::Poll(e)).into_future())
}
Err(ref e) if i < maxn => {
error!("Unknown error from poll token api: {}", e);
Box::new(Ok(future::Loop::Continue(i + 1)).into_future())
}
// Too many attempts.
Ok(None) | Err(_) => {
error!("Too many poll attempts");
Box::new(Err(RequestError::Poll(PollError::TimedOut)).into_future())
}
})
})
}))
.await?;
log::debug!("Presenting code to user");
self.flow_delegate
.present_user_code(&device_auth_resp)
.await;
self.wait_for_device_token(
hyper_client,
&self.app_secret,
&device_auth_resp,
&self.grant_type,
)
.await
}
async fn wait_for_device_token<C>(
&self,
hyper_client: &hyper::Client<C>,
app_secret: &ApplicationSecret,
device_auth_resp: &DeviceAuthResponse,
grant_type: &str,
) -> Result<TokenInfo, Error>
where
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
{
let mut interval = device_auth_resp.interval;
log::debug!("Polling every {:?} for device token", interval);
loop {
tokio::time::delay_for(interval).await;
interval = match Self::poll_token(
&app_secret,
hyper_client,
&device_auth_resp.device_code,
grant_type,
)
.await
{
Ok(token) => return Ok(token),
Err(Error::AuthError(AuthError { error, .. }))
if error.as_str() == "authorization_pending" =>
{
log::debug!("still waiting on authorization from the server");
interval
}
Err(Error::AuthError(AuthError { error, .. })) if error.as_str() == "slow_down" => {
let interval = interval + Duration::from_secs(5);
log::debug!(
"server requested slow_down. Increasing polling interval to {:?}",
interval
);
interval
}
Err(err) => return Err(err),
}
}
}
/// The first step involves asking the server for a code that the user
@@ -225,95 +124,40 @@ where
/// * If called after a successful result was returned at least once.
/// # Examples
/// See test-cases in source code for a more complete example.
fn request_code(
application_secret: ApplicationSecret,
client: hyper::Client<C>,
device_code_url: String,
scopes: Vec<String>,
) -> impl Future<Item = (PollInformation, String), Error = RequestError> {
// note: cloned() shouldn't be needed, see issue
// https://github.com/servo/rust-url/issues/81
async fn request_code<C, T>(
application_secret: &ApplicationSecret,
client: &hyper::Client<C>,
device_code_url: &str,
scopes: &[T],
) -> Result<DeviceAuthResponse, Error>
where
T: AsRef<str>,
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
{
let req = form_urlencoded::Serializer::new(String::new())
.extend_pairs(&[
("client_id", application_secret.client_id.clone()),
(
"scope",
scopes
.into_iter()
.intersperse(" ".to_string())
.collect::<String>(),
),
("client_id", application_secret.client_id.as_str()),
("scope", crate::helper::join(scopes, " ").as_str()),
])
.finish();
// note: works around bug in rustlang
// https://github.com/rust-lang/rust/issues/22252
let request = hyper::Request::post(device_code_url)
let req = hyper::Request::post(device_code_url)
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(hyper::Body::from(req))
.into_future();
request
.then(
move |request: Result<hyper::Request<hyper::Body>, http::Error>| {
let request = request.unwrap();
client.request(request)
},
)
.then(
|r: Result<hyper::Response<hyper::Body>, hyper::error::Error>| {
match r {
Err(err) => {
return Err(RequestError::ClientError(err));
}
Ok(res) => {
// This return type is defined in https://tools.ietf.org/html/draft-ietf-oauth-device-flow-15#section-3.2
// The alias is present as Google use a non-standard name for verification_uri.
// According to the standard interval is optional, however, all tested implementations provide it.
// verification_uri_complete is optional in the standard but not provided in tested implementations.
#[derive(Deserialize)]
struct JsonData {
device_code: String,
user_code: String,
#[serde(alias = "verification_url")]
verification_uri: String,
expires_in: Option<i64>,
interval: i64,
}
let json_str: String = res
.into_body()
.concat2()
.wait()
.map(|c| String::from_utf8(c.into_bytes().to_vec()).unwrap())
.unwrap(); // TODO: error handling
// check for error
match json::from_str::<JsonError>(&json_str) {
Err(_) => {} // ignore, move on
Ok(res) => return Err(RequestError::from(res)),
}
let decoded: JsonData = json::from_str(&json_str).unwrap();
let expires_in = decoded.expires_in.unwrap_or(60 * 60);
let pi = PollInformation {
user_code: decoded.user_code,
verification_url: decoded.verification_uri,
expires_at: Utc::now() + chrono::Duration::seconds(expires_in),
interval: Duration::from_secs(i64::abs(decoded.interval) as u64),
};
Ok((pi, decoded.device_code))
}
}
},
)
.unwrap();
log::debug!("requesting code from server: {:?}", req);
let (head, body) = client.request(req).await?.into_parts();
let body = hyper::body::to_bytes(body).await?;
log::debug!("received response; head: {:?}, body: {:?}", head, body);
DeviceAuthResponse::from_json(&body)
}
/// If the first call is successful, this method may be called.
/// As long as we are waiting for authentication, it will return `Ok(None)`.
/// You should call it within the interval given the previously returned
/// `PollInformation.interval` field.
/// `DeviceAuthResponse.interval` field.
///
/// The operation was successful once you receive an Ok(Some(Token)) for the first time.
/// Subsequent calls will return the previous result, which may also be an error state.
@@ -328,27 +172,22 @@ where
///
/// # Examples
/// See test-cases in source code for a more complete example.
fn poll_token<'a>(
application_secret: ApplicationSecret,
client: hyper::Client<C>,
device_code: String,
pi: PollInformation,
mut fd: FD,
) -> impl Future<Item = Option<Token>, Error = PollError> {
let expired = if pi.expires_at <= Utc::now() {
fd.expired(&pi.expires_at);
Err(PollError::Expired(pi.expires_at)).into_future()
} else {
Ok(()).into_future()
};
async fn poll_token<'a, C>(
application_secret: &ApplicationSecret,
client: &hyper::Client<C>,
device_code: &str,
grant_type: &str,
) -> Result<TokenInfo, Error>
where
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
{
// We should be ready for a new request
let req = form_urlencoded::Serializer::new(String::new())
.extend_pairs(&[
("client_id", &application_secret.client_id[..]),
("client_secret", &application_secret.client_secret),
("code", &device_code),
("grant_type", "http://oauth.net/grant_type/device/1.0"),
("client_id", application_secret.client_id.as_str()),
("client_secret", application_secret.client_secret.as_str()),
("code", device_code),
("grant_type", grant_type),
])
.finish();
@@ -356,184 +195,10 @@ where
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(hyper::Body::from(req))
.unwrap(); // TODO: Error checking
expired
.and_then(move |_| client.request(request).map_err(|e| PollError::HttpError(e)))
.map(|res| {
res.into_body()
.concat2()
.wait()
.map(|c| String::from_utf8(c.into_bytes().to_vec()).unwrap())
.unwrap() // TODO: error handling
})
.and_then(move |json_str: String| {
#[derive(Deserialize)]
struct JsonError {
error: String,
}
match json::from_str::<JsonError>(&json_str) {
Err(_) => {} // ignore, move on, it's not an error
Ok(res) => {
match res.error.as_ref() {
"access_denied" => {
fd.denied();
return Err(PollError::AccessDenied);
}
"authorization_pending" => return Ok(None),
s => {
return Err(PollError::Other(format!(
"server message '{}' not understood",
s
)))
}
};
}
}
// yes, we expect that !
let mut t: Token = json::from_str(&json_str).unwrap();
t.set_expiry_absolute();
Ok(Some(t.clone()))
})
}
}
#[cfg(test)]
mod tests {
use hyper;
use hyper_rustls::HttpsConnector;
use mockito;
use tokio;
use super::*;
use crate::authenticator::AuthFlow;
use crate::helper::parse_application_secret;
#[test]
fn test_device_end2end() {
#[derive(Clone)]
struct FD;
impl FlowDelegate for FD {
fn present_user_code(&mut self, pi: &PollInformation) {
assert_eq!("https://example.com/verify", pi.verification_url);
}
}
let server_url = mockito::server_url();
let app_secret = r#"{"installed":{"client_id":"902216714886-k2v9uei3p1dk6h686jbsn9mo96tnbvto.apps.googleusercontent.com","project_id":"yup-test-243420","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"iuMPN6Ne1PD7cos29Tk9rlqH","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}}"#;
let mut app_secret = parse_application_secret(app_secret).unwrap();
app_secret.token_uri = format!("{}/token", server_url);
let device_code_url = format!("{}/code", server_url);
let https = HttpsConnector::new(1);
let client = hyper::Client::builder()
.keep_alive(false)
.build::<_, hyper::Body>(https);
let mut flow = DeviceFlow::new(app_secret)
.delegate(FD)
.device_code_url(device_code_url)
.build_token_getter(client);
let mut rt = tokio::runtime::Builder::new()
.core_threads(1)
.panic_handler(|e| std::panic::resume_unwind(e))
.build()
.unwrap();
// Successful path
{
let code_response = r#"{"device_code": "devicecode", "user_code": "usercode", "verification_url": "https://example.com/verify", "expires_in": 1234567, "interval": 1}"#;
let _m = mockito::mock("POST", "/code")
.match_body(mockito::Matcher::Regex(
".*client_id=902216714886.*".to_string(),
))
.with_status(200)
.with_body(code_response)
.create();
let token_response = r#"{"access_token": "accesstoken", "refresh_token": "refreshtoken", "token_type": "Bearer", "expires_in": 1234567}"#;
let _m = mockito::mock("POST", "/token")
.match_body(mockito::Matcher::Regex(
".*client_secret=iuMPN6Ne1PD7cos29Tk9rlqH&code=devicecode.*".to_string(),
))
.with_status(200)
.with_body(token_response)
.create();
let fut = flow
.token(vec!["https://www.googleapis.com/scope/1"])
.then(|token| {
let token = token.unwrap();
assert_eq!("accesstoken", token.access_token);
Ok(()) as Result<(), ()>
});
rt.block_on(fut).expect("block_on");
_m.assert();
}
// Code is not delivered.
{
let code_response =
r#"{"error": "invalid_client_id", "error_description": "description"}"#;
let _m = mockito::mock("POST", "/code")
.match_body(mockito::Matcher::Regex(
".*client_id=902216714886.*".to_string(),
))
.with_status(400)
.with_body(code_response)
.create();
let token_response = r#"{"access_token": "accesstoken", "refresh_token": "refreshtoken", "token_type": "Bearer", "expires_in": 1234567}"#;
let _m = mockito::mock("POST", "/token")
.match_body(mockito::Matcher::Regex(
".*client_secret=iuMPN6Ne1PD7cos29Tk9rlqH&code=devicecode.*".to_string(),
))
.with_status(200)
.with_body(token_response)
.expect(0) // Never called!
.create();
let fut = flow
.token(vec!["https://www.googleapis.com/scope/1"])
.then(|token| {
assert!(token.is_err());
assert!(format!("{}", token.unwrap_err()).contains("invalid_client_id"));
Ok(()) as Result<(), ()>
});
rt.block_on(fut).expect("block_on");
_m.assert();
}
// Token is not delivered.
{
let code_response = r#"{"device_code": "devicecode", "user_code": "usercode", "verification_url": "https://example.com/verify", "expires_in": 1234567, "interval": 1}"#;
let _m = mockito::mock("POST", "/code")
.match_body(mockito::Matcher::Regex(
".*client_id=902216714886.*".to_string(),
))
.with_status(200)
.with_body(code_response)
.create();
let token_response = r#"{"error": "access_denied"}"#;
let _m = mockito::mock("POST", "/token")
.match_body(mockito::Matcher::Regex(
".*client_secret=iuMPN6Ne1PD7cos29Tk9rlqH&code=devicecode.*".to_string(),
))
.with_status(400)
.with_body(token_response)
.expect(1)
.create();
let fut = flow
.token(vec!["https://www.googleapis.com/scope/1"])
.then(|token| {
assert!(token.is_err());
assert!(format!("{}", token.unwrap_err()).contains("Access denied by user"));
Ok(()) as Result<(), ()>
});
rt.block_on(fut).expect("block_on");
_m.assert();
}
log::debug!("polling for token: {:?}", request);
let (head, body) = 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)
}
}

256
src/error.rs Normal file
View File

@@ -0,0 +1,256 @@
//! Module containing various error types.
use std::borrow::Cow;
use std::error::Error as StdError;
use std::fmt;
use std::io;
use serde::Deserialize;
/// Error returned by the authorization server.
/// https://tools.ietf.org/html/rfc6749#section-5.2
/// https://tools.ietf.org/html/rfc8628#section-3.5
#[derive(Deserialize, Debug, PartialEq, Eq)]
pub struct AuthError {
/// Error code from the server.
pub error: AuthErrorCode,
/// Human-readable text providing additional information.
pub error_description: Option<String>,
/// A URI identifying a human-readable web page with information about the error.
pub error_uri: Option<String>,
}
impl fmt::Display for AuthError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", &self.error.as_str())?;
if let Some(desc) = &self.error_description {
write!(f, ": {}", desc)?;
}
if let Some(uri) = &self.error_uri {
write!(f, "; See {} for more info", uri)?;
}
Ok(())
}
}
impl StdError for AuthError {}
/// The error code returned by the authorization server.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum AuthErrorCode {
/// invalid_request
InvalidRequest,
/// invalid_client
InvalidClient,
/// invalid_grant
InvalidGrant,
/// unauthorized_client
UnauthorizedClient,
/// unsupported_grant_type
UnsupportedGrantType,
/// invalid_scope
InvalidScope,
/// access_denied
AccessDenied,
/// expired_token
ExpiredToken,
/// other error
Other(String),
}
impl AuthErrorCode {
/// The error code as a &str
pub fn as_str(&self) -> &str {
match self {
AuthErrorCode::InvalidRequest => "invalid_request",
AuthErrorCode::InvalidClient => "invalid_client",
AuthErrorCode::InvalidGrant => "invalid_grant",
AuthErrorCode::UnauthorizedClient => "unauthorized_client",
AuthErrorCode::UnsupportedGrantType => "unsupported_grant_type",
AuthErrorCode::InvalidScope => "invalid_scope",
AuthErrorCode::AccessDenied => "access_denied",
AuthErrorCode::ExpiredToken => "expired_token",
AuthErrorCode::Other(s) => s.as_str(),
}
}
fn from_string<'a>(s: impl Into<Cow<'a, str>>) -> AuthErrorCode {
let s = s.into();
match s.as_ref() {
"invalid_request" => AuthErrorCode::InvalidRequest,
"invalid_client" => AuthErrorCode::InvalidClient,
"invalid_grant" => AuthErrorCode::InvalidGrant,
"unauthorized_client" => AuthErrorCode::UnauthorizedClient,
"unsupported_grant_type" => AuthErrorCode::UnsupportedGrantType,
"invalid_scope" => AuthErrorCode::InvalidScope,
"access_denied" => AuthErrorCode::AccessDenied,
"expired_token" => AuthErrorCode::ExpiredToken,
_ => AuthErrorCode::Other(s.into_owned()),
}
}
}
impl From<String> for AuthErrorCode {
fn from(s: String) -> Self {
AuthErrorCode::from_string(s)
}
}
impl<'a> From<&'a str> for AuthErrorCode {
fn from(s: &str) -> Self {
AuthErrorCode::from_string(s)
}
}
impl<'de> Deserialize<'de> for AuthErrorCode {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct V;
impl<'de> serde::de::Visitor<'de> for V {
type Value = AuthErrorCode;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("any string")
}
fn visit_string<E: serde::de::Error>(self, value: String) -> Result<Self::Value, E> {
Ok(value.into())
}
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
Ok(value.into())
}
}
deserializer.deserialize_string(V)
}
}
/// A helper type to deserialize either an AuthError or another piece of data.
#[derive(Deserialize, Debug)]
#[serde(untagged)]
pub(crate) enum AuthErrorOr<T> {
AuthError(AuthError),
Data(T),
}
impl<T> AuthErrorOr<T> {
pub(crate) fn into_result(self) -> Result<T, AuthError> {
match self {
AuthErrorOr::AuthError(err) => Result::Err(err),
AuthErrorOr::Data(value) => Result::Ok(value),
}
}
}
/// Encapsulates all possible results of the `token(...)` operation
#[derive(Debug)]
pub enum Error {
/// Indicates connection failure
HttpError(hyper::Error),
/// The server returned an error.
AuthError(AuthError),
/// Error while decoding a JSON response.
JSONError(serde_json::Error),
/// Error within user input.
UserError(String),
/// A lower level IO error.
LowLevelError(io::Error),
}
impl From<hyper::Error> for Error {
fn from(error: hyper::Error) -> Error {
Error::HttpError(error)
}
}
impl From<AuthError> for Error {
fn from(value: AuthError) -> Error {
Error::AuthError(value)
}
}
impl From<serde_json::Error> for Error {
fn from(value: serde_json::Error) -> Error {
Error::JSONError(value)
}
}
impl From<io::Error> for Error {
fn from(value: io::Error) -> Error {
Error::LowLevelError(value)
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
Error::HttpError(ref err) => err.fmt(f),
Error::AuthError(ref err) => err.fmt(f),
Error::JSONError(ref e) => {
write!(
f,
"JSON Error; this might be a bug with unexpected server responses! {}",
e
)?;
Ok(())
}
Error::UserError(ref s) => s.fmt(f),
Error::LowLevelError(ref e) => e.fmt(f),
}
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match *self {
Error::HttpError(ref err) => Some(err),
Error::AuthError(ref err) => Some(err),
Error::JSONError(ref err) => Some(err),
Error::LowLevelError(ref err) => Some(err),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_auth_error_code_deserialize() {
assert_eq!(
AuthErrorCode::InvalidRequest,
serde_json::from_str(r#""invalid_request""#).unwrap()
);
assert_eq!(
AuthErrorCode::InvalidClient,
serde_json::from_str(r#""invalid_client""#).unwrap()
);
assert_eq!(
AuthErrorCode::InvalidGrant,
serde_json::from_str(r#""invalid_grant""#).unwrap()
);
assert_eq!(
AuthErrorCode::UnauthorizedClient,
serde_json::from_str(r#""unauthorized_client""#).unwrap()
);
assert_eq!(
AuthErrorCode::UnsupportedGrantType,
serde_json::from_str(r#""unsupported_grant_type""#).unwrap()
);
assert_eq!(
AuthErrorCode::InvalidScope,
serde_json::from_str(r#""invalid_scope""#).unwrap()
);
assert_eq!(
AuthErrorCode::AccessDenied,
serde_json::from_str(r#""access_denied""#).unwrap()
);
assert_eq!(
AuthErrorCode::ExpiredToken,
serde_json::from_str(r#""expired_token""#).unwrap()
);
assert_eq!(
AuthErrorCode::Other("undefined".to_owned()),
serde_json::from_str(r#""undefined""#).unwrap()
);
}
}

View File

@@ -1,63 +1,72 @@
#![allow(dead_code)]
//! Helper functions allowing you to avoid writing boilerplate code for common operations, such as
//! parsing JSON or reading files.
// Copyright (c) 2016 Google Inc (lewinb@google.com).
//
// Refer to the project root for licensing information.
use serde_json;
use std::fs;
use std::io::{self, Read};
use std::path::Path;
use crate::service_account::ServiceAccountKey;
use crate::types::{ApplicationSecret, ConsoleApplicationSecret};
/// Read an application secret from a file.
pub fn read_application_secret(path: &Path) -> io::Result<ApplicationSecret> {
let mut secret = String::new();
let mut file = fs::OpenOptions::new().read(true).open(path)?;
file.read_to_string(&mut secret)?;
use std::io;
use std::path::Path;
parse_application_secret(&secret)
/// Read an application secret from a file.
pub async fn read_application_secret<P: AsRef<Path>>(path: P) -> io::Result<ApplicationSecret> {
parse_application_secret(tokio::fs::read(path).await?)
}
/// Read an application secret from a JSON string.
pub fn parse_application_secret<S: AsRef<str>>(secret: S) -> io::Result<ApplicationSecret> {
let result: serde_json::Result<ConsoleApplicationSecret> =
serde_json::from_str(secret.as_ref());
match result {
Err(e) => Err(io::Error::new(
pub fn parse_application_secret<S: AsRef<[u8]>>(secret: S) -> io::Result<ApplicationSecret> {
let decoded: ConsoleApplicationSecret =
serde_json::from_slice(secret.as_ref()).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Bad application secret: {}", e),
)
})?;
if let Some(web) = decoded.web {
Ok(web)
} else if let Some(installed) = decoded.installed {
Ok(installed)
} else {
Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Bad application secret: {}", e),
)),
Ok(decoded) => {
if decoded.web.is_some() {
Ok(decoded.web.unwrap())
} else if decoded.installed.is_some() {
Ok(decoded.installed.unwrap())
} else {
Err(io::Error::new(
io::ErrorKind::InvalidData,
"Unknown application secret format",
))
}
}
"Unknown application secret format",
))
}
}
/// Read a service account key from a JSON file. You can download the JSON keys from the Google
/// Cloud Console or the respective console of your service provider.
pub fn service_account_key_from_file<S: AsRef<Path>>(path: S) -> io::Result<ServiceAccountKey> {
let mut key = String::new();
let mut file = fs::OpenOptions::new().read(true).open(path)?;
file.read_to_string(&mut key)?;
match serde_json::from_str(&key) {
Err(e) => Err(io::Error::new(io::ErrorKind::InvalidData, format!("{}", e))),
Ok(decoded) => Ok(decoded),
}
pub async fn read_service_account_key<P: AsRef<Path>>(path: P) -> io::Result<ServiceAccountKey> {
let key = tokio::fs::read(path).await?;
serde_json::from_slice(&key).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Bad service account key: {}", e),
)
})
}
pub(crate) fn join<T>(pieces: &[T], separator: &str) -> String
where
T: AsRef<str>,
{
let mut iter = pieces.iter();
let first = match iter.next() {
Some(p) => p,
None => return String::new(),
};
let num_separators = pieces.len() - 1;
let pieces_size: usize = pieces.iter().map(|p| p.as_ref().len()).sum();
let size = pieces_size + separator.len() * num_separators;
let mut result = String::with_capacity(size);
result.push_str(first.as_ref());
for p in iter {
result.push_str(separator);
result.push_str(p.as_ref());
}
debug_assert_eq!(size, result.len());
result
}

File diff suppressed because it is too large Load Diff

View File

@@ -20,72 +20,60 @@
//! based on the Google APIs; it may or may not work with other providers.
//!
//! # Installed Flow Usage
//! The `InstalledFlow` involves showing a URL to the user (or opening it in a browser)
//! The installed flow involves showing a URL to the user (or opening it in a browser)
//! and then either prompting the user to enter a displayed code, or make the authorizing
//! website redirect to a web server spun up by this library and running on localhost.
//!
//! In order to use the interactive method, use the `InstalledInteractive` `FlowType`;
//! for the redirect method, use `InstalledRedirect`, with the port number to let the
//! server listen on.
//! In order to use the interactive method, use the `Interactive` `InstalledFlowReturnMethod`;
//! for the redirect method, use `HTTPRedirect`.
//!
//! You can implement your own `AuthenticatorDelegate` in order to customize the flow;
//! the `InstalledFlow` uses the `present_user_url` method.
//! the installed flow uses the `present_user_url` method.
//!
//! The returned `Token` is stored permanently in the given token storage in order to
//! authorize future API requests to the same scopes.
//! The returned `Token` will be stored in memory in order to authorize future
//! API requests to the same scopes. The tokens can optionally be persisted to
//! disk by using `persist_tokens_to_disk` when creating the authenticator.
//!
//! The following example, which is derived from the (actual and runnable) example in
//! `examples/test-installed/`, shows the basics of using this crate:
//!
//! ```test_harness,no_run
//! use futures::prelude::*;
//! use yup_oauth2::GetToken;
//! use yup_oauth2::{Authenticator, InstalledFlow};
//! use yup_oauth2::{InstalledFlowAuthenticator, InstalledFlowReturnMethod};
//!
//! use hyper::client::Client;
//! use hyper_rustls::HttpsConnector;
//!
//! use std::path::Path;
//!
//! fn main() {
//! #[tokio::main]
//! async fn main() {
//! // Read application secret from a file. Sometimes it's easier to compile it directly into
//! // the binary. The clientsecret file contains JSON like `{"installed":{"client_id": ... }}`
//! let secret = yup_oauth2::read_application_secret(Path::new("clientsecret.json"))
//! let secret = yup_oauth2::read_application_secret("clientsecret.json")
//! .await
//! .expect("clientsecret.json");
//!
//! // Create an authenticator that uses an InstalledFlow to authenticate. The
//! // authentication tokens are persisted to a file named tokencache.json. The
//! // authenticator takes care of caching tokens to disk and refreshing tokens once
//! // they've expired.
//! let mut auth = Authenticator::new(
//! InstalledFlow::new(secret, yup_oauth2::InstalledFlowReturnMethod::HTTPRedirect(0))
//! )
//! // authentication tokens are persisted to a file named tokencache.json. The
//! // authenticator takes care of caching tokens to disk and refreshing tokens once
//! // they've expired.
//! let mut auth = InstalledFlowAuthenticator::builder(secret, InstalledFlowReturnMethod::HTTPRedirect)
//! .persist_tokens_to_disk("tokencache.json")
//! .build()
//! .await
//! .unwrap();
//!
//! let s = "https://www.googleapis.com/auth/drive.file".to_string();
//! let scopes = vec![s];
//! let scopes = &["https://www.googleapis.com/auth/drive.file"];
//!
//! // token(<scopes>) is the one important function of this crate; it does everything to
//! // obtain a token that can be sent e.g. as Bearer token.
//! let tok = auth.token(scopes);
//! // Finally we print the token.
//! let fut = tok.map_err(|e| println!("error: {:?}", e)).and_then(|t| {
//! println!("The token is {:?}", t);
//! Ok(())
//! });
//!
//! tokio::run(fut)
//! match auth.token(scopes).await {
//! Ok(token) => println!("The token is {:?}", token),
//! Err(e) => println!("error: {:?}", e),
//! }
//! }
//! ```
//!
#[macro_use]
extern crate serde_derive;
mod authenticator;
mod authenticator_delegate;
#![deny(missing_docs)]
pub mod authenticator;
pub mod authenticator_delegate;
mod device;
pub mod error;
mod helper;
mod installed;
mod refresh;
@@ -93,17 +81,16 @@ mod service_account;
mod storage;
mod types;
pub use crate::authenticator::{AuthFlow, Authenticator};
pub use crate::authenticator_delegate::{
AuthenticatorDelegate, DefaultAuthenticatorDelegate, DefaultFlowDelegate, FlowDelegate,
PollInformation,
#[doc(inline)]
pub use crate::authenticator::{
DeviceFlowAuthenticator, InstalledFlowAuthenticator, ServiceAccountAuthenticator,
};
pub use crate::device::{DeviceFlow, GOOGLE_DEVICE_CODE_URL};
pub use crate::helper::*;
pub use crate::installed::{InstalledFlow, InstalledFlowReturnMethod};
pub use crate::service_account::*;
pub use crate::storage::{DiskTokenStorage, MemoryStorage, NullStorage, TokenStorage};
pub use crate::types::{
ApplicationSecret, ConsoleApplicationSecret, FlowType, GetToken, PollError, RefreshResult,
RequestError, Scheme, Token, TokenType,
};
pub use crate::installed::InstalledFlowReturnMethod;
pub use crate::service_account::ServiceAccountKey;
#[doc(inline)]
pub use crate::error::Error;
pub use crate::types::{AccessToken, ApplicationSecret, ConsoleApplicationSecret};

View File

@@ -1,12 +1,7 @@
use crate::types::{ApplicationSecret, JsonError, RefreshResult, RequestError};
use crate::error::Error;
use crate::types::{ApplicationSecret, TokenInfo};
use super::Token;
use chrono::Utc;
use futures::stream::Stream;
use futures::Future;
use hyper;
use hyper::header;
use serde_json as json;
use url::form_urlencoded;
/// Implements the [OAuth2 Refresh Token Flow](https://developers.google.com/youtube/v3/guides/authentication#devices).
@@ -14,7 +9,7 @@ use url::form_urlencoded;
/// Refresh an expired access token, as obtained by any other authentication flow.
/// This flow is useful when your `Token` is expired and allows to obtain a new
/// and valid access token.
pub struct RefreshFlow;
pub(crate) struct RefreshFlow;
impl RefreshFlow {
/// Attempt to refresh the given token, and obtain a new, valid one.
@@ -31,155 +26,41 @@ impl RefreshFlow {
///
/// # Examples
/// Please see the crate landing page for an example.
pub fn refresh_token<'a, C: 'static + hyper::client::connect::Connect>(
client: hyper::Client<C>,
client_secret: ApplicationSecret,
refresh_token: String,
) -> impl 'a + Future<Item = RefreshResult, Error = RequestError> {
pub(crate) async fn refresh_token<C>(
client: &hyper::Client<C>,
client_secret: &ApplicationSecret,
refresh_token: &str,
) -> Result<TokenInfo, Error>
where
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
{
log::debug!(
"refreshing access token with refresh token: {}",
refresh_token
);
let req = form_urlencoded::Serializer::new(String::new())
.extend_pairs(&[
("client_id", client_secret.client_id.clone()),
("client_secret", client_secret.client_secret.clone()),
("refresh_token", refresh_token.to_string()),
("grant_type", "refresh_token".to_string()),
("client_id", client_secret.client_id.as_str()),
("client_secret", client_secret.client_secret.as_str()),
("refresh_token", refresh_token),
("grant_type", "refresh_token"),
])
.finish();
let request = hyper::Request::post(client_secret.token_uri.clone())
let request = hyper::Request::post(&client_secret.token_uri)
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(hyper::Body::from(req))
.unwrap(); // TODO: error handling
client
.request(request)
.then(|r| {
match r {
Err(err) => return Err(RefreshResult::Error(err)),
Ok(res) => {
Ok(res
.into_body()
.concat2()
.wait()
.map(|c| String::from_utf8(c.into_bytes().to_vec()).unwrap())
.unwrap()) // TODO: error handling
}
}
})
.then(move |maybe_json_str: Result<String, RefreshResult>| {
if let Err(e) = maybe_json_str {
return Ok(e);
}
let json_str = maybe_json_str.unwrap();
#[derive(Deserialize)]
struct JsonToken {
access_token: String,
token_type: String,
expires_in: i64,
}
match json::from_str::<JsonError>(&json_str) {
Err(_) => {}
Ok(res) => {
return Ok(RefreshResult::RefreshError(
res.error,
res.error_description,
))
}
}
let t: JsonToken = json::from_str(&json_str).unwrap();
Ok(RefreshResult::Success(Token {
access_token: t.access_token,
token_type: t.token_type,
refresh_token: Some(refresh_token.to_string()),
expires_in: None,
expires_in_timestamp: Some(Utc::now().timestamp() + t.expires_in),
}))
})
.map_err(RequestError::Refresh)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::helper;
use hyper;
use hyper_rustls::HttpsConnector;
use mockito;
use tokio;
#[test]
fn test_refresh_end2end() {
let server_url = mockito::server_url();
let app_secret = r#"{"installed":{"client_id":"902216714886-k2v9uei3p1dk6h686jbsn9mo96tnbvto.apps.googleusercontent.com","project_id":"yup-test-243420","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"iuMPN6Ne1PD7cos29Tk9rlqH","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}}"#;
let mut app_secret = helper::parse_application_secret(app_secret).unwrap();
app_secret.token_uri = format!("{}/token", server_url);
let refresh_token = "my-refresh-token".to_string();
let https = HttpsConnector::new(1);
let client = hyper::Client::builder()
.keep_alive(false)
.build::<_, hyper::Body>(https);
let mut rt = tokio::runtime::Builder::new()
.core_threads(1)
.panic_handler(|e| std::panic::resume_unwind(e))
.build()
.unwrap();
// Success
{
let _m = mockito::mock("POST", "/token")
.match_body(
mockito::Matcher::Regex(".*client_id=902216714886-k2v9uei3p1dk6h686jbsn9mo96tnbvto.apps.googleusercontent.com.*refresh_token=my-refresh-token.*".to_string()))
.with_status(200)
.with_body(r#"{"access_token": "new-access-token", "token_type": "Bearer", "expires_in": 1234567}"#)
.create();
let fut = RefreshFlow::refresh_token(
client.clone(),
app_secret.clone(),
refresh_token.clone(),
)
.then(|rr| {
let rr = rr.unwrap();
match rr {
RefreshResult::Success(tok) => {
assert_eq!("new-access-token", tok.access_token);
assert_eq!("Bearer", tok.token_type);
}
_ => panic!(format!("unexpected RefreshResult {:?}", rr)),
}
Ok(()) as Result<(), ()>
});
rt.block_on(fut).expect("block_on");
_m.assert();
}
// Refresh error.
{
let _m = mockito::mock("POST", "/token")
.match_body(
mockito::Matcher::Regex(".*client_id=902216714886-k2v9uei3p1dk6h686jbsn9mo96tnbvto.apps.googleusercontent.com.*refresh_token=my-refresh-token.*".to_string()))
.with_status(400)
.with_body(r#"{"error": "invalid_token"}"#)
.create();
let fut = RefreshFlow::refresh_token(client, app_secret, refresh_token).then(|rr| {
let rr = rr.unwrap();
match rr {
RefreshResult::RefreshError(e, None) => {
assert_eq!(e, "invalid_token");
}
_ => panic!(format!("unexpected RefreshResult {:?}", rr)),
}
Ok(())
});
tokio::run(fut);
_m.assert();
}
log::debug!("Sending request: {:?}", request);
let (head, body) = client.request(request).await?.into_parts();
let body = hyper::body::to_bytes(body).await?;
log::debug!("Received response; head: {:?}, body: {:?}", head, body);
let mut token = TokenInfo::from_json(&body)?;
// If the refresh result contains a refresh_token use it, otherwise
// continue using our previous refresh_token.
token
.refresh_token
.get_or_insert_with(|| refresh_token.to_owned());
Ok(token)
}
}

View File

@@ -11,551 +11,237 @@
//! Copyright (c) 2016 Google Inc (lewinb@google.com).
//!
use std::default::Default;
use std::sync::{Arc, Mutex};
use crate::error::Error;
use crate::types::TokenInfo;
use crate::authenticator::{DefaultHyperClient, HyperClientBuilder};
use crate::storage::{hash_scopes, MemoryStorage, TokenStorage};
use crate::types::{ApplicationSecret, GetToken, JsonError, RequestError, StringError, Token};
use std::io;
use futures::stream::Stream;
use futures::{future, prelude::*};
use hyper::header;
use url::form_urlencoded;
use rustls::{
self,
internal::pemfile,
sign::{self, SigningKey},
PrivateKey,
};
use std::io;
use serde::{Deserialize, Serialize};
use url::form_urlencoded;
use base64;
use chrono;
use hyper;
use serde_json;
const GRANT_TYPE: &'static str = "urn:ietf:params:oauth:grant-type:jwt-bearer";
const GOOGLE_RS256_HEAD: &'static str = "{\"alg\":\"RS256\",\"typ\":\"JWT\"}";
const GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:jwt-bearer";
const GOOGLE_RS256_HEAD: &str = r#"{"alg":"RS256","typ":"JWT"}"#;
/// Encodes s as Base64
fn encode_base64<T: AsRef<[u8]>>(s: T) -> String {
base64::encode_config(s.as_ref(), base64::URL_SAFE)
fn append_base64<T: AsRef<[u8]> + ?Sized>(s: &T, out: &mut String) {
base64::encode_config_buf(s, base64::URL_SAFE, out)
}
/// Decode a PKCS8 formatted RSA key.
fn decode_rsa_key(pem_pkcs8: &str) -> Result<PrivateKey, io::Error> {
let private = pem_pkcs8.to_string().replace("\\n", "\n").into_bytes();
let mut private_reader: &[u8] = private.as_ref();
let private_keys = pemfile::pkcs8_private_keys(&mut private_reader);
let private_keys = pemfile::pkcs8_private_keys(&mut pem_pkcs8.as_bytes());
if let Ok(pk) = private_keys {
if pk.len() > 0 {
Ok(pk[0].clone())
} else {
Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Not enough private keys in PEM",
))
match private_keys {
Ok(mut keys) if !keys.is_empty() => {
keys.truncate(1);
Ok(keys.remove(0))
}
} else {
Err(io::Error::new(
Ok(_) => Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Not enough private keys in PEM",
)),
Err(_) => Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Error reading key from PEM",
))
)),
}
}
/// JSON schema of secret service account key. You can obtain the key from
/// the Cloud Console at https://console.cloud.google.com/.
///
/// You can use `helpers::service_account_key_from_file()` as a quick way to read a JSON client
/// You can use `helpers::read_service_account_key()` as a quick way to read a JSON client
/// secret into a ServiceAccountKey.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ServiceAccountKey {
#[serde(rename = "type")]
/// key_type
pub key_type: Option<String>,
/// project_id
pub project_id: Option<String>,
/// private_key_id
pub private_key_id: Option<String>,
pub private_key: Option<String>,
pub client_email: Option<String>,
/// private_key
pub private_key: String,
/// client_email
pub client_email: String,
/// client_id
pub client_id: Option<String>,
/// auth_uri
pub auth_uri: Option<String>,
pub token_uri: Option<String>,
/// token_uri
pub token_uri: String,
/// auth_provider_x509_cert_url
pub auth_provider_x509_cert_url: Option<String>,
/// client_x509_cert_url
pub client_x509_cert_url: Option<String>,
}
/// Permissions requested for a JWT.
/// See https://developers.google.com/identity/protocols/OAuth2ServiceAccount#authorizingrequests.
#[derive(Serialize, Debug)]
struct Claims {
iss: String,
aud: String,
struct Claims<'a> {
iss: &'a str,
aud: &'a str,
exp: i64,
iat: i64,
sub: Option<String>,
subject: Option<&'a str>,
scope: String,
}
/// A JSON Web Token ready for signing.
struct JWT {
/// The value of GOOGLE_RS256_HEAD.
header: String,
/// A Claims struct, expressing the set of desired permissions etc.
claims: Claims,
}
impl<'a> Claims<'a> {
fn new<T>(key: &'a ServiceAccountKey, scopes: &[T], subject: Option<&'a str>) -> Self
where
T: AsRef<str>,
{
let iat = chrono::Utc::now().timestamp();
let expiry = iat + 3600 - 5; // Max validity is 1h.
impl JWT {
/// Create a new JWT from claims.
fn new(claims: Claims) -> JWT {
JWT {
header: GOOGLE_RS256_HEAD.to_string(),
claims: claims,
let scope = crate::helper::join(scopes, " ");
Claims {
iss: &key.client_email,
aud: &key.token_uri,
exp: expiry,
iat,
subject,
scope,
}
}
}
/// Set JWT header. Default is `{"alg":"RS256","typ":"JWT"}`.
#[allow(dead_code)]
pub fn set_header(&mut self, head: String) {
self.header = head;
}
/// A JSON Web Token ready for signing.
pub(crate) struct JWTSigner {
signer: Box<dyn rustls::sign::Signer>,
}
/// Encodes the first two parts (header and claims) to base64 and assembles them into a form
/// ready to be signed.
fn encode_claims(&self) -> String {
let mut head = encode_base64(&self.header);
let claims = encode_base64(serde_json::to_string(&self.claims).unwrap());
head.push_str(".");
head.push_str(&claims);
head
}
/// Sign a JWT base string with `private_key`, which is a PKCS8 string.
fn sign(&self, private_key: &str) -> Result<String, io::Error> {
let mut jwt_head = self.encode_claims();
impl JWTSigner {
fn new(private_key: &str) -> Result<Self, io::Error> {
let key = decode_rsa_key(private_key)?;
let signing_key = sign::RSASigningKey::new(&key)
.map_err(|_| io::Error::new(io::ErrorKind::Other, "Couldn't initialize signer"))?;
let signer = signing_key
.choose_scheme(&[rustls::SignatureScheme::RSA_PKCS1_SHA256])
.ok_or(io::Error::new(
io::ErrorKind::Other,
"Couldn't choose signing scheme",
))?;
let signature = signer
.sign(jwt_head.as_bytes())
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("{}", e)))?;
let signature_b64 = encode_base64(signature);
.ok_or_else(|| {
io::Error::new(io::ErrorKind::Other, "Couldn't choose signing scheme")
})?;
Ok(JWTSigner { signer })
}
fn sign_claims(&self, claims: &Claims) -> Result<String, rustls::TLSError> {
let mut jwt_head = Self::encode_claims(claims);
let signature = self.signer.sign(jwt_head.as_bytes())?;
jwt_head.push_str(".");
jwt_head.push_str(&signature_b64);
append_base64(&signature, &mut jwt_head);
Ok(jwt_head)
}
}
/// Set `iss`, `aud`, `exp`, `iat`, `scope` field in the returned `Claims`. `scopes` is an iterator
/// yielding strings with OAuth scopes.
fn init_claims_from_key<'a, I, T>(key: &ServiceAccountKey, scopes: I) -> Claims
where
T: AsRef<str> + 'a,
I: IntoIterator<Item = &'a T>,
{
let iat = chrono::Utc::now().timestamp();
let expiry = iat + 3600 - 5; // Max validity is 1h.
let mut scopes_string = scopes.into_iter().fold(String::new(), |mut acc, sc| {
acc.push_str(sc.as_ref());
acc.push_str(" ");
acc
});
scopes_string.pop();
Claims {
iss: key.client_email.clone().unwrap(),
aud: key.token_uri.clone().unwrap(),
exp: expiry,
iat: iat,
sub: None,
scope: scopes_string,
/// Encodes the first two parts (header and claims) to base64 and assembles them into a form
/// ready to be signed.
fn encode_claims(claims: &Claims) -> String {
let mut head = String::new();
append_base64(GOOGLE_RS256_HEAD, &mut head);
head.push_str(".");
append_base64(&serde_json::to_string(&claims).unwrap(), &mut head);
head
}
}
/// A token source (`GetToken`) yielding OAuth tokens for services that use ServiceAccount authorization.
/// This token source caches token and automatically renews expired ones, meaning you do not need
/// (and you also should not) use this with `Authenticator`. Just use it directly.
#[derive(Clone)]
pub struct ServiceAccountAccess<C> {
client: C,
pub struct ServiceAccountFlowOpts {
pub(crate) key: ServiceAccountKey,
pub(crate) subject: Option<String>,
}
/// ServiceAccountFlow can fetch oauth tokens using a service account.
pub struct ServiceAccountFlow {
key: ServiceAccountKey,
sub: Option<String>,
subject: Option<String>,
signer: JWTSigner,
}
impl ServiceAccountAccess<DefaultHyperClient> {
/// Create a new ServiceAccountAccess with the provided key.
pub fn new(key: ServiceAccountKey) -> Self {
ServiceAccountAccess {
client: DefaultHyperClient,
key,
sub: None,
}
}
}
impl<C> ServiceAccountAccess<C>
where
C: HyperClientBuilder,
C::Connector: 'static,
{
/// Use the provided hyper client.
pub fn hyper_client<NewC: HyperClientBuilder>(
self,
hyper_client: NewC,
) -> ServiceAccountAccess<NewC> {
ServiceAccountAccess {
client: hyper_client,
key: self.key,
sub: self.sub,
}
impl ServiceAccountFlow {
pub(crate) fn new(opts: ServiceAccountFlowOpts) -> Result<Self, io::Error> {
let signer = JWTSigner::new(&opts.key.private_key)?;
Ok(ServiceAccountFlow {
key: opts.key,
subject: opts.subject,
signer,
})
}
/// Use the provided sub.
pub fn sub(self, sub: String) -> Self {
ServiceAccountAccess {
sub: Some(sub),
..self
}
}
/// Build the configured ServiceAccountAccess.
pub fn build(self) -> impl GetToken {
ServiceAccountAccessImpl::new(self.client.build_hyper_client(), self.key, self.sub)
}
}
#[derive(Clone)]
struct ServiceAccountAccessImpl<C> {
client: hyper::Client<C, hyper::Body>,
key: ServiceAccountKey,
cache: Arc<Mutex<MemoryStorage>>,
sub: Option<String>,
}
impl<C> ServiceAccountAccessImpl<C>
where
C: hyper::client::connect::Connect,
{
fn new(client: hyper::Client<C>, key: ServiceAccountKey, sub: Option<String>) -> Self {
ServiceAccountAccessImpl {
client,
key,
cache: Arc::new(Mutex::new(MemoryStorage::default())),
sub,
}
}
}
/// This is the schema of the server's response.
#[derive(Deserialize, Debug)]
struct TokenResponse {
access_token: Option<String>,
token_type: Option<String>,
expires_in: Option<i64>,
}
impl TokenResponse {
fn to_oauth_token(self) -> Token {
let expires_ts = chrono::Utc::now().timestamp() + self.expires_in.unwrap_or(0);
Token {
access_token: self.access_token.unwrap(),
token_type: self.token_type.unwrap(),
refresh_token: Some(String::new()),
expires_in: self.expires_in,
expires_in_timestamp: Some(expires_ts),
}
}
}
impl<'a, C: 'static + hyper::client::connect::Connect> ServiceAccountAccessImpl<C> {
/// Send a request for a new Bearer token to the OAuth provider.
fn request_token(
client: hyper::client::Client<C>,
sub: Option<String>,
key: ServiceAccountKey,
scopes: Vec<String>,
) -> impl Future<Item = Token, Error = RequestError> {
let mut claims = init_claims_from_key(&key, &scopes);
claims.sub = sub.clone();
let signed = JWT::new(claims)
.sign(key.private_key.as_ref().unwrap())
.into_future();
signed
.map_err(RequestError::LowLevelError)
.map(|signed| {
form_urlencoded::Serializer::new(String::new())
.extend_pairs(vec![
("grant_type".to_string(), GRANT_TYPE.to_string()),
("assertion".to_string(), signed),
])
.finish()
})
.map(|rqbody| {
hyper::Request::post(key.token_uri.unwrap())
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(hyper::Body::from(rqbody))
.unwrap()
})
.and_then(move |request| client.request(request).map_err(RequestError::ClientError))
.and_then(|response| {
response
.into_body()
.concat2()
.map_err(RequestError::ClientError)
})
.map(|c| String::from_utf8(c.into_bytes().to_vec()).unwrap())
.and_then(|s| {
if let Ok(jse) = serde_json::from_str::<JsonError>(&s) {
Err(RequestError::NegativeServerResponse(
jse.error,
jse.error_description,
))
} else {
serde_json::from_str(&s).map_err(RequestError::JSONError)
}
})
.then(|token: Result<TokenResponse, RequestError>| match token {
Err(e) => return Err(e),
Ok(token) => {
if token.access_token.is_none()
|| token.token_type.is_none()
|| token.expires_in.is_none()
{
Err(RequestError::BadServerResponse(format!(
"Token response lacks fields: {:?}",
token
)))
} else {
Ok(token.to_oauth_token())
}
}
})
}
}
impl<C: 'static> GetToken for ServiceAccountAccessImpl<C>
where
C: hyper::client::connect::Connect,
{
fn token<I, T>(
&mut self,
scopes: I,
) -> Box<dyn Future<Item = Token, Error = RequestError> + Send>
pub(crate) async fn token<C, T>(
&self,
hyper_client: &hyper::Client<C>,
scopes: &[T],
) -> Result<TokenInfo, Error>
where
T: Into<String>,
I: IntoIterator<Item = T>,
T: AsRef<str>,
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
{
let (hash, scps0) = hash_scopes(scopes);
let cache = self.cache.clone();
let scps = scps0.clone();
let cache_lookup = futures::lazy(move || {
match cache
.lock()
.unwrap()
.get(hash, &scps.iter().map(|s| s.as_str()).collect())
{
Ok(Some(token)) => {
if !token.expired() {
return Ok(token);
}
return Err(StringError::new("expired token in cache", None));
}
Err(e) => return Err(StringError::new(format!("cache lookup error: {}", e), None)),
Ok(None) => return Err(StringError::new("no token in cache", None)),
}
});
let cache = self.cache.clone();
let req_token = Self::request_token(
self.client.clone(),
self.sub.clone(),
self.key.clone(),
scps0.iter().map(|s| s.to_string()).collect(),
)
.then(move |r| match r {
Ok(token) => {
let _ = cache.lock().unwrap().set(
hash,
&scps0.iter().map(|s| s.as_str()).collect(),
Some(token.clone()),
);
Box::new(future::ok(token))
}
Err(e) => Box::new(future::err(e)),
});
Box::new(cache_lookup.then(|r| match r {
Ok(t) => Box::new(Ok(t).into_future())
as Box<dyn Future<Item = Token, Error = RequestError> + Send>,
Err(_) => {
Box::new(req_token) as Box<dyn Future<Item = Token, Error = RequestError> + Send>
}
}))
}
/// Returns an empty ApplicationSecret as tokens for service accounts don't need to be
/// refreshed (they are simply reissued).
fn application_secret(&self) -> ApplicationSecret {
Default::default()
}
fn api_key(&mut self) -> Option<String> {
None
let claims = Claims::new(&self.key, scopes, self.subject.as_ref().map(|x| x.as_str()));
let signed = self.signer.sign_claims(&claims).map_err(|_| {
Error::LowLevelError(io::Error::new(
io::ErrorKind::Other,
"unable to sign claims",
))
})?;
let rqbody = form_urlencoded::Serializer::new(String::new())
.extend_pairs(&[("grant_type", GRANT_TYPE), ("assertion", signed.as_str())])
.finish();
let request = hyper::Request::post(&self.key.token_uri)
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(hyper::Body::from(rqbody))
.unwrap();
log::debug!("requesting token from service account: {:?}", request);
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)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::helper::service_account_key_from_file;
use crate::types::GetToken;
use hyper;
use crate::helper::read_service_account_key;
use hyper_rustls::HttpsConnector;
use mockito::{self, mock};
use tokio;
#[test]
fn test_mocked_http() {
env_logger::try_init().unwrap();
let server_url = &mockito::server_url();
let client_secret = r#"{
"type": "service_account",
"project_id": "yup-test-243420",
"private_key_id": "26de294916614a5ebdf7a065307ed3ea9941902b",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDemmylrvp1KcOn\n9yTAVVKPpnpYznvBvcAU8Qjwr2fSKylpn7FQI54wCk5VJVom0jHpAmhxDmNiP8yv\nHaqsef+87Oc0n1yZ71/IbeRcHZc2OBB33/LCFqf272kThyJo3qspEqhuAw0e8neg\nLQb4jpm9PsqR8IjOoAtXQSu3j0zkXemMYFy93PWHjVpPEUX16NGfsWH7oxspBHOk\n9JPGJL8VJdbiAoDSDgF0y9RjJY5I52UeHNhMsAkTYs6mIG4kKXt2+T9tAyHw8aho\nwmuytQAfydTflTfTG8abRtliF3nil2taAc5VB07dP1b4dVYy/9r6M8Z0z4XM7aP+\nNdn2TKm3AgMBAAECggEAWi54nqTlXcr2M5l535uRb5Xz0f+Q/pv3ceR2iT+ekXQf\n+mUSShOr9e1u76rKu5iDVNE/a7H3DGopa7ZamzZvp2PYhSacttZV2RbAIZtxU6th\n7JajPAM+t9klGh6wj4jKEcE30B3XVnbHhPJI9TCcUyFZoscuPXt0LLy/z8Uz0v4B\nd5JARwyxDMb53VXwukQ8nNY2jP7WtUig6zwE5lWBPFMbi8GwGkeGZOruAK5sPPwY\nGBAlfofKANI7xKx9UXhRwisB4+/XI1L0Q6xJySv9P+IAhDUI6z6kxR+WkyT/YpG3\nX9gSZJc7qEaxTIuDjtep9GTaoEqiGntjaFBRKoe+VQKBgQDzM1+Ii+REQqrGlUJo\nx7KiVNAIY/zggu866VyziU6h5wjpsoW+2Npv6Dv7nWvsvFodrwe50Y3IzKtquIal\nVd8aa50E72JNImtK/o5Nx6xK0VySjHX6cyKENxHRDnBmNfbALRM+vbD9zMD0lz2q\nmns/RwRGq3/98EqxP+nHgHSr9QKBgQDqUYsFAAfvfT4I75Glc9svRv8IsaemOm07\nW1LCwPnj1MWOhsTxpNF23YmCBupZGZPSBFQobgmHVjQ3AIo6I2ioV6A+G2Xq/JCF\nmzfbvZfqtbbd+nVgF9Jr1Ic5T4thQhAvDHGUN77BpjEqZCQLAnUWJx9x7e2xvuBl\n1A6XDwH/ewKBgQDv4hVyNyIR3nxaYjFd7tQZYHTOQenVffEAd9wzTtVbxuo4sRlR\nNM7JIRXBSvaATQzKSLHjLHqgvJi8LITLIlds1QbNLl4U3UVddJbiy3f7WGTqPFfG\nkLhUF4mgXpCpkMLxrcRU14Bz5vnQiDmQRM4ajS7/kfwue00BZpxuZxst3QKBgQCI\nRI3FhaQXyc0m4zPfdYYVc4NjqfVmfXoC1/REYHey4I1XetbT9Nb/+ow6ew0UbgSC\nUZQjwwJ1m1NYXU8FyovVwsfk9ogJ5YGiwYb1msfbbnv/keVq0c/Ed9+AG9th30qM\nIf93hAfClITpMz2mzXIMRQpLdmQSR4A2l+E4RjkSOwKBgQCB78AyIdIHSkDAnCxz\nupJjhxEhtQ88uoADxRoEga7H/2OFmmPsqfytU4+TWIdal4K+nBCBWRvAX1cU47vH\nJOlSOZI0gRKe0O4bRBQc8GXJn/ubhYSxI02IgkdGrIKpOb5GG10m85ZvqsXw3bKn\nRVHMD0ObF5iORjZUqD0yRitAdg==\n-----END PRIVATE KEY-----\n",
"client_email": "yup-test-sa-1@yup-test-243420.iam.gserviceaccount.com",
"client_id": "102851967901799660408",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/yup-test-sa-1%40yup-test-243420.iam.gserviceaccount.com"
}"#;
let mut key: ServiceAccountKey = serde_json::from_str(client_secret).unwrap();
key.token_uri = Some(format!("{}/token", server_url));
let json_response = r#"{
"access_token": "ya29.c.ElouBywiys0LyNaZoLPJcp1Fdi2KjFMxzvYKLXkTdvM-rDfqKlvEq6PiMhGoGHx97t5FAvz3eb_ahdwlBjSStxHtDVQB4ZPRJQ_EOi-iS7PnayahU2S9Jp8S6rk",
"expires_in": 3600,
"token_type": "Bearer"
}"#;
let bad_json_response = r#"{
"access_token": "ya29.c.ElouBywiys0LyNaZoLPJcp1Fdi2KjFMxzvYKLXkTdvM-rDfqKlvEq6PiMhGoGHx97t5FAvz3eb_ahdwlBjSStxHtDVQB4ZPRJQ_EOi-iS7PnayahU2S9Jp8S6rk",
"token_type": "Bearer"
}"#;
let https = HttpsConnector::new(1);
let client = hyper::Client::builder()
.keep_alive(false)
.build::<_, hyper::Body>(https);
let mut rt = tokio::runtime::Builder::new()
.core_threads(1)
.panic_handler(|e| std::panic::resume_unwind(e))
.build()
.unwrap();
// Successful path.
{
let _m = mock("POST", "/token")
.with_status(200)
.with_header("content-type", "text/json")
.with_body(json_response)
.expect(1)
.create();
let mut acc = ServiceAccountAccessImpl::new(client.clone(), key.clone(), None);
let fut = acc
.token(vec!["https://www.googleapis.com/auth/pubsub"])
.and_then(|tok| {
assert!(tok.access_token.contains("ya29.c.ElouBywiys0Ly"));
assert_eq!(Some(3600), tok.expires_in);
Ok(())
});
rt.block_on(fut).expect("block_on");
assert!(acc
.cache
.lock()
.unwrap()
.get(
3502164897243251857,
&vec!["https://www.googleapis.com/auth/pubsub"]
)
.unwrap()
.is_some());
// Test that token is in cache (otherwise mock will tell us)
let fut = acc
.token(vec!["https://www.googleapis.com/auth/pubsub"])
.and_then(|tok| {
assert!(tok.access_token.contains("ya29.c.ElouBywiys0Ly"));
assert_eq!(Some(3600), tok.expires_in);
Ok(())
});
rt.block_on(fut).expect("block_on 2");
_m.assert();
}
// Malformed response.
{
let _m = mock("POST", "/token")
.with_status(200)
.with_header("content-type", "text/json")
.with_body(bad_json_response)
.create();
let mut acc = ServiceAccountAccess::new(key.clone())
.hyper_client(client.clone())
.build();
let fut = acc
.token(vec!["https://www.googleapis.com/auth/pubsub"])
.then(|result| {
assert!(result.is_err());
Ok(()) as Result<(), ()>
});
rt.block_on(fut).expect("block_on");
_m.assert();
}
rt.shutdown_on_idle().wait().expect("shutdown");
}
// Valid but deactivated key.
const TEST_PRIVATE_KEY_PATH: &'static str = "examples/Sanguine-69411a0c0eea.json";
// Uncomment this test to verify that we can successfully obtain tokens.
//#[test]
//#[tokio::test]
#[allow(dead_code)]
fn test_service_account_e2e() {
let key = service_account_key_from_file(&TEST_PRIVATE_KEY_PATH.to_string()).unwrap();
let https = HttpsConnector::new(4);
let runtime = tokio::runtime::Runtime::new().unwrap();
async fn test_service_account_e2e() {
let key = read_service_account_key(TEST_PRIVATE_KEY_PATH)
.await
.unwrap();
let acc = ServiceAccountFlow::new(ServiceAccountFlowOpts { key, subject: None }).unwrap();
let https = HttpsConnector::new();
let client = hyper::Client::builder()
.executor(runtime.executor())
.build(https);
let mut acc = ServiceAccountAccess::new(key).hyper_client(client).build();
.keep_alive(false)
.build::<_, hyper::Body>(https);
println!(
"{:?}",
acc.token(vec!["https://www.googleapis.com/auth/pubsub"])
.wait()
acc.token(&client, &["https://www.googleapis.com/auth/pubsub"])
.await
);
}
#[test]
fn test_jwt_initialize_claims() {
let key = service_account_key_from_file(TEST_PRIVATE_KEY_PATH).unwrap();
#[tokio::test]
async fn test_jwt_initialize_claims() {
let key = read_service_account_key(TEST_PRIVATE_KEY_PATH)
.await
.unwrap();
let scopes = vec!["scope1", "scope2", "scope3"];
let claims = super::init_claims_from_key(&key, &scopes);
let claims = Claims::new(&key, &scopes, None);
assert_eq!(
claims.iss,
@@ -571,13 +257,15 @@ mod tests {
assert_eq!(claims.exp - claims.iat, 3595);
}
#[test]
fn test_jwt_sign() {
let key = service_account_key_from_file(TEST_PRIVATE_KEY_PATH).unwrap();
#[tokio::test]
async fn test_jwt_sign() {
let key = read_service_account_key(TEST_PRIVATE_KEY_PATH)
.await
.unwrap();
let scopes = vec!["scope1", "scope2", "scope3"];
let claims = super::init_claims_from_key(&key, &scopes);
let jwt = super::JWT::new(claims);
let signature = jwt.sign(key.private_key.as_ref().unwrap());
let signer = JWTSigner::new(&key.private_key).unwrap();
let claims = Claims::new(&key, &scopes, None);
let signature = signer.sign_claims(&claims);
assert!(signature.is_ok());

View File

@@ -2,291 +2,430 @@
//
// See project root for licensing information.
//
use crate::types::TokenInfo;
use std::cmp::Ordering;
use std::collections::hash_map::DefaultHasher;
use std::error::Error;
use std::fmt;
use std::fs;
use std::hash::{Hash, Hasher};
use std::collections::HashMap;
use std::io;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use crate::types::Token;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
/// Implements a specialized storage to set and retrieve `Token` instances.
/// The `scope_hash` represents the signature of the scopes for which the given token
/// should be stored or retrieved.
/// For completeness, the underlying, sorted scopes are provided as well. They might be
/// useful for presentation to the user.
pub trait TokenStorage {
type Error: 'static + Error + Send + Sync;
// The storage layer allows retrieving tokens for scopes that have been
// previously granted tokens. One wrinkle is that a token granted for a set
// of scopes X is also valid for any subset of X's scopes. So when retrieving a
// token for a set of scopes provided by the caller it's beneficial to compare
// that set to all previously stored tokens to see if it is a subset of any
// existing set. To do this efficiently we store a bloom filter along with each
// token that represents the set of scopes the token is associated with. The
// bloom filter allows for efficiently skipping any entries that are
// definitively not a superset.
// The current implementation uses a 64bit bloom filter with 4 hash functions.
/// If `token` is None, it is invalid or revoked and should be removed from storage.
/// Otherwise, it should be saved.
fn set(
&mut self,
scope_hash: u64,
scopes: &Vec<&str>,
token: Option<Token>,
) -> Result<(), Self::Error>;
/// A `None` result indicates that there is no token for the given scope_hash.
fn get(&self, scope_hash: u64, scopes: &Vec<&str>) -> Result<Option<Token>, Self::Error>;
/// ScopeHash is a hash value derived from a list of scopes. The hash value
/// represents a fingerprint of the set of scopes *independent* of the ordering.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
struct ScopeHash(u64);
/// ScopeFilter represents a filter for a set of scopes. It can definitively
/// prove that a given list of scopes is not a subset of another.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
struct ScopeFilter(u64);
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
enum FilterResponse {
Maybe,
No,
}
/// Calculate a hash value describing the scopes, and return a sorted Vec of the scopes.
pub fn hash_scopes<I, T>(scopes: I) -> (u64, Vec<String>)
where
T: Into<String>,
I: IntoIterator<Item = T>,
{
let mut sv: Vec<String> = scopes.into_iter().map(Into::into).collect();
sv.sort();
let mut sh = DefaultHasher::new();
sv.hash(&mut sh);
(sh.finish(), sv)
impl ScopeFilter {
/// Determine if this ScopeFilter could be a subset of the provided filter.
fn is_subset_of(self, filter: ScopeFilter) -> FilterResponse {
if self.0 & filter.0 == self.0 {
FilterResponse::Maybe
} else {
FilterResponse::No
}
}
}
/// A storage that remembers nothing.
#[derive(Default)]
pub struct NullStorage;
#[derive(Debug)]
pub struct NullError;
impl Error for NullError {
fn description(&self) -> &str {
"NULL"
}
pub(crate) struct ScopeSet<'a, T> {
hash: ScopeHash,
filter: ScopeFilter,
scopes: &'a [T],
}
impl fmt::Display for NullError {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
"NULL-ERROR".fmt(f)
}
}
impl TokenStorage for NullStorage {
type Error = NullError;
fn set(&mut self, _: u64, _: &Vec<&str>, _: Option<Token>) -> Result<(), NullError> {
Ok(())
}
fn get(&self, _: u64, _: &Vec<&str>) -> Result<Option<Token>, NullError> {
Ok(None)
}
}
/// A storage that remembers values for one session only.
#[derive(Debug, Default)]
pub struct MemoryStorage {
tokens: Vec<JSONToken>,
}
impl MemoryStorage {
pub fn new() -> MemoryStorage {
Default::default()
}
}
impl TokenStorage for MemoryStorage {
type Error = NullError;
fn set(
&mut self,
scope_hash: u64,
scopes: &Vec<&str>,
token: Option<Token>,
) -> Result<(), NullError> {
let matched = self.tokens.iter().find_position(|x| x.hash == scope_hash);
if let Some(_) = matched {
self.tokens.retain(|x| x.hash != scope_hash);
// Implement Clone manually. Auto derive fails to work correctly because we want
// Clone to be implemented regardless of whether T is Clone or not.
impl<'a, T> Clone for ScopeSet<'a, T> {
fn clone(&self) -> Self {
ScopeSet {
hash: self.hash,
filter: self.filter,
scopes: self.scopes,
}
match token {
Some(t) => {
self.tokens.push(JSONToken {
hash: scope_hash,
scopes: Some(scopes.iter().map(|x| x.to_string()).collect()),
token: t.clone(),
});
()
}
None => {}
};
Ok(())
}
}
impl<'a, T> Copy for ScopeSet<'a, T> {}
fn get(&self, scope_hash: u64, scopes: &Vec<&str>) -> Result<Option<Token>, NullError> {
let scopes: Vec<_> = scopes.iter().sorted().unique().collect();
impl<'a, T> ScopeSet<'a, T>
where
T: AsRef<str>,
{
// implement an inherent from method even though From is implemented. This
// is because passing an array ref like &[&str; 1] (&["foo"]) will be auto
// deref'd to a slice on function boundaries, but it will not implement the
// From trait. This inherent method just serves to auto deref from array
// refs to slices and proxy to the From impl.
pub fn from(scopes: &'a [T]) -> Self {
let (hash, filter) = scopes.iter().fold(
(ScopeHash(0), ScopeFilter(0)),
|(mut scope_hash, mut scope_filter), scope| {
let h = seahash::hash(scope.as_ref().as_bytes());
for t in &self.tokens {
if let Some(token_scopes) = &t.scopes {
let matched = token_scopes
.iter()
.filter(|x| scopes.contains(&&&x[..]))
.count();
if matched >= scopes.len() {
return Result::Ok(Some(t.token.clone()));
// Use the first 4 6-bit chunks of the seahash as the 4 hash values
// in the bloom filter.
for i in 0..4 {
// h is a hash derived value in the range 0..64
let h = (h >> (6 * i)) & 0b11_1111;
scope_filter.0 |= 1 << h;
}
} else if scope_hash == t.hash {
return Result::Ok(Some(t.token.clone()));
}
// xor the hashes together to get an order independent fingerprint.
scope_hash.0 ^= h;
(scope_hash, scope_filter)
},
);
ScopeSet {
hash,
filter,
scopes,
}
}
}
pub(crate) enum Storage {
Memory { tokens: Mutex<JSONTokens> },
Disk(DiskStorage),
}
impl Storage {
pub(crate) async fn set<T>(
&self,
scopes: ScopeSet<'_, T>,
token: TokenInfo,
) -> Result<(), io::Error>
where
T: AsRef<str>,
{
match self {
Storage::Memory { tokens } => tokens.lock().unwrap().set(scopes, token),
Storage::Disk(disk_storage) => disk_storage.set(scopes, token).await,
}
}
pub(crate) fn get<T>(&self, scopes: ScopeSet<T>) -> Option<TokenInfo>
where
T: AsRef<str>,
{
match self {
Storage::Memory { tokens } => tokens.lock().unwrap().get(scopes),
Storage::Disk(disk_storage) => disk_storage.get(scopes),
}
Result::Ok(None)
}
}
/// A single stored token.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone)]
struct JSONToken {
pub hash: u64,
pub scopes: Option<Vec<String>>,
pub token: Token,
scopes: Vec<String>,
token: TokenInfo,
hash: ScopeHash,
filter: ScopeFilter,
}
impl PartialEq for JSONToken {
fn eq(&self, other: &Self) -> bool {
self.hash == other.hash
impl<'de> Deserialize<'de> for JSONToken {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct RawJSONToken {
scopes: Vec<String>,
token: TokenInfo,
}
let RawJSONToken { scopes, token } = RawJSONToken::deserialize(deserializer)?;
let ScopeSet { hash, filter, .. } = ScopeSet::from(&scopes);
Ok(JSONToken {
scopes,
token,
hash,
filter,
})
}
}
impl Eq for JSONToken {}
impl PartialOrd for JSONToken {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for JSONToken {
fn cmp(&self, other: &Self) -> Ordering {
self.hash.cmp(&other.hash)
impl Serialize for JSONToken {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
#[derive(Serialize)]
struct RawJSONToken<'a> {
scopes: &'a [String],
token: &'a TokenInfo,
}
RawJSONToken {
scopes: &self.scopes,
token: &self.token,
}
.serialize(serializer)
}
}
/// List of tokens in a JSON object
#[derive(Serialize, Deserialize)]
struct JSONTokens {
pub tokens: Vec<JSONToken>,
#[derive(Debug, Clone)]
pub(crate) struct JSONTokens {
token_map: HashMap<ScopeHash, JSONToken>,
}
/// Serializes tokens to a JSON file on disk.
#[derive(Default)]
pub struct DiskTokenStorage {
location: String,
tokens: Vec<JSONToken>,
impl Serialize for JSONTokens {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.collect_seq(self.token_map.values())
}
}
impl DiskTokenStorage {
pub fn new<S: AsRef<str>>(location: S) -> Result<DiskTokenStorage, io::Error> {
let mut dts = DiskTokenStorage {
location: location.as_ref().to_owned(),
tokens: Vec::new(),
impl<'de> Deserialize<'de> for JSONTokens {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct V;
impl<'de> serde::de::Visitor<'de> for V {
type Value = JSONTokens;
// Format a message stating what data this Visitor expects to receive.
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a sequence of JSONToken's")
}
fn visit_seq<M>(self, mut access: M) -> Result<Self::Value, M::Error>
where
M: serde::de::SeqAccess<'de>,
{
let mut token_map = HashMap::with_capacity(access.size_hint().unwrap_or(0));
while let Some(json_token) = access.next_element::<JSONToken>()? {
token_map.insert(json_token.hash, json_token);
}
Ok(JSONTokens { token_map })
}
}
// Instantiate our Visitor and ask the Deserializer to drive
// it over the input data.
deserializer.deserialize_seq(V)
}
}
impl JSONTokens {
pub(crate) fn new() -> Self {
JSONTokens {
token_map: HashMap::new(),
}
}
async fn load_from_file(filename: &Path) -> Result<Self, io::Error> {
let contents = tokio::fs::read(filename).await?;
serde_json::from_slice(&contents).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
fn get<T>(
&self,
ScopeSet {
hash,
filter,
scopes,
}: ScopeSet<T>,
) -> Option<TokenInfo>
where
T: AsRef<str>,
{
if let Some(json_token) = self.token_map.get(&hash) {
return Some(json_token.token.clone());
}
let requested_scopes_are_subset_of = |other_scopes: &[String]| {
scopes
.iter()
.all(|s| other_scopes.iter().any(|t| t.as_str() == s.as_ref()))
};
// No exact match for the scopes provided. Search for any tokens that
// exist for a superset of the scopes requested.
self.token_map
.values()
.filter(|json_token| filter.is_subset_of(json_token.filter) == FilterResponse::Maybe)
.find(|v: &&JSONToken| requested_scopes_are_subset_of(&v.scopes))
.map(|t: &JSONToken| t.token.clone())
}
fn set<T>(
&mut self,
ScopeSet {
hash,
filter,
scopes,
}: ScopeSet<T>,
token: TokenInfo,
) -> Result<(), io::Error>
where
T: AsRef<str>,
{
use std::collections::hash_map::Entry;
match self.token_map.entry(hash) {
Entry::Occupied(mut entry) => {
entry.get_mut().token = token;
}
Entry::Vacant(entry) => {
let json_token = JSONToken {
scopes: scopes.iter().map(|x| x.as_ref().to_owned()).collect(),
token,
hash,
filter,
};
entry.insert(json_token.clone());
}
}
Ok(())
}
}
pub(crate) struct DiskStorage {
tokens: Mutex<JSONTokens>,
filename: PathBuf,
}
impl DiskStorage {
pub(crate) async fn new(filename: PathBuf) -> Result<Self, io::Error> {
let tokens = match JSONTokens::load_from_file(&filename).await {
Ok(tokens) => tokens,
Err(e) if e.kind() == io::ErrorKind::NotFound => JSONTokens::new(),
Err(e) => return Err(e),
};
// best-effort
let read_result = dts.load_from_file();
match read_result {
Result::Ok(()) => Result::Ok(dts),
Result::Err(e) => {
match e.kind() {
io::ErrorKind::NotFound => Result::Ok(dts), // File not found; ignore and create new one
_ => Result::Err(e), // e.g. PermissionDenied
}
}
}
Ok(DiskStorage {
tokens: Mutex::new(tokens),
filename,
})
}
fn load_from_file(&mut self) -> Result<(), io::Error> {
let mut f = fs::OpenOptions::new().read(true).open(&self.location)?;
let mut contents = String::new();
match f.read_to_string(&mut contents) {
Result::Err(e) => return Result::Err(e),
Result::Ok(_sz) => (),
}
let tokens: JSONTokens;
match serde_json::from_str(&contents) {
Result::Err(e) => return Result::Err(io::Error::new(io::ErrorKind::InvalidData, e)),
Result::Ok(t) => tokens = t,
}
for t in tokens.tokens {
self.tokens.push(t);
}
return Result::Ok(());
pub(crate) async fn set<T>(
&self,
scopes: ScopeSet<'_, T>,
token: TokenInfo,
) -> Result<(), io::Error>
where
T: AsRef<str>,
{
use tokio::io::AsyncWriteExt;
let json = {
use std::ops::Deref;
let mut lock = self.tokens.lock().unwrap();
lock.set(scopes, token)?;
serde_json::to_string(lock.deref())
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
};
let mut f = open_writeable_file(&self.filename).await?;
f.write_all(json.as_bytes()).await?;
Ok(())
}
pub fn dump_to_file(&mut self) -> Result<(), io::Error> {
let mut jsontokens = JSONTokens { tokens: Vec::new() };
for token in self.tokens.iter() {
jsontokens.tokens.push((*token).clone());
}
let serialized;
match serde_json::to_string(&jsontokens) {
Result::Err(e) => return Result::Err(io::Error::new(io::ErrorKind::InvalidData, e)),
Result::Ok(s) => serialized = s,
}
let mut f = fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&self.location)?;
f.write(serialized.as_ref()).map(|_| ())
pub(crate) fn get<T>(&self, scopes: ScopeSet<T>) -> Option<TokenInfo>
where
T: AsRef<str>,
{
self.tokens.lock().unwrap().get(scopes)
}
}
impl TokenStorage for DiskTokenStorage {
type Error = io::Error;
fn set(
&mut self,
scope_hash: u64,
scopes: &Vec<&str>,
token: Option<Token>,
) -> Result<(), Self::Error> {
let matched = self.tokens.iter().find_position(|x| x.hash == scope_hash);
if let Some(_) = matched {
self.tokens.retain(|x| x.hash != scope_hash);
}
#[cfg(unix)]
async fn open_writeable_file(
filename: impl AsRef<Path>,
) -> Result<tokio::fs::File, tokio::io::Error> {
// Ensure if the file is created it's only readable and writable by the
// current user.
use std::os::unix::fs::OpenOptionsExt;
let opts: tokio::fs::OpenOptions = {
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create(true).truncate(true).mode(0o600);
opts.into()
};
opts.open(filename).await
}
match token {
None => (),
Some(t) => {
self.tokens.push(JSONToken {
hash: scope_hash,
scopes: Some(scopes.iter().map(|x| x.to_string()).collect()),
token: t.clone(),
});
()
}
}
self.dump_to_file()
#[cfg(not(unix))]
async fn open_writeable_file(
filename: impl AsRef<Path>,
) -> Result<tokio::fs::File, tokio::io::Error> {
// I don't have knowledge of windows or other platforms to know how to
// create a file that's only readable by the current user.
tokio::fs::File::create(filename).await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scope_filter() {
let foo = ScopeSet::from(&["foo"]).filter;
let bar = ScopeSet::from(&["bar"]).filter;
let foobar = ScopeSet::from(&["foo", "bar"]).filter;
// foo and bar are both subsets of foobar. This condition should hold no
// matter what changes are made to the bloom filter implementation.
assert!(foo.is_subset_of(foobar) == FilterResponse::Maybe);
assert!(bar.is_subset_of(foobar) == FilterResponse::Maybe);
// These conditions hold under the current bloom filter implementation
// because "foo" and "bar" don't collide, but if the bloom filter
// implementations change it could be valid for them to return Maybe.
assert!(foo.is_subset_of(bar) == FilterResponse::No);
assert!(bar.is_subset_of(foo) == FilterResponse::No);
assert!(foobar.is_subset_of(foo) == FilterResponse::No);
assert!(foobar.is_subset_of(bar) == FilterResponse::No);
}
fn get(&self, scope_hash: u64, scopes: &Vec<&str>) -> Result<Option<Token>, Self::Error> {
let scopes: Vec<_> = scopes.iter().sorted().unique().collect();
for t in &self.tokens {
if let Some(token_scopes) = &t.scopes {
let matched = token_scopes
.iter()
.filter(|x| scopes.contains(&&&x[..]))
.count();
// we may have some of the tokens as denormalized (many namespaces repeated)
if matched >= scopes.len() {
return Result::Ok(Some(t.token.clone()));
}
} else if scope_hash == t.hash {
return Result::Ok(Some(t.token.clone()));
}
#[tokio::test]
async fn test_disk_storage() {
let new_token = |access_token: &str| TokenInfo {
access_token: access_token.to_owned(),
refresh_token: None,
expires_at: None,
};
let scope_set = ScopeSet::from(&["myscope"]);
let tempdir = tempfile::tempdir().unwrap();
{
let storage = DiskStorage::new(tempdir.path().join("tokenstorage.json"))
.await
.unwrap();
assert!(storage.get(scope_set).is_none());
storage
.set(scope_set, new_token("my_access_token"))
.await
.unwrap();
assert_eq!(storage.get(scope_set), Some(new_token("my_access_token")));
}
{
// Create a new DiskStorage instance and verify the tokens were read from disk correctly.
let storage = DiskStorage::new(tempdir.path().join("tokenstorage.json"))
.await
.unwrap();
assert_eq!(storage.get(scope_set), Some(new_token("my_access_token")));
}
Result::Ok(None)
}
}

View File

@@ -1,343 +1,115 @@
use chrono::{DateTime, TimeZone, Utc};
use hyper;
use std::error::Error;
use std::fmt;
use std::io;
use std::str::FromStr;
use crate::error::{AuthErrorOr, Error};
use futures::prelude::*;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
/// A marker trait for all Flows
pub trait Flow {
fn type_id() -> FlowType;
/// Represents an access token returned by oauth2 servers. All access 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,
expires_at: Option<DateTime<Utc>>,
}
#[derive(Deserialize, Debug)]
pub struct JsonError {
pub error: String,
pub error_description: Option<String>,
pub error_uri: Option<String>,
}
impl AccessToken {
/// A string representation of the access token.
pub fn as_str(&self) -> &str {
&self.value
}
/// All possible outcomes of the refresh flow
#[derive(Debug)]
pub enum RefreshResult {
/// Indicates connection failure
Error(hyper::Error),
/// The server did not answer with a new token, providing the server message
RefreshError(String, Option<String>),
/// The refresh operation finished successfully, providing a new `Token`
Success(Token),
}
/// The time the access token will expire, if any.
pub fn expiration_time(&self) -> Option<DateTime<Utc>> {
self.expires_at
}
/// Encapsulates all possible results of a `poll_token(...)` operation in the Device flow.
#[derive(Debug)]
pub enum PollError {
/// Connection failure - retry if you think it's worth it
HttpError(hyper::Error),
/// Indicates we are expired, including the expiration date
Expired(DateTime<Utc>),
/// Indicates that the user declined access. String is server response
AccessDenied,
/// Indicates that too many attempts failed.
TimedOut,
/// Other type of error.
Other(String),
}
/// Encapsulates all possible results of the `token(...)` operation
#[derive(Debug)]
pub enum RequestError {
/// Indicates connection failure
ClientError(hyper::Error),
/// The OAuth client was not found
InvalidClient,
/// Some requested scopes were invalid. String contains the scopes as part of
/// the server error message
InvalidScope(String),
/// A 'catch-all' variant containing the server error and description
/// First string is the error code, the second may be a more detailed description
NegativeServerResponse(String, Option<String>),
/// A malformed server response.
BadServerResponse(String),
/// Error while decoding a JSON response.
JSONError(serde_json::error::Error),
/// Error within user input.
UserError(String),
/// A lower level IO error.
LowLevelError(io::Error),
/// A poll error occurred in the DeviceFlow.
Poll(PollError),
/// An error occurred while refreshing tokens.
Refresh(RefreshResult),
/// Error in token cache layer
Cache(Box<dyn Error + Send + Sync>),
}
impl From<hyper::Error> for RequestError {
fn from(error: hyper::Error) -> RequestError {
RequestError::ClientError(error)
/// 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.
pub fn is_expired(&self) -> bool {
// 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 From<JsonError> for RequestError {
fn from(value: JsonError) -> RequestError {
match &*value.error {
"invalid_client" => RequestError::InvalidClient,
"invalid_scope" => RequestError::InvalidScope(
value
.error_description
.unwrap_or("no description provided".to_string()),
),
_ => RequestError::NegativeServerResponse(value.error, value.error_description),
impl AsRef<str> for AccessToken {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl From<TokenInfo> for AccessToken {
fn from(value: TokenInfo) -> Self {
AccessToken {
value: value.access_token,
expires_at: value.expires_at,
}
}
}
impl fmt::Display for RequestError {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
RequestError::ClientError(ref err) => err.fmt(f),
RequestError::InvalidClient => "Invalid Client".fmt(f),
RequestError::InvalidScope(ref scope) => writeln!(f, "Invalid Scope: '{}'", scope),
RequestError::NegativeServerResponse(ref error, ref desc) => {
error.fmt(f)?;
if let &Some(ref desc) = desc {
write!(f, ": {}", desc)?;
}
"\n".fmt(f)
}
RequestError::BadServerResponse(ref s) => s.fmt(f),
RequestError::JSONError(ref e) => format!(
"JSON Error; this might be a bug with unexpected server responses! {}",
e
)
.fmt(f),
RequestError::UserError(ref s) => s.fmt(f),
RequestError::LowLevelError(ref e) => e.fmt(f),
RequestError::Poll(ref pe) => pe.fmt(f),
RequestError::Refresh(ref rr) => format!("{:?}", rr).fmt(f),
RequestError::Cache(ref e) => e.fmt(f),
}
}
}
impl Error for RequestError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match *self {
RequestError::ClientError(ref err) => Some(err),
RequestError::LowLevelError(ref err) => Some(err),
RequestError::JSONError(ref err) => Some(err),
_ => None,
}
}
}
#[derive(Debug)]
pub struct StringError {
error: String,
}
impl fmt::Display for StringError {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
self.description().fmt(f)
}
}
impl StringError {
pub fn new<S: AsRef<str>>(error: S, desc: Option<S>) -> StringError {
let mut error = error.as_ref().to_string();
if let Some(d) = desc {
error.push_str(": ");
error.push_str(d.as_ref());
}
StringError { error: error }
}
}
impl<'a> From<&'a dyn Error> for StringError {
fn from(err: &'a dyn Error) -> StringError {
StringError::new(err.description().to_string(), None)
}
}
impl From<String> for StringError {
fn from(value: String) -> StringError {
StringError::new(value, None)
}
}
impl Error for StringError {
fn description(&self) -> &str {
&self.error
}
}
/// Represents all implemented token types
#[derive(Clone, PartialEq, Debug)]
pub enum TokenType {
/// Means that whoever bears the access token will be granted access
Bearer,
}
impl AsRef<str> for TokenType {
fn as_ref(&self) -> &'static str {
match *self {
TokenType::Bearer => "Bearer",
}
}
}
impl FromStr for TokenType {
type Err = ();
fn from_str(s: &str) -> Result<TokenType, ()> {
match s {
"Bearer" => Ok(TokenType::Bearer),
_ => Err(()),
}
}
}
/// A scheme for use in `hyper::header::Authorization`
#[derive(Clone, PartialEq, Debug)]
pub struct Scheme {
/// The type of our access token
pub token_type: TokenType,
/// The token returned by one of the Authorization Flows
pub access_token: String,
}
impl std::convert::Into<hyper::header::HeaderValue> for Scheme {
fn into(self) -> hyper::header::HeaderValue {
hyper::header::HeaderValue::from_str(&format!(
"{} {}",
self.token_type.as_ref(),
self.access_token
))
.expect("Invalid Scheme header value")
}
}
impl FromStr for Scheme {
type Err = &'static str;
fn from_str(s: &str) -> Result<Scheme, &'static str> {
let parts: Vec<&str> = s.split(' ').collect();
if parts.len() != 2 {
return Err("Expected two parts: <token_type> <token>");
}
match <TokenType as FromStr>::from_str(parts[0]) {
Ok(t) => Ok(Scheme {
token_type: t,
access_token: parts[1].to_string(),
}),
Err(_) => Err("Couldn't parse token type"),
}
}
}
/// A provider for authorization tokens, yielding tokens valid for a given scope.
/// The `api_key()` method is an alternative in case there are no scopes or
/// if no user is involved.
pub trait GetToken {
fn token<I, T>(
&mut self,
scopes: I,
) -> Box<dyn Future<Item = Token, Error = RequestError> + Send>
where
T: Into<String>,
I: IntoIterator<Item = T>;
fn api_key(&mut self) -> Option<String>;
/// Return an application secret with at least token_uri, client_secret, and client_id filled
/// in. This is used for refreshing tokens without interaction from the flow.
fn application_secret(&self) -> ApplicationSecret;
}
/// 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.
///
/// The type is tuned to be suitable for direct de-serialization from server
/// replies, as well as for serialization for later reuse. This is the reason
/// for the two fields dealing with expiry - once in relative in and once in
/// absolute terms.
///
/// Utility methods make common queries easier, see `expired()`.
#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
pub struct Token {
pub(crate) struct TokenInfo {
/// used when authenticating calls to oauth2 enabled services.
pub access_token: String,
pub(crate) access_token: String,
/// used to refresh an expired access_token.
pub refresh_token: Option<String>,
/// The token type as string - usually 'Bearer'.
pub token_type: String,
/// access_token will expire after this amount of time.
/// Prefer using expiry_date()
pub expires_in: Option<i64>,
/// timestamp is seconds since epoch indicating when the token will expire in absolute terms.
/// use expiry_date() to convert to DateTime.
pub expires_in_timestamp: Option<i64>,
pub(crate) refresh_token: Option<String>,
/// The time when the token expires.
pub(crate) expires_at: Option<DateTime<Utc>>,
}
impl Token {
impl TokenInfo {
pub(crate) fn from_json(json_data: &[u8]) -> Result<TokenInfo, Error> {
#[derive(Deserialize)]
struct RawToken {
access_token: String,
refresh_token: Option<String>,
token_type: String,
expires_in: Option<i64>,
}
let RawToken {
access_token,
refresh_token,
token_type,
expires_in,
} = serde_json::from_slice::<AuthErrorOr<RawToken>>(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());
}
let expires_at = expires_in
.map(|seconds_from_now| Utc::now() + chrono::Duration::seconds(seconds_from_now));
Ok(TokenInfo {
access_token,
refresh_token,
expires_at,
})
}
/// Returns true if we are expired.
///
/// # Panics
/// * if our access_token is unset
pub fn expired(&self) -> bool {
if self.access_token.len() == 0 {
panic!("called expired() on unset token");
}
if let Some(expiry_date) = self.expiry_date() {
expiry_date - chrono::Duration::minutes(1) <= Utc::now()
} else {
false
}
pub fn is_expired(&self) -> bool {
self.expires_at
.map(|expiration_time| expiration_time - chrono::Duration::minutes(1) <= Utc::now())
.unwrap_or(false)
}
/// Returns a DateTime object representing our expiry date.
pub fn expiry_date(&self) -> Option<DateTime<Utc>> {
let expires_in_timestamp = self.expires_in_timestamp?;
Utc.timestamp(expires_in_timestamp, 0).into()
}
/// Adjust our stored expiry format to be absolute, using the current time.
pub fn set_expiry_absolute(&mut self) -> &mut Token {
if self.expires_in_timestamp.is_some() {
assert!(self.expires_in.is_none());
return self;
}
if let Some(expires_in) = self.expires_in {
self.expires_in_timestamp = Some(Utc::now().timestamp() + expires_in);
self.expires_in = None;
}
self
}
}
/// All known authentication types, for suitable constants
#[derive(Clone)]
pub enum FlowType {
/// [device authentication](https://developers.google.com/youtube/v3/guides/authentication#devices). Only works
/// for certain scopes.
/// Contains the device token URL; for google, that is
/// https://accounts.google.com/o/oauth2/device/code (exported as `GOOGLE_DEVICE_CODE_URL`)
Device(String),
/// [installed app flow](https://developers.google.com/identity/protocols/OAuth2InstalledApp). Required
/// for Drive, Calendar, Gmail...; Requires user to paste a code from the browser.
InstalledInteractive,
/// Same as InstalledInteractive, but uses a redirect: The OAuth provider redirects the user's
/// browser to a web server that is running on localhost. This may not work as well with the
/// Windows Firewall, but is more comfortable otherwise. The integer describes which port to
/// bind to (default: 8080)
InstalledRedirect(u16),
}
/// Represents either 'installed' or 'web' applications in a json secrets file.
@@ -352,8 +124,8 @@ pub struct ApplicationSecret {
pub token_uri: String,
/// The authorization server endpoint URI.
pub auth_uri: String,
/// The redirect uris.
pub redirect_uris: Vec<String>,
/// Name of the google project the credentials are associated with
pub project_id: Option<String>,
/// The service account email associated with the client.
@@ -369,14 +141,15 @@ pub struct ApplicationSecret {
/// as returned by the [google developer console](https://code.google.com/apis/console)
#[derive(Deserialize, Serialize, Default)]
pub struct ConsoleApplicationSecret {
/// web app secret
pub web: Option<ApplicationSecret>,
/// installed app secret
pub installed: Option<ApplicationSecret>,
}
#[cfg(test)]
pub mod tests {
use super::*;
use hyper;
pub const SECRET: &'static str =
"{\"installed\":{\"auth_uri\":\"https://accounts.google.com/o/oauth2/auth\",\
@@ -394,25 +167,4 @@ pub mod tests {
Err(err) => panic!(err),
}
}
#[test]
fn schema() {
let s = Scheme {
token_type: TokenType::Bearer,
access_token: "foo".to_string(),
};
let mut headers = hyper::HeaderMap::new();
headers.insert(hyper::header::AUTHORIZATION, s.into());
assert_eq!(
format!("{:?}", headers),
"{\"authorization\": \"Bearer foo\"}".to_string()
);
}
#[test]
fn parse_schema() {
let auth = Scheme::from_str("Bearer foo").unwrap();
assert_eq!(auth.token_type, TokenType::Bearer);
assert_eq!(auth.access_token, "foo".to_string());
}
}

618
tests/tests.rs Normal file
View File

@@ -0,0 +1,618 @@
use yup_oauth2::{
authenticator::Authenticator,
authenticator_delegate::{DeviceAuthResponse, DeviceFlowDelegate, InstalledFlowDelegate},
error::{AuthError, AuthErrorCode},
ApplicationSecret, DeviceFlowAuthenticator, Error, InstalledFlowAuthenticator,
InstalledFlowReturnMethod, ServiceAccountAuthenticator, ServiceAccountKey,
};
use std::future::Future;
use std::path::PathBuf;
use std::pin::Pin;
use httptest::{mappers::*, responders::json_encoded, Expectation, Server};
use hyper::client::connect::HttpConnector;
use hyper::Uri;
use hyper_rustls::HttpsConnector;
use url::form_urlencoded;
/// Utility function for parsing json. Useful in unit tests. Simply wrap the
/// json! macro in a from_value to deserialize the contents to arbitrary structs.
macro_rules! parse_json {
($($json:tt)+) => {
::serde_json::from_value(::serde_json::json!($($json)+)).expect("failed to deserialize")
}
}
async fn create_device_flow_auth(server: &Server) -> Authenticator<HttpsConnector<HttpConnector>> {
let app_secret: ApplicationSecret = parse_json!({
"client_id": "902216714886-k2v9uei3p1dk6h686jbsn9mo96tnbvto.apps.googleusercontent.com",
"project_id": "yup-test-243420",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": server.url_str("/token"),
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_secret": "iuMPN6Ne1PD7cos29Tk9rlqH",
"redirect_uris": ["urn:ietf:wg:oauth:2.0:oob","http://localhost"],
});
struct FD;
impl DeviceFlowDelegate for FD {
fn present_user_code<'a>(
&'a self,
pi: &'a DeviceAuthResponse,
) -> Pin<Box<dyn Future<Output = ()> + 'a + Send>> {
assert_eq!("https://example.com/verify", pi.verification_uri);
Box::pin(async {})
}
}
DeviceFlowAuthenticator::builder(app_secret)
.flow_delegate(Box::new(FD))
.device_code_url(server.url_str("/code"))
.build()
.await
.unwrap()
}
#[tokio::test]
async fn test_device_success() {
let _ = env_logger::try_init();
let server = Server::run();
server.expect(
Expectation::matching(all_of![
request::method("POST"),
request::path("/code"),
request::body(url_decoded(contains_entry((
"client_id",
matches("902216714886")
)))),
])
.respond_with(json_encoded(serde_json::json!({
"device_code": "devicecode",
"user_code": "usercode",
"verification_url": "https://example.com/verify",
"expires_in": 1234567,
"interval": 1
}))),
);
server.expect(
Expectation::matching(all_of![
request::method("POST"),
request::path("/token"),
request::body(url_decoded(all_of![
contains_entry(("client_secret", "iuMPN6Ne1PD7cos29Tk9rlqH")),
contains_entry(("code", "devicecode")),
])),
])
.respond_with(json_encoded(serde_json::json!({
"access_token": "accesstoken",
"refresh_token": "refreshtoken",
"token_type": "Bearer",
"expires_in": 1234567
}))),
);
let auth = create_device_flow_auth(&server).await;
let token = auth
.token(&["https://www.googleapis.com/scope/1"])
.await
.expect("token failed");
assert_eq!("accesstoken", token.as_str());
}
#[tokio::test]
async fn test_device_no_code() {
let _ = env_logger::try_init();
let server = Server::run();
server.expect(
Expectation::matching(all_of![
request::method("POST"),
request::path("/code"),
request::body(url_decoded(contains_entry((
"client_id",
matches("902216714886")
)))),
])
.respond_with(json_encoded(serde_json::json!({
"error": "invalid_client_id",
"error_description": "description"
}))),
);
let auth = create_device_flow_auth(&server).await;
let res = auth.token(&["https://www.googleapis.com/scope/1"]).await;
assert!(res.is_err());
assert!(format!("{}", res.unwrap_err()).contains("invalid_client_id"));
}
#[tokio::test]
async fn test_device_no_token() {
let _ = env_logger::try_init();
let server = Server::run();
server.expect(
Expectation::matching(all_of![
request::method("POST"),
request::path("/code"),
request::body(url_decoded(contains_entry((
"client_id",
matches("902216714886")
)))),
])
.respond_with(json_encoded(serde_json::json!({
"device_code": "devicecode",
"user_code": "usercode",
"verification_url": "https://example.com/verify",
"expires_in": 1234567,
"interval": 1
}))),
);
server.expect(
Expectation::matching(all_of![
request::method("POST"),
request::path("/token"),
request::body(url_decoded(all_of![
contains_entry(("client_secret", "iuMPN6Ne1PD7cos29Tk9rlqH")),
contains_entry(("code", "devicecode")),
])),
])
.respond_with(json_encoded(serde_json::json!({
"error": "access_denied"
}))),
);
let auth = create_device_flow_auth(&server).await;
let res = auth.token(&["https://www.googleapis.com/scope/1"]).await;
assert!(res.is_err());
assert!(format!("{}", res.unwrap_err()).contains("access_denied"));
}
async fn create_installed_flow_auth(
server: &Server,
method: InstalledFlowReturnMethod,
filename: Option<PathBuf>,
) -> Authenticator<HttpsConnector<HttpConnector>> {
let app_secret: ApplicationSecret = parse_json!({
"client_id": "902216714886-k2v9uei3p1dk6h686jbsn9mo96tnbvto.apps.googleusercontent.com",
"project_id": "yup-test-243420",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": server.url_str("/token"),
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_secret": "iuMPN6Ne1PD7cos29Tk9rlqH",
"redirect_uris": ["urn:ietf:wg:oauth:2.0:oob","http://localhost"],
});
struct FD(hyper::Client<HttpsConnector<HttpConnector>>);
impl InstalledFlowDelegate for FD {
/// Depending on need_code, return the pre-set code or send the code to the server at
/// the redirect_uri given in the url.
fn present_user_url<'a>(
&'a self,
url: &'a str,
need_code: bool,
) -> Pin<Box<dyn Future<Output = Result<String, String>> + Send + 'a>> {
use std::str::FromStr;
Box::pin(async move {
if need_code {
Ok("authorizationcode".to_owned())
} else {
// Parse presented url to obtain redirect_uri with location of local
// code-accepting server.
let uri = Uri::from_str(url.as_ref()).unwrap();
let query = uri.query().unwrap();
let parsed = form_urlencoded::parse(query.as_bytes()).into_owned();
let mut rduri = None;
for (k, v) in parsed {
if k == "redirect_uri" {
rduri = Some(v);
break;
}
}
if rduri.is_none() {
return Err("no redirect_uri!".into());
}
let mut rduri = rduri.unwrap();
rduri.push_str("?code=authorizationcode");
let rduri = Uri::from_str(rduri.as_ref()).unwrap();
// Hit server.
self.0
.get(rduri)
.await
.map_err(|e| e.to_string())
.map(|_| "".to_string())
}
})
}
}
let mut builder = InstalledFlowAuthenticator::builder(app_secret, method).flow_delegate(
Box::new(FD(hyper::Client::builder().build(HttpsConnector::new()))),
);
builder = if let Some(filename) = filename {
builder.persist_tokens_to_disk(filename)
} else {
builder
};
builder.build().await.unwrap()
}
#[tokio::test]
async fn test_installed_interactive_success() {
let _ = env_logger::try_init();
let server = Server::run();
let auth =
create_installed_flow_auth(&server, InstalledFlowReturnMethod::Interactive, None).await;
server.expect(
Expectation::matching(all_of![
request::method("POST"),
request::path("/token"),
request::body(url_decoded(all_of![
contains_entry(("code", "authorizationcode")),
contains_entry(("client_id", matches("9022167.*"))),
]))
])
.respond_with(json_encoded(serde_json::json!({
"access_token": "accesstoken",
"refresh_token": "refreshtoken",
"token_type": "Bearer",
"expires_in": 12345678
}))),
);
let tok = auth
.token(&["https://googleapis.com/some/scope"])
.await
.expect("failed to get token");
assert_eq!("accesstoken", tok.as_str());
}
#[tokio::test]
async fn test_installed_redirect_success() {
let _ = env_logger::try_init();
let server = Server::run();
let auth =
create_installed_flow_auth(&server, InstalledFlowReturnMethod::HTTPRedirect, None).await;
server.expect(
Expectation::matching(all_of![
request::method("POST"),
request::path("/token"),
request::body(url_decoded(all_of![
contains_entry(("code", "authorizationcode")),
contains_entry(("client_id", matches("9022167.*"))),
]))
])
.respond_with(json_encoded(serde_json::json!({
"access_token": "accesstoken",
"refresh_token": "refreshtoken",
"token_type": "Bearer",
"expires_in": 12345678
}))),
);
let tok = auth
.token(&["https://googleapis.com/some/scope"])
.await
.expect("failed to get token");
assert_eq!("accesstoken", tok.as_str());
}
#[tokio::test]
async fn test_installed_error() {
let _ = env_logger::try_init();
let server = Server::run();
let auth =
create_installed_flow_auth(&server, InstalledFlowReturnMethod::Interactive, None).await;
server.expect(
Expectation::matching(all_of![
request::method("POST"),
request::path("/token"),
request::body(url_decoded(all_of![
contains_entry(("code", "authorizationcode")),
contains_entry(("client_id", matches("9022167.*"))),
]))
])
.respond_with(
http::Response::builder()
.status(404)
.body(serde_json::json!({"error": "invalid_code"}).to_string())
.unwrap(),
),
);
let tokr = auth.token(&["https://googleapis.com/some/scope"]).await;
assert!(tokr.is_err());
assert!(format!("{}", tokr.unwrap_err()).contains("invalid_code"));
}
async fn create_service_account_auth(
server: &Server,
) -> Authenticator<HttpsConnector<HttpConnector>> {
let key: ServiceAccountKey = parse_json!({
"type": "service_account",
"project_id": "yup-test-243420",
"private_key_id": "26de294916614a5ebdf7a065307ed3ea9941902b",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDemmylrvp1KcOn\n9yTAVVKPpnpYznvBvcAU8Qjwr2fSKylpn7FQI54wCk5VJVom0jHpAmhxDmNiP8yv\nHaqsef+87Oc0n1yZ71/IbeRcHZc2OBB33/LCFqf272kThyJo3qspEqhuAw0e8neg\nLQb4jpm9PsqR8IjOoAtXQSu3j0zkXemMYFy93PWHjVpPEUX16NGfsWH7oxspBHOk\n9JPGJL8VJdbiAoDSDgF0y9RjJY5I52UeHNhMsAkTYs6mIG4kKXt2+T9tAyHw8aho\nwmuytQAfydTflTfTG8abRtliF3nil2taAc5VB07dP1b4dVYy/9r6M8Z0z4XM7aP+\nNdn2TKm3AgMBAAECggEAWi54nqTlXcr2M5l535uRb5Xz0f+Q/pv3ceR2iT+ekXQf\n+mUSShOr9e1u76rKu5iDVNE/a7H3DGopa7ZamzZvp2PYhSacttZV2RbAIZtxU6th\n7JajPAM+t9klGh6wj4jKEcE30B3XVnbHhPJI9TCcUyFZoscuPXt0LLy/z8Uz0v4B\nd5JARwyxDMb53VXwukQ8nNY2jP7WtUig6zwE5lWBPFMbi8GwGkeGZOruAK5sPPwY\nGBAlfofKANI7xKx9UXhRwisB4+/XI1L0Q6xJySv9P+IAhDUI6z6kxR+WkyT/YpG3\nX9gSZJc7qEaxTIuDjtep9GTaoEqiGntjaFBRKoe+VQKBgQDzM1+Ii+REQqrGlUJo\nx7KiVNAIY/zggu866VyziU6h5wjpsoW+2Npv6Dv7nWvsvFodrwe50Y3IzKtquIal\nVd8aa50E72JNImtK/o5Nx6xK0VySjHX6cyKENxHRDnBmNfbALRM+vbD9zMD0lz2q\nmns/RwRGq3/98EqxP+nHgHSr9QKBgQDqUYsFAAfvfT4I75Glc9svRv8IsaemOm07\nW1LCwPnj1MWOhsTxpNF23YmCBupZGZPSBFQobgmHVjQ3AIo6I2ioV6A+G2Xq/JCF\nmzfbvZfqtbbd+nVgF9Jr1Ic5T4thQhAvDHGUN77BpjEqZCQLAnUWJx9x7e2xvuBl\n1A6XDwH/ewKBgQDv4hVyNyIR3nxaYjFd7tQZYHTOQenVffEAd9wzTtVbxuo4sRlR\nNM7JIRXBSvaATQzKSLHjLHqgvJi8LITLIlds1QbNLl4U3UVddJbiy3f7WGTqPFfG\nkLhUF4mgXpCpkMLxrcRU14Bz5vnQiDmQRM4ajS7/kfwue00BZpxuZxst3QKBgQCI\nRI3FhaQXyc0m4zPfdYYVc4NjqfVmfXoC1/REYHey4I1XetbT9Nb/+ow6ew0UbgSC\nUZQjwwJ1m1NYXU8FyovVwsfk9ogJ5YGiwYb1msfbbnv/keVq0c/Ed9+AG9th30qM\nIf93hAfClITpMz2mzXIMRQpLdmQSR4A2l+E4RjkSOwKBgQCB78AyIdIHSkDAnCxz\nupJjhxEhtQ88uoADxRoEga7H/2OFmmPsqfytU4+TWIdal4K+nBCBWRvAX1cU47vH\nJOlSOZI0gRKe0O4bRBQc8GXJn/ubhYSxI02IgkdGrIKpOb5GG10m85ZvqsXw3bKn\nRVHMD0ObF5iORjZUqD0yRitAdg==\n-----END PRIVATE KEY-----\n",
"client_email": "yup-test-sa-1@yup-test-243420.iam.gserviceaccount.com",
"client_id": "102851967901799660408",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": server.url_str("/token"),
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/yup-test-sa-1%40yup-test-243420.iam.gserviceaccount.com"
});
ServiceAccountAuthenticator::builder(key)
.build()
.await
.unwrap()
}
#[tokio::test]
async fn test_service_account_success() {
use chrono::Utc;
let _ = env_logger::try_init();
let server = Server::run();
let auth = create_service_account_auth(&server).await;
server.expect(
Expectation::matching(all_of![
request::method("POST"),
request::path("/token"),
]).respond_with(json_encoded(serde_json::json!({
"access_token": "ya29.c.ElouBywiys0LyNaZoLPJcp1Fdi2KjFMxzvYKLXkTdvM-rDfqKlvEq6PiMhGoGHx97t5FAvz3eb_ahdwlBjSStxHtDVQB4ZPRJQ_EOi-iS7PnayahU2S9Jp8S6rk",
"expires_in": 3600,
"token_type": "Bearer"
})))
);
let tok = auth
.token(&["https://www.googleapis.com/auth/pubsub"])
.await
.expect("token failed");
assert!(tok.as_str().contains("ya29.c.ElouBywiys0Ly"));
assert!(Utc::now() + chrono::Duration::seconds(3600) >= tok.expiration_time().unwrap());
}
#[tokio::test]
async fn test_service_account_error() {
let _ = env_logger::try_init();
let server = Server::run();
let auth = create_service_account_auth(&server).await;
server.expect(
Expectation::matching(all_of![request::method("POST"), request::path("/token"),])
.respond_with(json_encoded(serde_json::json!({
"error": "access_denied",
}))),
);
let result = auth
.token(&["https://www.googleapis.com/auth/pubsub"])
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_refresh() {
let _ = env_logger::try_init();
let server = Server::run();
let auth =
create_installed_flow_auth(&server, InstalledFlowReturnMethod::Interactive, None).await;
// We refresh a token whenever it's within 1 minute of expiring. So
// acquiring a token that expires in 59 seconds will force a refresh on
// the next token call.
server.expect(
Expectation::matching(all_of![
request::method("POST"),
request::path("/token"),
request::body(url_decoded(all_of![
contains_entry(("code", "authorizationcode")),
contains_entry(("client_id", matches("^9022167"))),
]))
])
.respond_with(json_encoded(serde_json::json!({
"access_token": "accesstoken",
"refresh_token": "refreshtoken",
"token_type": "Bearer",
"expires_in": 59,
}))),
);
let tok = auth
.token(&["https://googleapis.com/some/scope"])
.await
.expect("failed to get token");
assert_eq!("accesstoken", tok.as_str());
server.expect(
Expectation::matching(all_of![
request::method("POST"),
request::path("/token"),
request::body(url_decoded(all_of![
contains_entry(("refresh_token", "refreshtoken")),
contains_entry(("client_id", matches("^9022167"))),
]))
])
.respond_with(json_encoded(serde_json::json!({
"access_token": "accesstoken2",
"token_type": "Bearer",
"expires_in": 59,
}))),
);
let tok = auth
.token(&["https://googleapis.com/some/scope"])
.await
.expect("failed to get token");
assert_eq!("accesstoken2", tok.as_str());
server.expect(
Expectation::matching(all_of![
request::method("POST"),
request::path("/token"),
request::body(url_decoded(all_of![
contains_entry(("refresh_token", "refreshtoken")),
contains_entry(("client_id", matches("^9022167"))),
]))
])
.respond_with(json_encoded(serde_json::json!({
"access_token": "accesstoken3",
"token_type": "Bearer",
"expires_in": 59,
}))),
);
let tok = auth
.token(&["https://googleapis.com/some/scope"])
.await
.expect("failed to get token");
assert_eq!("accesstoken3", tok.as_str());
server.expect(
Expectation::matching(all_of![
request::method("POST"),
request::path("/token"),
request::body(url_decoded(all_of![
contains_entry(("refresh_token", "refreshtoken")),
contains_entry(("client_id", matches("^9022167"))),
]))
])
.respond_with(json_encoded(serde_json::json!({
"error": "invalid_request",
}))),
);
let tok_err = auth
.token(&["https://googleapis.com/some/scope"])
.await
.expect_err("token refresh succeeded unexpectedly");
match tok_err {
Error::AuthError(AuthError {
error: AuthErrorCode::InvalidRequest,
..
}) => {}
e => panic!("unexpected error on refresh: {:?}", e),
}
}
#[tokio::test]
async fn test_memory_storage() {
let _ = env_logger::try_init();
let server = Server::run();
let auth =
create_installed_flow_auth(&server, InstalledFlowReturnMethod::Interactive, None).await;
server.expect(
Expectation::matching(all_of![
request::method("POST"),
request::path("/token"),
request::body(url_decoded(all_of![
contains_entry(("code", "authorizationcode")),
contains_entry(("client_id", matches("^9022167"))),
]))
])
.respond_with(json_encoded(serde_json::json!({
"access_token": "accesstoken",
"refresh_token": "refreshtoken",
"token_type": "Bearer",
"expires_in": 12345678,
}))),
);
// Call token twice. Ensure that only one http request is made and
// identical tokens are returned.
let token1 = auth
.token(&["https://googleapis.com/some/scope"])
.await
.expect("failed to get token");
let token2 = auth
.token(&["https://googleapis.com/some/scope"])
.await
.expect("failed to get token");
assert_eq!(token1.as_str(), "accesstoken");
assert_eq!(token1, token2);
// Create a new authenticator. This authenticator does not share a cache
// with the previous one. Validate that it receives a different token.
let auth2 =
create_installed_flow_auth(&server, InstalledFlowReturnMethod::Interactive, None).await;
server.expect(
Expectation::matching(all_of![
request::method("POST"),
request::path("/token"),
request::body(url_decoded(all_of![
contains_entry(("code", "authorizationcode")),
contains_entry(("client_id", matches("^9022167"))),
]))
])
.respond_with(json_encoded(serde_json::json!({
"access_token": "accesstoken2",
"refresh_token": "refreshtoken2",
"token_type": "Bearer",
"expires_in": 12345678,
}))),
);
let token3 = auth2
.token(&["https://googleapis.com/some/scope"])
.await
.expect("failed to get token");
assert_eq!(token3.as_str(), "accesstoken2");
}
#[tokio::test]
async fn test_disk_storage() {
let _ = env_logger::try_init();
let server = Server::run();
let tempdir = tempfile::tempdir().unwrap();
let storage_path = tempdir.path().join("tokenstorage.json");
server.expect(
Expectation::matching(all_of![
request::method("POST"),
request::path("/token"),
request::body(url_decoded(all_of![
contains_entry(("code", "authorizationcode")),
contains_entry(("client_id", matches("^9022167"))),
])),
])
.respond_with(json_encoded(serde_json::json!({
"access_token": "accesstoken",
"refresh_token": "refreshtoken",
"token_type": "Bearer",
"expires_in": 12345678
}))),
);
{
let auth = create_installed_flow_auth(
&server,
InstalledFlowReturnMethod::Interactive,
Some(storage_path.clone()),
)
.await;
// Call token twice. Ensure that only one http request is made and
// identical tokens are returned.
let token1 = auth
.token(&["https://googleapis.com/some/scope"])
.await
.expect("failed to get token");
let token2 = auth
.token(&["https://googleapis.com/some/scope"])
.await
.expect("failed to get token");
assert_eq!(token1.as_str(), "accesstoken");
assert_eq!(token1, token2);
}
// Create a new authenticator. This authenticator uses the same token
// storage file as the previous one so should receive a token without
// making any http requests.
let auth = create_installed_flow_auth(
&server,
InstalledFlowReturnMethod::Interactive,
Some(storage_path.clone()),
)
.await;
// Call token twice. Ensure that identical tokens are returned.
let token1 = auth
.token(&["https://googleapis.com/some/scope"])
.await
.expect("failed to get token");
let token2 = auth
.token(&["https://googleapis.com/some/scope"])
.await
.expect("failed to get token");
assert_eq!(token1.as_str(), "accesstoken");
assert_eq!(token1, token2);
}