Skip to content
This repository was archived by the owner on Sep 10, 2024. It is now read-only.

Commit cba431d

Browse files
committed
Flatten the clients config
1 parent 48b6013 commit cba431d

File tree

4 files changed

+219
-197
lines changed

4 files changed

+219
-197
lines changed

crates/cli/src/sync.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,10 +259,10 @@ pub async fn config_sync(
259259
continue;
260260
}
261261

262-
let client_secret = client.client_secret();
262+
let client_secret = client.client_secret.as_deref();
263263
let client_auth_method = client.client_auth_method();
264-
let jwks = client.jwks();
265-
let jwks_uri = client.jwks_uri();
264+
let jwks = client.jwks.as_ref();
265+
let jwks_uri = client.jwks_uri.as_ref();
266266

267267
// TODO: should be moved somewhere else
268268
let encrypted_client_secret = client_secret

crates/config/src/sections/clients.rs

Lines changed: 134 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,15 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
use std::ops::{Deref, DerefMut};
15+
use std::ops::Deref;
1616

1717
use async_trait::async_trait;
18+
use figment::Figment;
1819
use mas_iana::oauth::OAuthClientAuthenticationMethod;
1920
use mas_jose::jwk::PublicJsonWebKeySet;
2021
use rand::Rng;
2122
use schemars::JsonSchema;
22-
use serde::{Deserialize, Serialize};
23-
use serde_with::skip_serializing_none;
24-
use thiserror::Error;
23+
use serde::{de::Error, Deserialize, Serialize};
2524
use ulid::Ulid;
2625
use url::Url;
2726

@@ -41,40 +40,42 @@ impl From<PublicJsonWebKeySet> for JwksOrJwksUri {
4140
}
4241

4342
/// Authentication method used by clients
44-
#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
45-
#[serde(tag = "client_auth_method", rename_all = "snake_case")]
43+
#[derive(JsonSchema, Serialize, Deserialize, Copy, Clone, Debug)]
44+
#[serde(rename_all = "snake_case")]
4645
pub enum ClientAuthMethodConfig {
4746
/// `none`: No authentication
4847
None,
4948

5049
/// `client_secret_basic`: `client_id` and `client_secret` used as basic
5150
/// authorization credentials
52-
ClientSecretBasic {
53-
/// The client secret
54-
client_secret: String,
55-
},
51+
ClientSecretBasic,
5652

5753
/// `client_secret_post`: `client_id` and `client_secret` sent in the
5854
/// request body
59-
ClientSecretPost {
60-
/// The client secret
61-
client_secret: String,
62-
},
55+
ClientSecretPost,
6356

6457
/// `client_secret_basic`: a `client_assertion` sent in the request body and
6558
/// signed using the `client_secret`
66-
ClientSecretJwt {
67-
/// The client secret
68-
client_secret: String,
69-
},
59+
ClientSecretJwt,
7060

7161
/// `client_secret_basic`: a `client_assertion` sent in the request body and
7262
/// signed by an asymmetric key
73-
PrivateKeyJwt(JwksOrJwksUri),
63+
PrivateKeyJwt,
64+
}
65+
66+
impl std::fmt::Display for ClientAuthMethodConfig {
67+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68+
match self {
69+
ClientAuthMethodConfig::None => write!(f, "none"),
70+
ClientAuthMethodConfig::ClientSecretBasic => write!(f, "client_secret_basic"),
71+
ClientAuthMethodConfig::ClientSecretPost => write!(f, "client_secret_post"),
72+
ClientAuthMethodConfig::ClientSecretJwt => write!(f, "client_secret_jwt"),
73+
ClientAuthMethodConfig::PrivateKeyJwt => write!(f, "private_key_jwt"),
74+
}
75+
}
7476
}
7577

