Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add seperate validate token method for microsoft provider #9

Merged
merged 3 commits into from
Oct 3, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ readme = 'README.md'
repository = 'https://github.com/kilork/openid'

[features]
uma2 = []
default = []
microsoft = []
uma2 = []

[dependencies]
lazy_static = '1.4'
Expand Down
155 changes: 89 additions & 66 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,90 @@ impl<C: CompactJson + Claims> Client<Discovered, C> {
}
}

pub fn validate_token_issuer<C: Claims>(claims: &C, config: &Config) -> Result<(), Error> {
if claims.iss() != &config.issuer {
let expected = config.issuer.as_str().to_string();
let actual = claims.iss().as_str().to_string();
return Err(Validation::Mismatch(Mismatch::Issuer { expected, actual }).into());
}

Ok(())
}

pub fn validate_token_nonce<C: Claims>(claims: &C, nonce: Option<&str>) -> Result<(), Error> {
match nonce {
Some(expected) => match claims.nonce() {
Some(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()),
},
None => {
if claims.nonce().is_some() {
return Err(Validation::Missing(Missing::Nonce).into());
}
}
}

Ok(())
}

pub fn validate_token_aud<C: Claims>(claims: &C, client_id: &str) -> Result<(), Error> {
if !claims.aud().contains(client_id) {
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() {
if claims.azp().is_none() {
return Err(Validation::Missing(Missing::AuthorizedParty).into());
}
}
// If there is an authorized party, it must be our client_id
if let Some(actual) = claims.azp() {
if actual != client_id {
let expected = client_id.to_string();
let actual = actual.to_string();
return Err(
Validation::Mismatch(Mismatch::AuthorizedParty { expected, actual }).into(),
);
}
}

Ok(())
}

pub fn validate_token_exp<C: Claims>(claims: &C, max_age: Option<&Duration>) -> Result<(), Error> {
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(
chrono::naive::NaiveDateTime::from_timestamp(claims.exp(), 0),
))
.into());
}

if let Some(max) = max_age {
match claims.auth_time() {
Some(time) => {
let age = chrono::Duration::seconds(now.timestamp() - time);
if age >= *max {
return Err(Validation::Expired(Expiry::MaxAge(age)).into());
}
}
None => return Err(Validation::Missing(Missing::AuthTime).into()),
}
}

Ok(())
}

impl<C: CompactJson + Claims, P: Provider + Configurable> Client<P, C> {
/// Passthrough to the redirect_url stored in inth_oauth2 as a str.
pub fn redirect_url(&self) -> &str {
Expand Down Expand Up @@ -250,76 +334,15 @@ impl<C: CompactJson + Claims, P: Provider + Configurable> Client<P, C> {
max_age: Option<&Duration>,
) -> Result<(), Error> {
let claims = token.payload()?;
let config = self.config();

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());
}
validate_token_issuer(claims, config)?;

match nonce {
Some(expected) => match claims.nonce() {
Some(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()),
},
None => {
if claims.nonce().is_some() {
return Err(Validation::Missing(Missing::Nonce).into());
}
}
}
validate_token_nonce(claims, nonce)?;

if !claims.aud().contains(&self.client_id) {
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() {
if claims.azp().is_none() {
return Err(Validation::Missing(Missing::AuthorizedParty).into());
}
}
// If there is an authorized party, it must be our client_id
if let Some(actual) = claims.azp() {
if actual != &self.client_id {
let expected = self.client_id.to_string();
let actual = actual.to_string();
return Err(
Validation::Mismatch(Mismatch::AuthorizedParty { expected, actual }).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(
chrono::naive::NaiveDateTime::from_timestamp(claims.exp(), 0),
))
.into());
}
validate_token_aud(claims, &self.client_id)?;

if let Some(max) = max_age {
match claims.auth_time() {
Some(time) => {
let age = chrono::Duration::seconds(now.timestamp() - time);
if age >= *max {
return Err(Validation::Expired(Expiry::MaxAge(age)).into());
}
}
None => return Err(Validation::Missing(Missing::AuthTime).into()),
}
}
validate_token_exp(claims, max_age)?;

Ok(())
}
Expand Down
5 changes: 3 additions & 2 deletions src/deserializers.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use serde::Deserializer;
use de::Visitor;
use serde::de;
use serde::Deserializer;

pub fn bool_from_str_or_bool<'de, D>(deserializer: D) -> Result<bool, D::Error>
where D: Deserializer<'de>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(BoolOrStringVisitor)
}
Expand Down
36 changes: 36 additions & 0 deletions src/provider/microsoft.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use crate::{
client::{validate_token_aud, validate_token_exp, validate_token_nonce, Client},
error::Error,
Claims, Configurable, IdToken, Provider,
};
use biscuit::CompactJson;
use chrono::Duration;

/// Validate a decoded token for Microsoft OpenID. 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::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<C: CompactJson + Claims, P: Provider + Configurable>(
client: &Client<P, C>,
token: &IdToken<C>,
nonce: Option<&str>,
max_age: Option<&Duration>,
) -> Result<(), Error> {
let claims = token.payload()?;

validate_token_nonce(claims, nonce)?;

validate_token_aud(claims, &client.client_id)?;

validate_token_exp(claims, max_age)?;

Ok(())
}
3 changes: 3 additions & 0 deletions src/provider.rs → src/provider/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
/*!
OAuth 2.0 providers.
*/
#[cfg(feature = "microsoft")]
pub mod microsoft;

use url::Url;

/// OAuth 2.0 providers.
Expand Down
2 changes: 1 addition & 1 deletion src/userinfo.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use crate::deserializers::bool_from_str_or_bool;
use crate::Address;
use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
use url::Url;
use validator::Validate;
use validator_derive::Validate;
use crate::deserializers::bool_from_str_or_bool;

/// The userinfo struct contains all possible userinfo fields regardless of scope. [See spec.](https://openid.net/specs/openid-connect-basic-1_0.html#StandardClaims)
// TODO is there a way to use claims_supported in config to simplify this struct?
Expand Down