mirror of
https://github.com/element-hq/matrix-authentication-service.git
synced 2026-10-23 06:32:59 +00:00
Merge remote-tracking branch 'origin/main' into quenting/lowercase-username
This commit is contained in:
10 files changed
+484
-111
No files matched your search
@@ -7,9 +7,8 @@
|
||||
# https://rust-lang.github.io/mdBook/format/config.html
|
||||
[book]
|
||||
title = "Matrix Authentication Service"
|
||||
authors = ["The Matrix.org Foundation C.I.C."]
|
||||
authors = ["Element Backend Team"]
|
||||
language = "en"
|
||||
multilingual = false
|
||||
|
||||
src = "docs"
|
||||
|
||||
|
||||
+18
-19
@@ -202,25 +202,24 @@ pub async fn config_sync(
|
||||
continue;
|
||||
}
|
||||
|
||||
let encrypted_client_secret =
|
||||
if let Some(client_secret) = provider.client_secret.as_deref() {
|
||||
Some(encrypter.encrypt_to_string(client_secret.as_bytes())?)
|
||||
} else if let Some(mut siwa) = provider.sign_in_with_apple.clone() {
|
||||
// if private key file is defined and not private key (raw), we populate the
|
||||
// private key to hold the content of the private key file.
|
||||
// private key (raw) takes precedence so both can be defined
|
||||
// without issues
|
||||
if siwa.private_key.is_none()
|
||||
&& let Some(private_key_file) = siwa.private_key_file.take()
|
||||
{
|
||||
let key = tokio::fs::read_to_string(private_key_file).await?;
|
||||
siwa.private_key = Some(key);
|
||||
}
|
||||
let encoded = serde_json::to_vec(&siwa)?;
|
||||
Some(encrypter.encrypt_to_string(&encoded)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let encrypted_client_secret = if let Some(client_secret) = provider.client_secret {
|
||||
Some(encrypter.encrypt_to_string(client_secret.value().await?.as_bytes())?)
|
||||
} else if let Some(mut siwa) = provider.sign_in_with_apple.clone() {
|
||||
// if private key file is defined and not private key (raw), we populate the
|
||||
// private key to hold the content of the private key file.
|
||||
// private key (raw) takes precedence so both can be defined
|
||||
// without issues
|
||||
if siwa.private_key.is_none()
|
||||
&& let Some(private_key_file) = siwa.private_key_file.take()
|
||||
{
|
||||
let key = tokio::fs::read_to_string(private_key_file).await?;
|
||||
siwa.private_key = Some(key);
|
||||
}
|
||||
let encoded = serde_json::to_vec(&siwa)?;
|
||||
Some(encrypter.encrypt_to_string(&encoded)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let discovery_mode = match provider.discovery_mode {
|
||||
mas_config::UpstreamOAuth2DiscoveryMode::Oidc => {
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
|
||||
use std::ops::Deref;
|
||||
|
||||
use anyhow::bail;
|
||||
use camino::Utf8PathBuf;
|
||||
use mas_iana::oauth::OAuthClientAuthenticationMethod;
|
||||
use mas_jose::jwk::PublicJsonWebKeySet;
|
||||
use schemars::JsonSchema;
|
||||
@@ -16,7 +14,7 @@ use serde_with::serde_as;
|
||||
use ulid::Ulid;
|
||||
use url::Url;
|
||||
|
||||
use super::ConfigurationSection;
|
||||
use super::{ClientSecret, ClientSecretRaw, ConfigurationSection};
|
||||
|
||||
#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -31,66 +29,6 @@ impl From<PublicJsonWebKeySet> for JwksOrJwksUri {
|
||||
}
|
||||
}
|
||||
|
||||
/// Client secret config option.
|
||||
///
|
||||
/// It either holds the client secret value directly or references a file where
|
||||
/// the client secret is stored.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ClientSecret {
|
||||
File(Utf8PathBuf),
|
||||
Value(String),
|
||||
}
|
||||
|
||||
/// Client secret fields as serialized in JSON.
|
||||
#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
|
||||
struct ClientSecretRaw {
|
||||
/// Path to the file containing the client secret. The client secret is used
|
||||
/// by the `client_secret_basic`, `client_secret_post` and
|
||||
/// `client_secret_jwt` authentication methods.
|
||||
#[schemars(with = "Option<String>")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
client_secret_file: Option<Utf8PathBuf>,
|
||||
|
||||
/// Alternative to `client_secret_file`: Reads the client secret directly
|
||||
/// from the config.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
client_secret: Option<String>,
|
||||
}
|
||||
|
||||
impl TryFrom<ClientSecretRaw> for Option<ClientSecret> {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: ClientSecretRaw) -> Result<Self, Self::Error> {
|
||||
match (value.client_secret, value.client_secret_file) {
|
||||
(None, None) => Ok(None),
|
||||
(None, Some(path)) => Ok(Some(ClientSecret::File(path))),
|
||||
(Some(client_secret), None) => Ok(Some(ClientSecret::Value(client_secret))),
|
||||
(Some(_), Some(_)) => {
|
||||
bail!("Cannot specify both `client_secret` and `client_secret_file`")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<ClientSecret>> for ClientSecretRaw {
|
||||
fn from(value: Option<ClientSecret>) -> Self {
|
||||
match value {
|
||||
Some(ClientSecret::File(path)) => ClientSecretRaw {
|
||||
client_secret_file: Some(path),
|
||||
client_secret: None,
|
||||
},
|
||||
Some(ClientSecret::Value(client_secret)) => ClientSecretRaw {
|
||||
client_secret_file: None,
|
||||
client_secret: Some(client_secret),
|
||||
},
|
||||
None => ClientSecretRaw {
|
||||
client_secret_file: None,
|
||||
client_secret: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Authentication method used by clients
|
||||
#[derive(JsonSchema, Serialize, Deserialize, Copy, Clone, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -273,8 +211,7 @@ impl ClientConfig {
|
||||
/// Returns an error when the client secret could not be read from file.
|
||||
pub async fn client_secret(&self) -> anyhow::Result<Option<String>> {
|
||||
Ok(match &self.client_secret {
|
||||
Some(ClientSecret::File(path)) => Some(tokio::fs::read_to_string(path).await?),
|
||||
Some(ClientSecret::Value(client_secret)) => Some(client_secret.clone()),
|
||||
Some(client_secret) => Some(client_secret.value().await?),
|
||||
None => None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use anyhow::bail;
|
||||
use camino::Utf8PathBuf;
|
||||
use rand::Rng;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -303,3 +305,82 @@ impl ConfigurationSection for SyncConfig {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Client secret config option.
|
||||
///
|
||||
/// It either holds the client secret value directly or references a file where
|
||||
/// the client secret is stored.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ClientSecret {
|
||||
/// Path to the file containing the client secret.
|
||||
File(Utf8PathBuf),
|
||||
|
||||
/// Client secret value.
|
||||
Value(String),
|
||||
}
|
||||
|
||||
/// Client secret fields as serialized in JSON.
|
||||
#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ClientSecretRaw {
|
||||
/// Path to the file containing the client secret. The client secret is used
|
||||
/// by the `client_secret_basic`, `client_secret_post` and
|
||||
/// `client_secret_jwt` authentication methods.
|
||||
#[schemars(with = "Option<String>")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
client_secret_file: Option<Utf8PathBuf>,
|
||||
|
||||
/// Alternative to `client_secret_file`: Reads the client secret directly
|
||||
/// from the config.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
client_secret: Option<String>,
|
||||
}
|
||||
|
||||
impl ClientSecret {
|
||||
/// Returns the client secret.
|
||||
///
|
||||
/// If `client_secret_file` was given, the secret is read from that file.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error when the client secret could not be read from file.
|
||||
pub async fn value(&self) -> anyhow::Result<String> {
|
||||
Ok(match self {
|
||||
ClientSecret::File(path) => tokio::fs::read_to_string(path).await?,
|
||||
ClientSecret::Value(client_secret) => client_secret.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ClientSecretRaw> for Option<ClientSecret> {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: ClientSecretRaw) -> Result<Self, Self::Error> {
|
||||
match (value.client_secret, value.client_secret_file) {
|
||||
(None, None) => Ok(None),
|
||||
(None, Some(path)) => Ok(Some(ClientSecret::File(path))),
|
||||
(Some(client_secret), None) => Ok(Some(ClientSecret::Value(client_secret))),
|
||||
(Some(_), Some(_)) => {
|
||||
bail!("Cannot specify both `client_secret` and `client_secret_file`")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<ClientSecret>> for ClientSecretRaw {
|
||||
fn from(value: Option<ClientSecret>) -> Self {
|
||||
match value {
|
||||
Some(ClientSecret::File(path)) => ClientSecretRaw {
|
||||
client_secret_file: Some(path),
|
||||
client_secret: None,
|
||||
},
|
||||
Some(ClientSecret::Value(client_secret)) => ClientSecretRaw {
|
||||
client_secret_file: None,
|
||||
client_secret: Some(client_secret),
|
||||
},
|
||||
None => ClientSecretRaw {
|
||||
client_secret_file: None,
|
||||
client_secret: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,6 +240,23 @@ impl From<Encryption> for EncryptionRaw {
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads all keys from the given directory.
|
||||
async fn key_configs_from_path(path: &Utf8PathBuf) -> anyhow::Result<Vec<KeyConfig>> {
|
||||
let mut result = vec![];
|
||||
let mut read_dir = tokio::fs::read_dir(path).await?;
|
||||
while let Some(dir_entry) = read_dir.next_entry().await? {
|
||||
if !dir_entry.path().is_file() {
|
||||
continue;
|
||||
}
|
||||
result.push(KeyConfig {
|
||||
kid: None,
|
||||
password: None,
|
||||
key: Key::File(dir_entry.path().try_into()?),
|
||||
});
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Application secrets
|
||||
#[serde_as]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -250,9 +267,14 @@ pub struct SecretsConfig {
|
||||
#[serde(flatten)]
|
||||
encryption: Encryption,
|
||||
|
||||
/// List of private keys to use for signing and encrypting payloads
|
||||
#[serde(default)]
|
||||
keys: Vec<KeyConfig>,
|
||||
/// List of private keys to use for signing and encrypting payloads.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
keys: Option<Vec<KeyConfig>>,
|
||||
|
||||
/// Directory of private keys to use for signing and encrypting payloads.
|
||||
#[schemars(with = "Option<String>")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
keys_dir: Option<Utf8PathBuf>,
|
||||
}
|
||||
|
||||
impl SecretsConfig {
|
||||
@@ -263,7 +285,8 @@ impl SecretsConfig {
|
||||
/// Returns an error when a key could not be imported
|
||||
#[tracing::instrument(name = "secrets.load", skip_all)]
|
||||
pub async fn key_store(&self) -> anyhow::Result<Keystore> {
|
||||
let web_keys = try_join_all(self.keys.iter().map(KeyConfig::json_web_key)).await?;
|
||||
let key_configs = self.key_configs().await?;
|
||||
let web_keys = try_join_all(key_configs.iter().map(KeyConfig::json_web_key)).await?;
|
||||
|
||||
Ok(Keystore::new(JsonWebKeySet::new(web_keys)))
|
||||
}
|
||||
@@ -297,6 +320,21 @@ impl SecretsConfig {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a combined list of key configs given inline and from files.
|
||||
///
|
||||
/// If `keys_dir` was given, the keys are read from file.
|
||||
async fn key_configs(&self) -> anyhow::Result<Vec<KeyConfig>> {
|
||||
let mut key_configs = match &self.keys_dir {
|
||||
Some(keys_dir) => key_configs_from_path(keys_dir).await?,
|
||||
None => vec![],
|
||||
};
|
||||
|
||||
let inline_key_configs = self.keys.as_deref().unwrap_or_default();
|
||||
key_configs.extend(inline_key_configs.iter().cloned());
|
||||
|
||||
Ok(key_configs)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigurationSection for SecretsConfig {
|
||||
@@ -378,7 +416,8 @@ impl SecretsConfig {
|
||||
|
||||
Ok(Self {
|
||||
encryption: Encryption::Value(Standard.sample(&mut rng)),
|
||||
keys: vec![rsa_key, ec_p256_key, ec_p384_key, ec_k256_key],
|
||||
keys: Some(vec![rsa_key, ec_p256_key, ec_p384_key, ec_k256_key]),
|
||||
keys_dir: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -419,7 +458,8 @@ impl SecretsConfig {
|
||||
|
||||
Self {
|
||||
encryption: Encryption::Value([0xEA; 32]),
|
||||
keys: vec![rsa_key, ecdsa_key],
|
||||
keys: Some(vec![rsa_key, ecdsa_key]),
|
||||
keys_dir: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -435,6 +475,132 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_config() {
|
||||
task::spawn_blocking(|| {
|
||||
Jail::expect_with(|jail| {
|
||||
jail.create_file(
|
||||
"config.yaml",
|
||||
indoc::indoc! {r"
|
||||
secrets:
|
||||
encryption_file: encryption
|
||||
keys_dir: keys
|
||||
"},
|
||||
)?;
|
||||
jail.create_file(
|
||||
"encryption",
|
||||
"0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff",
|
||||
)?;
|
||||
jail.create_dir("keys")?;
|
||||
jail.create_file(
|
||||
"keys/key1",
|
||||
indoc::indoc! {r"
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
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
|
||||
-----END RSA PRIVATE KEY-----
|
||||
"},
|
||||
)?;
|
||||
jail.create_file(
|
||||
"keys/key2",
|
||||
indoc::indoc! {r"
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIKlZz/GnH0idVH1PnAF4HQNwRafgBaE2tmyN1wjfdOQqoAoGCCqGSM49
|
||||
AwEHoUQDQgAEHrgPeG+Mt8eahih1h4qaPjhl7jT25cdzBkg3dbVks6gBR2Rx4ug9
|
||||
h27LAir5RqxByHvua2XsP46rSTChof78uw==
|
||||
-----END EC PRIVATE KEY-----
|
||||
"},
|
||||
)?;
|
||||
|
||||
let config = Figment::new()
|
||||
.merge(Yaml::file("config.yaml"))
|
||||
.extract_inner::<SecretsConfig>("secrets")?;
|
||||
|
||||
Handle::current().block_on(async move {
|
||||
assert!(
|
||||
matches!(config.encryption, Encryption::File(ref p) if p == "encryption")
|
||||
);
|
||||
assert_eq!(
|
||||
config.encryption().await.unwrap(),
|
||||
[
|
||||
0, 0, 17, 17, 34, 34, 51, 51, 68, 68, 85, 85, 102, 102, 119, 119, 136,
|
||||
136, 153, 153, 170, 170, 187, 187, 204, 204, 221, 221, 238, 238, 255,
|
||||
255
|
||||
]
|
||||
);
|
||||
|
||||
let mut key_config = config.key_configs().await.unwrap();
|
||||
key_config.sort_by_key(|a| {
|
||||
if let Key::File(p) = &a.key {
|
||||
Some(p.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let key_store = config.key_store().await.unwrap();
|
||||
|
||||
assert!(key_config[0].kid.is_none());
|
||||
assert!(matches!(&key_config[0].key, Key::File(p) if p == "keys/key1"));
|
||||
assert!(key_store.iter().any(|k| k.kid() == Some("xmgGCzGtQFmhEOP0YAqBt-oZyVauSVMXcf4kwcgGZLc")));
|
||||
assert!(key_config[1].kid.is_none());
|
||||
assert!(matches!(&key_config[1].key, Key::File(p) if p == "keys/key2"));
|
||||
assert!(key_store.iter().any(|k| k.kid() == Some("ONUCn80fsiISFWKrVMEiirNVr-QEvi7uQI0QH9q9q4o")));
|
||||
});
|
||||
|
||||
Ok(())
|
||||
});
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_config_inline_secrets() {
|
||||
task::spawn_blocking(|| {
|
||||
@@ -487,4 +653,57 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_config_mixed_key_sources() {
|
||||
task::spawn_blocking(|| {
|
||||
Jail::expect_with(|jail| {
|
||||
jail.create_file(
|
||||
"config.yaml",
|
||||
indoc::indoc! {r"
|
||||
secrets:
|
||||
encryption_file: encryption
|
||||
keys_dir: keys
|
||||
keys:
|
||||
- kid: lekid0
|
||||
key: |
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIOtZfDuXZr/NC0V3sisR4Chf7RZg6a2dpZesoXMlsPeRoAoGCCqGSM49
|
||||
AwEHoUQDQgAECfpqx64lrR85MOhdMxNmIgmz8IfmM5VY9ICX9aoaArnD9FjgkBIl
|
||||
fGmQWxxXDSWH6SQln9tROVZaduenJqDtDw==
|
||||
-----END EC PRIVATE KEY-----
|
||||
"},
|
||||
)?;
|
||||
jail.create_dir("keys")?;
|
||||
jail.create_file(
|
||||
"keys/key_from_file",
|
||||
indoc::indoc! {r"
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIKlZz/GnH0idVH1PnAF4HQNwRafgBaE2tmyN1wjfdOQqoAoGCCqGSM49
|
||||
AwEHoUQDQgAEHrgPeG+Mt8eahih1h4qaPjhl7jT25cdzBkg3dbVks6gBR2Rx4ug9
|
||||
h27LAir5RqxByHvua2XsP46rSTChof78uw==
|
||||
-----END EC PRIVATE KEY-----
|
||||
"},
|
||||
)?;
|
||||
|
||||
let config = Figment::new()
|
||||
.merge(Yaml::file("config.yaml"))
|
||||
.extract_inner::<SecretsConfig>("secrets")?;
|
||||
|
||||
Handle::current().block_on(async move {
|
||||
let key_config = config.key_configs().await.unwrap();
|
||||
let key_store = config.key_store().await.unwrap();
|
||||
|
||||
assert!(key_config[0].kid.is_none());
|
||||
assert!(matches!(&key_config[0].key, Key::File(p) if p == "keys/key_from_file"));
|
||||
assert!(key_store.iter().any(|k| k.kid() == Some("ONUCn80fsiISFWKrVMEiirNVr-QEvi7uQI0QH9q9q4o")));
|
||||
assert!(key_store.iter().any(|k| k.kid() == Some("lekid0")));
|
||||
});
|
||||
|
||||
Ok(())
|
||||
});
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -10,11 +10,11 @@ use camino::Utf8PathBuf;
|
||||
use mas_iana::jose::JsonWebSignatureAlg;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize, de::Error};
|
||||
use serde_with::skip_serializing_none;
|
||||
use serde_with::{serde_as, skip_serializing_none};
|
||||
use ulid::Ulid;
|
||||
use url::Url;
|
||||
|
||||
use crate::ConfigurationSection;
|
||||
use crate::{ClientSecret, ClientSecretRaw, ConfigurationSection};
|
||||
|
||||
/// Upstream OAuth 2.0 providers configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
|
||||
@@ -475,6 +475,7 @@ impl OnBackchannelLogout {
|
||||
}
|
||||
|
||||
/// Configuration for one upstream OAuth 2 provider.
|
||||
#[serde_as]
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct Provider {
|
||||
@@ -541,8 +542,10 @@ pub struct Provider {
|
||||
///
|
||||
/// Used by the `client_secret_basic`, `client_secret_post`, and
|
||||
/// `client_secret_jwt` methods
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub client_secret: Option<String>,
|
||||
#[schemars(with = "ClientSecretRaw")]
|
||||
#[serde_as(as = "serde_with::TryFromInto<ClientSecretRaw>")]
|
||||
#[serde(flatten)]
|
||||
pub client_secret: Option<ClientSecret>,
|
||||
|
||||
/// The method to authenticate the client with the provider
|
||||
pub token_endpoint_auth_method: TokenAuthMethod,
|
||||
@@ -656,3 +659,110 @@ pub struct Provider {
|
||||
#[serde(default, skip_serializing_if = "OnBackchannelLogout::is_default")]
|
||||
pub on_backchannel_logout: OnBackchannelLogout,
|
||||
}
|
||||
|
||||
impl Provider {
|
||||
/// Returns the client secret.
|
||||
///
|
||||
/// If `client_secret_file` was given, the secret is read from that file.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error when the client secret could not be read from file.
|
||||
pub async fn client_secret(&self) -> anyhow::Result<Option<String>> {
|
||||
Ok(match &self.client_secret {
|
||||
Some(client_secret) => Some(client_secret.value().await?),
|
||||
None => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
use figment::{
|
||||
Figment, Jail,
|
||||
providers::{Format, Yaml},
|
||||
};
|
||||
use tokio::{runtime::Handle, task};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_config() {
|
||||
task::spawn_blocking(|| {
|
||||
Jail::expect_with(|jail| {
|
||||
jail.create_file(
|
||||
"config.yaml",
|
||||
r#"
|
||||
upstream_oauth2:
|
||||
providers:
|
||||
- id: 01GFWR28C4KNE04WG3HKXB7C9R
|
||||
client_id: upstream-oauth2
|
||||
token_endpoint_auth_method: none
|
||||
|
||||
- id: 01GFWR32NCQ12B8Z0J8CPXRRB6
|
||||
client_id: upstream-oauth2
|
||||
client_secret_file: secret
|
||||
token_endpoint_auth_method: client_secret_basic
|
||||
|
||||
- id: 01GFWR3WHR93Y5HK389H28VHZ9
|
||||
client_id: upstream-oauth2
|
||||
client_secret: c1!3n753c237
|
||||
token_endpoint_auth_method: client_secret_post
|
||||
|
||||
- id: 01GFWR43R2ZZ8HX9CVBNW9TJWG
|
||||
client_id: upstream-oauth2
|
||||
client_secret_file: secret
|
||||
token_endpoint_auth_method: client_secret_jwt
|
||||
|
||||
- id: 01GFWR4BNFDCC4QDG6AMSP1VRR
|
||||
client_id: upstream-oauth2
|
||||
token_endpoint_auth_method: private_key_jwt
|
||||
jwks:
|
||||
keys:
|
||||
- kid: "03e84aed4ef4431014e8617567864c4efaaaede9"
|
||||
kty: "RSA"
|
||||
alg: "RS256"
|
||||
use: "sig"
|
||||
e: "AQAB"
|
||||
n: "ma2uRyBeSEOatGuDpCiV9oIxlDWix_KypDYuhQfEzqi_BiF4fV266OWfyjcABbam59aJMNvOnKW3u_eZM-PhMCBij5MZ-vcBJ4GfxDJeKSn-GP_dJ09rpDcILh8HaWAnPmMoi4DC0nrfE241wPISvZaaZnGHkOrfN_EnA5DligLgVUbrA5rJhQ1aSEQO_gf1raEOW3DZ_ACU3qhtgO0ZBG3a5h7BPiRs2sXqb2UCmBBgwyvYLDebnpE7AotF6_xBIlR-Cykdap3GHVMXhrIpvU195HF30ZoBU4dMd-AeG6HgRt4Cqy1moGoDgMQfbmQ48Hlunv9_Vi2e2CLvYECcBw"
|
||||
|
||||
- kid: "d01c1abe249269f72ef7ca2613a86c9f05e59567"
|
||||
kty: "RSA"
|
||||
alg: "RS256"
|
||||
use: "sig"
|
||||
e: "AQAB"
|
||||
n: "0hukqytPwrj1RbMYhYoepCi3CN5k7DwYkTe_Cmb7cP9_qv4ok78KdvFXt5AnQxCRwBD7-qTNkkfMWO2RxUMBdQD0ED6tsSb1n5dp0XY8dSWiBDCX8f6Hr-KolOpvMLZKRy01HdAWcM6RoL9ikbjYHUEW1C8IJnw3MzVHkpKFDL354aptdNLaAdTCBvKzU9WpXo10g-5ctzSlWWjQuecLMQ4G1mNdsR1LHhUENEnOvgT8cDkX0fJzLbEbyBYkdMgKggyVPEB1bg6evG4fTKawgnf0IDSPxIU-wdS9wdSP9ZCJJPLi5CEp-6t6rE_sb2dGcnzjCGlembC57VwpkUvyMw"
|
||||
"#,
|
||||
)?;
|
||||
jail.create_file("secret", r"c1!3n753c237")?;
|
||||
|
||||
let config = Figment::new()
|
||||
.merge(Yaml::file("config.yaml"))
|
||||
.extract_inner::<UpstreamOAuth2Config>("upstream_oauth2")?;
|
||||
|
||||
assert_eq!(config.providers.len(), 5);
|
||||
|
||||
assert_eq!(
|
||||
config.providers[1].id,
|
||||
Ulid::from_str("01GFWR32NCQ12B8Z0J8CPXRRB6").unwrap()
|
||||
);
|
||||
|
||||
assert!(config.providers[0].client_secret.is_none());
|
||||
assert!(matches!(config.providers[1].client_secret, Some(ClientSecret::File(ref p)) if p == "secret"));
|
||||
assert!(matches!(config.providers[2].client_secret, Some(ClientSecret::Value(ref v)) if v == "c1!3n753c237"));
|
||||
assert!(matches!(config.providers[3].client_secret, Some(ClientSecret::File(ref p)) if p == "secret"));
|
||||
assert!(config.providers[4].client_secret.is_none());
|
||||
|
||||
Handle::current().block_on(async move {
|
||||
assert_eq!(config.providers[1].client_secret().await.unwrap().unwrap(), "c1!3n753c237");
|
||||
assert_eq!(config.providers[2].client_secret().await.unwrap().unwrap(), "c1!3n753c237");
|
||||
assert_eq!(config.providers[3].client_secret().await.unwrap().unwrap(), "c1!3n753c237");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}).await.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,9 @@ use std::{collections::BTreeMap, str::FromStr as _};
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use mas_config::{
|
||||
UpstreamOAuth2ClaimsImports, UpstreamOAuth2DiscoveryMode, UpstreamOAuth2ImportAction,
|
||||
UpstreamOAuth2OnBackchannelLogout, UpstreamOAuth2PkceMethod, UpstreamOAuth2ResponseMode,
|
||||
UpstreamOAuth2TokenAuthMethod,
|
||||
ClientSecret, UpstreamOAuth2ClaimsImports, UpstreamOAuth2DiscoveryMode,
|
||||
UpstreamOAuth2ImportAction, UpstreamOAuth2OnBackchannelLogout, UpstreamOAuth2PkceMethod,
|
||||
UpstreamOAuth2ResponseMode, UpstreamOAuth2TokenAuthMethod,
|
||||
};
|
||||
use mas_iana::jose::JsonWebSignatureAlg;
|
||||
use oauth2_types::scope::{OPENID, Scope, ScopeToken};
|
||||
@@ -328,7 +328,7 @@ impl OidcProvider {
|
||||
human_name: self.idp_name,
|
||||
brand_name: self.idp_brand,
|
||||
client_id,
|
||||
client_secret: self.client_secret,
|
||||
client_secret: self.client_secret.map(ClientSecret::Value),
|
||||
token_endpoint_auth_method,
|
||||
sign_in_with_apple: None,
|
||||
token_endpoint_auth_signing_alg: None,
|
||||
|
||||
+21
-5
@@ -1640,12 +1640,21 @@
|
||||
"pattern": "[0-9a-fA-F]{64}"
|
||||
},
|
||||
"keys": {
|
||||
"description": "List of private keys to use for signing and encrypting payloads",
|
||||
"type": "array",
|
||||
"description": "List of private keys to use for signing and encrypting payloads.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/KeyConfig"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
},
|
||||
"keys_dir": {
|
||||
"description": "Directory of private keys to use for signing and encrypting payloads.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2159,8 +2168,15 @@
|
||||
"description": "The client ID to use when authenticating with the provider",
|
||||
"type": "string"
|
||||
},
|
||||
"client_secret_file": {
|
||||
"description": "Path to the file containing the client secret. The client secret is used\n by the `client_secret_basic`, `client_secret_post` and\n `client_secret_jwt` authentication methods.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"client_secret": {
|
||||
"description": "The client secret to use when authenticating with the provider\n\n Used by the `client_secret_basic`, `client_secret_post`, and\n `client_secret_jwt` methods",
|
||||
"description": "Alternative to `client_secret_file`: Reads the client secret directly\n from the config.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
|
||||
@@ -222,7 +222,7 @@ The secret is not updated when the content of the file changes.
|
||||
> Changing the encryption secret afterwards will lead to a loss of all encrypted
|
||||
> information in the database.
|
||||
|
||||
### `secrets.keys`
|
||||
### Singing Keys
|
||||
|
||||
The service can use a number of key types for signing.
|
||||
The following key types are supported:
|
||||
@@ -232,15 +232,26 @@ The following key types are supported:
|
||||
- ECDSA with the P-384 (`secp384r1`) curve
|
||||
- ECDSA with the K-256 (`secp256k1`) curve
|
||||
|
||||
Each entry in the list corresponds to one signing key used by MAS.
|
||||
The key can either be specified inline (with the `key` property),
|
||||
or loaded from a file (with the `key_file` property).
|
||||
The following key formats are supported:
|
||||
|
||||
- PKCS#1 PEM or DER-encoded RSA private key
|
||||
- PKCS#8 PEM or DER-encoded RSA or ECDSA private key, encrypted or not
|
||||
- SEC1 PEM or DER-encoded ECDSA private key
|
||||
|
||||
The keys can be given as a directory path via `secrets.keys_dir`
|
||||
or, alternatively, as an inline configuration list via `secrets.keys`.
|
||||
|
||||
#### `secrets.keys_dir`
|
||||
|
||||
Path to the directory containing MAS signing key files.
|
||||
Only keys that don’t require a password are supported.
|
||||
|
||||
#### `secrets.keys`
|
||||
|
||||
Each entry in the list corresponds to one signing key used by MAS.
|
||||
The key can either be specified inline (with the `key` property),
|
||||
or loaded from a file (with the `key_file` property).
|
||||
|
||||
A [JWK Key ID] is automatically derived from each key.
|
||||
To override this default, set `kid` to a custom value.
|
||||
The `kid` can be any case-sensitive string value as long as it is unique to this list;
|
||||
@@ -642,7 +653,8 @@ upstream_oauth2:
|
||||
# The client secret to use to authenticate to the provider
|
||||
# This is only used by the `client_secret_post`, `client_secret_basic`
|
||||
# and `client_secret_jwk` authentication methods
|
||||
#client_secret: f4f6bb68a0269264877e9cb23b1856ab
|
||||
client_secret_file: secret
|
||||
# OR client_secret: f4f6bb68a0269264877e9cb23b1856ab
|
||||
|
||||
# Which authentication method to use to authenticate to the provider
|
||||
# Supported methods are:
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ set -eux
|
||||
# Install the dependencies if we're in the Cloudflare Pages build environment
|
||||
# In this environment, the CF_PAGES environment variable is set to 1
|
||||
if [ "${CF_PAGES:-""}" = "1" ]; then
|
||||
MDBOOK_VERSION=0.4.32
|
||||
MDBOOK_VERSION=0.5.0
|
||||
|
||||
# Install rustup
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --default-toolchain none -y
|
||||
|
||||
Reference in new issue
Block a user