Improve Token

Remove expires_in in favor of only having an expires_at DateTime field.
Add a from_json method that deserializes from json data into the
appropriate Token (or Error) and use that consistently throughout the
codebase.
This commit is contained in:
Glenn Griffin
2019-11-22 09:50:31 -08:00
parent d0880d07db
commit 0525926bb2
6 changed files with 46 additions and 119 deletions

View File

@@ -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"

View File

@@ -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::<AuthErrorOr<Token>>(&body)?.into_result()?;
t.set_expiry_absolute();
Ok(t)
Token::from_json(&body)
}
}

View File

@@ -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<String>,
token_type: String,
expires_in: Option<i64>,
}
let JSONTokenResponse {
access_token,
refresh_token,
token_type,
expires_in,
} = serde_json::from_slice::<AuthErrorOr<_>>(&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.

View File

@@ -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::<AuthErrorOr<_>>(&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)
}
}

View File

@@ -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::<AuthErrorOr<_>>(&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.

View File

@@ -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<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>,
/// The time when the token expires.
pub expires_at: Option<DateTime<Utc>>,
}
impl Token {
pub(crate) fn from_json(json_data: &[u8]) -> Result<Token, 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()?;
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<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
self.expires_at
.map(|expiration_time| expiration_time - chrono::Duration::minutes(1) <= Utc::now())
.unwrap_or(false)
}
}