Files
yup-oauth2/src/types.rs
Glenn Griffin 696577aa01 Accept scopes as a slice of anything that can produce a &str.
Along with the public facing change the implementation has been modified
to no longer clone the scopes instead using the pointer to the scopes
the user provided. This greatly reduces the number of allocations on
each token() call.

Note that this also changes the hashing method used for token storage in
an incompatible way with the previous implementation. The previous
implementation pre-sorted the vector and hashed the contents to make the
result independent of the ordering of the scopes. Instead we now combine
the hash values of each scope together with XOR, thus producing a hash
value that does not depend on order without needing to allocate another
vector and sort.
2019-12-18 08:53:22 -08:00

396 lines
13 KiB
Rust

use chrono::{DateTime, TimeZone, Utc};
use hyper;
use std::error::Error;
use std::fmt;
use std::io;
use std::pin::Pin;
use std::str::FromStr;
use futures::prelude::*;
#[derive(Deserialize, Debug)]
pub struct JsonError {
pub error: String,
pub error_description: Option<String>,
pub error_uri: Option<String>,
}
/// 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),
}
/// 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)
}
}
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 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: Send + Sync {
fn token<'a, T>(
&'a self,
scopes: &'a [T],
) -> Pin<Box<dyn Future<Output = Result<Token, RequestError>> + Send + 'a>>
where
T: AsRef<str> + Sync;
fn api_key(&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 {
/// used when authenticating calls to oauth2 enabled services.
pub 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>,
}
impl Token {
/// 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
}
}
/// 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
}
}
/// Represents either 'installed' or 'web' applications in a json secrets file.
/// See `ConsoleApplicationSecret` for more information
#[derive(Deserialize, Serialize, Clone, Default)]
pub struct ApplicationSecret {
/// The client ID.
pub client_id: String,
/// The client secret.
pub client_secret: String,
/// The token server endpoint URI.
pub token_uri: String,
/// The authorization server endpoint URI.
pub auth_uri: String,
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.
pub client_email: Option<String>,
/// The URL of the public x509 certificate, used to verify the signature on JWTs, such
/// as ID tokens, signed by the authentication provider.
pub auth_provider_x509_cert_url: Option<String>,
/// The URL of the public x509 certificate, used to verify JWTs signed by the client.
pub client_x509_cert_url: Option<String>,
}
/// A type to facilitate reading and writing the json secret file
/// as returned by the [google developer console](https://code.google.com/apis/console)
#[derive(Deserialize, Serialize, Default)]
pub struct ConsoleApplicationSecret {
pub web: Option<ApplicationSecret>,
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\",\
\"client_secret\":\"UqkDJd5RFwnHoiG5x5Rub8SI\",\"token_uri\":\"https://accounts.google.\
com/o/oauth2/token\",\"client_email\":\"\",\"redirect_uris\":[\"urn:ietf:wg:oauth:2.0:\
oob\",\"oob\"],\"client_x509_cert_url\":\"\",\"client_id\":\
\"14070749909-vgip2f1okm7bkvajhi9jugan6126io9v.apps.googleusercontent.com\",\
\"auth_provider_x509_cert_url\":\"https://www.googleapis.com/oauth2/v1/certs\"}}";
#[test]
fn console_secret() {
use serde_json as json;
match json::from_str::<ConsoleApplicationSecret>(SECRET) {
Ok(s) => assert!(s.installed.is_some() && s.web.is_none()),
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());
}
}