mirror of
https://github.com/OMGeeky/yup-oauth2.git
synced 2026-01-07 03:31:31 +01:00
Refactor error handling and as a consequence delegates.
This Removes RefreshError and PollError. Both those types can be fully represented within Error and there seems little value in distinguishing that they were resulting from device polling or refreshes. In either case the user will need to handle the response from token() calls similarly. This also removes the AuthenticatorDelegate since it only served to notify users when refreshes failed, which can already be done by looking at the return code from token. DeviceFlow no longer has the ability to set a wait_timeout. This is trivial to do by wrapping the token() call in a tokio::Timeout future so there's little benefit for users specifying this value. The DeviceFlowDelegate also no longer has the ability to specify when to abort, or alter the interval polling happens on, but it does gain understanding of the 'slow_down' response as documented in the oauth rfc. It seemed very unlikely the delegate was going to do anything other that timeout after a given time and that's already possible using tokio::Timeout so it needlessly complicated the implementation.
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
//! Module contianing the core functionality for OAuth2 Authentication.
|
||||
use crate::authenticator_delegate::{
|
||||
AuthenticatorDelegate, DefaultAuthenticatorDelegate, DeviceFlowDelegate, InstalledFlowDelegate,
|
||||
};
|
||||
use crate::authenticator_delegate::{DeviceFlowDelegate, InstalledFlowDelegate};
|
||||
use crate::device::DeviceFlow;
|
||||
use crate::error::Error;
|
||||
use crate::installed::{InstalledFlow, InstalledFlowReturnMethod};
|
||||
@@ -15,13 +13,11 @@ use std::borrow::Cow;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Authenticator is responsible for fetching tokens, handling refreshing tokens,
|
||||
/// and optionally persisting tokens to disk.
|
||||
pub struct Authenticator<C> {
|
||||
hyper_client: hyper::Client<C>,
|
||||
auth_delegate: Box<dyn AuthenticatorDelegate>,
|
||||
storage: Storage,
|
||||
auth_flow: AuthFlow,
|
||||
}
|
||||
@@ -49,19 +45,9 @@ where
|
||||
Some(app_secret),
|
||||
) => {
|
||||
// token is expired but has a refresh token.
|
||||
let token = match RefreshFlow::refresh_token(
|
||||
&self.hyper_client,
|
||||
app_secret,
|
||||
&refresh_token,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(err) => {
|
||||
self.auth_delegate.token_refresh_failed(&err);
|
||||
return Err(err.into());
|
||||
}
|
||||
Ok(token) => token,
|
||||
};
|
||||
let token =
|
||||
RefreshFlow::refresh_token(&self.hyper_client, app_secret, &refresh_token)
|
||||
.await?;
|
||||
self.storage.set(hashed_scopes, token.clone()).await;
|
||||
Ok(token)
|
||||
}
|
||||
@@ -78,7 +64,6 @@ where
|
||||
/// Configure an Authenticator using the builder pattern.
|
||||
pub struct AuthenticatorBuilder<C, F> {
|
||||
hyper_client_builder: C,
|
||||
auth_delegate: Box<dyn AuthenticatorDelegate>,
|
||||
storage_type: StorageType,
|
||||
auth_flow: F,
|
||||
}
|
||||
@@ -158,12 +143,10 @@ impl ServiceAccountAuthenticator {
|
||||
/// ```
|
||||
/// # async fn foo() {
|
||||
/// # let custom_hyper_client = hyper::Client::new();
|
||||
/// # let custom_auth_delegate = yup_oauth2::authenticator_delegate::DefaultAuthenticatorDelegate;
|
||||
/// # 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")
|
||||
/// .auth_delegate(Box::new(custom_auth_delegate))
|
||||
/// .build()
|
||||
/// .await
|
||||
/// .expect("failed to create authenticator");
|
||||
@@ -173,7 +156,6 @@ impl<C, F> AuthenticatorBuilder<C, F> {
|
||||
async fn common_build(
|
||||
hyper_client_builder: C,
|
||||
storage_type: StorageType,
|
||||
auth_delegate: Box<dyn AuthenticatorDelegate>,
|
||||
auth_flow: AuthFlow,
|
||||
) -> io::Result<Authenticator<C::Connector>>
|
||||
where
|
||||
@@ -190,7 +172,6 @@ impl<C, F> AuthenticatorBuilder<C, F> {
|
||||
Ok(Authenticator {
|
||||
hyper_client,
|
||||
storage,
|
||||
auth_delegate,
|
||||
auth_flow,
|
||||
})
|
||||
}
|
||||
@@ -198,7 +179,6 @@ impl<C, F> AuthenticatorBuilder<C, F> {
|
||||
fn with_auth_flow(auth_flow: F) -> AuthenticatorBuilder<DefaultHyperClient, F> {
|
||||
AuthenticatorBuilder {
|
||||
hyper_client_builder: DefaultHyperClient,
|
||||
auth_delegate: Box::new(DefaultAuthenticatorDelegate),
|
||||
storage_type: StorageType::Memory,
|
||||
auth_flow,
|
||||
}
|
||||
@@ -211,7 +191,6 @@ impl<C, F> AuthenticatorBuilder<C, F> {
|
||||
) -> AuthenticatorBuilder<hyper::Client<NewC>, F> {
|
||||
AuthenticatorBuilder {
|
||||
hyper_client_builder: hyper_client,
|
||||
auth_delegate: self.auth_delegate,
|
||||
storage_type: self.storage_type,
|
||||
auth_flow: self.auth_flow,
|
||||
}
|
||||
@@ -224,17 +203,6 @@ impl<C, F> AuthenticatorBuilder<C, F> {
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Use the provided authenticator delegate.
|
||||
pub fn auth_delegate(
|
||||
self,
|
||||
auth_delegate: Box<dyn AuthenticatorDelegate>,
|
||||
) -> AuthenticatorBuilder<C, F> {
|
||||
AuthenticatorBuilder {
|
||||
auth_delegate,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ## Methods available when building a device flow Authenticator.
|
||||
@@ -245,7 +213,6 @@ impl<C, F> AuthenticatorBuilder<C, F> {
|
||||
/// let authenticator = yup_oauth2::DeviceFlowAuthenticator::builder(app_secret)
|
||||
/// .device_code_url("foo")
|
||||
/// .flow_delegate(Box::new(custom_flow_delegate))
|
||||
/// .wait_duration(std::time::Duration::from_secs(120))
|
||||
/// .grant_type("foo")
|
||||
/// .build()
|
||||
/// .await
|
||||
@@ -275,17 +242,6 @@ impl<C> AuthenticatorBuilder<C, DeviceFlow> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Use the provided wait duration.
|
||||
pub fn wait_duration(self, wait_duration: Duration) -> Self {
|
||||
AuthenticatorBuilder {
|
||||
auth_flow: DeviceFlow {
|
||||
wait_duration,
|
||||
..self.auth_flow
|
||||
},
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Use the provided grant type.
|
||||
pub fn grant_type(self, grant_type: impl Into<Cow<'static, str>>) -> Self {
|
||||
AuthenticatorBuilder {
|
||||
@@ -305,7 +261,6 @@ impl<C> AuthenticatorBuilder<C, DeviceFlow> {
|
||||
Self::common_build(
|
||||
self.hyper_client_builder,
|
||||
self.storage_type,
|
||||
self.auth_delegate,
|
||||
AuthFlow::DeviceFlow(self.auth_flow),
|
||||
)
|
||||
.await
|
||||
@@ -348,7 +303,6 @@ impl<C> AuthenticatorBuilder<C, InstalledFlow> {
|
||||
Self::common_build(
|
||||
self.hyper_client_builder,
|
||||
self.storage_type,
|
||||
self.auth_delegate,
|
||||
AuthFlow::InstalledFlow(self.auth_flow),
|
||||
)
|
||||
.await
|
||||
@@ -389,7 +343,6 @@ impl<C> AuthenticatorBuilder<C, ServiceAccountFlowOpts> {
|
||||
Self::common_build(
|
||||
self.hyper_client_builder,
|
||||
self.storage_type,
|
||||
self.auth_delegate,
|
||||
AuthFlow::ServiceAccountFlow(service_account_auth_flow),
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -1,25 +1,11 @@
|
||||
//! Module containing types related to delegates.
|
||||
|
||||
use crate::error::RefreshError;
|
||||
|
||||
use std::fmt;
|
||||
use std::pin::Pin;
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::{DateTime, Local, Utc};
|
||||
use futures::prelude::*;
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
/// Contains state of pending authentication requests
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PollInformation {
|
||||
@@ -36,43 +22,9 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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: Send + Sync {
|
||||
/// 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(&self, _: &RefreshError) {}
|
||||
}
|
||||
|
||||
/// DeviceFlowDelegate methods are called when a device flow needs to ask the
|
||||
/// application what to do in certain cases.
|
||||
pub trait DeviceFlowDelegate: Send + Sync {
|
||||
/// 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(&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(&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.
|
||||
fn pending(&self, _: &PollInformation) -> Retry {
|
||||
Retry::After(Duration::from_secs(5))
|
||||
}
|
||||
|
||||
/// The server has returned a `user_code` which must be shown to the user,
|
||||
/// along with the `verification_url`.
|
||||
/// # Notes
|
||||
@@ -136,12 +88,6 @@ async fn present_user_url(url: &str, need_code: bool) -> Result<String, String>
|
||||
}
|
||||
}
|
||||
|
||||
/// Uses all default implementations by AuthenticatorDelegate, and makes the trait's
|
||||
/// implementation usable in the first place.
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct DefaultAuthenticatorDelegate;
|
||||
impl AuthenticatorDelegate for DefaultAuthenticatorDelegate {}
|
||||
|
||||
/// Uses all default implementations in the DeviceFlowDelegate trait.
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct DefaultDeviceFlowDelegate;
|
||||
|
||||
111
src/device.rs
111
src/device.rs
@@ -1,14 +1,13 @@
|
||||
use crate::authenticator_delegate::{
|
||||
DefaultDeviceFlowDelegate, DeviceFlowDelegate, PollInformation, Retry,
|
||||
DefaultDeviceFlowDelegate, DeviceFlowDelegate, PollInformation,
|
||||
};
|
||||
use crate::error::{Error, JsonErrorOr, PollError};
|
||||
use crate::error::{AuthError, AuthErrorOr, Error};
|
||||
use crate::types::{ApplicationSecret, Token};
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::time::Duration;
|
||||
|
||||
use ::log::error;
|
||||
use chrono::{DateTime, Utc};
|
||||
use chrono::Utc;
|
||||
use futures::prelude::*;
|
||||
use hyper::header;
|
||||
use serde::Deserialize;
|
||||
@@ -27,7 +26,6 @@ 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) wait_duration: Duration,
|
||||
pub(crate) grant_type: Cow<'static, str>,
|
||||
}
|
||||
|
||||
@@ -39,7 +37,6 @@ impl DeviceFlow {
|
||||
app_secret,
|
||||
device_code_url: GOOGLE_DEVICE_CODE_URL.into(),
|
||||
flow_delegate: Box::new(DefaultDeviceFlowDelegate),
|
||||
wait_duration: Duration::from_secs(120),
|
||||
grant_type: GOOGLE_GRANT_TYPE.into(),
|
||||
}
|
||||
}
|
||||
@@ -61,18 +58,14 @@ impl DeviceFlow {
|
||||
)
|
||||
.await?;
|
||||
self.flow_delegate.present_user_code(&pollinf);
|
||||
tokio::timer::Timeout::new(
|
||||
self.wait_for_device_token(
|
||||
hyper_client,
|
||||
&self.app_secret,
|
||||
&pollinf,
|
||||
&device_code,
|
||||
&self.grant_type,
|
||||
),
|
||||
self.wait_duration,
|
||||
self.wait_for_device_token(
|
||||
hyper_client,
|
||||
&self.app_secret,
|
||||
&pollinf,
|
||||
&device_code,
|
||||
&self.grant_type,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| Error::Poll(PollError::TimedOut))?
|
||||
}
|
||||
|
||||
async fn wait_for_device_token<C>(
|
||||
@@ -89,28 +82,19 @@ impl DeviceFlow {
|
||||
let mut interval = pollinf.interval;
|
||||
loop {
|
||||
tokio::timer::delay_for(interval).await;
|
||||
interval = match Self::poll_token(
|
||||
&app_secret,
|
||||
hyper_client,
|
||||
device_code,
|
||||
grant_type,
|
||||
pollinf.expires_at,
|
||||
&*self.flow_delegate as &dyn DeviceFlowDelegate,
|
||||
)
|
||||
.await
|
||||
interval = match Self::poll_token(&app_secret, hyper_client, device_code, grant_type)
|
||||
.await
|
||||
{
|
||||
Ok(None) => match self.flow_delegate.pending(&pollinf) {
|
||||
Retry::Abort | Retry::Skip => return Err(Error::Poll(PollError::TimedOut)),
|
||||
Retry::After(d) => d,
|
||||
},
|
||||
Ok(Some(tok)) => return Ok(tok),
|
||||
Err(e @ PollError::AccessDenied)
|
||||
| Err(e @ PollError::TimedOut)
|
||||
| Err(e @ PollError::Expired(_)) => return Err(Error::Poll(e)),
|
||||
Err(ref e) => {
|
||||
error!("Unknown error from poll token api: {}", e);
|
||||
pollinf.interval
|
||||
Ok(token) => return Ok(token),
|
||||
Err(Error::AuthError(AuthError { error, .. }))
|
||||
if error.as_str() == "authorization_pending" =>
|
||||
{
|
||||
interval
|
||||
}
|
||||
Err(Error::AuthError(AuthError { error, .. })) if error.as_str() == "slow_down" => {
|
||||
interval + Duration::from_secs(5)
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,7 +154,7 @@ impl DeviceFlow {
|
||||
|
||||
let json_bytes = resp.into_body().try_concat().await?;
|
||||
let decoded: JsonData =
|
||||
serde_json::from_slice::<JsonErrorOr<_>>(&json_bytes)?.into_result()?;
|
||||
serde_json::from_slice::<AuthErrorOr<_>>(&json_bytes)?.into_result()?;
|
||||
let expires_in = decoded.expires_in.unwrap_or(60 * 60);
|
||||
let pi = PollInformation {
|
||||
user_code: decoded.user_code,
|
||||
@@ -204,17 +188,10 @@ impl DeviceFlow {
|
||||
client: &hyper::Client<C>,
|
||||
device_code: &str,
|
||||
grant_type: &str,
|
||||
expires_at: DateTime<Utc>,
|
||||
flow_delegate: &dyn DeviceFlowDelegate,
|
||||
) -> Result<Option<Token>, PollError>
|
||||
) -> Result<Token, Error>
|
||||
where
|
||||
C: hyper::client::connect::Connect + 'static,
|
||||
{
|
||||
if expires_at <= Utc::now() {
|
||||
flow_delegate.expired(expires_at);
|
||||
return Err(PollError::Expired(expires_at));
|
||||
}
|
||||
|
||||
// We should be ready for a new request
|
||||
let req = form_urlencoded::Serializer::new(String::new())
|
||||
.extend_pairs(&[
|
||||
@@ -229,44 +206,11 @@ impl DeviceFlow {
|
||||
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
|
||||
.body(hyper::Body::from(req))
|
||||
.unwrap(); // TODO: Error checking
|
||||
let res = client
|
||||
.request(request)
|
||||
.await
|
||||
.map_err(PollError::HttpError)?;
|
||||
let body = res
|
||||
.into_body()
|
||||
.try_concat()
|
||||
.await
|
||||
.map_err(PollError::HttpError)?;
|
||||
#[derive(Deserialize)]
|
||||
struct JsonError {
|
||||
error: String,
|
||||
}
|
||||
|
||||
match serde_json::from_slice::<JsonError>(&body) {
|
||||
Err(_) => {} // ignore, move on, it's not an error
|
||||
Ok(res) => {
|
||||
match res.error.as_ref() {
|
||||
"access_denied" => {
|
||||
flow_delegate.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 = serde_json::from_slice(&body).unwrap();
|
||||
let res = client.request(request).await?;
|
||||
let body = res.into_body().try_concat().await?;
|
||||
let mut t = serde_json::from_slice::<AuthErrorOr<Token>>(&body)?.into_result()?;
|
||||
t.set_expiry_absolute();
|
||||
|
||||
Ok(Some(t))
|
||||
Ok(t)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,7 +251,6 @@ mod tests {
|
||||
app_secret,
|
||||
device_code_url: device_code_url.into(),
|
||||
flow_delegate: Box::new(FD),
|
||||
wait_duration: Duration::from_secs(5),
|
||||
grant_type: GOOGLE_GRANT_TYPE.into(),
|
||||
};
|
||||
|
||||
@@ -415,7 +358,7 @@ mod tests {
|
||||
.token(&client, &["https://www.googleapis.com/scope/1"])
|
||||
.await;
|
||||
assert!(res.is_err());
|
||||
assert!(format!("{}", res.unwrap_err()).contains("Access denied by user"));
|
||||
assert!(format!("{}", res.unwrap_err()).contains("access_denied"));
|
||||
_m.assert();
|
||||
}
|
||||
}
|
||||
|
||||
274
src/error.rs
274
src/error.rs
@@ -1,68 +1,142 @@
|
||||
//! Module containing various error types.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::error::Error as StdError;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
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)]
|
||||
pub(crate) struct JsonError {
|
||||
pub error: String,
|
||||
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>,
|
||||
}
|
||||
|
||||
/// A helper type to deserialize either a JsonError or another piece of data.
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub(crate) enum JsonErrorOr<T> {
|
||||
Err(JsonError),
|
||||
Data(T),
|
||||
}
|
||||
|
||||
impl<T> JsonErrorOr<T> {
|
||||
pub(crate) fn into_result(self) -> Result<T, JsonError> {
|
||||
match self {
|
||||
JsonErrorOr::Err(err) => Result::Err(err),
|
||||
JsonErrorOr::Data(value) => Result::Ok(value),
|
||||
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 {}
|
||||
|
||||
/// 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
|
||||
/// 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,
|
||||
/// Indicates that too many attempts failed.
|
||||
TimedOut,
|
||||
/// Other type of error.
|
||||
/// expired_token
|
||||
ExpiredToken,
|
||||
/// other error
|
||||
Other(String),
|
||||
}
|
||||
|
||||
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 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 StdError for PollError {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
match *self {
|
||||
PollError::HttpError(ref e) => Some(e),
|
||||
_ => None,
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,22 +147,13 @@ pub enum Error {
|
||||
/// Indicates connection failure
|
||||
HttpError(hyper::Error),
|
||||
/// The server returned an error.
|
||||
NegativeServerResponse {
|
||||
/// The error code
|
||||
error: String,
|
||||
/// Detailed description
|
||||
error_description: Option<String>,
|
||||
},
|
||||
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),
|
||||
/// A poll error occurred in the DeviceFlow.
|
||||
Poll(PollError),
|
||||
/// An error occurred while refreshing tokens.
|
||||
Refresh(RefreshError),
|
||||
}
|
||||
|
||||
impl From<hyper::Error> for Error {
|
||||
@@ -97,12 +162,9 @@ impl From<hyper::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JsonError> for Error {
|
||||
fn from(value: JsonError) -> Error {
|
||||
Error::NegativeServerResponse {
|
||||
error: value.error,
|
||||
error_description: value.error_description,
|
||||
}
|
||||
impl From<AuthError> for Error {
|
||||
fn from(value: AuthError) -> Error {
|
||||
Error::AuthError(value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,35 +174,21 @@ impl From<serde_json::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RefreshError> for Error {
|
||||
fn from(value: RefreshError) -> Error {
|
||||
Error::Refresh(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::NegativeServerResponse {
|
||||
ref error,
|
||||
ref error_description,
|
||||
} => {
|
||||
error.fmt(f)?;
|
||||
if let Some(ref desc) = *error_description {
|
||||
write!(f, ": {}", desc)?;
|
||||
}
|
||||
"\n".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::JSONError(ref e) => format!(
|
||||
"JSON Error; this might be a bug with unexpected server responses! {}",
|
||||
e
|
||||
)
|
||||
.fmt(f),
|
||||
Error::UserError(ref s) => s.fmt(f),
|
||||
Error::LowLevelError(ref e) => e.fmt(f),
|
||||
Error::Poll(ref pe) => pe.fmt(f),
|
||||
Error::Refresh(ref rr) => format!("{:?}", rr).fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,39 +197,55 @@ impl StdError for Error {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
match *self {
|
||||
Error::HttpError(ref err) => Some(err),
|
||||
Error::LowLevelError(ref err) => Some(err),
|
||||
Error::AuthError(ref err) => Some(err),
|
||||
Error::JSONError(ref err) => Some(err),
|
||||
Error::LowLevelError(ref err) => Some(err),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// All possible outcomes of the refresh flow
|
||||
#[derive(Debug)]
|
||||
pub enum RefreshError {
|
||||
/// Indicates connection failure
|
||||
HttpError(hyper::Error),
|
||||
/// The server did not answer with a new token, providing the server message
|
||||
ServerError(String, Option<String>),
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
impl From<hyper::Error> for RefreshError {
|
||||
fn from(value: hyper::Error) -> Self {
|
||||
RefreshError::HttpError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JsonError> for RefreshError {
|
||||
fn from(value: JsonError) -> Self {
|
||||
RefreshError::ServerError(value.error, value.error_description)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for RefreshError {
|
||||
fn from(_value: serde_json::Error) -> Self {
|
||||
RefreshError::ServerError(
|
||||
"failed to deserialize json token from refresh response".to_owned(),
|
||||
None,
|
||||
)
|
||||
#[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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Refer to the project root for licensing information.
|
||||
//
|
||||
use crate::authenticator_delegate::{DefaultInstalledFlowDelegate, InstalledFlowDelegate};
|
||||
use crate::error::{Error, JsonErrorOr};
|
||||
use crate::error::{AuthErrorOr, Error};
|
||||
use crate::types::{ApplicationSecret, Token};
|
||||
|
||||
use std::convert::AsRef;
|
||||
@@ -201,7 +201,7 @@ impl InstalledFlow {
|
||||
refresh_token,
|
||||
token_type,
|
||||
expires_in,
|
||||
} = serde_json::from_slice::<JsonErrorOr<_>>(&body)?.into_result()?;
|
||||
} = serde_json::from_slice::<AuthErrorOr<_>>(&body)?.into_result()?;
|
||||
let mut token = Token {
|
||||
access_token,
|
||||
refresh_token,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::error::{JsonErrorOr, RefreshError};
|
||||
use crate::error::{AuthErrorOr, Error};
|
||||
use crate::types::{ApplicationSecret, Token};
|
||||
|
||||
use chrono::Utc;
|
||||
@@ -33,7 +33,7 @@ impl RefreshFlow {
|
||||
client: &hyper::Client<C>,
|
||||
client_secret: &ApplicationSecret,
|
||||
refresh_token: &str,
|
||||
) -> Result<Token, RefreshError> {
|
||||
) -> Result<Token, Error> {
|
||||
let req = form_urlencoded::Serializer::new(String::new())
|
||||
.extend_pairs(&[
|
||||
("client_id", client_secret.client_id.as_str()),
|
||||
@@ -46,7 +46,7 @@ impl RefreshFlow {
|
||||
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
|
||||
.unwrap();
|
||||
|
||||
let resp = client.request(request).await?;
|
||||
let body = resp.into_body().try_concat().await?;
|
||||
@@ -62,7 +62,7 @@ impl RefreshFlow {
|
||||
access_token,
|
||||
token_type,
|
||||
expires_in,
|
||||
} = serde_json::from_slice::<JsonErrorOr<_>>(&body)?.into_result()?;
|
||||
} = serde_json::from_slice::<AuthErrorOr<_>>(&body)?.into_result()?;
|
||||
Ok(Token {
|
||||
access_token,
|
||||
token_type,
|
||||
@@ -116,13 +116,16 @@ mod tests {
|
||||
.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"}"#)
|
||||
.with_body(r#"{"error": "invalid_request"}"#)
|
||||
.create();
|
||||
|
||||
let rr = RefreshFlow::refresh_token(&client, &app_secret, refresh_token).await;
|
||||
match rr {
|
||||
Err(RefreshError::ServerError(e, None)) => {
|
||||
assert_eq!(e, "invalid_token");
|
||||
Err(Error::AuthError(auth_error)) => {
|
||||
assert_eq!(
|
||||
auth_error.error,
|
||||
crate::error::AuthErrorCode::InvalidRequest
|
||||
);
|
||||
}
|
||||
_ => panic!(format!("unexpected RefreshResult {:?}", rr)),
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
//! Copyright (c) 2016 Google Inc (lewinb@google.com).
|
||||
//!
|
||||
|
||||
use crate::error::{Error, JsonErrorOr};
|
||||
use crate::error::{AuthErrorOr, Error};
|
||||
use crate::types::Token;
|
||||
|
||||
use std::io;
|
||||
@@ -215,7 +215,7 @@ impl ServiceAccountFlow {
|
||||
access_token,
|
||||
token_type,
|
||||
expires_in,
|
||||
} = serde_json::from_slice::<JsonErrorOr<_>>(&body)?.into_result()?;
|
||||
} = serde_json::from_slice::<AuthErrorOr<_>>(&body)?.into_result()?;
|
||||
let expires_ts = chrono::Utc::now().timestamp() + expires_in;
|
||||
Ok(Token {
|
||||
access_token,
|
||||
|
||||
Reference in New Issue
Block a user