1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-08-06 06:02:40 +03:00

Flatten the clients config

This commit is contained in:
Quentin Gliech
2024-03-20 16:59:26 +01:00
parent 48b6013c4f
commit cba431d20e
4 changed files with 219 additions and 197 deletions

View File

@@ -259,10 +259,10 @@ pub async fn config_sync(
continue; continue;
} }
let client_secret = client.client_secret(); let client_secret = client.client_secret.as_deref();
let client_auth_method = client.client_auth_method(); let client_auth_method = client.client_auth_method();
let jwks = client.jwks(); let jwks = client.jwks.as_ref();
let jwks_uri = client.jwks_uri(); let jwks_uri = client.jwks_uri.as_ref();
// TODO: should be moved somewhere else // TODO: should be moved somewhere else
let encrypted_client_secret = client_secret let encrypted_client_secret = client_secret

View File

@@ -12,16 +12,15 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use std::ops::{Deref, DerefMut}; use std::ops::Deref;
use async_trait::async_trait; use async_trait::async_trait;
use figment::Figment;
use mas_iana::oauth::OAuthClientAuthenticationMethod; use mas_iana::oauth::OAuthClientAuthenticationMethod;
use mas_jose::jwk::PublicJsonWebKeySet; use mas_jose::jwk::PublicJsonWebKeySet;
use rand::Rng; use rand::Rng;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{de::Error, Deserialize, Serialize};
use serde_with::skip_serializing_none;
use thiserror::Error;
use ulid::Ulid; use ulid::Ulid;
use url::Url; use url::Url;
@@ -41,40 +40,42 @@ impl From<PublicJsonWebKeySet> for JwksOrJwksUri {
} }
/// Authentication method used by clients /// Authentication method used by clients
#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)] #[derive(JsonSchema, Serialize, Deserialize, Copy, Clone, Debug)]
#[serde(tag = "client_auth_method", rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum ClientAuthMethodConfig { pub enum ClientAuthMethodConfig {
/// `none`: No authentication /// `none`: No authentication
None, None,
/// `client_secret_basic`: `client_id` and `client_secret` used as basic /// `client_secret_basic`: `client_id` and `client_secret` used as basic
/// authorization credentials /// authorization credentials
ClientSecretBasic { ClientSecretBasic,
/// The client secret
client_secret: String,
},
/// `client_secret_post`: `client_id` and `client_secret` sent in the /// `client_secret_post`: `client_id` and `client_secret` sent in the
/// request body /// request body
ClientSecretPost { ClientSecretPost,
/// The client secret
client_secret: String,
},
/// `client_secret_basic`: a `client_assertion` sent in the request body and /// `client_secret_basic`: a `client_assertion` sent in the request body and
/// signed using the `client_secret` /// signed using the `client_secret`
ClientSecretJwt { ClientSecretJwt,
/// The client secret
client_secret: String,
},
/// `client_secret_basic`: a `client_assertion` sent in the request body and /// `client_secret_basic`: a `client_assertion` sent in the request body and
/// signed by an asymmetric key /// signed by an asymmetric key
PrivateKeyJwt(JwksOrJwksUri), PrivateKeyJwt,
}
impl std::fmt::Display for ClientAuthMethodConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ClientAuthMethodConfig::None => write!(f, "none"),
ClientAuthMethodConfig::ClientSecretBasic => write!(f, "client_secret_basic"),
ClientAuthMethodConfig::ClientSecretPost => write!(f, "client_secret_post"),
ClientAuthMethodConfig::ClientSecretJwt => write!(f, "client_secret_jwt"),
ClientAuthMethodConfig::PrivateKeyJwt => write!(f, "private_key_jwt"),
}
}
} }
/// An OAuth 2.0 client configuration /// An OAuth 2.0 client configuration
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ClientConfig { pub struct ClientConfig {
/// The client ID /// The client ID
@@ -86,67 +87,121 @@ pub struct ClientConfig {
pub client_id: Ulid, pub client_id: Ulid,
/// Authentication method used for this client /// Authentication method used for this client
#[serde(flatten)] client_auth_method: ClientAuthMethodConfig,
pub client_auth_method: ClientAuthMethodConfig,
/// The client secret, used by the `client_secret_basic`,
/// `client_secret_post` and `client_secret_jwt` authentication methods
#[serde(skip_serializing_if = "Option::is_none")]
pub client_secret: Option<String>,
/// The JSON Web Key Set (JWKS) used by the `private_key_jwt` authentication
/// method. Mutually exclusive with `jwks_uri`
#[serde(skip_serializing_if = "Option::is_none")]
pub jwks: Option<PublicJsonWebKeySet>,
/// The URL of the JSON Web Key Set (JWKS) used by the `private_key_jwt`
/// authentication method. Mutually exclusive with `jwks`
#[serde(skip_serializing_if = "Option::is_none")]
pub jwks_uri: Option<Url>,
/// List of allowed redirect URIs /// List of allowed redirect URIs
#[serde(default)] #[serde(default, skip_serializing_if = "Vec::is_empty")]
pub redirect_uris: Vec<Url>, pub redirect_uris: Vec<Url>,
} }
#[derive(Debug, Error)]
#[error("Invalid redirect URI")]
pub struct InvalidRedirectUriError;
impl ClientConfig { impl ClientConfig {
#[doc(hidden)] fn validate(&self) -> Result<(), figment::error::Error> {
#[must_use] let auth_method = self.client_auth_method;
pub fn client_secret(&self) -> Option<&str> { match self.client_auth_method {
match &self.client_auth_method { ClientAuthMethodConfig::PrivateKeyJwt => {
ClientAuthMethodConfig::ClientSecretPost { client_secret } if self.jwks.is_none() && self.jwks_uri.is_none() {
| ClientAuthMethodConfig::ClientSecretBasic { client_secret } let error = figment::error::Error::custom(
| ClientAuthMethodConfig::ClientSecretJwt { client_secret } => Some(client_secret), "jwks or jwks_uri is required for private_key_jwt",
_ => None, );
return Err(error.with_path("client_auth_method"));
}
if self.jwks.is_some() && self.jwks_uri.is_some() {
let error =
figment::error::Error::custom("jwks and jwks_uri are mutually exclusive");
return Err(error.with_path("jwks"));
}
if self.client_secret.is_some() {
let error = figment::error::Error::custom(
"client_secret is not allowed with private_key_jwt",
);
return Err(error.with_path("client_secret"));
}
}
ClientAuthMethodConfig::ClientSecretPost
| ClientAuthMethodConfig::ClientSecretBasic
| ClientAuthMethodConfig::ClientSecretJwt => {
if self.client_secret.is_none() {
let error = figment::error::Error::custom(format!(
"client_secret is required for {auth_method}"
));
return Err(error.with_path("client_auth_method"));
}
if self.jwks.is_some() {
let error = figment::error::Error::custom(format!(
"jwks is not allowed with {auth_method}"
));
return Err(error.with_path("jwks"));
}
if self.jwks_uri.is_some() {
let error = figment::error::Error::custom(format!(
"jwks_uri is not allowed with {auth_method}"
));
return Err(error.with_path("jwks_uri"));
}
}
ClientAuthMethodConfig::None => {
if self.client_secret.is_some() {
let error = figment::error::Error::custom(
"client_secret is not allowed with none authentication method",
);
return Err(error.with_path("client_secret"));
}
if self.jwks.is_some() {
let error = figment::error::Error::custom(
"jwks is not allowed with none authentication method",
);
return Err(error);
}
if self.jwks_uri.is_some() {
let error = figment::error::Error::custom(
"jwks_uri is not allowed with none authentication method",
);
return Err(error);
}
}
} }
Ok(())
} }
#[doc(hidden)] /// Authentication method used for this client
#[must_use] #[must_use]
pub fn client_auth_method(&self) -> OAuthClientAuthenticationMethod { pub fn client_auth_method(&self) -> OAuthClientAuthenticationMethod {
match &self.client_auth_method { match self.client_auth_method {
ClientAuthMethodConfig::None => OAuthClientAuthenticationMethod::None, ClientAuthMethodConfig::None => OAuthClientAuthenticationMethod::None,
ClientAuthMethodConfig::ClientSecretBasic { .. } => { ClientAuthMethodConfig::ClientSecretBasic => {
OAuthClientAuthenticationMethod::ClientSecretBasic OAuthClientAuthenticationMethod::ClientSecretBasic
} }
ClientAuthMethodConfig::ClientSecretPost { .. } => { ClientAuthMethodConfig::ClientSecretPost => {
OAuthClientAuthenticationMethod::ClientSecretPost OAuthClientAuthenticationMethod::ClientSecretPost
} }
ClientAuthMethodConfig::ClientSecretJwt { .. } => { ClientAuthMethodConfig::ClientSecretJwt => {
OAuthClientAuthenticationMethod::ClientSecretJwt OAuthClientAuthenticationMethod::ClientSecretJwt
} }
ClientAuthMethodConfig::PrivateKeyJwt(_) => { ClientAuthMethodConfig::PrivateKeyJwt => OAuthClientAuthenticationMethod::PrivateKeyJwt,
OAuthClientAuthenticationMethod::PrivateKeyJwt
}
}
}
#[doc(hidden)]
#[must_use]
pub fn jwks(&self) -> Option<&PublicJsonWebKeySet> {
match &self.client_auth_method {
ClientAuthMethodConfig::PrivateKeyJwt(JwksOrJwksUri::Jwks(jwks)) => Some(jwks),
_ => None,
}
}
#[doc(hidden)]
#[must_use]
pub fn jwks_uri(&self) -> Option<&Url> {
match &self.client_auth_method {
ClientAuthMethodConfig::PrivateKeyJwt(JwksOrJwksUri::JwksUri(jwks_uri)) => {
Some(jwks_uri)
}
_ => None,
} }
} }
} }
@@ -154,7 +209,7 @@ impl ClientConfig {
/// List of OAuth 2.0/OIDC clients config /// List of OAuth 2.0/OIDC clients config
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)] #[serde(transparent)]
pub struct ClientsConfig(Vec<ClientConfig>); pub struct ClientsConfig(#[schemars(with = "Vec::<ClientConfig>")] Vec<ClientConfig>);
impl Deref for ClientsConfig { impl Deref for ClientsConfig {
type Target = Vec<ClientConfig>; type Target = Vec<ClientConfig>;
@@ -164,12 +219,6 @@ impl Deref for ClientsConfig {
} }
} }
impl DerefMut for ClientsConfig {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl IntoIterator for ClientsConfig { impl IntoIterator for ClientsConfig {
type Item = ClientConfig; type Item = ClientConfig;
type IntoIter = std::vec::IntoIter<ClientConfig>; type IntoIter = std::vec::IntoIter<ClientConfig>;
@@ -190,6 +239,21 @@ impl ConfigurationSection for ClientsConfig {
Ok(Self::default()) Ok(Self::default())
} }
fn validate(&self, figment: &Figment) -> Result<(), figment::error::Error> {
for (index, client) in self.0.iter().enumerate() {
client.validate().map_err(|mut err| {
// Save the error location information in the error
err.metadata = figment.find_metadata(Self::PATH.unwrap()).cloned();
err.profile = Some(figment::Profile::Default);
err.path.insert(0, Self::PATH.unwrap().to_owned());
err.path.insert(1, format!("{index}"));
err
})?;
}
Ok(())
}
fn test() -> Self { fn test() -> Self {
Self::default() Self::default()
} }

View File

@@ -29,17 +29,29 @@ pub trait ConfigurationSection: Sized + DeserializeOwned + Serialize {
where where
R: Rng + Send; R: Rng + Send;
/// Validate the configuration section
///
/// # Errors
///
/// Returns an error if the configuration is invalid
fn validate(&self, _figment: &Figment) -> Result<(), FigmentError> {
Ok(())
}
/// Extract configuration from a Figment instance. /// Extract configuration from a Figment instance.
/// ///
/// # Errors /// # Errors
/// ///
/// Returns an error if the configuration could not be loaded /// Returns an error if the configuration could not be loaded
fn extract(figment: &Figment) -> Result<Self, FigmentError> { fn extract(figment: &Figment) -> Result<Self, FigmentError> {
if let Some(path) = Self::PATH { let this: Self = if let Some(path) = Self::PATH {
figment.extract_inner(path) figment.extract_inner(path)?
} else { } else {
figment.extract() figment.extract()?
} };
this.validate(figment)?;
Ok(this)
} }
/// Generate config used in unit tests /// Generate config used in unit tests

View File

@@ -239,126 +239,8 @@
"ClientConfig": { "ClientConfig": {
"description": "An OAuth 2.0 client configuration", "description": "An OAuth 2.0 client configuration",
"type": "object", "type": "object",
"oneOf": [
{
"description": "`none`: No authentication",
"type": "object",
"required": [
"client_auth_method"
],
"properties": {
"client_auth_method": {
"type": "string",
"enum": [
"none"
]
}
}
},
{
"description": "`client_secret_basic`: `client_id` and `client_secret` used as basic authorization credentials",
"type": "object",
"required": [
"client_auth_method",
"client_secret"
],
"properties": {
"client_auth_method": {
"type": "string",
"enum": [
"client_secret_basic"
]
},
"client_secret": {
"description": "The client secret",
"type": "string"
}
}
},
{
"description": "`client_secret_post`: `client_id` and `client_secret` sent in the request body",
"type": "object",
"required": [
"client_auth_method",
"client_secret"
],
"properties": {
"client_auth_method": {
"type": "string",
"enum": [
"client_secret_post"
]
},
"client_secret": {
"description": "The client secret",
"type": "string"
}
}
},
{
"description": "`client_secret_basic`: a `client_assertion` sent in the request body and signed using the `client_secret`",
"type": "object",
"required": [
"client_auth_method",
"client_secret"
],
"properties": {
"client_auth_method": {
"type": "string",
"enum": [
"client_secret_jwt"
]
},
"client_secret": {
"description": "The client secret",
"type": "string"
}
}
},
{
"description": "`client_secret_basic`: a `client_assertion` sent in the request body and signed by an asymmetric key",
"type": "object",
"oneOf": [
{
"type": "object",
"required": [
"jwks"
],
"properties": {
"jwks": {
"$ref": "#/definitions/JsonWebKeySet_for_JsonWebKeyPublicParameters"
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"jwks_uri"
],
"properties": {
"jwks_uri": {
"type": "string",
"format": "uri"
}
},
"additionalProperties": false
}
],
"required": [
"client_auth_method"
],
"properties": {
"client_auth_method": {
"type": "string",
"enum": [
"private_key_jwt"
]
}
}
}
],
"required": [ "required": [
"client_auth_method",
"client_id" "client_id"
], ],
"properties": { "properties": {
@@ -367,9 +249,33 @@
"type": "string", "type": "string",
"pattern": "^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$" "pattern": "^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$"
}, },
"client_auth_method": {
"description": "Authentication method used for this client",
"allOf": [
{
"$ref": "#/definitions/ClientAuthMethodConfig"
}
]
},
"client_secret": {
"description": "The client secret, used by the `client_secret_basic`, `client_secret_post` and `client_secret_jwt` authentication methods",
"type": "string"
},
"jwks": {
"description": "The JSON Web Key Set (JWKS) used by the `private_key_jwt` authentication method. Mutually exclusive with `jwks_uri`",
"allOf": [
{
"$ref": "#/definitions/JsonWebKeySet_for_JsonWebKeyPublicParameters"
}
]
},
"jwks_uri": {
"description": "The URL of the JSON Web Key Set (JWKS) used by the `private_key_jwt` authentication method. Mutually exclusive with `jwks`",
"type": "string",
"format": "uri"
},
"redirect_uris": { "redirect_uris": {
"description": "List of allowed redirect URIs", "description": "List of allowed redirect URIs",
"default": [],
"type": "array", "type": "array",
"items": { "items": {
"type": "string", "type": "string",
@@ -378,6 +284,46 @@
} }
} }
}, },
"ClientAuthMethodConfig": {
"description": "Authentication method used by clients",
"oneOf": [
{
"description": "`none`: No authentication",
"type": "string",
"enum": [
"none"
]
},
{
"description": "`client_secret_basic`: `client_id` and `client_secret` used as basic authorization credentials",
"type": "string",
"enum": [
"client_secret_basic"
]
},
{
"description": "`client_secret_post`: `client_id` and `client_secret` sent in the request body",
"type": "string",
"enum": [
"client_secret_post"
]
},
{
"description": "`client_secret_basic`: a `client_assertion` sent in the request body and signed using the `client_secret`",
"type": "string",
"enum": [
"client_secret_jwt"
]
},
{
"description": "`client_secret_basic`: a `client_assertion` sent in the request body and signed by an asymmetric key",
"type": "string",
"enum": [
"private_key_jwt"
]
}
]
},
"JsonWebKeySet_for_JsonWebKeyPublicParameters": { "JsonWebKeySet_for_JsonWebKeyPublicParameters": {
"type": "object", "type": "object",
"required": [ "required": [