More documentation

This commit is contained in:
Matthew Scheirer 2017-09-19 13:32:01 -04:00
parent 65ca0b4fd3
commit b3012f1940
3 changed files with 66 additions and 22 deletions

View File

@ -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)] #[derive(Debug, Deserialize, Serialize)]
pub struct Config { pub struct Config {
#[serde(with = "url_serde")] pub issuer: Url, #[serde(with = "url_serde")] pub issuer: Url,

View File

@ -1,7 +1,7 @@
pub use biscuit::errors::Error as Jose; pub use biscuit::errors::Error as Jose;
pub use serde_json::Error as Json; 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 Http;
pub use reqwest::UrlError as Url; pub use reqwest::UrlError as Url;
use std::fmt::{Display, Formatter, Result}; use std::fmt::{Display, Formatter, Result};
@ -22,7 +22,7 @@ pub enum Error {
Jose(Jose), Jose(Jose),
Json(Json), Json(Json),
Oauth(Oauth), Oauth(Oauth),
Reqwest(Reqwest), Http(Http),
Url(Url), Url(Url),
Decode(Decode), Decode(Decode),
Validation(Validation), Validation(Validation),
@ -34,7 +34,7 @@ pub enum Error {
from!(Error, Jose); from!(Error, Jose);
from!(Error, Json); from!(Error, Json);
from!(Error, Oauth); from!(Error, Oauth);
from!(Error, Reqwest); from!(Error, Http);
from!(Error, Url); from!(Error, Url);
from!(Error, Decode); from!(Error, Decode);
from!(Error, Validation); from!(Error, Validation);
@ -47,7 +47,7 @@ impl Display for Error {
Jose(ref err) => Display::fmt(err, f), Jose(ref err) => Display::fmt(err, f),
Json(ref err) => Display::fmt(err, f), Json(ref err) => Display::fmt(err, f),
Oauth(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), Url(ref err) => Display::fmt(err, f),
Decode(ref err) => Display::fmt(err, f), Decode(ref err) => Display::fmt(err, f),
Validation(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(), Jose(ref err) => err.description(),
Json(ref err) => err.description(), Json(ref err) => err.description(),
Oauth(ref err) => err.description(), Oauth(ref err) => err.description(),
Reqwest(ref err) => err.description(), Http(ref err) => err.description(),
Url(ref err) => err.description(), Url(ref err) => err.description(),
Decode(ref err) => err.description(), Decode(ref err) => err.description(),
Validation(ref err) => err.description(), Validation(ref err) => err.description(),
@ -81,7 +81,7 @@ impl ErrorTrait for Error {
Jose(ref err) => Some(err), Jose(ref err) => Some(err),
Json(ref err) => Some(err), Json(ref err) => Some(err),
Oauth(ref err) => Some(err), Oauth(ref err) => Some(err),
Reqwest(ref err) => Some(err), Http(ref err) => Some(err),
Url(ref err) => Some(err), Url(ref err) => Some(err),
Decode(_) => None, Decode(_) => None,
Validation(_) => None, Validation(_) => None,
@ -117,7 +117,7 @@ impl Display for Decode {
fn fmt(&self, f: &mut Formatter) -> Result { fn fmt(&self, f: &mut Formatter) -> Result {
use Decode::*; use Decode::*;
match *self { 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) => MissingKey(ref id) =>
write!(f, "Token wants this key id not in the key set: {}", id), write!(f, "Token wants this key id not in the key set: {}", id),
EmptySet => write!(f, "JWK Set is empty!") EmptySet => write!(f, "JWK Set is empty!")
@ -139,9 +139,9 @@ impl ErrorTrait for Validation {
Mismatch(ref mm) => { Mismatch(ref mm) => {
use error::Mismatch::*; use error::Mismatch::*;
match *mm { match *mm {
Authorized {..} => "Client id and token authorized party mismatch", AuthorizedParty {..} => "Client id and token authorized party mismatch",
Issuer {..} => "Config issuer and token issuer mismatch", Issuer {..} => "Config issuer and token issuer mismatch",
Nonce {..} => "Supplied nonce and token nonce mismatch", Nonce {..} => "Supplied nonce and token nonce mismatch",
} }
} }
Missing(ref mi) => { Missing(ref mi) => {
@ -180,7 +180,7 @@ impl Display for Validation {
#[derive(Debug)] #[derive(Debug)]
pub enum Mismatch { pub enum Mismatch {
Authorized { expected: String, actual: String }, AuthorizedParty { expected: String, actual: String },
Issuer { expected: String, actual: String }, Issuer { expected: String, actual: String },
Nonce { expected: String, actual: String }, Nonce { expected: String, actual: String },
} }
@ -189,7 +189,7 @@ impl Display for Mismatch {
fn fmt(&self, f: &mut Formatter) -> Result { fn fmt(&self, f: &mut Formatter) -> Result {
use error::Mismatch::*; use error::Mismatch::*;
match *self { match *self {
Authorized { ref expected, ref actual } => AuthorizedParty { ref expected, ref actual } =>
write!(f, "Client ID and Token authorized party mismatch: '{}', '{}'", expected, actual), write!(f, "Client ID and Token authorized party mismatch: '{}', '{}'", expected, actual),
Issuer { ref expected, ref actual } => Issuer { ref expected, ref actual } =>
write!(f, "Configured issuer and token issuer mismatch: '{}' '{}'", expected, actual), write!(f, "Configured issuer and token issuer mismatch: '{}' '{}'", expected, actual),

View File

@ -145,6 +145,7 @@ impl Client {
} }
} }
/// Passthrough to the inth_oauth2::client's request token.
pub fn request_token(&self, pub fn request_token(&self,
client: &reqwest::Client, client: &reqwest::Client,
auth_code: &str, auth_code: &str,
@ -219,9 +220,19 @@ impl Client {
Ok(token) 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> { pub fn decode_token(&self, token: &mut IdToken) -> Result<(), Error> {
// This is an early escape if the token is already decoded // This is an early return if the token is already decoded
token.encoded()?; if let Compact::Decoded { .. } = *token {
return Ok(())
}
let header = token.unverified_header()?; let header = token.unverified_header()?;
// 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
@ -229,13 +240,15 @@ impl Client {
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(token_kid))? self.jwks.find(&token_kid).ok_or(Decode::MissingKey(token_kid))?
} else { } 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)? self.jwks.keys.first().as_ref().ok_or(Decode::EmptySet)?
}; };
if let Some(alg) = key.common.algorithm.as_ref() { if let Some(alg) = key.common.algorithm.as_ref() {
if let &jwa::Algorithm::Signature(alg) = alg { if let &jwa::Algorithm::Signature(sig) = alg {
if header.registered.algorithm != alg { if header.registered.algorithm != sig {
return wrong_key!(alg, header.registered.algorithm); return wrong_key!(sig, header.registered.algorithm);
} }
} else { } else {
return wrong_key!(SignatureAlgorithm::default(), alg); 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( pub fn validate_token(
&self, &self,
token: &IdToken, token: &IdToken,
@ -283,15 +309,14 @@ impl Client {
) -> Result<(), Error> { ) -> Result<(), Error> {
let claims = token.payload()?; let claims = token.payload()?;
if claims.iss != self.config().issuer { if claims.iss != self.config().issuer {
let expected = self.config().issuer.as_str().to_string(); let expected = self.config().issuer.as_str().to_string();
let actual = claims.iss.as_str().to_string(); let actual = claims.iss.as_str().to_string();
return Err(Validation::Mismatch(Mismatch::Issuer { expected, actual }).into()); return Err(Validation::Mismatch(Mismatch::Issuer { expected, actual }).into());
} }
if let Some(expected) = nonce { match nonce {
match claims.nonce { Some(expected) => match claims.nonce {
Some(ref actual) => { Some(ref actual) => {
if expected != actual { if expected != actual {
let expected = expected.to_string(); let expected = expected.to_string();
@ -302,6 +327,9 @@ impl Client {
} }
None => return Err(Validation::Missing(Missing::Nonce).into()), 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) { if !claims.aud.contains(&self.oauth.client_id) {
@ -318,7 +346,9 @@ impl Client {
if actual != &self.oauth.client_id { if actual != &self.oauth.client_id {
let expected = self.oauth.client_id.to_string(); let expected = self.oauth.client_id.to_string();
let actual = actual.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(()) 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 pub fn request_userinfo(&self, client: &reqwest::Client, token: &Token
) -> Result<Userinfo, Error> { ) -> Result<Userinfo, Error> {
match self.config().userinfo_endpoint { match self.config().userinfo_endpoint {
@ -361,7 +401,8 @@ impl Client {
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();
let mut resp = client.get(url.clone())? 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()?; let info: Userinfo = resp.json()?;
if claims.sub != info.sub { if claims.sub != info.sub {
let expected = info.sub.clone(); let expected = info.sub.clone();
@ -423,6 +464,7 @@ pub struct Userinfo {
#[serde(default)] pub updated_at: Option<i64>, #[serde(default)] pub updated_at: Option<i64>,
} }
/// The four values for the preferred display parameter in the Options. See spec for details.
pub enum Display { pub enum Display {
Page, Page,
Popup, 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)] #[derive(PartialEq, Eq, Hash)]
pub enum Prompt { pub enum Prompt {
None, None,