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> {
|
pub(crate) fn secure(url: &Url) -> Result<(), Error> {
|
||||||
if url.scheme() != "https" {
|
if url.scheme() != "https" {
|
||||||
Err(Error::Insecure)
|
Err(Error::Insecure(url.clone()))
|
||||||
} else {
|
} else {
|
||||||
Ok(())
|
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 inth_oauth2::ClientError as Oauth;
|
||||||
pub use reqwest::Error as Reqwest;
|
pub use reqwest::Error as Reqwest;
|
||||||
|
|
||||||
|
use std::fmt::{Display, Formatter, Result};
|
||||||
|
use std::error::Error as ErrorTrait;
|
||||||
|
|
||||||
macro_rules! from {
|
macro_rules! from {
|
||||||
($to:ident, $from:ident) => {
|
($to:ident, $from:ident) => {
|
||||||
impl From<$from> for $to {
|
impl From<$from> for $to {
|
||||||
|
@ -22,7 +25,7 @@ pub enum Error {
|
||||||
Decode(Decode),
|
Decode(Decode),
|
||||||
Validation(Validation),
|
Validation(Validation),
|
||||||
Userinfo(Userinfo),
|
Userinfo(Userinfo),
|
||||||
Insecure,
|
Insecure(::reqwest::Url),
|
||||||
MissingOpenidScope,
|
MissingOpenidScope,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,13 +37,75 @@ from!(Error, Decode);
|
||||||
from!(Error, Validation);
|
from!(Error, Validation);
|
||||||
from!(Error, Userinfo);
|
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)]
|
#[derive(Debug)]
|
||||||
pub enum Decode {
|
pub enum Decode {
|
||||||
MissingKid,
|
MissingKid,
|
||||||
MissingKey,
|
MissingKey(String),
|
||||||
EmptySet,
|
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)]
|
#[derive(Debug)]
|
||||||
pub enum Validation {
|
pub enum Validation {
|
||||||
Mismatch(Mismatch),
|
Mismatch(Mismatch),
|
||||||
|
@ -48,30 +113,138 @@ pub enum Validation {
|
||||||
Expired(Expiry),
|
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)]
|
#[derive(Debug)]
|
||||||
pub enum Mismatch {
|
pub enum Mismatch {
|
||||||
Audience,
|
Authorized { expected: String, actual: String },
|
||||||
Authorized,
|
Issuer { expected: String, actual: String },
|
||||||
Issuer,
|
Nonce { expected: String, actual: String },
|
||||||
Nonce,
|
}
|
||||||
|
|
||||||
|
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)]
|
#[derive(Debug)]
|
||||||
pub enum Missing {
|
pub enum Missing {
|
||||||
|
Audience,
|
||||||
AuthorizedParty,
|
AuthorizedParty,
|
||||||
AuthTime,
|
AuthTime,
|
||||||
Nonce,
|
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)]
|
#[derive(Debug)]
|
||||||
pub enum Expiry {
|
pub enum Expiry {
|
||||||
Expires,
|
Expires(::chrono::naive::NaiveDateTime),
|
||||||
MaxAge,
|
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)]
|
#[derive(Debug)]
|
||||||
pub enum Userinfo {
|
pub enum Userinfo {
|
||||||
NoUrl,
|
NoUrl,
|
||||||
MismatchIssuer,
|
MismatchIssuer { expected: String, actual: String },
|
||||||
MismatchSubject,
|
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.
|
//! you want.
|
||||||
//! ```
|
//! ```
|
||||||
//! use oidc;
|
//! use oidc;
|
||||||
//! use url;
|
//! use reqwest;
|
||||||
//! use std::default::Default;
|
//! use std::default::Default;
|
||||||
//!
|
//!
|
||||||
//! let id = "my client".to_string();
|
//! let id = "my client".to_string();
|
||||||
//! let secret = "a secret to everybody".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 issuer = oidc::issuer::google();
|
||||||
//! let client = oidc::discover(id, secret, redirect, issuer)?;
|
//! let client = oidc::discover(id, secret, redirect, issuer)?;
|
||||||
//! let scope = "openid";
|
//! let scope = "openid";
|
||||||
|
@ -28,12 +28,11 @@
|
||||||
//! ```
|
//! ```
|
||||||
//! use oidc;
|
//! use oidc;
|
||||||
//! use reqwest;
|
//! use reqwest;
|
||||||
//! use url;
|
|
||||||
//! use std::default::Default;
|
//! use std::default::Default;
|
||||||
//!
|
//!
|
||||||
//! let id = "my client".to_string();
|
//! let id = "my client".to_string();
|
||||||
//! let secret = "a secret to everybody".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 issuer = oidc::issuer::google();
|
||||||
//! let http = reqwest::Client::new()?;
|
//! 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
|
// If there is more than one key, the token MUST have a key id
|
||||||
let key = if self.jwks.keys.len() > 1 {
|
let key = if self.jwks.keys.len() > 1 {
|
||||||
let token_kid = header.registered.key_id.ok_or(Decode::MissingKid)?;
|
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 {
|
} else {
|
||||||
self.jwks.keys.first().as_ref().ok_or(Decode::EmptySet)?
|
self.jwks.keys.first().as_ref().ok_or(Decode::EmptySet)?
|
||||||
};
|
};
|
||||||
|
@ -276,15 +275,21 @@ impl Client {
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let claims = token.payload()?;
|
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 {
|
match claims.nonce {
|
||||||
Some(ref test) => {
|
Some(ref actual) => {
|
||||||
if test != nonce {
|
if expected != actual {
|
||||||
return Err(Validation::Mismatch(Mismatch::Nonce).into());
|
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()),
|
None => return Err(Validation::Missing(Missing::Nonce).into()),
|
||||||
|
@ -292,7 +297,7 @@ impl Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !claims.aud.contains(&self.oauth.client_id) {
|
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
|
// By spec, if there are multiple auds, we must have an azp
|
||||||
if let SingleOrMultiple::Multiple(_) = claims.aud {
|
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 there is an authorized party, it must be our client_id
|
||||||
if let Some(ref azp) = claims.azp {
|
if let Some(ref actual) = claims.azp {
|
||||||
if azp != &self.oauth.client_id {
|
if actual != &self.oauth.client_id {
|
||||||
return Err(Validation::Mismatch(Mismatch::Authorized).into());
|
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!")
|
panic!("chrono::Utc::now() can never be before this was written!")
|
||||||
}
|
}
|
||||||
if claims.exp <= now.timestamp() {
|
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 {
|
match claims.auth_time {
|
||||||
Some(time) => {
|
Some(time) => {
|
||||||
// This is not currently risky business. That could change.
|
let age = chrono::Duration::seconds(now.timestamp() - time);
|
||||||
if time >= (now - *age).timestamp() {
|
if age >= *max {
|
||||||
return Err(error::Validation::Expired(Expiry::MaxAge).into());
|
return Err(error::Validation::Expired(Expiry::MaxAge(age)).into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => return Err(Validation::Missing(Missing::AuthTime).into()),
|
None => return Err(Validation::Missing(Missing::AuthTime).into()),
|
||||||
|
@ -336,7 +343,9 @@ impl Client {
|
||||||
Some(ref url) => {
|
Some(ref url) => {
|
||||||
discovery::secure(&url)?;
|
discovery::secure(&url)?;
|
||||||
if url.origin() != self.config().issuer.origin() {
|
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 claims = token.id_token.payload()?;
|
||||||
let auth_code = token.access_token().to_string();
|
let auth_code = token.access_token().to_string();
|
||||||
|
@ -344,7 +353,9 @@ impl Client {
|
||||||
.header(header::Authorization(header::Bearer { token: auth_code })).send()?;
|
.header(header::Authorization(header::Bearer { token: auth_code })).send()?;
|
||||||
let info: Userinfo = resp.json()?;
|
let info: Userinfo = resp.json()?;
|
||||||
if claims.sub != info.sub {
|
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)
|
Ok(info)
|
||||||
}
|
}
|
||||||
|
@ -414,11 +425,11 @@ pub enum Display {
|
||||||
|
|
||||||
impl Display {
|
impl Display {
|
||||||
fn as_str(&self) -> &'static str {
|
fn as_str(&self) -> &'static str {
|
||||||
match *self {
|
match self {
|
||||||
Display::Page => "page",
|
Page => "page",
|
||||||
Display::Popup => "popup",
|
Popup => "popup",
|
||||||
Display::Touch => "touch",
|
Touch => "touch",
|
||||||
Display::Wap => "wap",
|
Wap => "wap",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -435,9 +446,9 @@ impl Prompt {
|
||||||
fn as_str(&self) -> &'static str {
|
fn as_str(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
&Prompt::None => "none",
|
&Prompt::None => "none",
|
||||||
&Prompt::Login => "login",
|
Login => "login",
|
||||||
&Prompt::Consent => "consent",
|
Consent => "consent",
|
||||||
&Prompt::SelectAccount => "select_account",
|
SelectAccount => "select_account",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue