From d0880d07dbb2800e7119ad71f414ba8044c62a35 Mon Sep 17 00:00:00 2001 From: Glenn Griffin Date: Thu, 21 Nov 2019 16:49:10 -0800 Subject: [PATCH] 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. --- src/authenticator.rs | 55 +------ src/authenticator_delegate.rs | 54 ------- src/device.rs | 111 ++++---------- src/error.rs | 274 +++++++++++++++++++++------------- src/installed.rs | 4 +- src/refresh.rs | 17 ++- src/service_account.rs | 4 +- 7 files changed, 214 insertions(+), 305 deletions(-) diff --git a/src/authenticator.rs b/src/authenticator.rs index af43e14..e639f99 100644 --- a/src/authenticator.rs +++ b/src/authenticator.rs @@ -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 { hyper_client: hyper::Client, - auth_delegate: Box, 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 { hyper_client_builder: C, - auth_delegate: Box, 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 AuthenticatorBuilder { async fn common_build( hyper_client_builder: C, storage_type: StorageType, - auth_delegate: Box, auth_flow: AuthFlow, ) -> io::Result> where @@ -190,7 +172,6 @@ impl AuthenticatorBuilder { Ok(Authenticator { hyper_client, storage, - auth_delegate, auth_flow, }) } @@ -198,7 +179,6 @@ impl AuthenticatorBuilder { fn with_auth_flow(auth_flow: F) -> AuthenticatorBuilder { AuthenticatorBuilder { hyper_client_builder: DefaultHyperClient, - auth_delegate: Box::new(DefaultAuthenticatorDelegate), storage_type: StorageType::Memory, auth_flow, } @@ -211,7 +191,6 @@ impl AuthenticatorBuilder { ) -> AuthenticatorBuilder, 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 AuthenticatorBuilder { ..self } } - - /// Use the provided authenticator delegate. - pub fn auth_delegate( - self, - auth_delegate: Box, - ) -> AuthenticatorBuilder { - AuthenticatorBuilder { - auth_delegate, - ..self - } - } } /// ## Methods available when building a device flow Authenticator. @@ -245,7 +213,6 @@ impl AuthenticatorBuilder { /// 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 AuthenticatorBuilder { } } - /// 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>) -> Self { AuthenticatorBuilder { @@ -305,7 +261,6 @@ impl AuthenticatorBuilder { Self::common_build( self.hyper_client_builder, self.storage_type, - self.auth_delegate, AuthFlow::DeviceFlow(self.auth_flow), ) .await @@ -348,7 +303,6 @@ impl AuthenticatorBuilder { Self::common_build( self.hyper_client_builder, self.storage_type, - self.auth_delegate, AuthFlow::InstalledFlow(self.auth_flow), ) .await @@ -389,7 +343,6 @@ impl AuthenticatorBuilder { Self::common_build( self.hyper_client_builder, self.storage_type, - self.auth_delegate, AuthFlow::ServiceAccountFlow(service_account_auth_flow), ) .await diff --git a/src/authenticator_delegate.rs b/src/authenticator_delegate.rs index 9907178..9374c1d 100644 --- a/src/authenticator_delegate.rs +++ b/src/authenticator_delegate.rs @@ -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) {} - - /// 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 } } -/// 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; diff --git a/src/device.rs b/src/device.rs index 1ab9efb..f3beaa7 100644 --- a/src/device.rs +++ b/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, - 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( @@ -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::>(&json_bytes)?.into_result()?; + serde_json::from_slice::>(&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, device_code: &str, grant_type: &str, - expires_at: DateTime, - flow_delegate: &dyn DeviceFlowDelegate, - ) -> Result, PollError> + ) -> Result 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::(&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::>(&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(); } } diff --git a/src/error.rs b/src/error.rs index 10b262f..0e132f5 100644 --- a/src/error.rs +++ b/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, + /// A URI identifying a human-readable web page with information about the error. pub error_uri: Option, } -/// A helper type to deserialize either a JsonError or another piece of data. -#[derive(Deserialize, Debug)] -#[serde(untagged)] -pub(crate) enum JsonErrorOr { - Err(JsonError), - Data(T), -} - -impl JsonErrorOr { - pub(crate) fn into_result(self) -> Result { - 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), - /// 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>) -> 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 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(deserializer: D) -> Result + 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(self, value: String) -> Result { + Ok(value.into()) + } + fn visit_str(self, value: &str) -> Result { + 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 { + AuthError(AuthError), + Data(T), +} + +impl AuthErrorOr { + pub(crate) fn into_result(self) -> Result { + 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, - }, + 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 for Error { @@ -97,12 +162,9 @@ impl From for Error { } } -impl From for Error { - fn from(value: JsonError) -> Error { - Error::NegativeServerResponse { - error: value.error, - error_description: value.error_description, - } +impl From for Error { + fn from(value: AuthError) -> Error { + Error::AuthError(value) } } @@ -112,35 +174,21 @@ impl From for Error { } } -impl From 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), -} +#[cfg(test)] +mod tests { + use super::*; -impl From for RefreshError { - fn from(value: hyper::Error) -> Self { - RefreshError::HttpError(value) - } -} - -impl From for RefreshError { - fn from(value: JsonError) -> Self { - RefreshError::ServerError(value.error, value.error_description) - } -} - -impl From 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() + ); } } diff --git a/src/installed.rs b/src/installed.rs index 54ae4de..5affa37 100644 --- a/src/installed.rs +++ b/src/installed.rs @@ -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::>(&body)?.into_result()?; + } = serde_json::from_slice::>(&body)?.into_result()?; let mut token = Token { access_token, refresh_token, diff --git a/src/refresh.rs b/src/refresh.rs index e312287..ccf2d4d 100644 --- a/src/refresh.rs +++ b/src/refresh.rs @@ -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, client_secret: &ApplicationSecret, refresh_token: &str, - ) -> Result { + ) -> Result { 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::>(&body)?.into_result()?; + } = serde_json::from_slice::>(&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)), } diff --git a/src/service_account.rs b/src/service_account.rs index 71ac5c2..89140bd 100644 --- a/src/service_account.rs +++ b/src/service_account.rs @@ -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::>(&body)?.into_result()?; + } = serde_json::from_slice::>(&body)?.into_result()?; let expires_ts = chrono::Utc::now().timestamp() + expires_in; Ok(Token { access_token,