use std::{collections::HashMap, ops::Deref};
use chrono::{DateTime, Duration, Utc};
use language_tags::LanguageTag;
use mas_iana::{
jose::{JsonWebEncryptionAlg, JsonWebEncryptionEnc, JsonWebSignatureAlg},
oauth::{OAuthAuthorizationEndpointResponseType, OAuthClientAuthenticationMethod},
};
use mas_jose::jwk::PublicJsonWebKeySet;
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, skip_serializing_none, TimestampSeconds};
use thiserror::Error;
use url::Url;
use crate::{
oidc::{ApplicationType, SubjectType},
requests::GrantType,
response_type::ResponseType,
};
mod client_metadata_serde;
use client_metadata_serde::ClientMetadataSerdeHelper;
pub const DEFAULT_RESPONSE_TYPES: [OAuthAuthorizationEndpointResponseType; 1] =
[OAuthAuthorizationEndpointResponseType::Code];
pub const DEFAULT_GRANT_TYPES: &[GrantType] = &[GrantType::AuthorizationCode];
pub const DEFAULT_APPLICATION_TYPE: ApplicationType = ApplicationType::Web;
pub const DEFAULT_TOKEN_AUTH_METHOD: &OAuthClientAuthenticationMethod =
&OAuthClientAuthenticationMethod::ClientSecretBasic;
pub const DEFAULT_SIGNING_ALGORITHM: &JsonWebSignatureAlg = &JsonWebSignatureAlg::Rs256;
pub const DEFAULT_ENCRYPTION_ENC_ALGORITHM: &JsonWebEncryptionEnc =
&JsonWebEncryptionEnc::A128CbcHs256;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Localized<T> {
non_localized: T,
localized: HashMap<LanguageTag, T>,
}
impl<T> Localized<T> {
pub fn new(non_localized: T, localized: impl IntoIterator<Item = (LanguageTag, T)>) -> Self {
Self {
non_localized,
localized: localized.into_iter().collect(),
}
}
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.localized.len() + 1
}
pub fn non_localized(&self) -> &T {
&self.non_localized
}
pub fn to_non_localized(self) -> T {
self.non_localized
}
pub fn get(&self, language: Option<&LanguageTag>) -> Option<&T> {
match language {
Some(lang) => self.localized.get(lang),
None => Some(&self.non_localized),
}
}
pub fn iter(&self) -> impl Iterator<Item = (Option<&LanguageTag>, &T)> {
Some(&self.non_localized)
.into_iter()
.map(|val| (None, val))
.chain(self.localized.iter().map(|(lang, val)| (Some(lang), val)))
}
}
impl<T> From<(T, HashMap<LanguageTag, T>)> for Localized<T> {
fn from(t: (T, HashMap<LanguageTag, T>)) -> Self {
Localized {
non_localized: t.0,
localized: t.1,
}
}
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
#[serde(from = "ClientMetadataSerdeHelper")]
pub struct ClientMetadata {
pub redirect_uris: Option<Vec<Url>>,
pub response_types: Option<Vec<ResponseType>>,
pub grant_types: Option<Vec<GrantType>>,
pub application_type: Option<ApplicationType>,
pub contacts: Option<Vec<String>>,
pub client_name: Option<Localized<String>>,
pub logo_uri: Option<Localized<Url>>,
pub client_uri: Option<Localized<Url>>,
pub policy_uri: Option<Localized<Url>>,
pub tos_uri: Option<Localized<Url>>,
pub jwks_uri: Option<Url>,
pub jwks: Option<PublicJsonWebKeySet>,
pub software_id: Option<String>,
pub software_version: Option<String>,
pub sector_identifier_uri: Option<Url>,
pub subject_type: Option<SubjectType>,
pub token_endpoint_auth_method: Option<OAuthClientAuthenticationMethod>,
pub token_endpoint_auth_signing_alg: Option<JsonWebSignatureAlg>,
pub id_token_signed_response_alg: Option<JsonWebSignatureAlg>,
pub id_token_encrypted_response_alg: Option<JsonWebEncryptionAlg>,
pub id_token_encrypted_response_enc: Option<JsonWebEncryptionEnc>,
pub userinfo_signed_response_alg: Option<JsonWebSignatureAlg>,
pub userinfo_encrypted_response_alg: Option<JsonWebEncryptionAlg>,
pub userinfo_encrypted_response_enc: Option<JsonWebEncryptionEnc>,
pub request_object_signing_alg: Option<JsonWebSignatureAlg>,
pub request_object_encryption_alg: Option<JsonWebEncryptionAlg>,
pub request_object_encryption_enc: Option<JsonWebEncryptionEnc>,
pub default_max_age: Option<Duration>,
pub require_auth_time: Option<bool>,
pub default_acr_values: Option<Vec<String>>,
pub initiate_login_uri: Option<Url>,
pub request_uris: Option<Vec<Url>>,
pub require_signed_request_object: Option<bool>,
pub require_pushed_authorization_requests: Option<bool>,
pub introspection_signed_response_alg: Option<JsonWebSignatureAlg>,
pub introspection_encrypted_response_alg: Option<JsonWebEncryptionAlg>,
pub introspection_encrypted_response_enc: Option<JsonWebEncryptionEnc>,
pub post_logout_redirect_uris: Option<Vec<Url>>,
}
impl ClientMetadata {
#[allow(clippy::too_many_lines)]
pub fn validate(self) -> Result<VerifiedClientMetadata, ClientMetadataVerificationError> {
let grant_types = self.grant_types();
let has_implicit = grant_types.contains(&GrantType::Implicit);
let has_authorization_code = grant_types.contains(&GrantType::AuthorizationCode);
let has_both = has_implicit && has_authorization_code;
if let Some(uris) = &self.redirect_uris {
if let Some(uri) = uris.iter().find(|uri| uri.fragment().is_some()) {
return Err(ClientMetadataVerificationError::RedirectUriWithFragment(
uri.clone(),
));
}
} else if has_authorization_code || has_implicit {
return Err(ClientMetadataVerificationError::MissingRedirectUris);
}
let response_type_code = [OAuthAuthorizationEndpointResponseType::Code.into()];
let response_types = match &self.response_types {
Some(types) => &types[..],
None if has_authorization_code || has_implicit => &response_type_code[..],
None => &[],
};
for response_type in response_types {
let has_code = response_type.has_code();
let has_id_token = response_type.has_id_token();
let has_token = response_type.has_token();
let is_ok = has_code && has_both
|| !has_code && has_implicit
|| has_authorization_code && !has_id_token && !has_token
|| !has_code && !has_id_token && !has_token;
if !is_ok {
return Err(ClientMetadataVerificationError::IncoherentResponseType(
response_type.clone(),
));
}
}
if self.jwks_uri.is_some() && self.jwks.is_some() {
return Err(ClientMetadataVerificationError::JwksUriAndJwksMutuallyExclusive);
}
if let Some(url) = self
.sector_identifier_uri
.as_ref()
.filter(|url| url.scheme() != "https")
{
return Err(ClientMetadataVerificationError::UrlNonHttpsScheme(
"sector_identifier_uri",
url.clone(),
));
}
if *self.token_endpoint_auth_method() == OAuthClientAuthenticationMethod::PrivateKeyJwt
&& self.jwks_uri.is_none()
&& self.jwks.is_none()
{
return Err(ClientMetadataVerificationError::MissingJwksForTokenMethod);
}
if let Some(alg) = &self.token_endpoint_auth_signing_alg {
if *alg == JsonWebSignatureAlg::None {
return Err(ClientMetadataVerificationError::UnauthorizedSigningAlgNone(
"token_endpoint",
));
}
} else if matches!(
self.token_endpoint_auth_method(),
OAuthClientAuthenticationMethod::PrivateKeyJwt
| OAuthClientAuthenticationMethod::ClientSecretJwt
) {
return Err(ClientMetadataVerificationError::MissingAuthSigningAlg(
"token_endpoint",
));
}
if *self.id_token_signed_response_alg() == JsonWebSignatureAlg::None
&& response_types.iter().any(ResponseType::has_id_token)
{
return Err(ClientMetadataVerificationError::IdTokenSigningAlgNone);
}
if self.id_token_encrypted_response_enc.is_some() {
self.id_token_encrypted_response_alg.as_ref().ok_or(
ClientMetadataVerificationError::MissingEncryptionAlg("id_token"),
)?;
}
if self.userinfo_encrypted_response_enc.is_some() {
self.userinfo_encrypted_response_alg.as_ref().ok_or(
ClientMetadataVerificationError::MissingEncryptionAlg("userinfo"),
)?;
}
if self.request_object_encryption_enc.is_some() {
self.request_object_encryption_alg.as_ref().ok_or(
ClientMetadataVerificationError::MissingEncryptionAlg("request_object"),
)?;
}
if let Some(url) = self
.initiate_login_uri
.as_ref()
.filter(|url| url.scheme() != "https")
{
return Err(ClientMetadataVerificationError::UrlNonHttpsScheme(
"initiate_login_uri",
url.clone(),
));
}
if self.introspection_encrypted_response_enc.is_some() {
self.introspection_encrypted_response_alg.as_ref().ok_or(
ClientMetadataVerificationError::MissingEncryptionAlg("introspection"),
)?;
}
Ok(VerifiedClientMetadata { inner: self })
}
#[must_use]
pub fn response_types(&self) -> Vec<ResponseType> {
self.response_types.clone().unwrap_or_else(|| {
DEFAULT_RESPONSE_TYPES
.into_iter()
.map(ResponseType::from)
.collect()
})
}
#[must_use]
pub fn grant_types(&self) -> &[GrantType] {
self.grant_types.as_deref().unwrap_or(DEFAULT_GRANT_TYPES)
}
#[must_use]
pub fn application_type(&self) -> ApplicationType {
self.application_type
.clone()
.unwrap_or(DEFAULT_APPLICATION_TYPE)
}
#[must_use]
pub fn token_endpoint_auth_method(&self) -> &OAuthClientAuthenticationMethod {
self.token_endpoint_auth_method
.as_ref()
.unwrap_or(DEFAULT_TOKEN_AUTH_METHOD)
}
#[must_use]
pub fn id_token_signed_response_alg(&self) -> &JsonWebSignatureAlg {
self.id_token_signed_response_alg
.as_ref()
.unwrap_or(DEFAULT_SIGNING_ALGORITHM)
}
#[must_use]
pub fn id_token_encrypted_response(
&self,
) -> Option<(&JsonWebEncryptionAlg, &JsonWebEncryptionEnc)> {
self.id_token_encrypted_response_alg.as_ref().map(|alg| {
(
alg,
self.id_token_encrypted_response_enc
.as_ref()
.unwrap_or(DEFAULT_ENCRYPTION_ENC_ALGORITHM),
)
})
}
#[must_use]
pub fn userinfo_encrypted_response(
&self,
) -> Option<(&JsonWebEncryptionAlg, &JsonWebEncryptionEnc)> {
self.userinfo_encrypted_response_alg.as_ref().map(|alg| {
(
alg,
self.userinfo_encrypted_response_enc
.as_ref()
.unwrap_or(DEFAULT_ENCRYPTION_ENC_ALGORITHM),
)
})
}
#[must_use]
pub fn request_object_encryption(
&self,
) -> Option<(&JsonWebEncryptionAlg, &JsonWebEncryptionEnc)> {
self.request_object_encryption_alg.as_ref().map(|alg| {
(
alg,
self.request_object_encryption_enc
.as_ref()
.unwrap_or(DEFAULT_ENCRYPTION_ENC_ALGORITHM),
)
})
}
#[must_use]
pub fn require_auth_time(&self) -> bool {
self.require_auth_time.unwrap_or_default()
}
#[must_use]
pub fn require_signed_request_object(&self) -> bool {
self.require_signed_request_object.unwrap_or_default()
}
#[must_use]
pub fn require_pushed_authorization_requests(&self) -> bool {
self.require_pushed_authorization_requests
.unwrap_or_default()
}
#[must_use]
pub fn introspection_encrypted_response(
&self,
) -> Option<(&JsonWebEncryptionAlg, &JsonWebEncryptionEnc)> {
self.introspection_encrypted_response_alg
.as_ref()
.map(|alg| {
(
alg,
self.introspection_encrypted_response_enc
.as_ref()
.unwrap_or(DEFAULT_ENCRYPTION_ENC_ALGORITHM),
)
})
}
}
#[derive(Serialize, Debug, PartialEq, Eq, Clone)]
#[serde(into = "ClientMetadataSerdeHelper")]
pub struct VerifiedClientMetadata {
inner: ClientMetadata,
}
impl VerifiedClientMetadata {
#[must_use]
pub fn redirect_uris(&self) -> &[Url] {
match &self.redirect_uris {
Some(v) => v,
None => &[],
}
}
}
impl Deref for VerifiedClientMetadata {
type Target = ClientMetadata;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
#[derive(Debug, Error)]
pub enum ClientMetadataVerificationError {
#[error("redirect URIs are missing")]
MissingRedirectUris,
#[error("redirect URI with fragment: {0}")]
RedirectUriWithFragment(Url),
#[error("'{0}' response type not compatible with grant types")]
IncoherentResponseType(ResponseType),
#[error("jwks_uri and jwks are mutually exclusive")]
JwksUriAndJwksMutuallyExclusive,
#[error("{0}'s URL doesn't use a https scheme: {1}")]
UrlNonHttpsScheme(&'static str, Url),
#[error("missing JWK Set for token auth method")]
MissingJwksForTokenMethod,
#[error("none signing alg unauthorized for {0}")]
UnauthorizedSigningAlgNone(&'static str),
#[error("{0} missing auth signing algorithm")]
MissingAuthSigningAlg(&'static str),
#[error("ID Token signing alg is none")]
IdTokenSigningAlgNone,
#[error("{0} missing encryption alg value")]
MissingEncryptionAlg(&'static str),
}
#[serde_as]
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct ClientRegistrationResponse {
pub client_id: String,
#[serde(default)]
pub client_secret: Option<String>,
#[serde(default)]
#[serde_as(as = "Option<TimestampSeconds<i64>>")]
pub client_id_issued_at: Option<DateTime<Utc>>,
#[serde(default)]
#[serde_as(as = "Option<TimestampSeconds<i64>>")]
pub client_secret_expires_at: Option<DateTime<Utc>>,
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use mas_iana::{
jose::{JsonWebEncryptionAlg, JsonWebEncryptionEnc, JsonWebSignatureAlg},
oauth::{OAuthAuthorizationEndpointResponseType, OAuthClientAuthenticationMethod},
};
use mas_jose::jwk::PublicJsonWebKeySet;
use url::Url;
use super::{ClientMetadata, ClientMetadataVerificationError};
use crate::{requests::GrantType, response_type::ResponseType};
fn valid_client_metadata() -> ClientMetadata {
ClientMetadata {
redirect_uris: Some(vec![Url::parse("http://localhost/oidc").unwrap()]),
..Default::default()
}
}
fn jwks() -> PublicJsonWebKeySet {
serde_json::from_value(serde_json::json!({
"keys": [
{
"alg": "RS256",
"kty": "RSA",
"n": "tCwhHOxX_ylh5kVwfVqW7QIBTIsPjkjCjVCppDrynuF_3msEdtEaG64eJUz84ODFNMCC0BQ57G7wrKQVWkdSDxWUEqGk2BixBiHJRWZdofz1WOBTdPVicvHW5Zl_aIt7uXWMdOp_SODw-O2y2f05EqbFWFnR2-1y9K8KbiOp82CD72ny1Jbb_3PxTs2Z0F4ECAtTzpDteaJtjeeueRjr7040JAjQ-5fpL5D1g8x14LJyVIo-FL_y94NPFbMp7UCi69CIfVHXFO8WYFz949og-47mWRrID5lS4zpx-QLuvNhUb_lSqmylUdQB3HpRdOcYdj3xwy4MHJuu7tTaf0AmCQ",
"use": "sig",
"kid": "d98f49bc6ca4581eae8dfadd494fce10ea23aab0",
"e": "AQAB"
}
]
})).unwrap()
}
#[test]
fn validate_required_metadata() {
let metadata = valid_client_metadata();
metadata.validate().unwrap();
}
#[test]
fn validate_redirect_uris() {
let mut metadata = ClientMetadata::default();
assert_matches!(
metadata.clone().validate(),
Err(ClientMetadataVerificationError::MissingRedirectUris)
);
let wrong_uri = Url::parse("http://localhost/#fragment").unwrap();
metadata.redirect_uris = Some(vec![
Url::parse("http://localhost/").unwrap(),
wrong_uri.clone(),
]);
let uri = assert_matches!(
metadata.clone().validate(),
Err(ClientMetadataVerificationError::RedirectUriWithFragment(uri)) => uri
);
assert_eq!(uri, wrong_uri);
metadata.redirect_uris = Some(vec![
Url::parse("http://localhost/").unwrap(),
Url::parse("http://localhost/oidc").unwrap(),
Url::parse("http://localhost/?oidc").unwrap(),
Url::parse("http://localhost/my-client?oidc").unwrap(),
]);
metadata.validate().unwrap();
}
#[test]
#[allow(clippy::too_many_lines)]
fn validate_response_types() {
let mut metadata = valid_client_metadata();
metadata.response_types = Some(vec![OAuthAuthorizationEndpointResponseType::Code.into()]);
metadata.clone().validate().unwrap();
let response_type: ResponseType =
OAuthAuthorizationEndpointResponseType::CodeIdToken.into();
metadata.response_types = Some(vec![response_type.clone()]);
let res = assert_matches!(metadata.clone().validate(), Err(ClientMetadataVerificationError::IncoherentResponseType(res)) => res);
assert_eq!(res, response_type);
let response_type: ResponseType =
OAuthAuthorizationEndpointResponseType::CodeIdTokenToken.into();
metadata.response_types = Some(vec![response_type.clone()]);
let res = assert_matches!(metadata.clone().validate(), Err(ClientMetadataVerificationError::IncoherentResponseType(res)) => res);
assert_eq!(res, response_type);
let response_type: ResponseType = OAuthAuthorizationEndpointResponseType::CodeToken.into();
metadata.response_types = Some(vec![response_type.clone()]);
let res = assert_matches!(metadata.clone().validate(), Err(ClientMetadataVerificationError::IncoherentResponseType(res)) => res);
assert_eq!(res, response_type);
let response_type: ResponseType = OAuthAuthorizationEndpointResponseType::IdToken.into();
metadata.response_types = Some(vec![response_type.clone()]);
let res = assert_matches!(metadata.clone().validate(), Err(ClientMetadataVerificationError::IncoherentResponseType(res)) => res);
assert_eq!(res, response_type);
let response_type: ResponseType =
OAuthAuthorizationEndpointResponseType::IdTokenToken.into();
metadata.response_types = Some(vec![response_type.clone()]);
let res = assert_matches!(metadata.clone().validate(), Err(ClientMetadataVerificationError::IncoherentResponseType(res)) => res);
assert_eq!(res, response_type);
let response_type: ResponseType =
OAuthAuthorizationEndpointResponseType::IdTokenToken.into();
metadata.response_types = Some(vec![response_type.clone()]);
let res = assert_matches!(metadata.clone().validate(), Err(ClientMetadataVerificationError::IncoherentResponseType(res)) => res);
assert_eq!(res, response_type);
metadata.response_types = Some(vec![OAuthAuthorizationEndpointResponseType::None.into()]);
metadata.clone().validate().unwrap();
metadata.grant_types = Some(vec![GrantType::Implicit]);
let response_type: ResponseType = OAuthAuthorizationEndpointResponseType::Code.into();
metadata.response_types = Some(vec![response_type.clone()]);
let res = assert_matches!(metadata.clone().validate(), Err(ClientMetadataVerificationError::IncoherentResponseType(res)) => res);
assert_eq!(res, response_type);
let response_type: ResponseType =
OAuthAuthorizationEndpointResponseType::CodeIdToken.into();
metadata.response_types = Some(vec![response_type.clone()]);
let res = assert_matches!(metadata.clone().validate(), Err(ClientMetadataVerificationError::IncoherentResponseType(res)) => res);
assert_eq!(res, response_type);
let response_type: ResponseType =
OAuthAuthorizationEndpointResponseType::CodeIdTokenToken.into();
metadata.response_types = Some(vec![response_type.clone()]);
let res = assert_matches!(metadata.clone().validate(), Err(ClientMetadataVerificationError::IncoherentResponseType(res)) => res);
assert_eq!(res, response_type);
let response_type: ResponseType = OAuthAuthorizationEndpointResponseType::CodeToken.into();
metadata.response_types = Some(vec![response_type.clone()]);
let res = assert_matches!(metadata.clone().validate(), Err(ClientMetadataVerificationError::IncoherentResponseType(res)) => res);
assert_eq!(res, response_type);
metadata.response_types =
Some(vec![OAuthAuthorizationEndpointResponseType::IdToken.into()]);
metadata.clone().validate().unwrap();
metadata.response_types = Some(vec![
OAuthAuthorizationEndpointResponseType::IdTokenToken.into()
]);
metadata.clone().validate().unwrap();
metadata.response_types = Some(vec![OAuthAuthorizationEndpointResponseType::Token.into()]);
metadata.clone().validate().unwrap();
metadata.response_types = Some(vec![OAuthAuthorizationEndpointResponseType::None.into()]);
metadata.clone().validate().unwrap();
metadata.grant_types = Some(vec![GrantType::AuthorizationCode, GrantType::Implicit]);
metadata.response_types = Some(vec![OAuthAuthorizationEndpointResponseType::Code.into()]);
metadata.clone().validate().unwrap();
metadata.response_types = Some(vec![
OAuthAuthorizationEndpointResponseType::CodeIdToken.into()
]);
metadata.clone().validate().unwrap();
metadata.response_types = Some(vec![
OAuthAuthorizationEndpointResponseType::CodeIdTokenToken.into(),
]);
metadata.clone().validate().unwrap();
metadata.response_types = Some(vec![
OAuthAuthorizationEndpointResponseType::CodeToken.into()
]);
metadata.clone().validate().unwrap();
metadata.response_types =
Some(vec![OAuthAuthorizationEndpointResponseType::IdToken.into()]);
metadata.clone().validate().unwrap();
metadata.response_types = Some(vec![
OAuthAuthorizationEndpointResponseType::IdTokenToken.into()
]);
metadata.clone().validate().unwrap();
metadata.response_types = Some(vec![OAuthAuthorizationEndpointResponseType::Token.into()]);
metadata.clone().validate().unwrap();
metadata.response_types = Some(vec![OAuthAuthorizationEndpointResponseType::None.into()]);
metadata.clone().validate().unwrap();
metadata.grant_types = Some(vec![GrantType::RefreshToken, GrantType::ClientCredentials]);
let response_type: ResponseType = OAuthAuthorizationEndpointResponseType::Code.into();
metadata.response_types = Some(vec![response_type.clone()]);
let res = assert_matches!(metadata.clone().validate(), Err(ClientMetadataVerificationError::IncoherentResponseType(res)) => res);
assert_eq!(res, response_type);
let response_type: ResponseType =
OAuthAuthorizationEndpointResponseType::CodeIdToken.into();
metadata.response_types = Some(vec![response_type.clone()]);
let res = assert_matches!(metadata.clone().validate(), Err(ClientMetadataVerificationError::IncoherentResponseType(res)) => res);
assert_eq!(res, response_type);
let response_type: ResponseType =
OAuthAuthorizationEndpointResponseType::CodeIdTokenToken.into();
metadata.response_types = Some(vec![response_type.clone()]);
let res = assert_matches!(metadata.clone().validate(), Err(ClientMetadataVerificationError::IncoherentResponseType(res)) => res);
assert_eq!(res, response_type);
let response_type: ResponseType = OAuthAuthorizationEndpointResponseType::CodeToken.into();
metadata.response_types = Some(vec![response_type.clone()]);
let res = assert_matches!(metadata.clone().validate(), Err(ClientMetadataVerificationError::IncoherentResponseType(res)) => res);
assert_eq!(res, response_type);
let response_type: ResponseType = OAuthAuthorizationEndpointResponseType::IdToken.into();
metadata.response_types = Some(vec![response_type.clone()]);
let res = assert_matches!(metadata.clone().validate(), Err(ClientMetadataVerificationError::IncoherentResponseType(res)) => res);
assert_eq!(res, response_type);
let response_type: ResponseType =
OAuthAuthorizationEndpointResponseType::IdTokenToken.into();
metadata.response_types = Some(vec![response_type.clone()]);
let res = assert_matches!(metadata.clone().validate(), Err(ClientMetadataVerificationError::IncoherentResponseType(res)) => res);
assert_eq!(res, response_type);
let response_type: ResponseType = OAuthAuthorizationEndpointResponseType::Token.into();
metadata.response_types = Some(vec![response_type.clone()]);
let res = assert_matches!(metadata.clone().validate(), Err(ClientMetadataVerificationError::IncoherentResponseType(res)) => res);
assert_eq!(res, response_type);
metadata.response_types = Some(vec![OAuthAuthorizationEndpointResponseType::None.into()]);
metadata.validate().unwrap();
}
#[test]
fn validate_jwks() {
let mut metadata = valid_client_metadata();
metadata.jwks_uri = Some(Url::parse("http://localhost/jwks").unwrap());
metadata.clone().validate().unwrap();
metadata.jwks = Some(jwks());
assert_matches!(
metadata.clone().validate(),
Err(ClientMetadataVerificationError::JwksUriAndJwksMutuallyExclusive)
);
metadata.jwks_uri = None;
metadata.validate().unwrap();
}
#[test]
fn validate_sector_identifier_uri() {
let mut metadata = valid_client_metadata();
let identifier_uri = Url::parse("http://localhost/").unwrap();
metadata.sector_identifier_uri = Some(identifier_uri.clone());
let (field, url) = assert_matches!(
metadata.clone().validate(),
Err(ClientMetadataVerificationError::UrlNonHttpsScheme(field, url)) => (field, url)
);
assert_eq!(field, "sector_identifier_uri");
assert_eq!(url, identifier_uri);
metadata.sector_identifier_uri = Some(Url::parse("https://localhost/").unwrap());
metadata.validate().unwrap();
}
#[test]
fn validate_token_endpoint_auth_method() {
let mut metadata = valid_client_metadata();
metadata.token_endpoint_auth_signing_alg = Some(JsonWebSignatureAlg::None);
let field = assert_matches!(
metadata.clone().validate(),
Err(ClientMetadataVerificationError::UnauthorizedSigningAlgNone(field)) => field
);
assert_eq!(field, "token_endpoint");
metadata.token_endpoint_auth_method = Some(OAuthClientAuthenticationMethod::PrivateKeyJwt);
metadata.token_endpoint_auth_signing_alg = Some(JsonWebSignatureAlg::Rs256);
assert_matches!(
metadata.clone().validate(),
Err(ClientMetadataVerificationError::MissingJwksForTokenMethod)
);
metadata.jwks_uri = Some(Url::parse("https://localhost/jwks").unwrap());
metadata.clone().validate().unwrap();
metadata.jwks_uri = None;
metadata.jwks = Some(jwks());
metadata.clone().validate().unwrap();
metadata.token_endpoint_auth_signing_alg = None;
let field = assert_matches!(
metadata.clone().validate(),
Err(ClientMetadataVerificationError::MissingAuthSigningAlg(field)) => field
);
assert_eq!(field, "token_endpoint");
metadata.token_endpoint_auth_method =
Some(OAuthClientAuthenticationMethod::ClientSecretJwt);
metadata.jwks = None;
let field = assert_matches!(
metadata.clone().validate(),
Err(ClientMetadataVerificationError::MissingAuthSigningAlg(field)) => field
);
assert_eq!(field, "token_endpoint");
metadata.token_endpoint_auth_signing_alg = Some(JsonWebSignatureAlg::Rs256);
metadata.validate().unwrap();
}
#[test]
fn validate_id_token_signed_response_alg() {
let mut metadata = valid_client_metadata();
metadata.id_token_signed_response_alg = Some(JsonWebSignatureAlg::None);
metadata.grant_types = Some(vec![GrantType::AuthorizationCode, GrantType::Implicit]);
metadata.response_types = Some(vec![
OAuthAuthorizationEndpointResponseType::CodeIdToken.into()
]);
assert_matches!(
metadata.clone().validate(),
Err(ClientMetadataVerificationError::IdTokenSigningAlgNone)
);
metadata.response_types = Some(vec![
OAuthAuthorizationEndpointResponseType::CodeIdTokenToken.into(),
]);
assert_matches!(
metadata.clone().validate(),
Err(ClientMetadataVerificationError::IdTokenSigningAlgNone)
);
metadata.response_types =
Some(vec![OAuthAuthorizationEndpointResponseType::IdToken.into()]);
assert_matches!(
metadata.clone().validate(),
Err(ClientMetadataVerificationError::IdTokenSigningAlgNone)
);
metadata.response_types = Some(vec![
OAuthAuthorizationEndpointResponseType::IdTokenToken.into()
]);
assert_matches!(
metadata.clone().validate(),
Err(ClientMetadataVerificationError::IdTokenSigningAlgNone)
);
metadata.response_types = Some(vec![
OAuthAuthorizationEndpointResponseType::Code.into(),
OAuthAuthorizationEndpointResponseType::CodeToken.into(),
OAuthAuthorizationEndpointResponseType::Token.into(),
OAuthAuthorizationEndpointResponseType::None.into(),
]);
metadata.validate().unwrap();
}
#[test]
fn validate_id_token_encrypted_response() {
let mut metadata = valid_client_metadata();
metadata.id_token_encrypted_response_enc = Some(JsonWebEncryptionEnc::A128CbcHs256);
let field = assert_matches!(
metadata.clone().validate(),
Err(ClientMetadataVerificationError::MissingEncryptionAlg(field)) => field
);
assert_eq!(field, "id_token");
metadata.id_token_encrypted_response_alg = Some(JsonWebEncryptionAlg::RsaOaep);
metadata.validate().unwrap();
}
#[test]
fn validate_userinfo_encrypted_response() {
let mut metadata = valid_client_metadata();
metadata.userinfo_encrypted_response_enc = Some(JsonWebEncryptionEnc::A128CbcHs256);
let field = assert_matches!(
metadata.clone().validate(),
Err(ClientMetadataVerificationError::MissingEncryptionAlg(field)) => field
);
assert_eq!(field, "userinfo");
metadata.userinfo_encrypted_response_alg = Some(JsonWebEncryptionAlg::RsaOaep);
metadata.validate().unwrap();
}
#[test]
fn validate_request_object_encryption() {
let mut metadata = valid_client_metadata();
metadata.request_object_encryption_enc = Some(JsonWebEncryptionEnc::A128CbcHs256);
let field = assert_matches!(
metadata.clone().validate(),
Err(ClientMetadataVerificationError::MissingEncryptionAlg(field)) => field
);
assert_eq!(field, "request_object");
metadata.request_object_encryption_alg = Some(JsonWebEncryptionAlg::RsaOaep);
metadata.validate().unwrap();
}
#[test]
fn validate_initiate_login_uri() {
let mut metadata = valid_client_metadata();
let initiate_uri = Url::parse("http://localhost/").unwrap();
metadata.initiate_login_uri = Some(initiate_uri.clone());
let (field, url) = assert_matches!(
metadata.clone().validate(),
Err(ClientMetadataVerificationError::UrlNonHttpsScheme(field, url)) => (field, url)
);
assert_eq!(field, "initiate_login_uri");
assert_eq!(url, initiate_uri);
metadata.initiate_login_uri = Some(Url::parse("https://localhost/").unwrap());
metadata.validate().unwrap();
}
#[test]
fn validate_introspection_encrypted_response() {
let mut metadata = valid_client_metadata();
metadata.introspection_encrypted_response_enc = Some(JsonWebEncryptionEnc::A128CbcHs256);
let field = assert_matches!(
metadata.clone().validate(),
Err(ClientMetadataVerificationError::MissingEncryptionAlg(field)) => field
);
assert_eq!(field, "introspection");
metadata.introspection_encrypted_response_alg = Some(JsonWebEncryptionAlg::RsaOaep);
metadata.validate().unwrap();
}
}