use std::{fmt, ops::Deref};
use language_tags::LanguageTag;
use mas_iana::{
jose::{JsonWebEncryptionAlg, JsonWebEncryptionEnc, JsonWebSignatureAlg},
oauth::{OAuthAccessTokenType, OAuthClientAuthenticationMethod, PkceCodeChallengeMethod},
};
use serde::{Deserialize, Serialize};
use serde_with::{
formats::SpaceSeparator, serde_as, skip_serializing_none, DeserializeFromStr, SerializeDisplay,
StringWithSeparator,
};
use thiserror::Error;
use url::Url;
use crate::{
requests::{Display, GrantType, Prompt, ResponseMode},
response_type::ResponseType,
};
#[derive(SerializeDisplay, DeserializeFromStr, Clone, PartialEq, Eq, Hash, Debug)]
pub enum AuthenticationMethodOrAccessTokenType {
AuthenticationMethod(OAuthClientAuthenticationMethod),
AccessTokenType(OAuthAccessTokenType),
Unknown(String),
}
impl core::fmt::Display for AuthenticationMethodOrAccessTokenType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::AuthenticationMethod(m) => m.fmt(f),
Self::AccessTokenType(t) => t.fmt(f),
Self::Unknown(s) => s.fmt(f),
}
}
}
impl core::str::FromStr for AuthenticationMethodOrAccessTokenType {
type Err = core::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match OAuthClientAuthenticationMethod::from_str(s) {
Ok(OAuthClientAuthenticationMethod::Unknown(_)) | Err(_) => {}
Ok(m) => return Ok(m.into()),
}
match OAuthAccessTokenType::from_str(s) {
Ok(OAuthAccessTokenType::Unknown(_)) | Err(_) => {}
Ok(m) => return Ok(m.into()),
}
Ok(Self::Unknown(s.to_owned()))
}
}
impl AuthenticationMethodOrAccessTokenType {
#[must_use]
pub fn authentication_method(&self) -> Option<&OAuthClientAuthenticationMethod> {
match self {
Self::AuthenticationMethod(m) => Some(m),
_ => None,
}
}
#[must_use]
pub fn access_token_type(&self) -> Option<&OAuthAccessTokenType> {
match self {
Self::AccessTokenType(t) => Some(t),
_ => None,
}
}
}
impl From<OAuthClientAuthenticationMethod> for AuthenticationMethodOrAccessTokenType {
fn from(t: OAuthClientAuthenticationMethod) -> Self {
Self::AuthenticationMethod(t)
}
}
impl From<OAuthAccessTokenType> for AuthenticationMethodOrAccessTokenType {
fn from(t: OAuthAccessTokenType) -> Self {
Self::AccessTokenType(t)
}
}
#[derive(SerializeDisplay, DeserializeFromStr, Clone, PartialEq, Eq, Hash, Debug)]
pub enum ApplicationType {
Web,
Native,
Unknown(String),
}
impl core::fmt::Display for ApplicationType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Web => f.write_str("web"),
Self::Native => f.write_str("native"),
Self::Unknown(s) => f.write_str(s),
}
}
}
impl core::str::FromStr for ApplicationType {
type Err = core::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"web" => Ok(Self::Web),
"native" => Ok(Self::Native),
s => Ok(Self::Unknown(s.to_owned())),
}
}
}
#[derive(SerializeDisplay, DeserializeFromStr, Clone, PartialEq, Eq, Hash, Debug)]
pub enum SubjectType {
Public,
Pairwise,
Unknown(String),
}
impl core::fmt::Display for SubjectType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Public => f.write_str("public"),
Self::Pairwise => f.write_str("pairwise"),
Self::Unknown(s) => f.write_str(s),
}
}
}
impl core::str::FromStr for SubjectType {
type Err = core::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"public" => Ok(Self::Public),
"pairwise" => Ok(Self::Pairwise),
s => Ok(Self::Unknown(s.to_owned())),
}
}
}
#[derive(SerializeDisplay, DeserializeFromStr, Clone, PartialEq, Eq, Hash, Debug)]
pub enum ClaimType {
Normal,
Aggregated,
Distributed,
Unknown(String),
}
impl core::fmt::Display for ClaimType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Normal => f.write_str("normal"),
Self::Aggregated => f.write_str("aggregated"),
Self::Distributed => f.write_str("distributed"),
Self::Unknown(s) => f.write_str(s),
}
}
}
impl core::str::FromStr for ClaimType {
type Err = core::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"normal" => Ok(Self::Normal),
"aggregated" => Ok(Self::Aggregated),
"distributed" => Ok(Self::Distributed),
s => Ok(Self::Unknown(s.to_owned())),
}
}
}
#[derive(
SerializeDisplay, DeserializeFromStr, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash,
)]
#[non_exhaustive]
pub enum AccountManagementAction {
Profile,
SessionsList,
SessionView,
SessionEnd,
AccountDeactivate,
CrossSigningReset,
Unknown(String),
}
impl core::fmt::Display for AccountManagementAction {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Profile => write!(f, "org.matrix.profile"),
Self::SessionsList => write!(f, "org.matrix.sessions_list"),
Self::SessionView => write!(f, "org.matrix.session_view"),
Self::SessionEnd => write!(f, "org.matrix.session_end"),
Self::AccountDeactivate => write!(f, "org.matrix.account_deactivate"),
Self::CrossSigningReset => write!(f, "org.matrix.cross_signing_reset"),
Self::Unknown(value) => write!(f, "{value}"),
}
}
}
impl core::str::FromStr for AccountManagementAction {
type Err = core::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"org.matrix.profile" => Ok(Self::Profile),
"org.matrix.sessions_list" => Ok(Self::SessionsList),
"org.matrix.session_view" => Ok(Self::SessionView),
"org.matrix.session_end" => Ok(Self::SessionEnd),
"org.matrix.account_deactivate" => Ok(Self::AccountDeactivate),
"org.matrix.cross_signing_reset" => Ok(Self::CrossSigningReset),
value => Ok(Self::Unknown(value.to_owned())),
}
}
}
pub static DEFAULT_RESPONSE_MODES_SUPPORTED: &[ResponseMode] =
&[ResponseMode::Query, ResponseMode::Fragment];
pub static DEFAULT_GRANT_TYPES_SUPPORTED: &[GrantType] =
&[GrantType::AuthorizationCode, GrantType::Implicit];
pub static DEFAULT_AUTH_METHODS_SUPPORTED: &[OAuthClientAuthenticationMethod] =
&[OAuthClientAuthenticationMethod::ClientSecretBasic];
pub static DEFAULT_CLAIM_TYPES_SUPPORTED: &[ClaimType] = &[ClaimType::Normal];
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct ProviderMetadata {
pub issuer: Option<String>,
pub authorization_endpoint: Option<Url>,
pub token_endpoint: Option<Url>,
pub jwks_uri: Option<Url>,
pub registration_endpoint: Option<Url>,
pub scopes_supported: Option<Vec<String>>,
pub response_types_supported: Option<Vec<ResponseType>>,
pub response_modes_supported: Option<Vec<ResponseMode>>,
pub grant_types_supported: Option<Vec<GrantType>>,
pub token_endpoint_auth_methods_supported: Option<Vec<OAuthClientAuthenticationMethod>>,
pub token_endpoint_auth_signing_alg_values_supported: Option<Vec<JsonWebSignatureAlg>>,
pub service_documentation: Option<Url>,
pub ui_locales_supported: Option<Vec<LanguageTag>>,
pub op_policy_uri: Option<Url>,
pub op_tos_uri: Option<Url>,
pub revocation_endpoint: Option<Url>,
pub revocation_endpoint_auth_methods_supported: Option<Vec<OAuthClientAuthenticationMethod>>,
pub revocation_endpoint_auth_signing_alg_values_supported: Option<Vec<JsonWebSignatureAlg>>,
pub introspection_endpoint: Option<Url>,
pub introspection_endpoint_auth_methods_supported:
Option<Vec<AuthenticationMethodOrAccessTokenType>>,
pub introspection_endpoint_auth_signing_alg_values_supported: Option<Vec<JsonWebSignatureAlg>>,
pub code_challenge_methods_supported: Option<Vec<PkceCodeChallengeMethod>>,
pub userinfo_endpoint: Option<Url>,
pub acr_values_supported: Option<Vec<String>>,
pub subject_types_supported: Option<Vec<SubjectType>>,
pub id_token_signing_alg_values_supported: Option<Vec<JsonWebSignatureAlg>>,
pub id_token_encryption_alg_values_supported: Option<Vec<JsonWebEncryptionAlg>>,
pub id_token_encryption_enc_values_supported: Option<Vec<JsonWebEncryptionEnc>>,
pub userinfo_signing_alg_values_supported: Option<Vec<JsonWebSignatureAlg>>,
pub userinfo_encryption_alg_values_supported: Option<Vec<JsonWebEncryptionAlg>>,
pub userinfo_encryption_enc_values_supported: Option<Vec<JsonWebEncryptionEnc>>,
pub request_object_signing_alg_values_supported: Option<Vec<JsonWebSignatureAlg>>,
pub request_object_encryption_alg_values_supported: Option<Vec<JsonWebEncryptionAlg>>,
pub request_object_encryption_enc_values_supported: Option<Vec<JsonWebEncryptionEnc>>,
pub display_values_supported: Option<Vec<Display>>,
pub claim_types_supported: Option<Vec<ClaimType>>,
pub claims_supported: Option<Vec<String>>,
pub claims_locales_supported: Option<Vec<LanguageTag>>,
pub claims_parameter_supported: Option<bool>,
pub request_parameter_supported: Option<bool>,
pub request_uri_parameter_supported: Option<bool>,
pub require_request_uri_registration: Option<bool>,
pub require_signed_request_object: Option<bool>,
pub pushed_authorization_request_endpoint: Option<Url>,
pub require_pushed_authorization_requests: Option<bool>,
pub prompt_values_supported: Option<Vec<Prompt>>,
pub device_authorization_endpoint: Option<Url>,
pub end_session_endpoint: Option<Url>,
pub account_management_uri: Option<Url>,
pub account_management_actions_supported: Option<Vec<AccountManagementAction>>,
}
impl ProviderMetadata {
pub fn validate(
self,
issuer: &str,
) -> Result<VerifiedProviderMetadata, ProviderMetadataVerificationError> {
let metadata = self.insecure_verify_metadata()?;
if metadata.issuer() != issuer {
return Err(ProviderMetadataVerificationError::IssuerUrlsDontMatch);
}
validate_url(
"issuer",
&metadata
.issuer()
.parse()
.map_err(|_| ProviderMetadataVerificationError::IssuerNotUrl)?,
ExtraUrlRestrictions::NoQueryOrFragment,
)?;
validate_url(
"authorization_endpoint",
metadata.authorization_endpoint(),
ExtraUrlRestrictions::NoFragment,
)?;
validate_url(
"token_endpoint",
metadata.token_endpoint(),
ExtraUrlRestrictions::NoFragment,
)?;
validate_url("jwks_uri", metadata.jwks_uri(), ExtraUrlRestrictions::None)?;
if let Some(url) = &metadata.registration_endpoint {
validate_url("registration_endpoint", url, ExtraUrlRestrictions::None)?;
}
if let Some(scopes) = &metadata.scopes_supported {
if !scopes.iter().any(|s| s == "openid") {
return Err(ProviderMetadataVerificationError::ScopesMissingOpenid);
}
}
validate_signing_alg_values_supported(
"token_endpoint",
metadata
.token_endpoint_auth_signing_alg_values_supported
.iter()
.flatten(),
metadata
.token_endpoint_auth_methods_supported
.iter()
.flatten(),
)?;
if let Some(url) = &metadata.revocation_endpoint {
validate_url("revocation_endpoint", url, ExtraUrlRestrictions::NoFragment)?;
}
validate_signing_alg_values_supported(
"revocation_endpoint",
metadata
.revocation_endpoint_auth_signing_alg_values_supported
.iter()
.flatten(),
metadata
.revocation_endpoint_auth_methods_supported
.iter()
.flatten(),
)?;
if let Some(url) = &metadata.introspection_endpoint {
validate_url("introspection_endpoint", url, ExtraUrlRestrictions::None)?;
}
let introspection_methods = metadata
.introspection_endpoint_auth_methods_supported
.as_ref()
.map(|v| {
v.iter()
.filter_map(AuthenticationMethodOrAccessTokenType::authentication_method)
.collect::<Vec<_>>()
});
validate_signing_alg_values_supported(
"introspection_endpoint",
metadata
.introspection_endpoint_auth_signing_alg_values_supported
.iter()
.flatten(),
introspection_methods.into_iter().flatten(),
)?;
if let Some(url) = &metadata.userinfo_endpoint {
validate_url("userinfo_endpoint", url, ExtraUrlRestrictions::None)?;
}
if let Some(url) = &metadata.pushed_authorization_request_endpoint {
validate_url(
"pushed_authorization_request_endpoint",
url,
ExtraUrlRestrictions::None,
)?;
}
if let Some(url) = &metadata.end_session_endpoint {
validate_url("end_session_endpoint", url, ExtraUrlRestrictions::None)?;
}
Ok(metadata)
}
pub fn insecure_verify_metadata(
self,
) -> Result<VerifiedProviderMetadata, ProviderMetadataVerificationError> {
self.issuer
.as_ref()
.ok_or(ProviderMetadataVerificationError::MissingIssuer)?;
self.authorization_endpoint
.as_ref()
.ok_or(ProviderMetadataVerificationError::MissingAuthorizationEndpoint)?;
self.token_endpoint
.as_ref()
.ok_or(ProviderMetadataVerificationError::MissingTokenEndpoint)?;
self.jwks_uri
.as_ref()
.ok_or(ProviderMetadataVerificationError::MissingJwksUri)?;
self.response_types_supported
.as_ref()
.ok_or(ProviderMetadataVerificationError::MissingResponseTypesSupported)?;
self.subject_types_supported
.as_ref()
.ok_or(ProviderMetadataVerificationError::MissingSubjectTypesSupported)?;
self.id_token_signing_alg_values_supported
.as_ref()
.ok_or(ProviderMetadataVerificationError::MissingIdTokenSigningAlgValuesSupported)?;
Ok(VerifiedProviderMetadata { inner: self })
}
#[must_use]
pub fn response_modes_supported(&self) -> &[ResponseMode] {
self.response_modes_supported
.as_deref()
.unwrap_or(DEFAULT_RESPONSE_MODES_SUPPORTED)
}
#[must_use]
pub fn grant_types_supported(&self) -> &[GrantType] {
self.grant_types_supported
.as_deref()
.unwrap_or(DEFAULT_GRANT_TYPES_SUPPORTED)
}
#[must_use]
pub fn token_endpoint_auth_methods_supported(&self) -> &[OAuthClientAuthenticationMethod] {
self.token_endpoint_auth_methods_supported
.as_deref()
.unwrap_or(DEFAULT_AUTH_METHODS_SUPPORTED)
}
#[must_use]
pub fn revocation_endpoint_auth_methods_supported(&self) -> &[OAuthClientAuthenticationMethod] {
self.revocation_endpoint_auth_methods_supported
.as_deref()
.unwrap_or(DEFAULT_AUTH_METHODS_SUPPORTED)
}
#[must_use]
pub fn claim_types_supported(&self) -> &[ClaimType] {
self.claim_types_supported
.as_deref()
.unwrap_or(DEFAULT_CLAIM_TYPES_SUPPORTED)
}
#[must_use]
pub fn claims_parameter_supported(&self) -> bool {
self.claims_parameter_supported.unwrap_or(false)
}
#[must_use]
pub fn request_parameter_supported(&self) -> bool {
self.request_parameter_supported.unwrap_or(false)
}
#[must_use]
pub fn request_uri_parameter_supported(&self) -> bool {
self.request_uri_parameter_supported.unwrap_or(true)
}
#[must_use]
pub fn require_request_uri_registration(&self) -> bool {
self.require_request_uri_registration.unwrap_or(false)
}
#[must_use]
pub fn require_signed_request_object(&self) -> bool {
self.require_signed_request_object.unwrap_or(false)
}
#[must_use]
pub fn require_pushed_authorization_requests(&self) -> bool {
self.require_pushed_authorization_requests.unwrap_or(false)
}
}
#[derive(Debug, Clone)]
pub struct VerifiedProviderMetadata {
inner: ProviderMetadata,
}
impl VerifiedProviderMetadata {
#[must_use]
pub fn issuer(&self) -> &str {
match &self.issuer {
Some(u) => u,
None => unreachable!(),
}
}
#[must_use]
pub fn authorization_endpoint(&self) -> &Url {
match &self.authorization_endpoint {
Some(u) => u,
None => unreachable!(),
}
}
#[must_use]
pub fn token_endpoint(&self) -> &Url {
match &self.token_endpoint {
Some(u) => u,
None => unreachable!(),
}
}
#[must_use]
pub fn jwks_uri(&self) -> &Url {
match &self.jwks_uri {
Some(u) => u,
None => unreachable!(),
}
}
#[must_use]
pub fn response_types_supported(&self) -> &[ResponseType] {
match &self.response_types_supported {
Some(u) => u,
None => unreachable!(),
}
}
#[must_use]
pub fn subject_types_supported(&self) -> &[SubjectType] {
match &self.subject_types_supported {
Some(u) => u,
None => unreachable!(),
}
}
#[must_use]
pub fn id_token_signing_alg_values_supported(&self) -> &[JsonWebSignatureAlg] {
match &self.id_token_signing_alg_values_supported {
Some(u) => u,
None => unreachable!(),
}
}
}
impl Deref for VerifiedProviderMetadata {
type Target = ProviderMetadata;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
#[derive(Debug, Error)]
pub enum ProviderMetadataVerificationError {
#[error("issuer is missing")]
MissingIssuer,
#[error("issuer is not a valid URL")]
IssuerNotUrl,
#[error("authorization endpoint is missing")]
MissingAuthorizationEndpoint,
#[error("token endpoint is missing")]
MissingTokenEndpoint,
#[error("JWK Set URI is missing")]
MissingJwksUri,
#[error("supported response types are missing")]
MissingResponseTypesSupported,
#[error("supported subject types are missing")]
MissingSubjectTypesSupported,
#[error("supported ID token signing algorithm values are missing")]
MissingIdTokenSigningAlgValuesSupported,
#[error("{0}'s URL doesn't use a https scheme: {1}")]
UrlNonHttpsScheme(&'static str, Url),
#[error("{0}'s URL contains a query: {1}")]
UrlWithQuery(&'static str, Url),
#[error("{0}'s URL contains a fragment: {1}")]
UrlWithFragment(&'static str, Url),
#[error("issuer URLs don't match")]
IssuerUrlsDontMatch,
#[error("missing openid scope")]
ScopesMissingOpenid,
#[error("missing `code` response type")]
ResponseTypesMissingCode,
#[error("missing `id_token` response type")]
ResponseTypesMissingIdToken,
#[error("missing `id_token token` response type")]
ResponseTypesMissingIdTokenToken,
#[error("missing `authorization_code` grant type")]
GrantTypesMissingAuthorizationCode,
#[error("missing `implicit` grant type")]
GrantTypesMissingImplicit,
#[error("{0} missing auth signing algorithm values")]
MissingAuthSigningAlgValues(&'static str),
#[error("{0} signing algorithm values contain `none`")]
SigningAlgValuesWithNone(&'static str),
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum ExtraUrlRestrictions {
None,
NoFragment,
NoQueryOrFragment,
}
impl ExtraUrlRestrictions {
fn can_have_fragment(self) -> bool {
self == Self::None
}
fn can_have_query(self) -> bool {
self != Self::NoQueryOrFragment
}
}
fn validate_url(
field: &'static str,
url: &Url,
restrictions: ExtraUrlRestrictions,
) -> Result<(), ProviderMetadataVerificationError> {
if url.scheme() != "https" {
return Err(ProviderMetadataVerificationError::UrlNonHttpsScheme(
field,
url.clone(),
));
}
if !restrictions.can_have_query() && url.query().is_some() {
return Err(ProviderMetadataVerificationError::UrlWithQuery(
field,
url.clone(),
));
}
if !restrictions.can_have_fragment() && url.fragment().is_some() {
return Err(ProviderMetadataVerificationError::UrlWithFragment(
field,
url.clone(),
));
}
Ok(())
}
fn validate_signing_alg_values_supported<'a>(
endpoint: &'static str,
values: impl Iterator<Item = &'a JsonWebSignatureAlg>,
mut methods: impl Iterator<Item = &'a OAuthClientAuthenticationMethod>,
) -> Result<(), ProviderMetadataVerificationError> {
let mut no_values = true;
for value in values {
if *value == JsonWebSignatureAlg::None {
return Err(ProviderMetadataVerificationError::SigningAlgValuesWithNone(
endpoint,
));
}
no_values = false;
}
if no_values
&& methods.any(|method| {
matches!(
method,
OAuthClientAuthenticationMethod::ClientSecretJwt
| OAuthClientAuthenticationMethod::PrivateKeyJwt
)
})
{
return Err(ProviderMetadataVerificationError::MissingAuthSigningAlgValues(endpoint));
}
Ok(())
}
#[skip_serializing_none]
#[serde_as]
#[derive(Default, Serialize, Deserialize, Clone)]
pub struct RpInitiatedLogoutRequest {
pub id_token_hint: Option<String>,
pub logout_hint: Option<String>,
pub client_id: Option<String>,
pub post_logout_redirect_uri: Option<Url>,
pub state: Option<String>,
#[serde_as(as = "Option<StringWithSeparator::<SpaceSeparator, LanguageTag>>")]
#[serde(default)]
pub ui_locales: Option<Vec<LanguageTag>>,
}
impl fmt::Debug for RpInitiatedLogoutRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RpInitiatedLogoutRequest")
.field("logout_hint", &self.logout_hint)
.field("post_logout_redirect_uri", &self.post_logout_redirect_uri)
.field("ui_locales", &self.ui_locales)
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use mas_iana::{
jose::JsonWebSignatureAlg,
oauth::{OAuthAuthorizationEndpointResponseType, OAuthClientAuthenticationMethod},
};
use url::Url;
use super::*;
fn valid_provider_metadata() -> (ProviderMetadata, String) {
let issuer = "https://localhost".to_owned();
let metadata = ProviderMetadata {
issuer: Some(issuer.clone()),
authorization_endpoint: Some(Url::parse("https://localhost/auth").unwrap()),
token_endpoint: Some(Url::parse("https://localhost/token").unwrap()),
jwks_uri: Some(Url::parse("https://localhost/jwks").unwrap()),
response_types_supported: Some(vec![
OAuthAuthorizationEndpointResponseType::Code.into()
]),
subject_types_supported: Some(vec![SubjectType::Public]),
id_token_signing_alg_values_supported: Some(vec![JsonWebSignatureAlg::Rs256]),
..Default::default()
};
(metadata, issuer)
}
#[test]
fn validate_required_metadata() {
let (metadata, issuer) = valid_provider_metadata();
metadata.validate(&issuer).unwrap();
}
#[test]
fn validate_issuer() {
let (mut metadata, issuer) = valid_provider_metadata();
metadata.issuer = None;
assert_matches!(
metadata.clone().validate(&issuer),
Err(ProviderMetadataVerificationError::MissingIssuer)
);
metadata.issuer = Some("not-an-url".to_owned());
assert_matches!(
metadata.clone().validate("not-an-url"),
Err(ProviderMetadataVerificationError::IssuerNotUrl)
);
metadata.issuer = Some("https://example.com/".to_owned());
assert_matches!(
metadata.clone().validate(&issuer),
Err(ProviderMetadataVerificationError::IssuerUrlsDontMatch)
);
let issuer = "http://localhost/".to_owned();
metadata.issuer = Some(issuer.clone());
let (field, url) = assert_matches!(
metadata.clone().validate(&issuer),
Err(ProviderMetadataVerificationError::UrlNonHttpsScheme(field, url)) => (field, url)
);
assert_eq!(field, "issuer");
assert_eq!(url.as_str(), issuer);
let issuer = "https://localhost/?query".to_owned();
metadata.issuer = Some(issuer.clone());
let (field, url) = assert_matches!(
metadata.clone().validate(&issuer),
Err(ProviderMetadataVerificationError::UrlWithQuery(field, url)) => (field, url)
);
assert_eq!(field, "issuer");
assert_eq!(url.as_str(), issuer);
let issuer = "https://localhost/#fragment".to_owned();
metadata.issuer = Some(issuer.clone());
let (field, url) = assert_matches!(
metadata.clone().validate(&issuer),
Err(ProviderMetadataVerificationError::UrlWithFragment(field, url)) => (field, url)
);
assert_eq!(field, "issuer");
assert_eq!(url.as_str(), issuer);
let issuer = "https://localhost/issuer1".to_owned();
metadata.issuer = Some(issuer.clone());
metadata.validate(&issuer).unwrap();
}
#[test]
fn validate_authorization_endpoint() {
let (mut metadata, issuer) = valid_provider_metadata();
metadata.authorization_endpoint = None;
assert_matches!(
metadata.clone().validate(&issuer),
Err(ProviderMetadataVerificationError::MissingAuthorizationEndpoint)
);
let endpoint = Url::parse("http://localhost/auth").unwrap();
metadata.authorization_endpoint = Some(endpoint.clone());
let (field, url) = assert_matches!(
metadata.clone().validate(&issuer),
Err(ProviderMetadataVerificationError::UrlNonHttpsScheme(field, url)) => (field, url)
);
assert_eq!(field, "authorization_endpoint");
assert_eq!(url, endpoint);
let endpoint = Url::parse("https://localhost/auth#fragment").unwrap();
metadata.authorization_endpoint = Some(endpoint.clone());
let (field, url) = assert_matches!(
metadata.clone().validate(&issuer),
Err(ProviderMetadataVerificationError::UrlWithFragment(field, url)) => (field, url)
);
assert_eq!(field, "authorization_endpoint");
assert_eq!(url, endpoint);
metadata.authorization_endpoint = Some(Url::parse("https://localhost/auth?query").unwrap());
metadata.validate(&issuer).unwrap();
}
#[test]
fn validate_token_endpoint() {
let (mut metadata, issuer) = valid_provider_metadata();
metadata.token_endpoint = None;
assert_matches!(
metadata.clone().validate(&issuer),
Err(ProviderMetadataVerificationError::MissingTokenEndpoint)
);
let endpoint = Url::parse("http://localhost/token").unwrap();
metadata.token_endpoint = Some(endpoint.clone());
let (field, url) = assert_matches!(
metadata.clone().validate(&issuer),
Err(ProviderMetadataVerificationError::UrlNonHttpsScheme(field, url)) => (field, url)
);
assert_eq!(field, "token_endpoint");
assert_eq!(url, endpoint);
let endpoint = Url::parse("https://localhost/token#fragment").unwrap();
metadata.token_endpoint = Some(endpoint.clone());
let (field, url) = assert_matches!(
metadata.clone().validate(&issuer),
Err(ProviderMetadataVerificationError::UrlWithFragment(field, url)) => (field, url)
);
assert_eq!(field, "token_endpoint");
assert_eq!(url, endpoint);
metadata.token_endpoint = Some(Url::parse("https://localhost/token?query").unwrap());
metadata.validate(&issuer).unwrap();
}
#[test]
fn validate_jwks_uri() {
let (mut metadata, issuer) = valid_provider_metadata();
metadata.jwks_uri = None;
assert_matches!(
metadata.clone().validate(&issuer),
Err(ProviderMetadataVerificationError::MissingJwksUri)
);
let endpoint = Url::parse("http://localhost/jwks").unwrap();
metadata.jwks_uri = Some(endpoint.clone());
let (field, url) = assert_matches!(
metadata.clone().validate(&issuer),
Err(ProviderMetadataVerificationError::UrlNonHttpsScheme(field, url)) => (field, url)
);
assert_eq!(field, "jwks_uri");
assert_eq!(url, endpoint);
metadata.jwks_uri = Some(Url::parse("https://localhost/token?query#fragment").unwrap());
metadata.validate(&issuer).unwrap();
}
#[test]
fn validate_registration_endpoint() {
let (mut metadata, issuer) = valid_provider_metadata();
let endpoint = Url::parse("http://localhost/registration").unwrap();
metadata.registration_endpoint = Some(endpoint.clone());
let (field, url) = assert_matches!(
metadata.clone().validate(&issuer),
Err(ProviderMetadataVerificationError::UrlNonHttpsScheme(field, url)) => (field, url)
);
assert_eq!(field, "registration_endpoint");
assert_eq!(url, endpoint);
metadata.registration_endpoint = None;
metadata.clone().validate(&issuer).unwrap();
metadata.registration_endpoint =
Some(Url::parse("https://localhost/registration?query#fragment").unwrap());
metadata.validate(&issuer).unwrap();
}
#[test]
fn validate_scopes_supported() {
let (mut metadata, issuer) = valid_provider_metadata();
metadata.scopes_supported = Some(vec!["custom".to_owned()]);
assert_matches!(
metadata.clone().validate(&issuer),
Err(ProviderMetadataVerificationError::ScopesMissingOpenid)
);
metadata.scopes_supported = None;
metadata.clone().validate(&issuer).unwrap();
metadata.scopes_supported = Some(vec!["openid".to_owned(), "custom".to_owned()]);
metadata.validate(&issuer).unwrap();
}
#[test]
fn validate_response_types_supported() {
let (mut metadata, issuer) = valid_provider_metadata();
metadata.response_types_supported = None;
assert_matches!(
metadata.clone().validate(&issuer),
Err(ProviderMetadataVerificationError::MissingResponseTypesSupported)
);
metadata.response_types_supported =
Some(vec![OAuthAuthorizationEndpointResponseType::Code.into()]);
metadata.validate(&issuer).unwrap();
}
#[test]
fn validate_token_endpoint_signing_alg_values_supported() {
let (mut metadata, issuer) = valid_provider_metadata();
metadata.token_endpoint_auth_signing_alg_values_supported = None;
metadata.token_endpoint_auth_methods_supported = None;
metadata.clone().validate(&issuer).unwrap();
metadata.token_endpoint_auth_signing_alg_values_supported =
Some(vec![JsonWebSignatureAlg::None]);
let endpoint = assert_matches!(
metadata.clone().validate(&issuer),
Err(ProviderMetadataVerificationError::SigningAlgValuesWithNone(endpoint)) => endpoint
);
assert_eq!(endpoint, "token_endpoint");
metadata.token_endpoint_auth_signing_alg_values_supported =
Some(vec![JsonWebSignatureAlg::Rs256, JsonWebSignatureAlg::EdDsa]);
metadata.clone().validate(&issuer).unwrap();
metadata.token_endpoint_auth_methods_supported =
Some(vec![OAuthClientAuthenticationMethod::ClientSecretJwt]);
metadata.token_endpoint_auth_signing_alg_values_supported = None;
let endpoint = assert_matches!(
metadata.clone().validate(&issuer),
Err(ProviderMetadataVerificationError::MissingAuthSigningAlgValues(endpoint)) => endpoint
);
assert_eq!(endpoint, "token_endpoint");
metadata.token_endpoint_auth_signing_alg_values_supported =
Some(vec![JsonWebSignatureAlg::Rs256]);
metadata.clone().validate(&issuer).unwrap();
metadata.token_endpoint_auth_methods_supported =
Some(vec![OAuthClientAuthenticationMethod::PrivateKeyJwt]);
metadata.token_endpoint_auth_signing_alg_values_supported = None;
let endpoint = assert_matches!(
metadata.clone().validate(&issuer),
Err(ProviderMetadataVerificationError::MissingAuthSigningAlgValues(endpoint)) => endpoint
);
assert_eq!(endpoint, "token_endpoint");
metadata.token_endpoint_auth_signing_alg_values_supported =
Some(vec![JsonWebSignatureAlg::Rs256]);
metadata.clone().validate(&issuer).unwrap();
metadata.token_endpoint_auth_methods_supported = Some(vec![
OAuthClientAuthenticationMethod::ClientSecretBasic,
OAuthClientAuthenticationMethod::ClientSecretPost,
]);
metadata.token_endpoint_auth_signing_alg_values_supported = None;
metadata.validate(&issuer).unwrap();
}
#[test]
fn validate_revocation_endpoint() {
let (mut metadata, issuer) = valid_provider_metadata();
metadata.revocation_endpoint = None;
metadata.clone().validate(&issuer).unwrap();
let endpoint = Url::parse("http://localhost/revocation").unwrap();
metadata.revocation_endpoint = Some(endpoint.clone());
let (field, url) = assert_matches!(
metadata.clone().validate(&issuer),
Err(ProviderMetadataVerificationError::UrlNonHttpsScheme(field, url)) => (field, url)
);
assert_eq!(field, "revocation_endpoint");
assert_eq!(url, endpoint);
let endpoint = Url::parse("https://localhost/revocation#fragment").unwrap();
metadata.revocation_endpoint = Some(endpoint.clone());
let (field, url) = assert_matches!(
metadata.clone().validate(&issuer),
Err(ProviderMetadataVerificationError::UrlWithFragment(field, url)) => (field, url)
);
assert_eq!(field, "revocation_endpoint");
assert_eq!(url, endpoint);
metadata.revocation_endpoint =
Some(Url::parse("https://localhost/revocation?query").unwrap());
metadata.validate(&issuer).unwrap();
}
#[test]
fn validate_revocation_endpoint_signing_alg_values_supported() {
let (mut metadata, issuer) = valid_provider_metadata();
metadata.revocation_endpoint_auth_signing_alg_values_supported = None;
metadata.revocation_endpoint_auth_methods_supported = None;
metadata.clone().validate(&issuer).unwrap();
metadata.revocation_endpoint_auth_signing_alg_values_supported =
Some(vec![JsonWebSignatureAlg::None]);
let endpoint = assert_matches!(
metadata.validate(&issuer),
Err(ProviderMetadataVerificationError::SigningAlgValuesWithNone(endpoint)) => endpoint
);
assert_eq!(endpoint, "revocation_endpoint");
}
#[test]
fn validate_introspection_endpoint() {
let (mut metadata, issuer) = valid_provider_metadata();
metadata.introspection_endpoint = None;
metadata.clone().validate(&issuer).unwrap();
let endpoint = Url::parse("http://localhost/introspection").unwrap();
metadata.introspection_endpoint = Some(endpoint.clone());
let (field, url) = assert_matches!(
metadata.clone().validate(&issuer),
Err(ProviderMetadataVerificationError::UrlNonHttpsScheme(field, url)) => (field, url)
);
assert_eq!(field, "introspection_endpoint");
assert_eq!(url, endpoint);
metadata.introspection_endpoint =
Some(Url::parse("https://localhost/introspection?query#fragment").unwrap());
metadata.validate(&issuer).unwrap();
}
#[test]
fn validate_introspection_endpoint_signing_alg_values_supported() {
let (mut metadata, issuer) = valid_provider_metadata();
metadata.introspection_endpoint_auth_signing_alg_values_supported = None;
metadata.introspection_endpoint_auth_methods_supported = None;
metadata.clone().validate(&issuer).unwrap();
metadata.introspection_endpoint_auth_signing_alg_values_supported =
Some(vec![JsonWebSignatureAlg::None]);
let endpoint = assert_matches!(
metadata.validate(&issuer),
Err(ProviderMetadataVerificationError::SigningAlgValuesWithNone(endpoint)) => endpoint
);
assert_eq!(endpoint, "introspection_endpoint");
}
#[test]
fn validate_userinfo_endpoint() {
let (mut metadata, issuer) = valid_provider_metadata();
metadata.userinfo_endpoint = None;
metadata.clone().validate(&issuer).unwrap();
let endpoint = Url::parse("http://localhost/userinfo").unwrap();
metadata.userinfo_endpoint = Some(endpoint.clone());
let (field, url) = assert_matches!(
metadata.clone().validate(&issuer),
Err(ProviderMetadataVerificationError::UrlNonHttpsScheme(field, url)) => (field, url)
);
assert_eq!(field, "userinfo_endpoint");
assert_eq!(url, endpoint);
metadata.userinfo_endpoint =
Some(Url::parse("https://localhost/userinfo?query#fragment").unwrap());
metadata.validate(&issuer).unwrap();
}
#[test]
fn validate_subject_types_supported() {
let (mut metadata, issuer) = valid_provider_metadata();
metadata.subject_types_supported = None;
assert_matches!(
metadata.clone().validate(&issuer),
Err(ProviderMetadataVerificationError::MissingSubjectTypesSupported)
);
metadata.subject_types_supported = Some(vec![SubjectType::Public, SubjectType::Pairwise]);
metadata.validate(&issuer).unwrap();
}
#[test]
fn validate_id_token_signing_alg_values_supported() {
let (mut metadata, issuer) = valid_provider_metadata();
metadata.id_token_signing_alg_values_supported = None;
assert_matches!(
metadata.clone().validate(&issuer),
Err(ProviderMetadataVerificationError::MissingIdTokenSigningAlgValuesSupported)
);
metadata.id_token_signing_alg_values_supported =
Some(vec![JsonWebSignatureAlg::Rs256, JsonWebSignatureAlg::EdDsa]);
metadata.validate(&issuer).unwrap();
}
#[test]
fn validate_pushed_authorization_request_endpoint() {
let (mut metadata, issuer) = valid_provider_metadata();
metadata.pushed_authorization_request_endpoint = None;
metadata.clone().validate(&issuer).unwrap();
let endpoint = Url::parse("http://localhost/par").unwrap();
metadata.pushed_authorization_request_endpoint = Some(endpoint.clone());
let (field, url) = assert_matches!(
metadata.clone().validate(&issuer),
Err(ProviderMetadataVerificationError::UrlNonHttpsScheme(field, url)) => (field, url)
);
assert_eq!(field, "pushed_authorization_request_endpoint");
assert_eq!(url, endpoint);
metadata.pushed_authorization_request_endpoint =
Some(Url::parse("https://localhost/par?query#fragment").unwrap());
metadata.validate(&issuer).unwrap();
}
#[test]
fn serialize_application_type() {
assert_eq!(
serde_json::to_string(&ApplicationType::Web).unwrap(),
"\"web\""
);
assert_eq!(
serde_json::to_string(&ApplicationType::Native).unwrap(),
"\"native\""
);
}
#[test]
fn deserialize_application_type() {
assert_eq!(
serde_json::from_str::<ApplicationType>("\"web\"").unwrap(),
ApplicationType::Web
);
assert_eq!(
serde_json::from_str::<ApplicationType>("\"native\"").unwrap(),
ApplicationType::Native
);
}
#[test]
fn serialize_subject_type() {
assert_eq!(
serde_json::to_string(&SubjectType::Public).unwrap(),
"\"public\""
);
assert_eq!(
serde_json::to_string(&SubjectType::Pairwise).unwrap(),
"\"pairwise\""
);
}
#[test]
fn deserialize_subject_type() {
assert_eq!(
serde_json::from_str::<SubjectType>("\"public\"").unwrap(),
SubjectType::Public
);
assert_eq!(
serde_json::from_str::<SubjectType>("\"pairwise\"").unwrap(),
SubjectType::Pairwise
);
}
#[test]
fn serialize_claim_type() {
assert_eq!(
serde_json::to_string(&ClaimType::Normal).unwrap(),
"\"normal\""
);
assert_eq!(
serde_json::to_string(&ClaimType::Aggregated).unwrap(),
"\"aggregated\""
);
assert_eq!(
serde_json::to_string(&ClaimType::Distributed).unwrap(),
"\"distributed\""
);
}
#[test]
fn deserialize_claim_type() {
assert_eq!(
serde_json::from_str::<ClaimType>("\"normal\"").unwrap(),
ClaimType::Normal
);
assert_eq!(
serde_json::from_str::<ClaimType>("\"aggregated\"").unwrap(),
ClaimType::Aggregated
);
assert_eq!(
serde_json::from_str::<ClaimType>("\"distributed\"").unwrap(),
ClaimType::Distributed
);
}
#[test]
fn deserialize_auth_method_or_token_type_type() {
assert_eq!(
serde_json::from_str::<AuthenticationMethodOrAccessTokenType>("\"none\"").unwrap(),
AuthenticationMethodOrAccessTokenType::AuthenticationMethod(
OAuthClientAuthenticationMethod::None
)
);
assert_eq!(
serde_json::from_str::<AuthenticationMethodOrAccessTokenType>("\"Bearer\"").unwrap(),
AuthenticationMethodOrAccessTokenType::AccessTokenType(OAuthAccessTokenType::Bearer)
);
assert_eq!(
serde_json::from_str::<AuthenticationMethodOrAccessTokenType>("\"unknown_value\"")
.unwrap(),
AuthenticationMethodOrAccessTokenType::Unknown("unknown_value".to_owned())
);
}
}