You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-07-31 09:24:31 +03:00
Allow to validate client metadata
According to OpenID Connect Dynamic Client Registration Spec 1.0. Introduce VerifiedClientMetadata.
This commit is contained in:
committed by
Quentin Gliech
parent
a543af4de3
commit
e202c3dd6d
@ -16,13 +16,13 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use axum::{response::IntoResponse, Extension, Json};
|
use axum::{response::IntoResponse, Extension, Json};
|
||||||
use hyper::StatusCode;
|
use hyper::StatusCode;
|
||||||
use mas_iana::oauth::{OAuthAuthorizationEndpointResponseType, OAuthClientAuthenticationMethod};
|
|
||||||
use mas_policy::{PolicyFactory, Violation};
|
use mas_policy::{PolicyFactory, Violation};
|
||||||
use mas_storage::oauth2::client::insert_client;
|
use mas_storage::oauth2::client::insert_client;
|
||||||
use oauth2_types::{
|
use oauth2_types::{
|
||||||
errors::{INVALID_CLIENT_METADATA, INVALID_REDIRECT_URI, SERVER_ERROR},
|
errors::{INVALID_CLIENT_METADATA, INVALID_REDIRECT_URI, SERVER_ERROR},
|
||||||
registration::{ClientMetadata, ClientRegistrationResponse},
|
registration::{
|
||||||
requests::GrantType,
|
ClientMetadata, ClientMetadataVerificationError, ClientRegistrationResponse, Localized,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||||
use sqlx::PgPool;
|
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
|
// TODO: there is probably a better way to do achieve this. ClientError only
|
||||||
// works for static strings
|
// works for static strings
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
@ -111,55 +123,18 @@ pub(crate) async fn post(
|
|||||||
) -> Result<impl IntoResponse, RouteError> {
|
) -> Result<impl IntoResponse, RouteError> {
|
||||||
info!(?body, "Client registration");
|
info!(?body, "Client registration");
|
||||||
|
|
||||||
// Let's validate a bunch of things on the client body first
|
// Validate the body
|
||||||
for uri in &body.redirect_uris {
|
let metadata = body.validate()?;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut policy = policy_factory.instantiate().await?;
|
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() {
|
if !res.valid() {
|
||||||
return Err(RouteError::PolicyDenied(res.violations));
|
return Err(RouteError::PolicyDenied(res.violations));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Contacts was checked by the policy
|
||||||
|
let contacts = metadata.contacts.as_deref().unwrap_or_default();
|
||||||
|
|
||||||
// Grab a txn
|
// Grab a txn
|
||||||
let mut txn = pool.begin().await?;
|
let mut txn = pool.begin().await?;
|
||||||
|
|
||||||
@ -173,23 +148,26 @@ pub(crate) async fn post(
|
|||||||
insert_client(
|
insert_client(
|
||||||
&mut txn,
|
&mut txn,
|
||||||
&client_id,
|
&client_id,
|
||||||
&body.redirect_uris,
|
metadata.redirect_uris(),
|
||||||
None,
|
None,
|
||||||
&body.response_types,
|
metadata.response_types(),
|
||||||
&body.grant_types,
|
metadata.grant_types(),
|
||||||
&body.contacts,
|
contacts,
|
||||||
body.client_name.as_deref(),
|
metadata
|
||||||
body.logo_uri.as_ref(),
|
.client_name
|
||||||
body.client_uri.as_ref(),
|
.as_ref()
|
||||||
body.policy_uri.as_ref(),
|
.map(|l| l.non_localized().as_ref()),
|
||||||
body.tos_uri.as_ref(),
|
metadata.logo_uri.as_ref().map(Localized::non_localized),
|
||||||
body.jwks_uri.as_ref(),
|
metadata.client_uri.as_ref().map(Localized::non_localized),
|
||||||
body.jwks.as_ref(),
|
metadata.policy_uri.as_ref().map(Localized::non_localized),
|
||||||
body.id_token_signed_response_alg,
|
metadata.tos_uri.as_ref().map(Localized::non_localized),
|
||||||
body.userinfo_signed_response_alg,
|
metadata.jwks_uri.as_ref(),
|
||||||
body.token_endpoint_auth_method,
|
metadata.jwks.as_ref(),
|
||||||
body.token_endpoint_auth_signing_alg,
|
metadata.id_token_signed_response_alg,
|
||||||
body.initiate_login_uri.as_ref(),
|
metadata.userinfo_signed_response_alg,
|
||||||
|
metadata.token_endpoint_auth_method,
|
||||||
|
metadata.token_endpoint_auth_signing_alg,
|
||||||
|
metadata.initiate_login_uri.as_ref(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![deny(clippy::all, clippy::str_to_string, rustdoc::broken_intra_doc_links)]
|
#![deny(clippy::all, clippy::str_to_string, rustdoc::broken_intra_doc_links)]
|
||||||
#![warn(clippy::pedantic)]
|
#![warn(clippy::pedantic)]
|
||||||
|
#![allow(clippy::module_name_repetitions)]
|
||||||
|
|
||||||
use mas_iana::oauth::OAuthAuthorizationEndpointResponseType;
|
use mas_iana::oauth::OAuthAuthorizationEndpointResponseType;
|
||||||
|
|
||||||
|
@ -778,7 +778,7 @@ pub enum ProviderMetadataVerificationError {
|
|||||||
/// The given endpoint is missing auth signing algorithm values, but they
|
/// The given endpoint is missing auth signing algorithm values, but they
|
||||||
/// are required because it supports at least one of the `client_secret_jwt`
|
/// are required because it supports at least one of the `client_secret_jwt`
|
||||||
/// or `private_key_jwt` authentication methods.
|
/// or `private_key_jwt` authentication methods.
|
||||||
#[error("{0} auth signing algorithm values contain `none`")]
|
#[error("{0} missing auth signing algorithm values")]
|
||||||
MissingAuthSigningAlgValues(&'static str),
|
MissingAuthSigningAlgValues(&'static str),
|
||||||
|
|
||||||
/// `none` is in the given endpoint's signing algorithm values, but is not
|
/// `none` is in the given endpoint's signing algorithm values, but is not
|
||||||
|
@ -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>>,
|
|
||||||
}
|
|
469
crates/oauth2-types/src/registration/client_metadata_serde.rs
Normal file
469
crates/oauth2-types/src/registration/client_metadata_serde.rs
Normal 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",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
1334
crates/oauth2-types/src/registration/mod.rs
Normal file
1334
crates/oauth2-types/src/registration/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -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)
|
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"}] {
|
violation[{"msg": "missing redirect_uris"}] {
|
||||||
not input.client_metadata.redirect_uris
|
not input.client_metadata.redirect_uris
|
||||||
}
|
}
|
||||||
|
@ -4,17 +4,22 @@ test_valid {
|
|||||||
allow with input.client_metadata as {
|
allow with input.client_metadata as {
|
||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"redirect_uris": ["https://example.com/callback"],
|
"redirect_uris": ["https://example.com/callback"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test_missing_client_uri {
|
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 {
|
test_insecure_client_uri {
|
||||||
not allow with input.client_metadata as {
|
not allow with input.client_metadata as {
|
||||||
"client_uri": "http://example.com/",
|
"client_uri": "http://example.com/",
|
||||||
"redirect_uris": ["https://example.com/callback"],
|
"redirect_uris": ["https://example.com/callback"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,6 +28,7 @@ test_tos_uri {
|
|||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"tos_uri": "https://example.com/tos",
|
"tos_uri": "https://example.com/tos",
|
||||||
"redirect_uris": ["https://example.com/callback"],
|
"redirect_uris": ["https://example.com/callback"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Insecure
|
# Insecure
|
||||||
@ -30,6 +36,7 @@ test_tos_uri {
|
|||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"tos_uri": "http://example.com/tos",
|
"tos_uri": "http://example.com/tos",
|
||||||
"redirect_uris": ["https://example.com/callback"],
|
"redirect_uris": ["https://example.com/callback"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Insecure, but allowed by the config
|
# Insecure, but allowed by the config
|
||||||
@ -37,6 +44,7 @@ test_tos_uri {
|
|||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"tos_uri": "http://example.com/tos",
|
"tos_uri": "http://example.com/tos",
|
||||||
"redirect_uris": ["https://example.com/callback"],
|
"redirect_uris": ["https://example.com/callback"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
with data.client_registration.allow_insecure_uris as true
|
with data.client_registration.allow_insecure_uris as true
|
||||||
|
|
||||||
@ -45,6 +53,7 @@ test_tos_uri {
|
|||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"tos_uri": "https://example.org/tos",
|
"tos_uri": "https://example.org/tos",
|
||||||
"redirect_uris": ["https://example.com/callback"],
|
"redirect_uris": ["https://example.com/callback"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Host mistmatch, but allowed by the config
|
# Host mistmatch, but allowed by the config
|
||||||
@ -52,6 +61,7 @@ test_tos_uri {
|
|||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"tos_uri": "https://example.org/tos",
|
"tos_uri": "https://example.org/tos",
|
||||||
"redirect_uris": ["https://example.com/callback"],
|
"redirect_uris": ["https://example.com/callback"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
with data.client_registration.allow_host_mismatch as true
|
with data.client_registration.allow_host_mismatch as true
|
||||||
}
|
}
|
||||||
@ -61,6 +71,7 @@ test_logo_uri {
|
|||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"logo_uri": "https://example.com/logo.png",
|
"logo_uri": "https://example.com/logo.png",
|
||||||
"redirect_uris": ["https://example.com/callback"],
|
"redirect_uris": ["https://example.com/callback"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Insecure
|
# Insecure
|
||||||
@ -68,6 +79,7 @@ test_logo_uri {
|
|||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"logo_uri": "http://example.com/logo.png",
|
"logo_uri": "http://example.com/logo.png",
|
||||||
"redirect_uris": ["https://example.com/callback"],
|
"redirect_uris": ["https://example.com/callback"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Insecure, but allowed by the config
|
# Insecure, but allowed by the config
|
||||||
@ -75,6 +87,7 @@ test_logo_uri {
|
|||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"logo_uri": "http://example.com/logo.png",
|
"logo_uri": "http://example.com/logo.png",
|
||||||
"redirect_uris": ["https://example.com/callback"],
|
"redirect_uris": ["https://example.com/callback"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
with data.client_registration.allow_insecure_uris as true
|
with data.client_registration.allow_insecure_uris as true
|
||||||
|
|
||||||
@ -83,6 +96,7 @@ test_logo_uri {
|
|||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"logo_uri": "https://example.org/logo.png",
|
"logo_uri": "https://example.org/logo.png",
|
||||||
"redirect_uris": ["https://example.com/callback"],
|
"redirect_uris": ["https://example.com/callback"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Host mistmatch, but allowed by the config
|
# Host mistmatch, but allowed by the config
|
||||||
@ -90,6 +104,7 @@ test_logo_uri {
|
|||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"logo_uri": "https://example.org/logo.png",
|
"logo_uri": "https://example.org/logo.png",
|
||||||
"redirect_uris": ["https://example.com/callback"],
|
"redirect_uris": ["https://example.com/callback"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
with data.client_registration.allow_host_mismatch as true
|
with data.client_registration.allow_host_mismatch as true
|
||||||
}
|
}
|
||||||
@ -99,6 +114,7 @@ test_policy_uri {
|
|||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"policy_uri": "https://example.com/policy",
|
"policy_uri": "https://example.com/policy",
|
||||||
"redirect_uris": ["https://example.com/callback"],
|
"redirect_uris": ["https://example.com/callback"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Insecure
|
# Insecure
|
||||||
@ -106,6 +122,7 @@ test_policy_uri {
|
|||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"policy_uri": "http://example.com/policy",
|
"policy_uri": "http://example.com/policy",
|
||||||
"redirect_uris": ["https://example.com/callback"],
|
"redirect_uris": ["https://example.com/callback"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Insecure, but allowed by the config
|
# Insecure, but allowed by the config
|
||||||
@ -113,6 +130,7 @@ test_policy_uri {
|
|||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"policy_uri": "http://example.com/policy",
|
"policy_uri": "http://example.com/policy",
|
||||||
"redirect_uris": ["https://example.com/callback"],
|
"redirect_uris": ["https://example.com/callback"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
with data.client_registration.allow_insecure_uris as true
|
with data.client_registration.allow_insecure_uris as true
|
||||||
|
|
||||||
@ -121,6 +139,7 @@ test_policy_uri {
|
|||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"policy_uri": "https://example.org/policy",
|
"policy_uri": "https://example.org/policy",
|
||||||
"redirect_uris": ["https://example.com/callback"],
|
"redirect_uris": ["https://example.com/callback"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Host mistmatch, but allowed by the config
|
# Host mistmatch, but allowed by the config
|
||||||
@ -128,24 +147,30 @@ test_policy_uri {
|
|||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"policy_uri": "https://example.org/policy",
|
"policy_uri": "https://example.org/policy",
|
||||||
"redirect_uris": ["https://example.com/callback"],
|
"redirect_uris": ["https://example.com/callback"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
with data.client_registration.allow_host_mismatch as true
|
with data.client_registration.allow_host_mismatch as true
|
||||||
}
|
}
|
||||||
|
|
||||||
test_redirect_uris {
|
test_redirect_uris {
|
||||||
# Missing 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
|
# redirect_uris is not an array
|
||||||
not allow with input.client_metadata as {
|
not allow with input.client_metadata as {
|
||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"redirect_uris": "https://example.com/callback",
|
"redirect_uris": "https://example.com/callback",
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Empty redirect_uris
|
# Empty redirect_uris
|
||||||
not allow with input.client_metadata as {
|
not allow with input.client_metadata as {
|
||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"redirect_uris": [],
|
"redirect_uris": [],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,6 +179,7 @@ test_web_redirect_uri {
|
|||||||
"application_type": "web",
|
"application_type": "web",
|
||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"redirect_uris": ["https://example.com/second/callback", "https://example.com/callback"],
|
"redirect_uris": ["https://example.com/second/callback", "https://example.com/callback"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Insecure URL
|
# Insecure URL
|
||||||
@ -161,6 +187,7 @@ test_web_redirect_uri {
|
|||||||
"application_type": "web",
|
"application_type": "web",
|
||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"redirect_uris": ["http://example.com/callback", "https://example.com/callback"],
|
"redirect_uris": ["http://example.com/callback", "https://example.com/callback"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Insecure URL, but allowed by the config
|
# Insecure URL, but allowed by the config
|
||||||
@ -168,6 +195,7 @@ test_web_redirect_uri {
|
|||||||
"application_type": "web",
|
"application_type": "web",
|
||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"redirect_uris": ["http://example.com/callback", "https://example.com/callback"],
|
"redirect_uris": ["http://example.com/callback", "https://example.com/callback"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
with data.client_registration.allow_insecure_uris as true
|
with data.client_registration.allow_insecure_uris as true
|
||||||
|
|
||||||
@ -176,6 +204,7 @@ test_web_redirect_uri {
|
|||||||
"application_type": "web",
|
"application_type": "web",
|
||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"redirect_uris": ["https://example.com/second/callback", "https://example.org/callback"],
|
"redirect_uris": ["https://example.com/second/callback", "https://example.org/callback"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Host mismatch, but allowed by the config
|
# Host mismatch, but allowed by the config
|
||||||
@ -183,6 +212,7 @@ test_web_redirect_uri {
|
|||||||
"application_type": "web",
|
"application_type": "web",
|
||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"redirect_uris": ["https://example.com/second/callback", "https://example.org/callback"],
|
"redirect_uris": ["https://example.com/second/callback", "https://example.org/callback"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
with data.client_registration.allow_host_mismatch as true
|
with data.client_registration.allow_host_mismatch as true
|
||||||
|
|
||||||
@ -191,6 +221,7 @@ test_web_redirect_uri {
|
|||||||
"application_type": "web",
|
"application_type": "web",
|
||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"redirect_uris": ["com.example.app:/callback"],
|
"redirect_uris": ["com.example.app:/callback"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# localhost not allowed
|
# localhost not allowed
|
||||||
@ -198,6 +229,7 @@ test_web_redirect_uri {
|
|||||||
"application_type": "web",
|
"application_type": "web",
|
||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"redirect_uris": ["http://locahost:1234/callback"],
|
"redirect_uris": ["http://locahost:1234/callback"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# localhost not allowed
|
# localhost not allowed
|
||||||
@ -205,6 +237,7 @@ test_web_redirect_uri {
|
|||||||
"application_type": "web",
|
"application_type": "web",
|
||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"redirect_uris": ["http://127.0.0.1:1234/callback"],
|
"redirect_uris": ["http://127.0.0.1:1234/callback"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# localhost not allowed
|
# localhost not allowed
|
||||||
@ -212,6 +245,7 @@ test_web_redirect_uri {
|
|||||||
"application_type": "web",
|
"application_type": "web",
|
||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"redirect_uris": ["http://[::1]:1234/callback"],
|
"redirect_uris": ["http://[::1]:1234/callback"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -229,6 +263,7 @@ test_native_redirect_uri {
|
|||||||
"http://[::1]/callback",
|
"http://[::1]/callback",
|
||||||
"http://[::1]:1234/callback",
|
"http://[::1]:1234/callback",
|
||||||
],
|
],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# We don't allow HTTP URLs other than localhost
|
# We don't allow HTTP URLs other than localhost
|
||||||
@ -236,12 +271,14 @@ test_native_redirect_uri {
|
|||||||
"application_type": "native",
|
"application_type": "native",
|
||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"redirect_uris": ["https://example.com/"],
|
"redirect_uris": ["https://example.com/"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
|
|
||||||
not allow with input.client_metadata as {
|
not allow with input.client_metadata as {
|
||||||
"application_type": "native",
|
"application_type": "native",
|
||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"redirect_uris": ["http://example.com/"],
|
"redirect_uris": ["http://example.com/"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# We don't allow HTTPS on localhost
|
# We don't allow HTTPS on localhost
|
||||||
@ -249,6 +286,7 @@ test_native_redirect_uri {
|
|||||||
"application_type": "native",
|
"application_type": "native",
|
||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"redirect_uris": ["https://localhost:1234/"],
|
"redirect_uris": ["https://localhost:1234/"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Ensure we're not allowing localhost as a prefix
|
# Ensure we're not allowing localhost as a prefix
|
||||||
@ -256,6 +294,7 @@ test_native_redirect_uri {
|
|||||||
"application_type": "native",
|
"application_type": "native",
|
||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"redirect_uris": ["http://localhost.com/"],
|
"redirect_uris": ["http://localhost.com/"],
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# For custom schemes, it should match the client_uri hostname
|
# For custom schemes, it should match the client_uri hostname
|
||||||
@ -263,6 +302,7 @@ test_native_redirect_uri {
|
|||||||
"application_type": "native",
|
"application_type": "native",
|
||||||
"client_uri": "https://example.com/",
|
"client_uri": "https://example.com/",
|
||||||
"redirect_uris": ["org.example.app:/callback"],
|
"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")
|
redirect_uri := parse_uri("io.element.app:/callback")
|
||||||
reverse_dns_match(client_uri.host, redirect_uri.scheme)
|
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": [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -21,7 +21,7 @@ use std::io::Cursor;
|
|||||||
|
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use mas_data_model::{AuthorizationGrant, StorageBackend, User};
|
use mas_data_model::{AuthorizationGrant, StorageBackend, User};
|
||||||
use oauth2_types::registration::ClientMetadata;
|
use oauth2_types::registration::VerifiedClientMetadata;
|
||||||
use opa_wasm::Runtime;
|
use opa_wasm::Runtime;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
@ -205,7 +205,7 @@ impl Policy {
|
|||||||
#[tracing::instrument(skip(self))]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn evaluate_client_registration(
|
pub async fn evaluate_client_registration(
|
||||||
&mut self,
|
&mut self,
|
||||||
client_metadata: &ClientMetadata,
|
client_metadata: &VerifiedClientMetadata,
|
||||||
) -> Result<EvaluationResult, anyhow::Error> {
|
) -> Result<EvaluationResult, anyhow::Error> {
|
||||||
let client_metadata = serde_json::to_value(client_metadata)?;
|
let client_metadata = serde_json::to_value(client_metadata)?;
|
||||||
let input = serde_json::json!({
|
let input = serde_json::json!({
|
||||||
|
Reference in New Issue
Block a user