Skip to content

Commit

Permalink
Add support for specific JOSE header types (#161)
Browse files Browse the repository at this point in the history
Check https://tools.ietf.org/html/rfc7519#section-5.1 and https://tools.ietf.org/html/rfc7515#section-4.1.9 for more information on when the typ header should be set and to which value.

This commit allows to skip the check altogether as warranted by RFC 7515 the use of the header is optional but if set, according to RFC 7519, it should be set to "JWT". For other token types this is different (see RFC 9068).

Resolves #160.
  • Loading branch information
lmm-git authored Jun 17, 2024
1 parent 0252532 commit af598c2
Show file tree
Hide file tree
Showing 5 changed files with 408 additions and 19 deletions.
99 changes: 98 additions & 1 deletion src/jwt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,108 @@ new_type![
JsonWebTokenContentType(String)
];

/// Error type used when normalizing [`JsonWebTokenType`] objects
#[derive(Error, Debug)]
#[error("Invalid JWT type: {typ}")]
pub struct InvalidJsonWebTokenTypeError {
typ: String,
}

new_type![
#[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)]
/// JSON Web Token type field (typ)
///
/// This type stores the raw (deserialized) value.
///
/// To compare two different JSON Web Token types, please use the normalized version via [`JsonWebTokenType::normalize`].
#[derive(Deserialize, Hash, Serialize)]
JsonWebTokenType(String)

impl {
/// Expands a [`JsonWebTokenType`] and produces a [`NormalizedJsonWebTokenType`] according to RFC2045 and RFC7515.
///
/// See [RFC 2045 section 5.1](https://tools.ietf.org/html/rfc2045#section-5.1) for the full Content-Type Header Field spec.
/// See [RFC 7515 section 4.19](https://tools.ietf.org/html/rfc7515#section-4.1.9) for specific requirements of JSON Web Token Types.
pub fn normalize(&self) -> Result<NormalizedJsonWebTokenType, InvalidJsonWebTokenTypeError> {
self.try_into()
}
}
];

/// Normalized JSON Web Token type field (typ)
///
/// This type stores the normalized value of a [`JsonWebTokenType`].
/// To retrieve a normalized value according to RFC2045 and RFC7515 see [`JsonWebTokenType::normalize`]
///
/// See [RFC 2045 section 5.1](https://tools.ietf.org/html/rfc2045#section-5.1) for the full Content-Type Header Field spec.
/// See [RFC 7515 section 4.1.9](https://tools.ietf.org/html/rfc7515#section-4.1.9) for specific requirements of JSON Web Token Types.
///
/// It is recommended to instantiate `NormalizedJsonWebTokenType` objects via [`JsonWebTokenType`] and then call [`JsonWebTokenType::normalize`].
///
/// ```rust
/// # use openidconnect::{NormalizedJsonWebTokenType, JsonWebTokenType};
/// let token_type = JsonWebTokenType::new("jwt+at".to_string()).normalize();
/// // normalized value looks like "application/jwt+at"
/// # assert_eq!(*token_type.unwrap(), "application/jwt+at")
/// ```
#[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize)]
pub struct NormalizedJsonWebTokenType(String);

impl std::ops::Deref for NormalizedJsonWebTokenType {
type Target = String;
fn deref(&self) -> &String {
&self.0
}
}
impl From<NormalizedJsonWebTokenType> for String {
fn from(t: NormalizedJsonWebTokenType) -> String {
t.0
}
}

impl From<NormalizedJsonWebTokenType> for JsonWebTokenType {
fn from(t: NormalizedJsonWebTokenType) -> JsonWebTokenType {
JsonWebTokenType::new(t.0)
}
}

impl TryFrom<&JsonWebTokenType> for NormalizedJsonWebTokenType {
type Error = InvalidJsonWebTokenTypeError;

/// Normalizes a [`JsonWebTokenType`] and produces a [`NormalizedJsonWebTokenType`] according to RFC2045.
///
/// See [RFC 2045 section 5.1](https://tools.ietf.org/html/rfc2045#section-5.1) for the full Content-Type Header Field spec.
/// See [RFC 7515 section 4.19](https://tools.ietf.org/html/rfc7515#section-4.1.9) for specific requirements of JSON Web Token Types.
fn try_from(t: &JsonWebTokenType) -> Result<NormalizedJsonWebTokenType, Self::Error> {
let lowercase_jwt_type = t.0.to_lowercase();
if let Some(slash_location) = lowercase_jwt_type.find('/') {
if let Some(semicolon_location) = lowercase_jwt_type.find(';') {
// If '/' is not before ';' as then the MIME type is invalid
// e.g. some;arg="1/2" is invalid, but application/some;arg=1 is valid
// OR
// If MIME type has not at least one character
// OR
// If MIME subtype has not at least one character
if slash_location > semicolon_location
|| slash_location == 0
|| slash_location.saturating_add(1) >= semicolon_location
{
Err(InvalidJsonWebTokenTypeError {
typ: lowercase_jwt_type,
})
} else {
Ok(NormalizedJsonWebTokenType(lowercase_jwt_type))
}
} else {
Ok(NormalizedJsonWebTokenType(lowercase_jwt_type))
}
} else {
Ok(NormalizedJsonWebTokenType(format!(
"application/{lowercase_jwt_type}"
)))
}
}
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum JsonWebTokenAlgorithm<JE, JS>
Expand Down
35 changes: 32 additions & 3 deletions src/jwt/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ use crate::core::{
CoreRsaPrivateSigningKey,
};
use crate::jwt::{
JsonWebToken, JsonWebTokenAccess, JsonWebTokenAlgorithm, JsonWebTokenJsonPayloadSerde,
JsonWebTokenPayloadSerde,
InvalidJsonWebTokenTypeError, JsonWebToken, JsonWebTokenAccess, JsonWebTokenAlgorithm,
JsonWebTokenJsonPayloadSerde, JsonWebTokenPayloadSerde,
};
use crate::JsonWebKeyId;
use crate::{JsonWebKeyId, JsonWebTokenType};

use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -364,3 +364,32 @@ fn test_invalid_deserialization() {
.expect("failed to deserialize");
assert_eq!(deserialized.unverified_payload().foo, "bar");
}

#[test]
fn test_json_web_token_type_normalization() {
fn assert_token_type_normalization(
jwt_type_string: &str,
expected_normalized_jwt_type_string: &str,
) -> Result<(), InvalidJsonWebTokenTypeError> {
let jwt_type = JsonWebTokenType::new(jwt_type_string.to_string());
let normalized_jwt_type = jwt_type.normalize()?;

assert_eq!(*normalized_jwt_type, expected_normalized_jwt_type_string);
Ok(())
}

assert_token_type_normalization("jwt", "application/jwt").unwrap();
assert_token_type_normalization("jwt;arg=some", "application/jwt;arg=some").unwrap();
assert!(assert_token_type_normalization("jwt;arg=some/other", "").is_err());
assert!(assert_token_type_normalization("/jwt;arg=some/other", "").is_err());
assert!(assert_token_type_normalization("application/;arg=some/other", "").is_err());
assert_token_type_normalization("application/jwt", "application/jwt").unwrap();
assert_token_type_normalization(
"application/jwt;arg=some/other",
"application/jwt;arg=some/other",
)
.unwrap();
assert_token_type_normalization("special/type", "special/type").unwrap();
assert_token_type_normalization("special/type;arg=some", "special/type;arg=some").unwrap();
assert_token_type_normalization("s/t;arg=some/o", "s/t;arg=some/o").unwrap();
}
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -705,7 +705,7 @@ pub use crate::discovery::{
};
pub use crate::id_token::IdTokenFields;
pub use crate::id_token::{IdToken, IdTokenClaims};
pub use crate::jwt::JsonWebTokenError;
pub use crate::jwt::{JsonWebTokenError, JsonWebTokenType, NormalizedJsonWebTokenType};
pub use crate::logout::{LogoutProviderMetadata, LogoutRequest, ProviderMetadataWithLogout};
pub use crate::token::TokenResponse;
// Flatten the module hierarchy involving types. They're only separated to improve code
Expand Down
76 changes: 67 additions & 9 deletions src/verification/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use crate::jwt::{JsonWebToken, JsonWebTokenJsonPayloadSerde};
use crate::jwt::{JsonWebToken, JsonWebTokenJsonPayloadSerde, NormalizedJsonWebTokenType};
use crate::user_info::UserInfoClaimsImpl;
use crate::{
AdditionalClaims, Audience, AuthenticationContextClass, ClientId, ClientSecret, GenderClaim,
IdTokenClaims, IssuerUrl, JsonWebKey, JsonWebKeyId, JsonWebKeySet, JsonWebTokenAccess,
JsonWebTokenAlgorithm, JsonWebTokenHeader, JweContentEncryptionAlgorithm, JwsSigningAlgorithm,
Nonce, SubjectIdentifier,
JsonWebTokenAlgorithm, JsonWebTokenHeader, JsonWebTokenType, JweContentEncryptionAlgorithm,
JwsSigningAlgorithm, Nonce, SubjectIdentifier,
};

use chrono::{DateTime, Utc};
Expand Down Expand Up @@ -118,6 +118,7 @@ pub(crate) struct JwtClaimsVerifier<'a, K>
where
K: JsonWebKey,
{
allowed_jose_types: Option<HashSet<NormalizedJsonWebTokenType>>,
allowed_algs: Option<HashSet<K::SigningAlgorithm>>,
aud_match_required: bool,
client_id: ClientId,
Expand All @@ -140,6 +141,15 @@ where
.cloned()
.collect(),
),
allowed_jose_types: Some(HashSet::from([
JsonWebTokenType::new("application/jwt".to_string())
.normalize()
.expect("application/jwt should be a valid JWT type"), // used by many IdP, but not standardized
JsonWebTokenType::new("application/jose".to_string())
.normalize()
.expect("application/jose should be a valid JWT type"), // standard as defined in https://tools.ietf.org/html/rfc7515#section-4.1.9
// we do not support JOSE+JSON, so we omit this here in the default configuration
])),
aud_match_required: true,
client_id,
client_secret: None,
Expand Down Expand Up @@ -181,6 +191,21 @@ where
self
}

/// Allows setting specific JOSE types. The verifier will check against them during verification.
///
/// See [RFC 7515 section 4.1.9](https://tools.ietf.org/html/rfc7515#section-4.1.9) for more details.
pub fn set_allowed_jose_types<I>(mut self, types: I) -> Self
where
I: IntoIterator<Item = NormalizedJsonWebTokenType>,
{
self.allowed_jose_types = Some(types.into_iter().collect());
self
}
pub fn allow_all_jose_types(mut self) -> Self {
self.allowed_jose_types = None;
self
}

pub fn set_client_secret(mut self, client_secret: ClientSecret) -> Self {
self.client_secret = Some(client_secret);
self
Expand All @@ -195,6 +220,7 @@ where
}

fn validate_jose_header<JE>(
&self,
jose_header: &JsonWebTokenHeader<JE, K::SigningAlgorithm>,
) -> Result<(), ClaimsVerificationError>
where
Expand All @@ -203,14 +229,29 @@ where
>,
{
// The 'typ' header field must either be omitted or have the canonicalized value JWT.
// see https://tools.ietf.org/html/rfc7519#section-5.1
if let Some(ref jwt_type) = jose_header.typ {
if jwt_type.to_uppercase() != "JWT" {
return Err(ClaimsVerificationError::Unsupported(format!(
"unexpected or unsupported JWT type `{}`",
**jwt_type
)));
if let Some(allowed_jose_types) = &self.allowed_jose_types {
// Check according to https://tools.ietf.org/html/rfc7515#section-4.1.9
// See https://tools.ietf.org/html/rfc2045#section-5.1 for the full Content-Type Header Field spec.
//
// For sake of simplicity, we do not support matching on application types with parameters like
// application/example;part="1/2". If you know your parameters exactly, just set the whole Content Type manually.
let valid_jwt_type = if let Ok(normalized_jwt_type) = jwt_type.normalize() {
allowed_jose_types.contains(&normalized_jwt_type)
} else {
false
};

if !valid_jwt_type {
return Err(ClaimsVerificationError::Unsupported(format!(
"unexpected or unsupported JWT type `{}`",
**jwt_type
)));
}
}
}

// The 'cty' header field must be omitted, since it's only used for JWTs that contain
// content types other than JSON-encoded claims. This may include nested JWTs, such as if
// JWE encryption is used. This is currently unsupported.
Expand Down Expand Up @@ -250,7 +291,7 @@ where
{
{
let jose_header = jwt.unverified_header();
Self::validate_jose_header(jose_header)?;
self.validate_jose_header(jose_header)?;

// The code below roughly follows the validation steps described in
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
Expand Down Expand Up @@ -569,6 +610,23 @@ where
self
}

/// Allows setting specific JOSE types. The verifier will check against them during verification.
///
/// See [RFC 7515 section 4.1.9](https://tools.ietf.org/html/rfc7515#section-4.1.9) for more details.
pub fn set_allowed_jose_types<I>(mut self, types: I) -> Self
where
I: IntoIterator<Item = NormalizedJsonWebTokenType>,
{
self.jwt_verifier = self.jwt_verifier.set_allowed_jose_types(types);
self
}

/// Allow all JSON Web Token Header types.
pub fn allow_all_jose_types(mut self) -> Self {
self.jwt_verifier = self.jwt_verifier.allow_all_jose_types();
self
}

/// Specifies a function for verifying the `acr` claim.
///
/// The function should return `Ok(())` if the claim is valid, or a string describing the error
Expand Down
Loading

0 comments on commit af598c2

Please sign in to comment.