7678
/// An OAuth 2.0 client configuration
77-
#[skip_serializing_none]
7879
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
7980
pub struct ClientConfig {
8081
/// The client ID
@@ -86,75 +87,129 @@ pub struct ClientConfig {
8687
pub client_id: Ulid,
8788

8889
/// Authentication method used for this client
89-
#[serde(flatten)]
90-
pub client_auth_method: ClientAuthMethodConfig,
90+
client_auth_method: ClientAuthMethodConfig,
91+
92+
/// The client secret, used by the `client_secret_basic`,
93+
/// `client_secret_post` and `client_secret_jwt` authentication methods
94+
#[serde(skip_serializing_if = "Option::is_none")]
95+
pub client_secret: Option<String>,
96+
97+
/// The JSON Web Key Set (JWKS) used by the `private_key_jwt` authentication
98+
/// method. Mutually exclusive with `jwks_uri`
99+
#[serde(skip_serializing_if = "Option::is_none")]
100+
pub jwks: Option<PublicJsonWebKeySet>,
101+
102+
/// The URL of the JSON Web Key Set (JWKS) used by the `private_key_jwt`
103+
/// authentication method. Mutually exclusive with `jwks`
104+
#[serde(skip_serializing_if = "Option::is_none")]
105+
pub jwks_uri: Option<Url>,
91106

92107
/// List of allowed redirect URIs
93-
#[serde(default)]
108+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
94109
pub redirect_uris: Vec<Url>,
95110
}
96111

97-
#[derive(Debug, Error)]
98-
#[error("Invalid redirect URI")]
99-
pub struct InvalidRedirectUriError;
100-
101112
impl ClientConfig {
102-
#[doc(hidden)]
103-
#[must_use]
104-
pub fn client_secret(&self) -> Option<&str> {
105-
match &self.client_auth_method {
106-
ClientAuthMethodConfig::ClientSecretPost { client_secret }
107-
| ClientAuthMethodConfig::ClientSecretBasic { client_secret }
108-
| ClientAuthMethodConfig::ClientSecretJwt { client_secret } => Some(client_secret),
109-
_ => None,
113+
fn validate(&self) -> Result<(), figment::error::Error> {
114+
let auth_method = self.client_auth_method;
115+
match self.client_auth_method {
116+
ClientAuthMethodConfig::PrivateKeyJwt => {
117+
if self.jwks.is_none() && self.jwks_uri.is_none() {
118+
let error = figment::error::Error::custom(
119+
"jwks or jwks_uri is required for private_key_jwt",
120+
);
121+
return Err(error.with_path("client_auth_method"));
122+
}
123+
124+
if self.jwks.is_some() && self.jwks_uri.is_some() {
125+
let error =
126+
figment::error::Error::custom("jwks and jwks_uri are mutually exclusive");
127+
return Err(error.with_path("jwks"));
128+
}
129+
130+
if self.client_secret.is_some() {
131+
let error = figment::error::Error::custom(
132+
"client_secret is not allowed with private_key_jwt",
133+
);
134+
return Err(error.with_path("client_secret"));
135+
}
136+
}
137+
138+
ClientAuthMethodConfig::ClientSecretPost
139+
| ClientAuthMethodConfig::ClientSecretBasic
140+
| ClientAuthMethodConfig::ClientSecretJwt => {
141+
if self.client_secret.is_none() {
142+
let error = figment::error::Error::custom(format!(
143+
"client_secret is required for {auth_method}"
144+
));
145+
return Err(error.with_path("client_auth_method"));
146+
}
147+
148+
if self.jwks.is_some() {
149+
let error = figment::error::Error::custom(format!(
150+
"jwks is not allowed with {auth_method}"
151+
));
152+
return Err(error.with_path("jwks"));
153+
}
154+
155+
if self.jwks_uri.is_some() {
156+
let error = figment::error::Error::custom(format!(
157+
"jwks_uri is not allowed with {auth_method}"
158+
));
159+
return Err(error.with_path("jwks_uri"));
160+
}
161+
}
162+
163+
ClientAuthMethodConfig::None => {
164+
if self.client_secret.is_some() {
165+
let error = figment::error::Error::custom(
166+
"client_secret is not allowed with none authentication method",
167+
);
168+
return Err(error.with_path("client_secret"));
169+
}
170+
171+
if self.jwks.is_some() {
172+
let error = figment::error::Error::custom(
173+
"jwks is not allowed with none authentication method",
174+
);
175+
return Err(error);
176+
}
177+
178+
if self.jwks_uri.is_some() {
179+
let error = figment::error::Error::custom(
180+
"jwks_uri is not allowed with none authentication method",
181+
);
182+
return Err(error);
183+
}
184+
}
110185
}
186+
187+
Ok(())
111188
}
112189

113-
#[doc(hidden)]
190+
/// Authentication method used for this client
114191
#[must_use]
115192
pub fn client_auth_method(&self) -> OAuthClientAuthenticationMethod {
116-
match &self.client_auth_method {
193+
match self.client_auth_method {
117194
ClientAuthMethodConfig::None => OAuthClientAuthenticationMethod::None,
118-
ClientAuthMethodConfig::ClientSecretBasic { .. } => {
195+
ClientAuthMethodConfig::ClientSecretBasic => {
119196
OAuthClientAuthenticationMethod::ClientSecretBasic
120197
}
121-
ClientAuthMethodConfig::ClientSecretPost { .. } => {
198+
ClientAuthMethodConfig::ClientSecretPost => {
122199
OAuthClientAuthenticationMethod::ClientSecretPost
123200
}
124-
ClientAuthMethodConfig::ClientSecretJwt { .. } => {
201+
ClientAuthMethodConfig::ClientSecretJwt => {
125202
OAuthClientAuthenticationMethod::ClientSecretJwt
126203
}
127-
ClientAuthMethodConfig::PrivateKeyJwt(_) => {
128-
OAuthClientAuthenticationMethod::PrivateKeyJwt
129-
}
130-
}
131-
}
132-
133-
#[doc(hidden)]
134-
#[must_use]
135-
pub fn jwks(&self) -> Option<&PublicJsonWebKeySet> {
136-
match &self.client_auth_method {
137-
ClientAuthMethodConfig::PrivateKeyJwt(JwksOrJwksUri::Jwks(jwks)) => Some(jwks),
138-
_ => None,
139-
}
140-
}
141-
142-
#[doc(hidden)]
143-
#[must_use]
144-
pub fn jwks_uri(&self) -> Option<&Url> {
145-
match &self.client_auth_method {
146-
ClientAuthMethodConfig::PrivateKeyJwt(JwksOrJwksUri::JwksUri(jwks_uri)) => {
147-
Some(jwks_uri)
148-
}
149-
_ => None,
204+
ClientAuthMethodConfig::PrivateKeyJwt => OAuthClientAuthenticationMethod::PrivateKeyJwt,
150205
}
151206
}
152207
}
153208

154209
/// List of OAuth 2.0/OIDC clients config
155210
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
156211
#[serde(transparent)]
157-
pub struct ClientsConfig(Vec<ClientConfig>);
212+
pub struct ClientsConfig(#[schemars(with = "Vec::<ClientConfig>")] Vec<ClientConfig>);
158213

159214
impl Deref for ClientsConfig {
160215
type Target = Vec<ClientConfig>;
@@ -164,12 +219,6 @@ impl Deref for ClientsConfig {
164219
}
165220
}
166221

167-
impl DerefMut for ClientsConfig {
168-
fn deref_mut(&mut self) -> &mut Self::Target {
169-
&mut self.0
170-
}
171-
}
172-
173222
impl IntoIterator for ClientsConfig {
174223
type Item = ClientConfig;
175224
type IntoIter = std::vec::IntoIter<ClientConfig>;
@@ -190,6 +239,21 @@ impl ConfigurationSection for ClientsConfig {
190239
Ok(Self::default())
191240
}
192241

242+
fn validate(&self, figment: &Figment) -> Result<(), figment::error::Error> {
243+
for (index, client) in self.0.iter().enumerate() {
244+
client.validate().map_err(|mut err| {
245+
// Save the error location information in the error
246+
err.metadata = figment.find_metadata(Self::PATH.unwrap()).cloned();
247+
err.profile = Some(figment::Profile::Default);
248+
err.path.insert(0, Self::PATH.unwrap().to_owned());
249+
err.path.insert(1, format!("{index}"));
250+
err
251+
})?;
252+
}
253+
254+
Ok(())
255+
}
256+
193257
fn test() -> Self {
194258
Self::default()
195259
}

crates/config/src/util.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,29 @@ pub trait ConfigurationSection: Sized + DeserializeOwned + Serialize {
2929
where
3030
R: Rng + Send;
3131

32+
/// Validate the configuration section
33+
///
34+
/// # Errors
35+
///
36+
/// Returns an error if the configuration is invalid
37+
fn validate(&self, _figment: &Figment) -> Result<(), FigmentError> {
38+
Ok(())
39+
}
40+
3241
/// Extract configuration from a Figment instance.
3342
///
3443
/// # Errors
3544
///
3645
/// Returns an error if the configuration could not be loaded
3746
fn extract(figment: &Figment) -> Result<Self, FigmentError> {
38-
if let Some(path) = Self::PATH {
39-
figment.extract_inner(path)
47+
let this: Self = if let Some(path) = Self::PATH {
48+
figment.extract_inner(path)?
4049
} else {
41-
figment.extract()
42-
}
50+
figment.extract()?
51+
};
52+
53+
this.validate(figment)?;
54+
Ok(this)
4355
}
4456

4557
/// Generate config used in unit tests

0 commit comments

Comments
 (0)