Skip to content

Commit

Permalink
add seperate validate token method for microsoft provider (#9)
Browse files Browse the repository at this point in the history
* add seperate validate token method for microsoft provider

* added authenticate method for Microsoft provider

* added docs about Microsoft OIDC
  • Loading branch information
kilork authored Oct 3, 2020
1 parent 013a3c2 commit 26e2f64
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 72 deletions.
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

0 comments on commit 26e2f64

Please sign in to comment.