From b3012f19404398942b4c5851c46101d9740f665c Mon Sep 17 00:00:00 2001 From: Matthew Scheirer Date: Tue, 19 Sep 2017 13:32:01 -0400 Subject: [PATCH] More documentation --- src/discovery.rs | 1 + src/error.rs | 24 +++++++++--------- src/lib.rs | 63 ++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 66 insertions(+), 22 deletions(-) diff --git a/src/discovery.rs b/src/discovery.rs index b63f9f1..2332687 100644 --- a/src/discovery.rs +++ b/src/discovery.rs @@ -16,6 +16,7 @@ pub(crate) fn secure(url: &Url) -> Result<(), Error> { } } +// TODO I wish we could impl default for this, but you cannot have a config without issuer etc #[derive(Debug, Deserialize, Serialize)] pub struct Config { #[serde(with = "url_serde")] pub issuer: Url, diff --git a/src/error.rs b/src/error.rs index 4b1686c..49cfaf1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,7 +1,7 @@ pub use biscuit::errors::Error as Jose; pub use serde_json::Error as Json; pub use inth_oauth2::ClientError as Oauth; -pub use reqwest::Error as Reqwest; +pub use reqwest::Error as Http; pub use reqwest::UrlError as Url; use std::fmt::{Display, Formatter, Result}; @@ -22,7 +22,7 @@ pub enum Error { Jose(Jose), Json(Json), Oauth(Oauth), - Reqwest(Reqwest), + Http(Http), Url(Url), Decode(Decode), Validation(Validation), @@ -34,7 +34,7 @@ pub enum Error { from!(Error, Jose); from!(Error, Json); from!(Error, Oauth); -from!(Error, Reqwest); +from!(Error, Http); from!(Error, Url); from!(Error, Decode); from!(Error, Validation); @@ -47,7 +47,7 @@ impl Display for Error { 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), + Http(ref err) => Display::fmt(err, f), Url(ref err) => Display::fmt(err, f), Decode(ref err) => Display::fmt(err, f), Validation(ref err) => Display::fmt(err, f), @@ -65,7 +65,7 @@ impl ErrorTrait for Error { Jose(ref err) => err.description(), Json(ref err) => err.description(), Oauth(ref err) => err.description(), - Reqwest(ref err) => err.description(), + Http(ref err) => err.description(), Url(ref err) => err.description(), Decode(ref err) => err.description(), Validation(ref err) => err.description(), @@ -81,7 +81,7 @@ impl ErrorTrait for Error { Jose(ref err) => Some(err), Json(ref err) => Some(err), Oauth(ref err) => Some(err), - Reqwest(ref err) => Some(err), + Http(ref err) => Some(err), Url(ref err) => Some(err), Decode(_) => None, Validation(_) => None, @@ -117,7 +117,7 @@ impl Display for Decode { fn fmt(&self, f: &mut Formatter) -> Result { use Decode::*; match *self { - MissingKid => write!(f, "Token Missing Key Id when key set has multiple keys"), + MissingKid => write!(f, "Token Missing a Key Id when the key set has multiple keys"), MissingKey(ref id) => write!(f, "Token wants this key id not in the key set: {}", id), EmptySet => write!(f, "JWK Set is empty!") @@ -139,9 +139,9 @@ impl ErrorTrait for Validation { 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", + AuthorizedParty {..} => "Client id and token authorized party mismatch", + Issuer {..} => "Config issuer and token issuer mismatch", + Nonce {..} => "Supplied nonce and token nonce mismatch", } } Missing(ref mi) => { @@ -180,7 +180,7 @@ impl Display for Validation { #[derive(Debug)] pub enum Mismatch { - Authorized { expected: String, actual: String }, + AuthorizedParty { expected: String, actual: String }, Issuer { expected: String, actual: String }, Nonce { expected: String, actual: String }, } @@ -189,7 +189,7 @@ impl Display for Mismatch { fn fmt(&self, f: &mut Formatter) -> Result { use error::Mismatch::*; match *self { - Authorized { ref expected, ref actual } => + AuthorizedParty { 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), diff --git a/src/lib.rs b/src/lib.rs index 400f5aa..1ec06cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -145,6 +145,7 @@ impl Client { } } + /// Passthrough to the inth_oauth2::client's request token. pub fn request_token(&self, client: &reqwest::Client, auth_code: &str, @@ -219,9 +220,19 @@ impl Client { Ok(token) } + /// Mutates a Compact::encoded Token to Compact::decoded. Errors are: + /// + /// - Decode::MissingKid if the keyset has multiple keys but the key id on the token is missing + /// - Decode::MissingKey if the given key id is not in the key set + /// - Decode::EmptySet if the keyset is empty + /// - Jose::WrongKeyType if the alg of the key and the alg in the token header mismatch + /// - Jose::WrongKeyType if the specified key alg isn't a signature algorithm + /// - Jose error if decoding fails pub fn decode_token(&self, token: &mut IdToken) -> Result<(), Error> { - // This is an early escape if the token is already decoded - token.encoded()?; + // This is an early return if the token is already decoded + if let Compact::Decoded { .. } = *token { + return Ok(()) + } let header = token.unverified_header()?; // If there is more than one key, the token MUST have a key id @@ -229,13 +240,15 @@ impl Client { let token_kid = header.registered.key_id.ok_or(Decode::MissingKid)?; self.jwks.find(&token_kid).ok_or(Decode::MissingKey(token_kid))? } else { + // TODO We would want to verify the keyset is >1 in the constructor + // rather than every decode call, but we can't return an error in new(). self.jwks.keys.first().as_ref().ok_or(Decode::EmptySet)? }; if let Some(alg) = key.common.algorithm.as_ref() { - if let &jwa::Algorithm::Signature(alg) = alg { - if header.registered.algorithm != alg { - return wrong_key!(alg, header.registered.algorithm); + if let &jwa::Algorithm::Signature(sig) = alg { + if header.registered.algorithm != sig { + return wrong_key!(sig, header.registered.algorithm); } } else { return wrong_key!(SignatureAlgorithm::default(), alg); @@ -275,6 +288,19 @@ impl Client { } } + /// Validate a decoded token. If you don't get an error, its valid! Nonce and max_age come from + /// your auth_uri options. Errors are: + /// + /// - Jose Error if the Token isn't decoded + /// - Validation::Mismatch::Issuer if the provider issuer and token issuer mismatch + /// - Validation::Mismatch::Nonce if a given nonce and the token nonce mismatch + /// - Validation::Missing::Nonce if either the token or args has a nonce and the other does not + /// - Validation::Missing::Audience if the token aud doesn't contain the client id + /// - Validation::Missing::AuthorizedParty if there are multiple audiences and azp is missing + /// - Validation::Mismatch::AuthorizedParty if the azp is not the client_id + /// - Validation::Expired::Expires if the current time is past the expiration time + /// - Validation::Expired::MaxAge is the token is older than the provided max_age + /// - Validation::Missing::Authtime if a max_age was given and the token has no auth time pub fn validate_token( &self, token: &IdToken, @@ -283,15 +309,14 @@ impl Client { ) -> Result<(), Error> { let claims = token.payload()?; - 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(expected) = nonce { - match claims.nonce { + match nonce { + Some(expected) => match claims.nonce { Some(ref actual) => { if expected != actual { let expected = expected.to_string(); @@ -302,6 +327,9 @@ impl Client { } None => return Err(Validation::Missing(Missing::Nonce).into()), } + None => if claims.nonce.is_some() { + return Err(Validation::Missing(Missing::Nonce).into()) + } } if !claims.aud.contains(&self.oauth.client_id) { @@ -318,7 +346,9 @@ impl Client { 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()); + return Err(Validation::Mismatch(Mismatch::AuthorizedParty { + expected, actual + }).into()); } } @@ -348,6 +378,16 @@ impl Client { Ok(()) } + /// Get a userinfo json document for a given token at the provider's userinfo endpoint. + /// Errors are: + /// + /// - Userinfo::NoUrl if this provider doesn't have a userinfo endpoint + /// - Error::Insecure if the userinfo url is not https + /// - Userinfo::MismatchIssuer if the userinfo origin does not match the provider's issuer + /// - Error::Jose if the token is not decoded + /// - Error::Http if something goes wrong getting the document + /// - Error::Json if the response is not a valid Userinfo document + /// - Userinfo::MismatchSubject if the returned userinfo document and tokens subject mismatch pub fn request_userinfo(&self, client: &reqwest::Client, token: &Token ) -> Result { match self.config().userinfo_endpoint { @@ -361,7 +401,8 @@ impl Client { let claims = token.id_token.payload()?; let auth_code = token.access_token().to_string(); let mut resp = client.get(url.clone())? - .header(header::Authorization(header::Bearer { token: auth_code })).send()?; + .header(header::Authorization(header::Bearer { token: auth_code })) + .send()?; let info: Userinfo = resp.json()?; if claims.sub != info.sub { let expected = info.sub.clone(); @@ -423,6 +464,7 @@ pub struct Userinfo { #[serde(default)] pub updated_at: Option, } +/// The four values for the preferred display parameter in the Options. See spec for details. pub enum Display { Page, Popup, @@ -442,6 +484,7 @@ impl Display { } } +/// The four possible values for the prompt parameter set in Options. See spec for details. #[derive(PartialEq, Eq, Hash)] pub enum Prompt { None,