1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-07-29 22:01:14 +03:00

Allow to validate client metadata

According to OpenID Connect Dynamic Client Registration Spec 1.0.
Introduce VerifiedClientMetadata.
This commit is contained in:
Kévin Commaille
2022-08-16 11:25:40 +02:00
committed by Quentin Gliech
parent a543af4de3
commit e202c3dd6d
9 changed files with 1923 additions and 235 deletions

View File

@ -16,13 +16,13 @@ use std::sync::Arc;
use axum::{response::IntoResponse, Extension, Json};
use hyper::StatusCode;
use mas_iana::oauth::{OAuthAuthorizationEndpointResponseType, OAuthClientAuthenticationMethod};
use mas_policy::{PolicyFactory, Violation};
use mas_storage::oauth2::client::insert_client;
use oauth2_types::{
errors::{INVALID_CLIENT_METADATA, INVALID_REDIRECT_URI, SERVER_ERROR},
registration::{ClientMetadata, ClientRegistrationResponse},
requests::GrantType,
registration::{
ClientMetadata, ClientMetadataVerificationError, ClientRegistrationResponse, Localized,
},
};
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use sqlx::PgPool;
@ -53,6 +53,18 @@ impl From<sqlx::Error> for RouteError {
}
}
impl From<ClientMetadataVerificationError> for RouteError {
fn from(e: ClientMetadataVerificationError) -> Self {
match e {
ClientMetadataVerificationError::MissingRedirectUris
| ClientMetadataVerificationError::RedirectUriWithFragment(_) => {
Self::InvalidRedirectUri
}
_ => Self::InvalidClientMetadata,
}
}
}
// TODO: there is probably a better way to do achieve this. ClientError only
// works for static strings
#[derive(serde::Serialize)]
@ -111,55 +123,18 @@ pub(crate) async fn post(
) -> Result<impl IntoResponse, RouteError> {
info!(?body, "Client registration");
// Let's validate a bunch of things on the client body first
for uri in &body.redirect_uris {
if uri.fragment().is_some() {
return Err(RouteError::InvalidRedirectUri);
}
}
// Check that the client did not send both a jwks and a jwks_uri
if body.jwks_uri.is_some() && body.jwks.is_some() {
return Err(RouteError::InvalidClientMetadata);
}
// Check that the grant_types and the response_types are coherent
let has_implicit = body.grant_types.contains(&GrantType::Implicit);
let has_authorization_code = body.grant_types.contains(&GrantType::AuthorizationCode);
let has_both = has_implicit && has_authorization_code;
for response_type in &body.response_types {
let is_ok = match response_type {
OAuthAuthorizationEndpointResponseType::Code => has_authorization_code,
OAuthAuthorizationEndpointResponseType::CodeIdToken
| OAuthAuthorizationEndpointResponseType::CodeIdTokenToken
| OAuthAuthorizationEndpointResponseType::CodeToken => has_both,
OAuthAuthorizationEndpointResponseType::IdToken
| OAuthAuthorizationEndpointResponseType::IdTokenToken
| OAuthAuthorizationEndpointResponseType::Token => has_implicit,
OAuthAuthorizationEndpointResponseType::None => true,
};
if !is_ok {
return Err(RouteError::InvalidClientMetadata);
}
}
// If the private_key_jwt auth method is used, check that we actually have a
// JWKS for that client
if body.token_endpoint_auth_method == Some(OAuthClientAuthenticationMethod::PrivateKeyJwt)
&& body.jwks_uri.is_none()
&& body.jwks.is_none()
{
return Err(RouteError::InvalidClientMetadata);
}
// Validate the body
let metadata = body.validate()?;
let mut policy = policy_factory.instantiate().await?;
let res = policy.evaluate_client_registration(&body).await?;
let res = policy.evaluate_client_registration(&metadata).await?;
if !res.valid() {
return Err(RouteError::PolicyDenied(res.violations));
}
// Contacts was checked by the policy
let contacts = metadata.contacts.as_deref().unwrap_or_default();
// Grab a txn
let mut txn = pool.begin().await?;
@ -173,23 +148,26 @@ pub(crate) async fn post(
insert_client(
&mut txn,
&client_id,
&body.redirect_uris,
metadata.redirect_uris(),
None,
&body.response_types,
&body.grant_types,
&body.contacts,
body.client_name.as_deref(),
body.logo_uri.as_ref(),
body.client_uri.as_ref(),
body.policy_uri.as_ref(),
body.tos_uri.as_ref(),
body.jwks_uri.as_ref(),
body.jwks.as_ref(),
body.id_token_signed_response_alg,
body.userinfo_signed_response_alg,
body.token_endpoint_auth_method,
body.token_endpoint_auth_signing_alg,
body.initiate_login_uri.as_ref(),
metadata.response_types(),
metadata.grant_types(),
contacts,
metadata
.client_name
.as_ref()
.map(|l| l.non_localized().as_ref()),
metadata.logo_uri.as_ref().map(Localized::non_localized),
metadata.client_uri.as_ref().map(Localized::non_localized),
metadata.policy_uri.as_ref().map(Localized::non_localized),
metadata.tos_uri.as_ref().map(Localized::non_localized),
metadata.jwks_uri.as_ref(),
metadata.jwks.as_ref(),
metadata.id_token_signed_response_alg,
metadata.userinfo_signed_response_alg,
metadata.token_endpoint_auth_method,
metadata.token_endpoint_auth_signing_alg,
metadata.initiate_login_uri.as_ref(),
)
.await?;

View File

@ -15,6 +15,7 @@
#![forbid(unsafe_code)]
#![deny(clippy::all, clippy::str_to_string, rustdoc::broken_intra_doc_links)]
#![warn(clippy::pedantic)]
#![allow(clippy::module_name_repetitions)]
use mas_iana::oauth::OAuthAuthorizationEndpointResponseType;

View File

@ -778,7 +778,7 @@ pub enum ProviderMetadataVerificationError {
/// The given endpoint is missing auth signing algorithm values, but they
/// are required because it supports at least one of the `client_secret_jwt`
/// or `private_key_jwt` authentication methods.
#[error("{0} auth signing algorithm values contain `none`")]
#[error("{0} missing auth signing algorithm values")]
MissingAuthSigningAlgValues(&'static str),
/// `none` is in the given endpoint's signing algorithm values, but is not

View File

@ -1,168 +0,0 @@
// Copyright 2022 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use chrono::{DateTime, Duration, Utc};
use mas_iana::{
jose::{JsonWebEncryptionAlg, JsonWebSignatureAlg},
oauth::{OAuthAuthorizationEndpointResponseType, OAuthClientAuthenticationMethod},
};
use mas_jose::JsonWebKeySet;
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, skip_serializing_none, DurationSeconds, TimestampSeconds};
use url::Url;
use crate::{
oidc::{ApplicationType, SubjectType},
requests::GrantType,
};
fn default_response_types() -> Vec<OAuthAuthorizationEndpointResponseType> {
vec![OAuthAuthorizationEndpointResponseType::Code]
}
fn default_grant_types() -> Vec<GrantType> {
vec![GrantType::AuthorizationCode]
}
const fn default_application_type() -> ApplicationType {
ApplicationType::Web
}
#[serde_as]
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct ClientMetadata {
pub redirect_uris: Vec<Url>,
#[serde(default = "default_response_types")]
pub response_types: Vec<OAuthAuthorizationEndpointResponseType>,
#[serde(default = "default_grant_types")]
pub grant_types: Vec<GrantType>,
#[serde(default = "default_application_type")]
pub application_type: ApplicationType,
#[serde(default)]
pub contacts: Vec<String>,
#[serde(default)]
pub client_name: Option<String>,
#[serde(default)]
pub logo_uri: Option<Url>,
#[serde(default)]
pub client_uri: Option<Url>,
#[serde(default)]
pub policy_uri: Option<Url>,
#[serde(default)]
pub tos_uri: Option<Url>,
#[serde(default)]
pub jwks_uri: Option<Url>,
#[serde(default)]
pub jwks: Option<JsonWebKeySet>,
#[serde(default)]
pub sector_identifier_uri: Option<Url>,
#[serde(default)]
pub subject_type: Option<SubjectType>,
#[serde(default)]
pub token_endpoint_auth_method: Option<OAuthClientAuthenticationMethod>,
#[serde(default)]
pub token_endpoint_auth_signing_alg: Option<JsonWebSignatureAlg>,
#[serde(default)]
pub id_token_signed_response_alg: Option<JsonWebSignatureAlg>,
#[serde(default)]
pub id_token_encrypted_response_alg: Option<JsonWebEncryptionAlg>,
#[serde(default)]
pub id_token_encrypted_response_enc: Option<JsonWebEncryptionAlg>,
#[serde(default)]
pub userinfo_signed_response_alg: Option<JsonWebSignatureAlg>,
#[serde(default)]
pub userinfo_encrypted_response_alg: Option<JsonWebEncryptionAlg>,
#[serde(default)]
pub userinfo_encrypted_response_enc: Option<JsonWebEncryptionAlg>,
#[serde(default)]
pub request_object_signing_alg: Option<JsonWebSignatureAlg>,
#[serde(default)]
pub request_object_encryption_alg: Option<JsonWebEncryptionAlg>,
#[serde(default)]
pub request_object_encryption_enc: Option<JsonWebEncryptionAlg>,
#[serde(default)]
#[serde_as(as = "Option<DurationSeconds<i64>>")]
pub default_max_age: Option<Duration>,
#[serde(default)]
pub require_auth_time: bool,
#[serde(default)]
pub default_acr_values: Vec<String>,
#[serde(default)]
pub initiate_login_uri: Option<Url>,
#[serde(default)]
pub request_uris: Option<Vec<Url>>,
#[serde(default)]
pub require_signed_request_object: bool,
#[serde(default)]
pub require_pushed_authorization_requests: bool,
#[serde(default)]
pub introspection_signed_response_alg: Option<JsonWebSignatureAlg>,
#[serde(default)]
pub introspection_encrypted_response_alg: Option<JsonWebEncryptionAlg>,
#[serde(default)]
pub introspection_encrypted_response_enc: Option<JsonWebEncryptionAlg>,
}
#[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>>,
}

View File

@ -0,0 +1,469 @@
// Copyright 2022 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{borrow::Cow, collections::HashMap};
use chrono::Duration;
use language_tags::LanguageTag;
use mas_iana::{
jose::{JsonWebEncryptionAlg, JsonWebEncryptionEnc, JsonWebSignatureAlg},
oauth::{OAuthAuthorizationEndpointResponseType, OAuthClientAuthenticationMethod},
};
use mas_jose::JsonWebKeySet;
use serde::{
de::{DeserializeOwned, Error},
ser::SerializeMap,
Deserialize, Serialize,
};
use serde_json::Value;
use serde_with::{serde_as, skip_serializing_none, DurationSeconds};
use url::Url;
use super::{ClientMetadata, Localized, VerifiedClientMetadata};
use crate::{
oidc::{ApplicationType, SubjectType},
requests::GrantType,
};
impl<T> Localized<T> {
fn serialize<M>(&self, map: &mut M, field_name: &str) -> Result<(), M::Error>
where
M: SerializeMap,
T: Serialize,
{
map.serialize_entry(field_name, &self.non_localized)?;
for (lang, localized) in &self.localized {
map.serialize_entry(&format!("{field_name}#{lang}"), localized)?;
}
Ok(())
}
fn deserialize(
map: &mut HashMap<String, HashMap<Option<LanguageTag>, Value>>,
field_name: &'static str,
) -> Result<Option<Self>, serde_json::Error>
where
T: DeserializeOwned,
{
let map = match map.remove(field_name) {
Some(map) => map,
None => return Ok(None),
};
let mut non_localized = None;
let mut localized = HashMap::with_capacity(map.len() - 1);
for (k, v) in map {
let value = serde_json::from_value(v)?;
if let Some(lang) = k {
localized.insert(lang, value);
} else {
non_localized = Some(value);
}
}
let non_localized = non_localized.ok_or_else(|| {
serde_json::Error::custom(format!(
"missing non-localized variant of field '{field_name}'"
))
})?;
Ok(Some(Localized {
non_localized,
localized,
}))
}
}
#[serde_as]
#[skip_serializing_none]
#[derive(Serialize, Deserialize)]
pub struct ClientMetadataSerdeHelper {
redirect_uris: Option<Vec<Url>>,
response_types: Option<Vec<OAuthAuthorizationEndpointResponseType>>,
grant_types: Option<Vec<GrantType>>,
application_type: Option<ApplicationType>,
contacts: Option<Vec<String>>,
jwks_uri: Option<Url>,
jwks: Option<JsonWebKeySet>,
sector_identifier_uri: Option<Url>,
subject_type: Option<SubjectType>,
token_endpoint_auth_method: Option<OAuthClientAuthenticationMethod>,
token_endpoint_auth_signing_alg: Option<JsonWebSignatureAlg>,
id_token_signed_response_alg: Option<JsonWebSignatureAlg>,
id_token_encrypted_response_alg: Option<JsonWebEncryptionAlg>,
id_token_encrypted_response_enc: Option<JsonWebEncryptionEnc>,
userinfo_signed_response_alg: Option<JsonWebSignatureAlg>,
userinfo_encrypted_response_alg: Option<JsonWebEncryptionAlg>,
userinfo_encrypted_response_enc: Option<JsonWebEncryptionEnc>,
request_object_signing_alg: Option<JsonWebSignatureAlg>,
request_object_encryption_alg: Option<JsonWebEncryptionAlg>,
request_object_encryption_enc: Option<JsonWebEncryptionEnc>,
#[serde_as(as = "Option<DurationSeconds<i64>>")]
default_max_age: Option<Duration>,
require_auth_time: Option<bool>,
default_acr_values: Option<Vec<String>>,
initiate_login_uri: Option<Url>,
request_uris: Option<Vec<Url>>,
require_signed_request_object: Option<bool>,
require_pushed_authorization_requests: Option<bool>,
introspection_signed_response_alg: Option<JsonWebSignatureAlg>,
introspection_encrypted_response_alg: Option<JsonWebEncryptionAlg>,
introspection_encrypted_response_enc: Option<JsonWebEncryptionEnc>,
#[serde(flatten)]
extra: ClientMetadataLocalizedFields,
}
impl From<VerifiedClientMetadata> for ClientMetadataSerdeHelper {
fn from(metadata: VerifiedClientMetadata) -> Self {
let VerifiedClientMetadata {
inner:
ClientMetadata {
redirect_uris,
response_types,
grant_types,
application_type,
contacts,
client_name,
logo_uri,
client_uri,
policy_uri,
tos_uri,
jwks_uri,
jwks,
sector_identifier_uri,
subject_type,
token_endpoint_auth_method,
token_endpoint_auth_signing_alg,
id_token_signed_response_alg,
id_token_encrypted_response_alg,
id_token_encrypted_response_enc,
userinfo_signed_response_alg,
userinfo_encrypted_response_alg,
userinfo_encrypted_response_enc,
request_object_signing_alg,
request_object_encryption_alg,
request_object_encryption_enc,
default_max_age,
require_auth_time,
default_acr_values,
initiate_login_uri,
request_uris,
require_signed_request_object,
require_pushed_authorization_requests,
introspection_signed_response_alg,
introspection_encrypted_response_alg,
introspection_encrypted_response_enc,
},
} = metadata;
ClientMetadataSerdeHelper {
redirect_uris,
response_types,
grant_types,
application_type,
contacts,
jwks_uri,
jwks,
sector_identifier_uri,
subject_type,
token_endpoint_auth_method,
token_endpoint_auth_signing_alg,
id_token_signed_response_alg,
id_token_encrypted_response_alg,
id_token_encrypted_response_enc,
userinfo_signed_response_alg,
userinfo_encrypted_response_alg,
userinfo_encrypted_response_enc,
request_object_signing_alg,
request_object_encryption_alg,
request_object_encryption_enc,
default_max_age,
require_auth_time,
default_acr_values,
initiate_login_uri,
request_uris,
require_signed_request_object,
require_pushed_authorization_requests,
introspection_signed_response_alg,
introspection_encrypted_response_alg,
introspection_encrypted_response_enc,
extra: ClientMetadataLocalizedFields {
client_name,
logo_uri,
client_uri,
policy_uri,
tos_uri,
},
}
}
}
impl From<ClientMetadataSerdeHelper> for ClientMetadata {
fn from(metadata: ClientMetadataSerdeHelper) -> Self {
let ClientMetadataSerdeHelper {
redirect_uris,
response_types,
grant_types,
application_type,
contacts,
jwks_uri,
jwks,
sector_identifier_uri,
subject_type,
token_endpoint_auth_method,
token_endpoint_auth_signing_alg,
id_token_signed_response_alg,
id_token_encrypted_response_alg,
id_token_encrypted_response_enc,
userinfo_signed_response_alg,
userinfo_encrypted_response_alg,
userinfo_encrypted_response_enc,
request_object_signing_alg,
request_object_encryption_alg,
request_object_encryption_enc,
default_max_age,
require_auth_time,
default_acr_values,
initiate_login_uri,
request_uris,
require_signed_request_object,
require_pushed_authorization_requests,
introspection_signed_response_alg,
introspection_encrypted_response_alg,
introspection_encrypted_response_enc,
extra:
ClientMetadataLocalizedFields {
client_name,
logo_uri,
client_uri,
policy_uri,
tos_uri,
},
} = metadata;
ClientMetadata {
redirect_uris,
response_types,
grant_types,
application_type,
contacts,
client_name,
logo_uri,
client_uri,
policy_uri,
tos_uri,
jwks_uri,
jwks,
sector_identifier_uri,
subject_type,
token_endpoint_auth_method,
token_endpoint_auth_signing_alg,
id_token_signed_response_alg,
id_token_encrypted_response_alg,
id_token_encrypted_response_enc,
userinfo_signed_response_alg,
userinfo_encrypted_response_alg,
userinfo_encrypted_response_enc,
request_object_signing_alg,
request_object_encryption_alg,
request_object_encryption_enc,
default_max_age,
require_auth_time,
default_acr_values,
initiate_login_uri,
request_uris,
require_signed_request_object,
require_pushed_authorization_requests,
introspection_signed_response_alg,
introspection_encrypted_response_alg,
introspection_encrypted_response_enc,
}
}
}
struct ClientMetadataLocalizedFields {
client_name: Option<Localized<String>>,
logo_uri: Option<Localized<Url>>,
client_uri: Option<Localized<Url>>,
policy_uri: Option<Localized<Url>>,
tos_uri: Option<Localized<Url>>,
}
impl Serialize for ClientMetadataLocalizedFields {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map = serializer.serialize_map(None)?;
if let Some(client_name) = &self.client_name {
client_name.serialize(&mut map, "client_name")?;
}
if let Some(logo_uri) = &self.logo_uri {
logo_uri.serialize(&mut map, "logo_uri")?;
}
if let Some(client_uri) = &self.client_uri {
client_uri.serialize(&mut map, "client_uri")?;
}
if let Some(policy_uri) = &self.policy_uri {
policy_uri.serialize(&mut map, "policy_uri")?;
}
if let Some(tos_uri) = &self.tos_uri {
tos_uri.serialize(&mut map, "tos_uri")?;
}
map.end()
}
}
impl<'de> Deserialize<'de> for ClientMetadataLocalizedFields {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let map = HashMap::<Cow<'de, str>, Value>::deserialize(deserializer)?;
let mut new_map: HashMap<String, HashMap<Option<LanguageTag>, Value>> = HashMap::new();
for (k, v) in map {
let (prefix, lang) = if let Some((prefix, lang)) = k.split_once('#') {
let lang = LanguageTag::parse(lang).map_err(|_| {
D::Error::invalid_value(serde::de::Unexpected::Str(lang), &"language tag")
})?;
(prefix.to_owned(), Some(lang))
} else {
(k.into_owned(), None)
};
new_map.entry(prefix).or_default().insert(lang, v);
}
let client_name =
Localized::deserialize(&mut new_map, "client_name").map_err(D::Error::custom)?;
let logo_uri =
Localized::deserialize(&mut new_map, "logo_uri").map_err(D::Error::custom)?;
let client_uri =
Localized::deserialize(&mut new_map, "client_uri").map_err(D::Error::custom)?;
let policy_uri =
Localized::deserialize(&mut new_map, "policy_uri").map_err(D::Error::custom)?;
let tos_uri = Localized::deserialize(&mut new_map, "tos_uri").map_err(D::Error::custom)?;
Ok(Self {
client_name,
logo_uri,
client_uri,
policy_uri,
tos_uri,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialize_localized_fields() {
let metadata = serde_json::json!({
"redirect_uris": ["http://localhost/oidc"],
"client_name": "Postbox",
"client_name#fr": "Boîte à lettres",
"client_uri": "https://localhost/",
"client_uri#fr": "https://localhost/fr",
"client_uri#de": "https://localhost/de",
});
let metadata: ClientMetadata = serde_json::from_value(metadata).unwrap();
let name = metadata.client_name.unwrap();
assert_eq!(name.non_localized(), "Postbox");
assert_eq!(
name.get(Some(&LanguageTag::parse("fr").unwrap())).unwrap(),
"Boîte à lettres"
);
assert_eq!(name.get(Some(&LanguageTag::parse("de").unwrap())), None);
let client_uri = metadata.client_uri.unwrap();
assert_eq!(client_uri.non_localized().as_ref(), "https://localhost/");
assert_eq!(
client_uri
.get(Some(&LanguageTag::parse("fr").unwrap()))
.unwrap()
.as_ref(),
"https://localhost/fr"
);
assert_eq!(
client_uri
.get(Some(&LanguageTag::parse("de").unwrap()))
.unwrap()
.as_ref(),
"https://localhost/de"
);
}
#[test]
fn serialize_localized_fields() {
let client_name = Localized::new(
"Postbox".to_owned(),
[(
LanguageTag::parse("fr").unwrap(),
"Boîte à lettres".to_owned(),
)],
);
let client_uri = Localized::new(
Url::parse("https://localhost").unwrap(),
[
(
LanguageTag::parse("fr").unwrap(),
Url::parse("https://localhost/fr").unwrap(),
),
(
LanguageTag::parse("de").unwrap(),
Url::parse("https://localhost/de").unwrap(),
),
],
);
let metadata = ClientMetadata {
redirect_uris: Some(vec![Url::parse("http://localhost/oidc").unwrap()]),
client_name: Some(client_name),
client_uri: Some(client_uri),
..Default::default()
}
.validate()
.unwrap();
assert_eq!(
serde_json::to_value(metadata).unwrap(),
serde_json::json!({
"redirect_uris": ["http://localhost/oidc"],
"client_name": "Postbox",
"client_name#fr": "Boîte à lettres",
"client_uri": "https://localhost/",
"client_uri#fr": "https://localhost/fr",
"client_uri#de": "https://localhost/de",
})
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -79,6 +79,18 @@ violation[{"msg": "logo_uri not on the same host as the client_uri"}] {
not host_matches_client_uri(input.client_metadata.logo_uri)
}
violation[{"msg": "missing contacts"}] {
not input.client_metadata.contacts
}
violation[{"msg": "invalid contacts"}] {
not is_array(input.client_metadata.contacts)
}
violation[{"msg": "empty contacts"}] {
count(input.client_metadata.contacts) == 0
}
violation[{"msg": "missing redirect_uris"}] {
not input.client_metadata.redirect_uris
}

View File

@ -4,17 +4,22 @@ test_valid {
allow with input.client_metadata as {
"client_uri": "https://example.com/",
"redirect_uris": ["https://example.com/callback"],
"contacts": ["contact@example.com"],
}
}
test_missing_client_uri {
not allow with input.client_metadata as {"redirect_uris": ["https://example.com/callback"]}
not allow with input.client_metadata as {
"redirect_uris": ["https://example.com/callback"],
"contacts": ["contact@example.com"],
}
}
test_insecure_client_uri {
not allow with input.client_metadata as {
"client_uri": "http://example.com/",
"redirect_uris": ["https://example.com/callback"],
"contacts": ["contact@example.com"],
}
}
@ -23,6 +28,7 @@ test_tos_uri {
"client_uri": "https://example.com/",
"tos_uri": "https://example.com/tos",
"redirect_uris": ["https://example.com/callback"],
"contacts": ["contact@example.com"],
}
# Insecure
@ -30,6 +36,7 @@ test_tos_uri {
"client_uri": "https://example.com/",
"tos_uri": "http://example.com/tos",
"redirect_uris": ["https://example.com/callback"],
"contacts": ["contact@example.com"],
}
# Insecure, but allowed by the config
@ -37,6 +44,7 @@ test_tos_uri {
"client_uri": "https://example.com/",
"tos_uri": "http://example.com/tos",
"redirect_uris": ["https://example.com/callback"],
"contacts": ["contact@example.com"],
}
with data.client_registration.allow_insecure_uris as true
@ -45,6 +53,7 @@ test_tos_uri {
"client_uri": "https://example.com/",
"tos_uri": "https://example.org/tos",
"redirect_uris": ["https://example.com/callback"],
"contacts": ["contact@example.com"],
}
# Host mistmatch, but allowed by the config
@ -52,6 +61,7 @@ test_tos_uri {
"client_uri": "https://example.com/",
"tos_uri": "https://example.org/tos",
"redirect_uris": ["https://example.com/callback"],
"contacts": ["contact@example.com"],
}
with data.client_registration.allow_host_mismatch as true
}
@ -61,6 +71,7 @@ test_logo_uri {
"client_uri": "https://example.com/",
"logo_uri": "https://example.com/logo.png",
"redirect_uris": ["https://example.com/callback"],
"contacts": ["contact@example.com"],
}
# Insecure
@ -68,6 +79,7 @@ test_logo_uri {
"client_uri": "https://example.com/",
"logo_uri": "http://example.com/logo.png",
"redirect_uris": ["https://example.com/callback"],
"contacts": ["contact@example.com"],
}
# Insecure, but allowed by the config
@ -75,6 +87,7 @@ test_logo_uri {
"client_uri": "https://example.com/",
"logo_uri": "http://example.com/logo.png",
"redirect_uris": ["https://example.com/callback"],
"contacts": ["contact@example.com"],
}
with data.client_registration.allow_insecure_uris as true
@ -83,6 +96,7 @@ test_logo_uri {
"client_uri": "https://example.com/",
"logo_uri": "https://example.org/logo.png",
"redirect_uris": ["https://example.com/callback"],
"contacts": ["contact@example.com"],
}
# Host mistmatch, but allowed by the config
@ -90,6 +104,7 @@ test_logo_uri {
"client_uri": "https://example.com/",
"logo_uri": "https://example.org/logo.png",
"redirect_uris": ["https://example.com/callback"],
"contacts": ["contact@example.com"],
}
with data.client_registration.allow_host_mismatch as true
}
@ -99,6 +114,7 @@ test_policy_uri {
"client_uri": "https://example.com/",
"policy_uri": "https://example.com/policy",
"redirect_uris": ["https://example.com/callback"],
"contacts": ["contact@example.com"],
}
# Insecure
@ -106,6 +122,7 @@ test_policy_uri {
"client_uri": "https://example.com/",
"policy_uri": "http://example.com/policy",
"redirect_uris": ["https://example.com/callback"],
"contacts": ["contact@example.com"],
}
# Insecure, but allowed by the config
@ -113,6 +130,7 @@ test_policy_uri {
"client_uri": "https://example.com/",
"policy_uri": "http://example.com/policy",
"redirect_uris": ["https://example.com/callback"],
"contacts": ["contact@example.com"],
}
with data.client_registration.allow_insecure_uris as true
@ -121,6 +139,7 @@ test_policy_uri {
"client_uri": "https://example.com/",
"policy_uri": "https://example.org/policy",
"redirect_uris": ["https://example.com/callback"],
"contacts": ["contact@example.com"],
}
# Host mistmatch, but allowed by the config
@ -128,24 +147,30 @@ test_policy_uri {
"client_uri": "https://example.com/",
"policy_uri": "https://example.org/policy",
"redirect_uris": ["https://example.com/callback"],
"contacts": ["contact@example.com"],
}
with data.client_registration.allow_host_mismatch as true
}
test_redirect_uris {
# Missing redirect_uris
not allow with input.client_metadata as {"client_uri": "https://example.com/"}
not allow with input.client_metadata as {
"client_uri": "https://example.com/",
"contacts": ["contact@example.com"],
}
# redirect_uris is not an array
not allow with input.client_metadata as {
"client_uri": "https://example.com/",
"redirect_uris": "https://example.com/callback",
"contacts": ["contact@example.com"],
}
# Empty redirect_uris
not allow with input.client_metadata as {
"client_uri": "https://example.com/",
"redirect_uris": [],
"contacts": ["contact@example.com"],
}
}
@ -154,6 +179,7 @@ test_web_redirect_uri {
"application_type": "web",
"client_uri": "https://example.com/",
"redirect_uris": ["https://example.com/second/callback", "https://example.com/callback"],
"contacts": ["contact@example.com"],
}
# Insecure URL
@ -161,6 +187,7 @@ test_web_redirect_uri {
"application_type": "web",
"client_uri": "https://example.com/",
"redirect_uris": ["http://example.com/callback", "https://example.com/callback"],
"contacts": ["contact@example.com"],
}
# Insecure URL, but allowed by the config
@ -168,6 +195,7 @@ test_web_redirect_uri {
"application_type": "web",
"client_uri": "https://example.com/",
"redirect_uris": ["http://example.com/callback", "https://example.com/callback"],
"contacts": ["contact@example.com"],
}
with data.client_registration.allow_insecure_uris as true
@ -176,6 +204,7 @@ test_web_redirect_uri {
"application_type": "web",
"client_uri": "https://example.com/",
"redirect_uris": ["https://example.com/second/callback", "https://example.org/callback"],
"contacts": ["contact@example.com"],
}
# Host mismatch, but allowed by the config
@ -183,6 +212,7 @@ test_web_redirect_uri {
"application_type": "web",
"client_uri": "https://example.com/",
"redirect_uris": ["https://example.com/second/callback", "https://example.org/callback"],
"contacts": ["contact@example.com"],
}
with data.client_registration.allow_host_mismatch as true
@ -191,6 +221,7 @@ test_web_redirect_uri {
"application_type": "web",
"client_uri": "https://example.com/",
"redirect_uris": ["com.example.app:/callback"],
"contacts": ["contact@example.com"],
}
# localhost not allowed
@ -198,6 +229,7 @@ test_web_redirect_uri {
"application_type": "web",
"client_uri": "https://example.com/",
"redirect_uris": ["http://locahost:1234/callback"],
"contacts": ["contact@example.com"],
}
# localhost not allowed
@ -205,6 +237,7 @@ test_web_redirect_uri {
"application_type": "web",
"client_uri": "https://example.com/",
"redirect_uris": ["http://127.0.0.1:1234/callback"],
"contacts": ["contact@example.com"],
}
# localhost not allowed
@ -212,6 +245,7 @@ test_web_redirect_uri {
"application_type": "web",
"client_uri": "https://example.com/",
"redirect_uris": ["http://[::1]:1234/callback"],
"contacts": ["contact@example.com"],
}
}
@ -229,6 +263,7 @@ test_native_redirect_uri {
"http://[::1]/callback",
"http://[::1]:1234/callback",
],
"contacts": ["contact@example.com"],
}
# We don't allow HTTP URLs other than localhost
@ -236,12 +271,14 @@ test_native_redirect_uri {
"application_type": "native",
"client_uri": "https://example.com/",
"redirect_uris": ["https://example.com/"],
"contacts": ["contact@example.com"],
}
not allow with input.client_metadata as {
"application_type": "native",
"client_uri": "https://example.com/",
"redirect_uris": ["http://example.com/"],
"contacts": ["contact@example.com"],
}
# We don't allow HTTPS on localhost
@ -249,6 +286,7 @@ test_native_redirect_uri {
"application_type": "native",
"client_uri": "https://example.com/",
"redirect_uris": ["https://localhost:1234/"],
"contacts": ["contact@example.com"],
}
# Ensure we're not allowing localhost as a prefix
@ -256,6 +294,7 @@ test_native_redirect_uri {
"application_type": "native",
"client_uri": "https://example.com/",
"redirect_uris": ["http://localhost.com/"],
"contacts": ["contact@example.com"],
}
# For custom schemes, it should match the client_uri hostname
@ -263,6 +302,7 @@ test_native_redirect_uri {
"application_type": "native",
"client_uri": "https://example.com/",
"redirect_uris": ["org.example.app:/callback"],
"contacts": ["contact@example.com"],
}
}
@ -271,3 +311,25 @@ test_reverse_dns_match {
redirect_uri := parse_uri("io.element.app:/callback")
reverse_dns_match(client_uri.host, redirect_uri.scheme)
}
test_contacts {
# Missing contacts
not allow with input.client_metadata as {
"client_uri": "https://example.com/",
"redirect_uris": ["https://example.com/callback"],
}
# contacts is not an array
not allow with input.client_metadata as {
"client_uri": "https://example.com/",
"redirect_uris": ["https://example.com/callback"],
"contacts": "contact@example.com",
}
# Empty contacts
not allow with input.client_metadata as {
"client_uri": "https://example.com/",
"redirect_uris": ["https://example.com/callback"],
"contacts": [],
}
}

View File

@ -21,7 +21,7 @@ use std::io::Cursor;
use anyhow::bail;
use mas_data_model::{AuthorizationGrant, StorageBackend, User};
use oauth2_types::registration::ClientMetadata;
use oauth2_types::registration::VerifiedClientMetadata;
use opa_wasm::Runtime;
use serde::Deserialize;
use thiserror::Error;
@ -205,7 +205,7 @@ impl Policy {
#[tracing::instrument(skip(self))]
pub async fn evaluate_client_registration(
&mut self,
client_metadata: &ClientMetadata,
client_metadata: &VerifiedClientMetadata,
) -> Result<EvaluationResult, anyhow::Error> {
let client_metadata = serde_json::to_value(client_metadata)?;
let input = serde_json::json!({