impld error and display for all errors
This commit is contained in:
parent
3886d27cba
commit
6240163e5e
|
@ -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(())
|
||||
}
|
||||
|
|
193
src/error.rs
193
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),
|
||||
}
|
||||
}
|
||||
}
|
71
src/lib.rs
71
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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue