diff --git a/Cargo.toml b/Cargo.toml index 5edf915..e75d053 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ edition = "2018" [dependencies] base64 = "0.10" -chrono = "0.4" +chrono = { version = "0.4", features = ["serde"] } http = "0.1" hyper = {version = "0.13.0-alpha.4", features = ["unstable-stream"]} hyper-rustls = "=0.18.0-alpha.2" diff --git a/src/device.rs b/src/device.rs index f3beaa7..31ff37f 100644 --- a/src/device.rs +++ b/src/device.rs @@ -208,9 +208,7 @@ impl DeviceFlow { .unwrap(); // TODO: Error checking 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(t) + Token::from_json(&body) } } diff --git a/src/installed.rs b/src/installed.rs index 5affa37..22034d8 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::{AuthErrorOr, Error}; +use crate::error::Error; use crate::types::{ApplicationSecret, Token}; use std::convert::AsRef; @@ -15,7 +15,6 @@ use std::sync::{Arc, Mutex}; use futures::future::FutureExt; use futures_util::try_stream::TryStreamExt; use hyper::header; -use serde::Deserialize; use tokio::sync::oneshot; use url::form_urlencoded; use url::percent_encoding::{percent_encode, QUERY_ENCODE_SET}; @@ -187,30 +186,7 @@ impl InstalledFlow { let request = Self::request_token(app_secret, authcode, redirect_uri, server_addr); let resp = hyper_client.request(request).await?; let body = resp.into_body().try_concat().await?; - - #[derive(Deserialize)] - struct JSONTokenResponse { - access_token: String, - refresh_token: Option, - token_type: String, - expires_in: Option, - } - - let JSONTokenResponse { - access_token, - refresh_token, - token_type, - expires_in, - } = serde_json::from_slice::>(&body)?.into_result()?; - let mut token = Token { - access_token, - refresh_token, - token_type, - expires_in, - expires_in_timestamp: None, - }; - token.set_expiry_absolute(); - Ok(token) + Token::from_json(&body) } /// Sends the authorization code to the provider in order to obtain access and refresh tokens. diff --git a/src/refresh.rs b/src/refresh.rs index ccf2d4d..059e1da 100644 --- a/src/refresh.rs +++ b/src/refresh.rs @@ -1,10 +1,8 @@ -use crate::error::{AuthErrorOr, Error}; +use crate::error::Error; use crate::types::{ApplicationSecret, Token}; -use chrono::Utc; use futures_util::try_stream::TryStreamExt; use hyper::header; -use serde::Deserialize; use url::form_urlencoded; /// Implements the [OAuth2 Refresh Token Flow](https://developers.google.com/youtube/v3/guides/authentication#devices). @@ -50,26 +48,7 @@ impl RefreshFlow { let resp = client.request(request).await?; let body = resp.into_body().try_concat().await?; - - #[derive(Deserialize)] - struct JsonToken { - access_token: String, - token_type: String, - expires_in: i64, - } - - let JsonToken { - access_token, - token_type, - expires_in, - } = serde_json::from_slice::>(&body)?.into_result()?; - Ok(Token { - access_token, - token_type, - refresh_token: Some(refresh_token.to_string()), - expires_in: None, - expires_in_timestamp: Some(Utc::now().timestamp() + expires_in), - }) + Token::from_json(&body) } } diff --git a/src/service_account.rs b/src/service_account.rs index 89140bd..168edc2 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::{AuthErrorOr, Error}; +use crate::error::Error; use crate::types::Token; use std::io; @@ -202,28 +202,7 @@ impl ServiceAccountFlow { .unwrap(); let response = hyper_client.request(request).await?; let body = response.into_body().try_concat().await?; - - /// This is the schema of the server's response. - #[derive(Deserialize, Debug)] - struct TokenResponse { - access_token: String, - token_type: String, - expires_in: i64, - } - - let TokenResponse { - access_token, - token_type, - expires_in, - } = serde_json::from_slice::>(&body)?.into_result()?; - let expires_ts = chrono::Utc::now().timestamp() + expires_in; - Ok(Token { - access_token, - token_type, - refresh_token: None, - expires_in: Some(expires_in), - expires_in_timestamp: Some(expires_ts), - }) + Token::from_json(&body) } } @@ -232,6 +211,7 @@ mod tests { use super::*; use crate::helper::read_service_account_key; use crate::parse_json; + use chrono::Utc; use hyper_rustls::HttpsConnector; use mockito::mock; @@ -263,8 +243,7 @@ mod tests { "token_type": "Bearer" }); let bad_json_response = serde_json::json!({ - "access_token": "ya29.c.ElouBywiys0LyNaZoLPJcp1Fdi2KjFMxzvYKLXkTdvM-rDfqKlvEq6PiMhGoGHx97t5FAvz3eb_ahdwlBjSStxHtDVQB4ZPRJQ_EOi-iS7PnayahU2S9Jp8S6rk", - "token_type": "Bearer" + "error": "access_denied", }); // Successful path. @@ -285,7 +264,7 @@ mod tests { .await .expect("token failed"); assert!(tok.access_token.contains("ya29.c.ElouBywiys0Ly")); - assert_eq!(Some(3600), tok.expires_in); + assert!(Utc::now() + chrono::Duration::seconds(3600) >= tok.expires_at.unwrap()); _m.assert(); } // Malformed response. diff --git a/src/types.rs b/src/types.rs index 34218b3..70a7aeb 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,4 +1,6 @@ -use chrono::{DateTime, TimeZone, Utc}; +use crate::error::{AuthErrorOr, Error}; + +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; /// Represents a token as returned by OAuth2 servers. @@ -21,50 +23,43 @@ pub struct Token { pub refresh_token: Option, /// 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, - /// 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, + /// The time when the token expires. + pub expires_at: Option>, } impl Token { + pub(crate) fn from_json(json_data: &[u8]) -> Result { + #[derive(Deserialize)] + struct RawToken { + access_token: String, + refresh_token: Option, + token_type: String, + expires_in: Option, + } + + let RawToken { + access_token, + refresh_token, + token_type, + expires_in, + } = serde_json::from_slice::>(json_data)?.into_result()?; + + let expires_at = expires_in + .map(|seconds_from_now| Utc::now() + chrono::Duration::seconds(seconds_from_now)); + + Ok(Token { + access_token, + refresh_token, + token_type, + expires_at, + }) + } + /// Returns true if we are expired. - /// - /// # Panics - /// * if our access_token is unset pub fn expired(&self) -> bool { - if self.access_token.is_empty() { - panic!("called expired() on unset token"); - } - if let Some(expiry_date) = self.expiry_date() { - expiry_date - chrono::Duration::minutes(1) <= Utc::now() - } else { - false - } - } - - /// Returns a DateTime object representing our expiry date. - pub fn expiry_date(&self) -> Option> { - 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 + self.expires_at + .map(|expiration_time| expiration_time - chrono::Duration::minutes(1) <= Utc::now()) + .unwrap_or(false) } }