Saving the generic token - just going to make it always Expiring
This commit is contained in:
parent
d0954586c9
commit
08d7765644
|
@ -12,14 +12,12 @@ readme = "README.md"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base64 = "0.6"
|
base64 = "0.6"
|
||||||
biscuit = { git = "https://github.com/Korvox/biscuit" }
|
biscuit = { git = "https://github.com/Korvox/biscuit" }
|
||||||
error-chain = "0.11"
|
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
inth-oauth2 = "0.13"
|
inth-oauth2 = "0.13"
|
||||||
reqwest = "0.7"
|
reqwest = "0.7"
|
||||||
serde = "1"
|
serde = "1"
|
||||||
serde_derive = "1"
|
serde_derive = "1"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
url = "1.5"
|
|
||||||
url_serde = "0.2"
|
url_serde = "0.2"
|
||||||
validator = "0.6"
|
validator = "0.6"
|
||||||
validator_derive = "0.6"
|
validator_derive = "0.6"
|
||||||
|
|
185
src/client.rs
185
src/client.rs
|
@ -1,16 +1,20 @@
|
||||||
use biscuit::Empty;
|
use biscuit::{Empty, SingleOrMultiple};
|
||||||
use biscuit::jwk::JWKSet;
|
use biscuit::jwa::{self, SignatureAlgorithm};
|
||||||
|
use biscuit::jwk::{AlgorithmParameters, JWKSet};
|
||||||
|
use biscuit::jws::{Compact, Secret};
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
use inth_oauth2;
|
use inth_oauth2;
|
||||||
use url::Url;
|
use reqwest::{self, Url};
|
||||||
use url_serde;
|
use url_serde;
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use discovery::{self, Config, Discovered};
|
use discovery::{self, Config, Discovered};
|
||||||
use error::{ErrorKind, Result};
|
use error::{self, Decode, Error, Expiry, Mismatch, Missing, Validation};
|
||||||
use token::{Expiring, Token};
|
use token::{Claims, Expiring, Token};
|
||||||
|
|
||||||
|
type IdToken = Compact<Claims, Empty>;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct Params {
|
pub struct Params {
|
||||||
|
@ -119,6 +123,17 @@ pub struct Address {
|
||||||
pub country: Option<String>,
|
pub country: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Common pattern in the Client::decode function when dealing with mismatched keys
|
||||||
|
macro_rules! wrong_key {
|
||||||
|
($expected:expr, $actual:expr) => (
|
||||||
|
Err(error::Jose::WrongKeyType {
|
||||||
|
expected: format!("{:?}", $expected),
|
||||||
|
actual: format!("{:?}", $actual)
|
||||||
|
}.into()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
oauth: inth_oauth2::Client<Discovered>,
|
oauth: inth_oauth2::Client<Discovered>,
|
||||||
jwks: JWKSet<Empty>,
|
jwks: JWKSet<Empty>,
|
||||||
|
@ -126,9 +141,10 @@ pub struct Client {
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
/// Constructs a client from an issuer url and client parameters via discovery
|
/// Constructs a client from an issuer url and client parameters via discovery
|
||||||
pub fn discover(issuer: &Url, params: Params) -> Result<Self> {
|
pub fn discover(issuer: Url, params: Params) -> Result<Self, Error> {
|
||||||
let config = discovery::discover(issuer)?;
|
let client = reqwest::Client::new()?;
|
||||||
let jwks = discovery::jwks(&config.jwks_uri)?;
|
let config = discovery::discover(&client, issuer)?;
|
||||||
|
let jwks = discovery::jwks(&client, config.jwks_uri.clone())?;
|
||||||
let provider = Discovered { config };
|
let provider = Discovered { config };
|
||||||
Ok(Self::new(provider, params, jwks))
|
Ok(Self::new(provider, params, jwks))
|
||||||
}
|
}
|
||||||
|
@ -146,6 +162,13 @@ impl Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn request_token(&self,
|
||||||
|
client: &reqwest::Client,
|
||||||
|
auth_code: &str,
|
||||||
|
) -> Result<Token<Expiring>, error::Oauth> {
|
||||||
|
self.oauth.request_token(client, auth_code)
|
||||||
|
}
|
||||||
|
|
||||||
/// A reference to the config document of the provider obtained via discovery
|
/// A reference to the config document of the provider obtained via discovery
|
||||||
pub fn config(&self) -> &Config {
|
pub fn config(&self) -> &Config {
|
||||||
&self.oauth.provider.config
|
&self.oauth.provider.config
|
||||||
|
@ -154,9 +177,9 @@ impl Client {
|
||||||
/// Constructs the auth_url to redirect a client to the provider. Options are... optional. Use
|
/// Constructs the auth_url to redirect a client to the provider. Options are... optional. Use
|
||||||
/// them as needed. Keep the Options struct around for authentication, or at least the nonce
|
/// them as needed. Keep the Options struct around for authentication, or at least the nonce
|
||||||
/// and max_age parameter - we need to verify they stay the same and validate if you used them.
|
/// and max_age parameter - we need to verify they stay the same and validate if you used them.
|
||||||
pub fn auth_url(&self, scope: &str, state: &str, options: &Options) -> Result<Url>{
|
pub fn auth_url(&self, scope: &str, state: &str, options: &Options) -> Result<Url, Error>{
|
||||||
if !scope.contains("openid") {
|
if !scope.contains("openid") {
|
||||||
return Err(ErrorKind::MissingOpenidScope.into())
|
unimplemented!()
|
||||||
}
|
}
|
||||||
let mut url = self.oauth.auth_uri(Some(&scope), Some(state))?;
|
let mut url = self.oauth.auth_uri(Some(&scope), Some(state))?;
|
||||||
{
|
{
|
||||||
|
@ -193,10 +216,146 @@ impl Client {
|
||||||
Ok(url)
|
Ok(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Given an auth_code, request the token, validate it, and if userinfo_endpoint exists
|
/// Given an auth_code and auth options, request the token, decode, and validate it.
|
||||||
/// request that and give the response
|
|
||||||
pub fn authenticate(&self, auth_code: &str, options: &Options
|
pub fn authenticate(&self, auth_code: &str, options: &Options
|
||||||
) -> Result<(Token<Expiring>, Option<Userinfo>)> {
|
) -> Result<Token<Expiring>, Error> {
|
||||||
|
let client = reqwest::Client::new()?;
|
||||||
|
let mut token = self.request_token(&client, auth_code)?;
|
||||||
|
self.decode_token(&mut token.id_token)?;
|
||||||
|
self.validate_token(&token.id_token,
|
||||||
|
options.nonce.as_ref().map(String::as_ref),
|
||||||
|
options.max_age.as_ref())?;
|
||||||
|
Ok(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode_token(&self, token: &mut IdToken) -> Result<(), Error> {
|
||||||
|
// This is an early escape if the token is already decoded
|
||||||
|
token.encoded()?;
|
||||||
|
|
||||||
|
let header = token.unverified_header()?;
|
||||||
|
// 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)?
|
||||||
|
} else {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return wrong_key!(SignatureAlgorithm::default(), alg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let alg = header.registered.algorithm;
|
||||||
|
match key.algorithm {
|
||||||
|
// HMAC
|
||||||
|
AlgorithmParameters::OctectKey { ref value, .. } => {
|
||||||
|
match alg {
|
||||||
|
SignatureAlgorithm::HS256 |
|
||||||
|
SignatureAlgorithm::HS384 |
|
||||||
|
SignatureAlgorithm::HS512 => {
|
||||||
|
*token = token.decode(&Secret::Bytes(value.clone()), alg)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
_ => wrong_key!("HS256 | HS384 | HS512", alg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AlgorithmParameters::RSA(ref params) => {
|
||||||
|
match alg {
|
||||||
|
SignatureAlgorithm::RS256 |
|
||||||
|
SignatureAlgorithm::RS384 |
|
||||||
|
SignatureAlgorithm::RS512 => {
|
||||||
|
let pkcs = Secret::Pkcs {
|
||||||
|
n: params.n.clone(),
|
||||||
|
e: params.e.clone(),
|
||||||
|
};
|
||||||
|
*token = token.decode(&pkcs, alg)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
_ => wrong_key!("RS256 | RS384 | RS512", alg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AlgorithmParameters::EllipticCurve(_) => unimplemented!("No support for EC keys yet"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_token(
|
||||||
|
&self,
|
||||||
|
token: &IdToken,
|
||||||
|
nonce: Option<&str>,
|
||||||
|
max_age: Option<&Duration>
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let claims = token.payload()?;
|
||||||
|
|
||||||
|
if claims.iss != self.config().issuer {
|
||||||
|
return Err(Validation::Mismatch(Mismatch::Issuer).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref nonce) = nonce {
|
||||||
|
match claims.nonce {
|
||||||
|
Some(ref test) => {
|
||||||
|
if test != nonce {
|
||||||
|
return Err(Validation::Mismatch(Mismatch::Nonce).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => return Err(Validation::Missing(Missing::Nonce).into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !claims.aud.contains(&self.oauth.client_id) {
|
||||||
|
return Err(Validation::Mismatch(Mismatch::Audience).into());
|
||||||
|
}
|
||||||
|
// By spec, if there are multiple auds, we must have an azp
|
||||||
|
if let SingleOrMultiple::Multiple(_) = claims.aud {
|
||||||
|
if let None = claims.azp {
|
||||||
|
return Err(Validation::Missing(Missing::AuthorizedParty).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let now = Utc::now();
|
||||||
|
// Now should never be less than the time this code was written!
|
||||||
|
if now.timestamp() < 1504758600 {
|
||||||
|
panic!("chrono::Utc::now() can never be before this was written!")
|
||||||
|
}
|
||||||
|
if claims.exp <= now.timestamp() {
|
||||||
|
return Err(Validation::Expired(Expiry::Expires).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(age) = 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(error::Expiry::MaxAge).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => return Err(Validation::Missing(Missing::AuthTime).into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request_userinfo(&self, client: &reqwest::Client, token: &Token<Expiring>) -> Result<Userinfo, Error> {
|
||||||
|
match self.config().userinfo_endpoint {
|
||||||
|
Some(ref url) => {
|
||||||
|
if url.origin() != self.config().issuer.origin() {
|
||||||
|
return Err(error::Userinfo::MismatchIssuer.into());
|
||||||
|
}
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
None => Err(error::Userinfo::NoUrl.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,11 +1,11 @@
|
||||||
use biscuit::Empty;
|
use biscuit::Empty;
|
||||||
use biscuit::jwk::JWKSet;
|
use biscuit::jwk::JWKSet;
|
||||||
use inth_oauth2::provider::Provider;
|
use inth_oauth2::provider::Provider;
|
||||||
use url::Url;
|
use reqwest::{Client, Url};
|
||||||
use url_serde;
|
use url_serde;
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
use error::{Error, ErrorKind, Result};
|
use error::Error;
|
||||||
use token::{Expiring, Token};
|
use token::{Expiring, Token};
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
|
@ -124,10 +124,24 @@ impl Provider for Discovered {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn discover(issuer: &Url) -> Result<Config> {
|
/// Get the discovery config document from the given issuer url. Errors are either a reqwest error
|
||||||
unimplemented!()
|
/// or an Insecure if the Url isn't https.
|
||||||
|
pub fn discover(client: &Client, issuer: Url) -> Result<Config, Error> {
|
||||||
|
if issuer.scheme() != "https" {
|
||||||
|
return Err(Error::Insecure)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn jwks(url: &Url) -> Result<JWKSet<Empty>> {
|
let mut resp = client.get(issuer)?.send()?;
|
||||||
unimplemented!()
|
resp.json().map_err(Error::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the JWK set from the given Url. Errors are either a reqwest error or an Insecure error if
|
||||||
|
/// the url isn't https.
|
||||||
|
pub fn jwks(client: &Client, url: Url) -> Result<JWKSet<Empty>, Error> {
|
||||||
|
if url.scheme() != "https" {
|
||||||
|
return Err(Error::Insecure)
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut resp = client.get(url)?.send()?;
|
||||||
|
resp.json().map_err(Error::from)
|
||||||
}
|
}
|
57
src/error.rs
57
src/error.rs
|
@ -1,45 +1,76 @@
|
||||||
use biscuit;
|
pub use biscuit::errors::Error as Jose;
|
||||||
use inth_oauth2;
|
pub use serde_json::Error as Json;
|
||||||
|
pub use inth_oauth2::ClientError as Oauth;
|
||||||
|
pub use reqwest::Error as Reqwest;
|
||||||
|
|
||||||
|
macro_rules! from {
|
||||||
|
($to:ident, $from:ident) => {
|
||||||
|
impl From<$from> for $to {
|
||||||
|
fn from(e: $from) -> Self {
|
||||||
|
$to::$from(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
Jose(Jose),
|
||||||
|
Json(Json),
|
||||||
|
Oauth(Oauth),
|
||||||
|
Reqwest(Reqwest),
|
||||||
|
Decode(Decode),
|
||||||
|
Validation(Validation),
|
||||||
|
Userinfo(Userinfo),
|
||||||
|
Insecure,
|
||||||
|
}
|
||||||
|
|
||||||
|
from!(Error, Jose);
|
||||||
|
from!(Error, Json);
|
||||||
|
from!(Error, Oauth);
|
||||||
|
from!(Error, Reqwest);
|
||||||
|
from!(Error, Decode);
|
||||||
|
from!(Error, Validation);
|
||||||
|
from!(Error, Userinfo);
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum Decode {
|
pub enum Decode {
|
||||||
MissingKid,
|
MissingKid,
|
||||||
MissingKey,
|
MissingKey,
|
||||||
EmptySet,
|
EmptySet,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum Validation {
|
pub enum Validation {
|
||||||
Mismatch(Mismatch),
|
Mismatch(Mismatch),
|
||||||
Missing(Missing),
|
Missing(Missing),
|
||||||
Expired(Expiry),
|
Expired(Expiry),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum Mismatch {
|
pub enum Mismatch {
|
||||||
Audience,
|
Audience,
|
||||||
Authorized,
|
Authorized,
|
||||||
Issuer,
|
Issuer,
|
||||||
Nonce,
|
Nonce,
|
||||||
Subject,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum Missing {
|
pub enum Missing {
|
||||||
AuthorizedParty,
|
AuthorizedParty,
|
||||||
AuthTime,
|
AuthTime,
|
||||||
Nonce,
|
Nonce,
|
||||||
OpenidScope,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum Expiry {
|
pub enum Expiry {
|
||||||
Expires,
|
Expires,
|
||||||
IssuedAt,
|
|
||||||
MaxAge,
|
MaxAge,
|
||||||
}
|
}
|
||||||
|
|
||||||
error_chain! {
|
#[derive(Debug)]
|
||||||
foreign_links {
|
pub enum Userinfo {
|
||||||
Oauth(inth_oauth2::ClientError);
|
NoUrl,
|
||||||
Biscuit(biscuit::errors::Error);
|
MismatchSubject,
|
||||||
}
|
|
||||||
|
|
||||||
errors {
|
|
||||||
MissingOpenidScope
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
use url::Url;
|
use reqwest::Url;
|
||||||
|
|
||||||
// TODO these should all be const, or even better, static Urls...
|
// TODO these should all be const, or even better, static Urls...
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
extern crate base64;
|
extern crate base64;
|
||||||
extern crate biscuit;
|
extern crate biscuit;
|
||||||
#[macro_use]
|
|
||||||
extern crate error_chain;
|
|
||||||
extern crate chrono;
|
extern crate chrono;
|
||||||
extern crate inth_oauth2;
|
extern crate inth_oauth2;
|
||||||
extern crate reqwest;
|
extern crate reqwest;
|
||||||
|
@ -9,7 +7,6 @@ extern crate serde;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde_derive;
|
extern crate serde_derive;
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
extern crate url;
|
|
||||||
extern crate url_serde;
|
extern crate url_serde;
|
||||||
extern crate validator;
|
extern crate validator;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
|
@ -19,3 +16,5 @@ pub mod client;
|
||||||
pub mod discovery;
|
pub mod discovery;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod token;
|
pub mod token;
|
||||||
|
|
||||||
|
pub use error::Error;
|
|
@ -3,8 +3,8 @@ use biscuit::{CompactJson, Empty, SingleOrMultiple};
|
||||||
use biscuit::jws::Compact;
|
use biscuit::jws::Compact;
|
||||||
use inth_oauth2::client::response::{FromResponse, ParseError};
|
use inth_oauth2::client::response::{FromResponse, ParseError};
|
||||||
use inth_oauth2::token::{self, Bearer, Lifetime};
|
use inth_oauth2::token::{self, Bearer, Lifetime};
|
||||||
|
use reqwest::Url;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use url::Url;
|
|
||||||
use url_serde;
|
use url_serde;
|
||||||
|
|
||||||
/// Rexported lifetime token types from oauth
|
/// Rexported lifetime token types from oauth
|
||||||
|
|
Loading…
Reference in New Issue