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 all commits
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ Implements [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-cor

Implements [UMA2](https://docs.kantarainitiative.org/uma/wg/oauth-uma-federated-authz-2.0-09.html) - User Managed Access, an extension to OIDC/OAuth2. Use feature flag `uma2` to enable this feature.

This is quick and dirty rewrite of [inth-oauth2](https://crates.io/crates/inth-oauth2) and [oidc](https://crates.io/crates/oidc) to use async / await. Basic idea was to solve particular task, as result most of good ideas from original crates were perverted and over-simplified.
It supports Microsoft OIDC with feature `microsoft`. This adds methods for authentication and token validation, those skip issuer check.

This library is a quick and dirty rewrite of [inth-oauth2](https://crates.io/crates/inth-oauth2) and [oidc](https://crates.io/crates/oidc) to use async / await. Basic idea was to solve particular task, as result most of good ideas from original crates were perverted and over-simplified.

Using [reqwest](https://crates.io/crates/reqwest) for the HTTP client and [biscuit](https://crates.io/crates/biscuit) for Javascript Object Signing and Encryption (JOSE).

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
4 changes: 3 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ Implements [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-cor

Implements [UMA2](https://docs.kantarainitiative.org/uma/wg/oauth-uma-federated-authz-2.0-09.html) - User Managed Access, an extension to OIDC/OAuth2. Use feature flag `uma2` to enable this feature.

This is quick and dirty rewrite of [inth-oauth2](https://crates.io/crates/inth-oauth2) and [oidc](https://crates.io/crates/oidc) to use async / await. Basic idea was to solve particular task, as result most of good ideas from original crates were perverted and over-simplified.
It supports Microsoft OIDC with feature `microsoft`. This adds methods for authentication and token validation, those skip issuer check.

This library is a quick and dirty rewrite of [inth-oauth2](https://crates.io/crates/inth-oauth2) and [oidc](https://crates.io/crates/oidc) to use async / await. Basic idea was to solve particular task, as result most of good ideas from original crates were perverted and over-simplified.

Using [reqwest](https://crates.io/crates/reqwest) for the HTTP client and [biscuit](https://crates.io/crates/biscuit) for Javascript Object Signing and Encryption (JOSE).

Expand Down
55 changes: 55 additions & 0 deletions src/provider/microsoft.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use crate::{
client::{validate_token_aud, validate_token_exp, validate_token_nonce, Client},
error::Error,
Claims, Configurable, IdToken, Provider, Token,
};
use biscuit::CompactJson;
use chrono::Duration;

/// Given an auth_code and auth options, request the token, decode, and validate it.
/// This validation is specific to Microsoft OIDC provider, it skips issuer validation.
pub async fn authenticate<C: CompactJson + Claims, P: Provider + Configurable>(
client: &Client<P, C>,
auth_code: &str,
nonce: Option<&str>,
max_age: Option<&Duration>,
) -> Result<Token<C>, Error> {
let bearer = client.request_token(auth_code).await.map_err(Error::from)?;
let mut token: Token<C> = bearer.into();

if let Some(mut id_token) = token.id_token.as_mut() {
client.decode_token(&mut id_token)?;
validate_token(client, &id_token, nonce, max_age)?;
}

Ok(token)
}

/// 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(())
}
6 changes: 6 additions & 0 deletions src/provider.rs → src/provider/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
/*!
OAuth 2.0 providers.
*/
#[cfg(feature = "microsoft")]
/// Microsoft OpenID Connect.
///
/// See [Microsoft identity platform and OpenID Connect protocol](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc)
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