impld error and display for all errors

This commit is contained in:
Matthew Scheirer 2017-09-17 16:47:13 -04:00
parent 3886d27cba
commit 6240163e5e
3 changed files with 225 additions and 41 deletions

View File

@ -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(())
}

View File

@ -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),
}
}
}

View File

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