diff --git a/src/discovery.rs b/src/discovery.rs index 382dce7..23a489a 100644 --- a/src/discovery.rs +++ b/src/discovery.rs @@ -11,7 +11,7 @@ use token::Token; pub(crate) fn secure(url: &Url) -> Result<(), Error> { if url.scheme() != "https" { - Err(Error::Insecure) + Err(Error::Insecure(url.clone())) } else { Ok(()) } diff --git a/src/error.rs b/src/error.rs index 3a7d1b0..c25a4e3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,6 +3,9 @@ pub use serde_json::Error as Json; pub use inth_oauth2::ClientError as Oauth; pub use reqwest::Error as Reqwest; +use std::fmt::{Display, Formatter, Result}; +use std::error::Error as ErrorTrait; + macro_rules! from { ($to:ident, $from:ident) => { impl From<$from> for $to { @@ -22,7 +25,7 @@ pub enum Error { Decode(Decode), Validation(Validation), Userinfo(Userinfo), - Insecure, + Insecure(::reqwest::Url), MissingOpenidScope, } @@ -34,13 +37,75 @@ from!(Error, Decode); from!(Error, Validation); from!(Error, Userinfo); +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + use Error::*; + match *self { + Jose(ref err) => Display::fmt(err, f), + Json(ref err) => Display::fmt(err, f), + Oauth(ref err) => Display::fmt(err, f), + Reqwest(ref err) => Display::fmt(err, f), + Decode(ref err) => Display::fmt(err, f), + Validation(ref err) => Display::fmt(err, f), + Userinfo(ref err) => Display::fmt(err, f), + Insecure(ref url) => write!(f, "Url must use HTTPS: '{}'", url), + MissingOpenidScope => write!(f, "") + } + } +} + +impl ErrorTrait for Error { + fn description(&self) -> &str { + use Error::*; + match *self { + Jose(ref err) => err.description(), + Json(ref err) => err.description(), + Oauth(ref err) => err.description(), + Reqwest(ref err) => err.description(), + Decode(ref err) => err.description(), + Validation(ref err) => err.description(), + Userinfo(ref err) => err.description(), + Insecure(_) => "URL must use TLS", + MissingOpenidScope => "Scope must contain Openid", + } + } + + fn cause(&self) -> Option<&ErrorTrait> { + unimplemented!() + } +} + #[derive(Debug)] pub enum Decode { MissingKid, - MissingKey, + MissingKey(String), EmptySet, } +impl ErrorTrait for Decode { + fn description(&self) -> &str { + match self { + MissingKid => "Missing Key Id", + &Decode::MissingKey(_) => "Token key not in key set", + EmptySet => "JWK Set is empty", + } + } + fn cause(&self) -> Option<&ErrorTrait> { + None + } +} + +impl Display for Decode { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + MissingKid => write!(f, "Token Missing Key Id when key set has multiple keys"), + &Decode::MissingKey(ref id) => + write!(f, "Token wants this key id not in the key set: {}", id), + EmptySet => write!(f, "JWK Set is empty!") + } + } +} + #[derive(Debug)] pub enum Validation { Mismatch(Mismatch), @@ -48,30 +113,138 @@ pub enum Validation { Expired(Expiry), } +impl ErrorTrait for Validation { + fn description(&self) -> &str { + use error::Validation::*; + match *self { + Mismatch(ref mm) => { + use error::Mismatch::*; + match *mm { + Authorized {..} => "Client id and token authorized party mismatch", + Issuer {..} => "Config issuer and token issuer mismatch", + Nonce {..} => "Supplied nonce and token nonce mismatch", + } + } + Missing(ref mi) => { + match mi { + Audience => "Token missing Audience", + AuthorizedParty => "Token missing AZP", + AuthTime => "Token missing Auth Time", + Nonce => "Token missing Nonce" + } + } + Expired(ref ex) => { + match *ex { + Expiry::Expires(_) => "Token expired", + Expiry::MaxAge(_) => "Token too old" + } + } + } + } + + fn cause(&self) -> Option<&ErrorTrait> { + None + } +} + +impl Display for Validation { + fn fmt(&self, f: &mut Formatter) -> Result { + use error::Validation::*; + match *self { + Mismatch(ref err) => err.fmt(f), + Missing(ref err) => err.fmt(f), + Expired(ref err) => err.fmt(f), + } + } +} + #[derive(Debug)] pub enum Mismatch { - Audience, - Authorized, - Issuer, - Nonce, + Authorized { expected: String, actual: String }, + Issuer { expected: String, actual: String }, + Nonce { expected: String, actual: String }, +} + +impl Display for Mismatch { + fn fmt(&self, f: &mut Formatter) -> Result { + use error::Mismatch::*; + match *self { + Authorized { ref expected, ref actual } => + write!(f, "Client ID and Token authorized party mismatch: '{}', '{}'", expected, actual), + Issuer { ref expected, ref actual } => + write!(f, "Configured issuer and token issuer mismatch: '{}' '{}'", expected, actual), + Nonce { ref expected, ref actual } => + write!(f, "Given nonce does not match token nonce: '{}', '{}'", expected, actual) + } + } } #[derive(Debug)] pub enum Missing { + Audience, AuthorizedParty, AuthTime, Nonce, } +impl Display for Missing { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Audience => write!(f, "Token missing Audience"), + AuthorizedParty => write!(f, "Token missing AZP"), + AuthTime => write!(f, "Token missing Auth Time"), + Nonce => write!(f, "Token missing Nonce") + } + } +} + #[derive(Debug)] pub enum Expiry { - Expires, - MaxAge, + Expires(::chrono::naive::NaiveDateTime), + MaxAge(::chrono::Duration) +} + +impl Display for Expiry { + fn fmt(&self, f: &mut Formatter) -> Result { + use Expiry::*; + match *self { + Expires(time) => write!(f, "Token expired at: {}", time), + MaxAge(age) => write!(f, "Token is too old: {}", age) + } + } } #[derive(Debug)] pub enum Userinfo { NoUrl, - MismatchIssuer, - MismatchSubject, + MismatchIssuer { expected: String, actual: String }, + MismatchSubject { expected: String, actual: String }, +} + +impl ErrorTrait for Userinfo { + fn description(&self) -> &str { + use error::Userinfo::*; + match *self { + NoUrl => "No url", + MismatchIssuer { .. } => "Mismatch issuer", + MismatchSubject { .. } => "Mismatch subject" + } + } + + fn cause(&self) -> Option<&ErrorTrait> { + None + } +} + +impl Display for Userinfo { + fn fmt(&self, f: &mut Formatter) -> Result { + use error::Userinfo::*; + match *self { + NoUrl => write!(f, "Config has no userinfo url"), + MismatchIssuer { ref expected, ref actual } => + write!(f, "Token and Userinfo Issuers mismatch: '{}', '{}'", expected, actual), + MismatchSubject { ref expected, ref actual } => + write!(f, "Token and Userinfo Subjects mismatch: '{}', '{}'", expected, actual), + } + } } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 7901bc6..5d521d2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,12 +5,12 @@ //! you want. //! ``` //! use oidc; -//! use url; +//! use reqwest; //! use std::default::Default; //! //! let id = "my client".to_string(); //! let secret = "a secret to everybody".to_string(); -//! let redirect = url::Url::parse("https://my-redirect.foo")?; +//! let redirect = reqwest::Url::parse("https://my-redirect.foo")?; //! let issuer = oidc::issuer::google(); //! let client = oidc::discover(id, secret, redirect, issuer)?; //! let scope = "openid"; @@ -28,12 +28,11 @@ //! ``` //! use oidc; //! use reqwest; -//! use url; //! use std::default::Default; //! //! let id = "my client".to_string(); //! let secret = "a secret to everybody".to_string(); -//! let redirect = url::Url::parse("https://my-redirect.foo")?; +//! let redirect = reqwest::Url::parse("https://my-redirect.foo")?; //! let issuer = oidc::issuer::google(); //! let http = reqwest::Client::new()?; //! @@ -220,7 +219,7 @@ impl Client { // If there is more than one key, the token MUST have a key id let key = if self.jwks.keys.len() > 1 { let token_kid = header.registered.key_id.ok_or(Decode::MissingKid)?; - self.jwks.find(&token_kid).ok_or(Decode::MissingKey)? + self.jwks.find(&token_kid).ok_or(Decode::MissingKey(token_kid))? } else { self.jwks.keys.first().as_ref().ok_or(Decode::EmptySet)? }; @@ -276,15 +275,21 @@ impl Client { ) -> Result<(), Error> { let claims = token.payload()?; - if claims.iss != self.config().issuer { - return Err(Validation::Mismatch(Mismatch::Issuer).into()); + + if claims.iss != self.config().issuer { + let expected = self.config().issuer.as_str().to_string(); + let actual = claims.iss.as_str().to_string(); + return Err(Validation::Mismatch(Mismatch::Issuer { expected, actual }).into()); } - if let Some(ref nonce) = nonce { + if let Some(expected) = nonce { match claims.nonce { - Some(ref test) => { - if test != nonce { - return Err(Validation::Mismatch(Mismatch::Nonce).into()); + Some(ref actual) => { + if expected != actual { + let expected = expected.to_string(); + let actual = actual.to_string(); + return Err(Validation::Mismatch( + Mismatch::Nonce { expected, actual }).into()); } } None => return Err(Validation::Missing(Missing::Nonce).into()), @@ -292,7 +297,7 @@ impl Client { } if !claims.aud.contains(&self.oauth.client_id) { - return Err(Validation::Mismatch(Mismatch::Audience).into()); + return Err(Validation::Missing(Missing::Audience).into()); } // By spec, if there are multiple auds, we must have an azp if let SingleOrMultiple::Multiple(_) = claims.aud { @@ -301,9 +306,11 @@ impl Client { } } // If there is an authorized party, it must be our client_id - if let Some(ref azp) = claims.azp { - if azp != &self.oauth.client_id { - return Err(Validation::Mismatch(Mismatch::Authorized).into()); + if let Some(ref actual) = claims.azp { + if actual != &self.oauth.client_id { + let expected = self.oauth.client_id.to_string(); + let actual = actual.to_string(); + return Err(Validation::Mismatch(Mismatch::Authorized { expected, actual }).into()); } } @@ -313,15 +320,15 @@ impl Client { panic!("chrono::Utc::now() can never be before this was written!") } if claims.exp <= now.timestamp() { - return Err(Validation::Expired(Expiry::Expires).into()); + return Err(Validation::Expired(Expiry::Expires(chrono::naive::NaiveDateTime::from_timestamp(claims.exp, 0))).into()); } - if let Some(age) = max_age { + if let Some(max) = max_age { match claims.auth_time { Some(time) => { - // This is not currently risky business. That could change. - if time >= (now - *age).timestamp() { - return Err(error::Validation::Expired(Expiry::MaxAge).into()); + let age = chrono::Duration::seconds(now.timestamp() - time); + if age >= *max { + return Err(error::Validation::Expired(Expiry::MaxAge(age)).into()); } } None => return Err(Validation::Missing(Missing::AuthTime).into()), @@ -336,7 +343,9 @@ impl Client { Some(ref url) => { discovery::secure(&url)?; if url.origin() != self.config().issuer.origin() { - return Err(error::Userinfo::MismatchIssuer.into()); + let expected = self.config().issuer.as_str().to_string(); + let actual = url.as_str().to_string(); + return Err(error::Userinfo::MismatchIssuer { expected, actual }.into()); } let claims = token.id_token.payload()?; let auth_code = token.access_token().to_string(); @@ -344,7 +353,9 @@ impl Client { .header(header::Authorization(header::Bearer { token: auth_code })).send()?; let info: Userinfo = resp.json()?; if claims.sub != info.sub { - return Err(error::Userinfo::MismatchSubject.into()) + let expected = info.sub.clone(); + let actual = claims.sub.clone(); + return Err(error::Userinfo::MismatchSubject { expected, actual }.into()) } Ok(info) } @@ -414,11 +425,11 @@ pub enum Display { impl Display { fn as_str(&self) -> &'static str { - match *self { - Display::Page => "page", - Display::Popup => "popup", - Display::Touch => "touch", - Display::Wap => "wap", + match self { + Page => "page", + Popup => "popup", + Touch => "touch", + Wap => "wap", } } } @@ -435,9 +446,9 @@ impl Prompt { fn as_str(&self) -> &'static str { match self { &Prompt::None => "none", - &Prompt::Login => "login", - &Prompt::Consent => "consent", - &Prompt::SelectAccount => "select_account", + Login => "login", + Consent => "consent", + SelectAccount => "select_account", } } }