You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-07-29 22:01:14 +03:00
Define upstream OAuth providers in the config
And adds CLI tool to sync them with the database (WIP)
This commit is contained in:
@ -12,10 +12,16 @@
|
|||||||
// 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::collections::HashSet;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use mas_config::{ConfigurationSection, RootConfig};
|
use mas_config::{ConfigurationSection, RootConfig};
|
||||||
|
use mas_storage::{upstream_oauth2::UpstreamOAuthProviderRepository, Repository, RepositoryAccess};
|
||||||
|
use mas_storage_pg::PgRepository;
|
||||||
use rand::SeedableRng;
|
use rand::SeedableRng;
|
||||||
use tracing::{info, info_span};
|
use tracing::{info, info_span, warn};
|
||||||
|
|
||||||
|
use crate::util::database_from_config;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
pub(super) struct Options {
|
pub(super) struct Options {
|
||||||
@ -33,27 +39,36 @@ enum Subcommand {
|
|||||||
|
|
||||||
/// Generate a new config file
|
/// Generate a new config file
|
||||||
Generate,
|
Generate,
|
||||||
|
|
||||||
|
/// Sync the clients and providers from the config file to the database
|
||||||
|
Sync {
|
||||||
|
/// Prune elements that are in the database but not in the config file
|
||||||
|
/// anymore
|
||||||
|
#[clap(long)]
|
||||||
|
prune: bool,
|
||||||
|
|
||||||
|
/// Do not actually write to the database
|
||||||
|
#[clap(long)]
|
||||||
|
dry_run: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Options {
|
impl Options {
|
||||||
pub async fn run(&self, root: &super::Options) -> anyhow::Result<()> {
|
pub async fn run(self, root: &super::Options) -> anyhow::Result<()> {
|
||||||
use Subcommand as SC;
|
use Subcommand as SC;
|
||||||
match &self.subcommand {
|
match self.subcommand {
|
||||||
SC::Dump => {
|
SC::Dump => {
|
||||||
let _span = info_span!("cli.config.dump").entered();
|
let _span = info_span!("cli.config.dump").entered();
|
||||||
|
|
||||||
let config: RootConfig = root.load_config()?;
|
let config: RootConfig = root.load_config()?;
|
||||||
|
|
||||||
serde_yaml::to_writer(std::io::stdout(), &config)?;
|
serde_yaml::to_writer(std::io::stdout(), &config)?;
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
SC::Check => {
|
SC::Check => {
|
||||||
let _span = info_span!("cli.config.check").entered();
|
let _span = info_span!("cli.config.check").entered();
|
||||||
|
|
||||||
let _config: RootConfig = root.load_config()?;
|
let _config: RootConfig = root.load_config()?;
|
||||||
info!(path = ?root.config, "Configuration file looks good");
|
info!(path = ?root.config, "Configuration file looks good");
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
SC::Generate => {
|
SC::Generate => {
|
||||||
let _span = info_span!("cli.config.generate").entered();
|
let _span = info_span!("cli.config.generate").entered();
|
||||||
@ -63,9 +78,60 @@ impl Options {
|
|||||||
let config = RootConfig::load_and_generate(rng).await?;
|
let config = RootConfig::load_and_generate(rng).await?;
|
||||||
|
|
||||||
serde_yaml::to_writer(std::io::stdout(), &config)?;
|
serde_yaml::to_writer(std::io::stdout(), &config)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
SC::Sync { prune, dry_run } => {
|
||||||
|
let _span = info_span!("cli.config.sync").entered();
|
||||||
|
|
||||||
|
let config: RootConfig = root.load_config()?;
|
||||||
|
let pool = database_from_config(&config.database).await?;
|
||||||
|
let mut repo = PgRepository::from_pool(&pool).await?.boxed();
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
prune,
|
||||||
|
dry_run,
|
||||||
|
"Syncing providers and clients defined in config to database"
|
||||||
|
);
|
||||||
|
|
||||||
|
let existing = repo.upstream_oauth_provider().all().await?;
|
||||||
|
|
||||||
|
let existing_ids = existing.iter().map(|p| p.id).collect::<HashSet<_>>();
|
||||||
|
let config_ids = config
|
||||||
|
.upstream_oauth2
|
||||||
|
.providers
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.id)
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
|
let needs_pruning = existing_ids.difference(&config_ids).collect::<Vec<_>>();
|
||||||
|
if prune {
|
||||||
|
for id in needs_pruning {
|
||||||
|
info!(provider.id = %id, "Deleting provider");
|
||||||
|
}
|
||||||
|
} else if !needs_pruning.is_empty() {
|
||||||
|
warn!(
|
||||||
|
"{} provider(s) in the database are not in the config. Run with `--prune` to delete them.",
|
||||||
|
needs_pruning.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for provider in config.upstream_oauth2.providers {
|
||||||
|
if existing_ids.contains(&provider.id) {
|
||||||
|
info!(%provider.id, "Updating provider");
|
||||||
|
} else {
|
||||||
|
info!(%provider.id, "Adding provider");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if dry_run {
|
||||||
|
info!("Dry run, rolling back changes");
|
||||||
|
repo.cancel().await?;
|
||||||
|
} else {
|
||||||
|
repo.save().await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ enum Subcommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Options {
|
impl Options {
|
||||||
pub async fn run(&self, root: &super::Options) -> anyhow::Result<()> {
|
pub async fn run(self, root: &super::Options) -> anyhow::Result<()> {
|
||||||
let _span = info_span!("cli.database.migrate").entered();
|
let _span = info_span!("cli.database.migrate").entered();
|
||||||
let config: DatabaseConfig = root.load_config()?;
|
let config: DatabaseConfig = root.load_config()?;
|
||||||
let pool = database_from_config(&config).await?;
|
let pool = database_from_config(&config).await?;
|
||||||
|
@ -65,10 +65,10 @@ fn print_headers(parts: &hyper::http::response::Parts) {
|
|||||||
|
|
||||||
impl Options {
|
impl Options {
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub async fn run(&self, root: &super::Options) -> anyhow::Result<()> {
|
pub async fn run(self, root: &super::Options) -> anyhow::Result<()> {
|
||||||
use Subcommand as SC;
|
use Subcommand as SC;
|
||||||
let http_client_factory = HttpClientFactory::new(10);
|
let http_client_factory = HttpClientFactory::new(10);
|
||||||
match &self.subcommand {
|
match self.subcommand {
|
||||||
SC::Http {
|
SC::Http {
|
||||||
show_headers,
|
show_headers,
|
||||||
json: false,
|
json: false,
|
||||||
@ -83,15 +83,13 @@ impl Options {
|
|||||||
let response = client.ready().await?.call(request).await?;
|
let response = client.ready().await?.call(request).await?;
|
||||||
let (parts, body) = response.into_parts();
|
let (parts, body) = response.into_parts();
|
||||||
|
|
||||||
if *show_headers {
|
if show_headers {
|
||||||
print_headers(&parts);
|
print_headers(&parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut body = hyper::body::aggregate(body).await?;
|
let mut body = hyper::body::aggregate(body).await?;
|
||||||
let mut stdout = tokio::io::stdout();
|
let mut stdout = tokio::io::stdout();
|
||||||
stdout.write_all_buf(&mut body).await?;
|
stdout.write_all_buf(&mut body).await?;
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SC::Http {
|
SC::Http {
|
||||||
@ -113,14 +111,12 @@ impl Options {
|
|||||||
client.ready().await?.call(request).await?;
|
client.ready().await?.call(request).await?;
|
||||||
let (parts, body) = response.into_parts();
|
let (parts, body) = response.into_parts();
|
||||||
|
|
||||||
if *show_headers {
|
if show_headers {
|
||||||
print_headers(&parts);
|
print_headers(&parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
let body = serde_json::to_string_pretty(&body)?;
|
let body = serde_json::to_string_pretty(&body)?;
|
||||||
println!("{body}");
|
println!("{body}");
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SC::Policy => {
|
SC::Policy => {
|
||||||
@ -130,8 +126,9 @@ impl Options {
|
|||||||
let policy_factory = policy_factory_from_config(&config).await?;
|
let policy_factory = policy_factory_from_config(&config).await?;
|
||||||
|
|
||||||
let _instance = policy_factory.instantiate().await?;
|
let _instance = policy_factory.instantiate().await?;
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -202,13 +202,13 @@ enum Subcommand {
|
|||||||
|
|
||||||
impl Options {
|
impl Options {
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
pub async fn run(&self, root: &super::Options) -> anyhow::Result<()> {
|
pub async fn run(self, root: &super::Options) -> anyhow::Result<()> {
|
||||||
use Subcommand as SC;
|
use Subcommand as SC;
|
||||||
let clock = SystemClock::default();
|
let clock = SystemClock::default();
|
||||||
// XXX: we should disallow SeedableRng::from_entropy
|
// XXX: we should disallow SeedableRng::from_entropy
|
||||||
let mut rng = rand_chacha::ChaChaRng::from_entropy();
|
let mut rng = rand_chacha::ChaChaRng::from_entropy();
|
||||||
|
|
||||||
match &self.subcommand {
|
match self.subcommand {
|
||||||
SC::SetPassword { username, password } => {
|
SC::SetPassword { username, password } => {
|
||||||
let _span =
|
let _span =
|
||||||
info_span!("cli.manage.set_password", user.username = %username).entered();
|
info_span!("cli.manage.set_password", user.username = %username).entered();
|
||||||
@ -222,11 +222,11 @@ impl Options {
|
|||||||
let mut repo = PgRepository::from_pool(&pool).await?.boxed();
|
let mut repo = PgRepository::from_pool(&pool).await?.boxed();
|
||||||
let user = repo
|
let user = repo
|
||||||
.user()
|
.user()
|
||||||
.find_by_username(username)
|
.find_by_username(&username)
|
||||||
.await?
|
.await?
|
||||||
.context("User not found")?;
|
.context("User not found")?;
|
||||||
|
|
||||||
let password = password.as_bytes().to_vec().into();
|
let password = password.into_bytes().into();
|
||||||
|
|
||||||
let (version, hashed_password) = password_manager.hash(&mut rng, password).await?;
|
let (version, hashed_password) = password_manager.hash(&mut rng, password).await?;
|
||||||
|
|
||||||
@ -254,13 +254,13 @@ impl Options {
|
|||||||
|
|
||||||
let user = repo
|
let user = repo
|
||||||
.user()
|
.user()
|
||||||
.find_by_username(username)
|
.find_by_username(&username)
|
||||||
.await?
|
.await?
|
||||||
.context("User not found")?;
|
.context("User not found")?;
|
||||||
|
|
||||||
let email = repo
|
let email = repo
|
||||||
.user_email()
|
.user_email()
|
||||||
.find(&user, email)
|
.find(&user, &email)
|
||||||
.await?
|
.await?
|
||||||
.context("Email not found")?;
|
.context("Email not found")?;
|
||||||
let email = repo.user_email().mark_as_verified(&clock, email).await?;
|
let email = repo.user_email().mark_as_verified(&clock, email).await?;
|
||||||
@ -302,7 +302,7 @@ impl Options {
|
|||||||
|
|
||||||
// TODO: should be moved somewhere else
|
// TODO: should be moved somewhere else
|
||||||
let encrypted_client_secret = client_secret
|
let encrypted_client_secret = client_secret
|
||||||
.map(|client_secret| encrypter.encryt_to_string(client_secret.as_bytes()))
|
.map(|client_secret| encrypter.encrypt_to_string(client_secret.as_bytes()))
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
|
||||||
repo.oauth2_client()
|
repo.oauth2_client()
|
||||||
@ -361,7 +361,7 @@ impl Options {
|
|||||||
|
|
||||||
let encrypted_client_secret = client_secret
|
let encrypted_client_secret = client_secret
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.map(|client_secret| encrypter.encryt_to_string(client_secret.as_bytes()))
|
.map(|client_secret| encrypter.encrypt_to_string(client_secret.as_bytes()))
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
|
||||||
let provider = repo
|
let provider = repo
|
||||||
@ -369,11 +369,11 @@ impl Options {
|
|||||||
.add(
|
.add(
|
||||||
&mut rng,
|
&mut rng,
|
||||||
&clock,
|
&clock,
|
||||||
issuer.clone(),
|
issuer,
|
||||||
scope.clone(),
|
scope,
|
||||||
token_endpoint_auth_method,
|
token_endpoint_auth_method,
|
||||||
token_endpoint_signing_alg,
|
token_endpoint_signing_alg,
|
||||||
client_id.clone(),
|
client_id,
|
||||||
encrypted_client_secret,
|
encrypted_client_secret,
|
||||||
UpstreamOAuthProviderClaimsImports::default(),
|
UpstreamOAuthProviderClaimsImports::default(),
|
||||||
)
|
)
|
||||||
@ -404,19 +404,19 @@ impl Options {
|
|||||||
|
|
||||||
let user = repo
|
let user = repo
|
||||||
.user()
|
.user()
|
||||||
.find_by_username(username)
|
.find_by_username(&username)
|
||||||
.await?
|
.await?
|
||||||
.context("User not found")?;
|
.context("User not found")?;
|
||||||
|
|
||||||
let device = if let Some(device_id) = device_id {
|
let device = if let Some(device_id) = device_id {
|
||||||
device_id.clone().try_into()?
|
device_id.try_into()?
|
||||||
} else {
|
} else {
|
||||||
Device::generate(&mut rng)
|
Device::generate(&mut rng)
|
||||||
};
|
};
|
||||||
|
|
||||||
let compat_session = repo
|
let compat_session = repo
|
||||||
.compat_session()
|
.compat_session()
|
||||||
.add(&mut rng, &clock, &user, device, *admin)
|
.add(&mut rng, &clock, &user, device, admin)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let token = TokenType::CompatAccessToken.generate(&mut rng);
|
let token = TokenType::CompatAccessToken.generate(&mut rng);
|
||||||
|
@ -60,17 +60,17 @@ pub struct Options {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Options {
|
impl Options {
|
||||||
pub async fn run(&self) -> anyhow::Result<()> {
|
pub async fn run(mut self) -> anyhow::Result<()> {
|
||||||
use Subcommand as S;
|
use Subcommand as S;
|
||||||
match &self.subcommand {
|
match self.subcommand.take() {
|
||||||
Some(S::Config(c)) => c.run(self).await,
|
Some(S::Config(c)) => c.run(&self).await,
|
||||||
Some(S::Database(c)) => c.run(self).await,
|
Some(S::Database(c)) => c.run(&self).await,
|
||||||
Some(S::Server(c)) => c.run(self).await,
|
Some(S::Server(c)) => c.run(&self).await,
|
||||||
Some(S::Worker(c)) => c.run(self).await,
|
Some(S::Worker(c)) => c.run(&self).await,
|
||||||
Some(S::Manage(c)) => c.run(self).await,
|
Some(S::Manage(c)) => c.run(&self).await,
|
||||||
Some(S::Templates(c)) => c.run(self).await,
|
Some(S::Templates(c)) => c.run(&self).await,
|
||||||
Some(S::Debug(c)) => c.run(self).await,
|
Some(S::Debug(c)) => c.run(&self).await,
|
||||||
None => self::server::Options::default().run(self).await,
|
None => self::server::Options::default().run(&self).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ pub(super) struct Options {
|
|||||||
|
|
||||||
impl Options {
|
impl Options {
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
pub async fn run(&self, root: &super::Options) -> anyhow::Result<()> {
|
pub async fn run(self, root: &super::Options) -> anyhow::Result<()> {
|
||||||
let span = info_span!("cli.run.init").entered();
|
let span = info_span!("cli.run.init").entered();
|
||||||
let config: RootConfig = root.load_config()?;
|
let config: RootConfig = root.load_config()?;
|
||||||
|
|
||||||
|
@ -35,9 +35,9 @@ enum Subcommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Options {
|
impl Options {
|
||||||
pub async fn run(&self, _root: &super::Options) -> anyhow::Result<()> {
|
pub async fn run(self, _root: &super::Options) -> anyhow::Result<()> {
|
||||||
use Subcommand as SC;
|
use Subcommand as SC;
|
||||||
match &self.subcommand {
|
match self.subcommand {
|
||||||
SC::Check { path } => {
|
SC::Check { path } => {
|
||||||
let _span = info_span!("cli.templates.check").entered();
|
let _span = info_span!("cli.templates.check").entered();
|
||||||
|
|
||||||
@ -45,7 +45,7 @@ impl Options {
|
|||||||
// XXX: we should disallow SeedableRng::from_entropy
|
// XXX: we should disallow SeedableRng::from_entropy
|
||||||
let mut rng = rand_chacha::ChaChaRng::from_entropy();
|
let mut rng = rand_chacha::ChaChaRng::from_entropy();
|
||||||
let url_builder = mas_router::UrlBuilder::new("https://example.com/".parse()?);
|
let url_builder = mas_router::UrlBuilder::new("https://example.com/".parse()?);
|
||||||
let templates = Templates::load(path.clone(), url_builder).await?;
|
let templates = Templates::load(path, url_builder).await?;
|
||||||
templates.check_render(clock.now(), &mut rng).await?;
|
templates.check_render(clock.now(), &mut rng).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -29,7 +29,7 @@ use crate::util::{database_from_config, mailer_from_config, templates_from_confi
|
|||||||
pub(super) struct Options {}
|
pub(super) struct Options {}
|
||||||
|
|
||||||
impl Options {
|
impl Options {
|
||||||
pub async fn run(&self, root: &super::Options) -> anyhow::Result<()> {
|
pub async fn run(self, root: &super::Options) -> anyhow::Result<()> {
|
||||||
let span = info_span!("cli.worker.init").entered();
|
let span = info_span!("cli.worker.init").entered();
|
||||||
let config: RootConfig = root.load_config()?;
|
let config: RootConfig = root.load_config()?;
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ pub enum ClientAuthMethodConfig {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/// `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 asymetric key
|
/// signed by an asymmetric key
|
||||||
PrivateKeyJwt(JwksOrJwksUri),
|
PrivateKeyJwt(JwksOrJwksUri),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ mod policy;
|
|||||||
mod secrets;
|
mod secrets;
|
||||||
mod telemetry;
|
mod telemetry;
|
||||||
mod templates;
|
mod templates;
|
||||||
|
mod upstream_oauth2;
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
clients::{ClientAuthMethodConfig, ClientConfig, ClientsConfig},
|
clients::{ClientAuthMethodConfig, ClientConfig, ClientsConfig},
|
||||||
@ -47,6 +48,7 @@ pub use self::{
|
|||||||
TelemetryConfig, TracingConfig, TracingExporterConfig,
|
TelemetryConfig, TracingConfig, TracingExporterConfig,
|
||||||
},
|
},
|
||||||
templates::TemplatesConfig,
|
templates::TemplatesConfig,
|
||||||
|
upstream_oauth2::UpstreamOAuth2Config,
|
||||||
};
|
};
|
||||||
use crate::util::ConfigurationSection;
|
use crate::util::ConfigurationSection;
|
||||||
|
|
||||||
@ -94,6 +96,10 @@ pub struct RootConfig {
|
|||||||
/// Configuration related to the OPA policies
|
/// Configuration related to the OPA policies
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub policy: PolicyConfig,
|
pub policy: PolicyConfig,
|
||||||
|
|
||||||
|
/// Configuration related to upstream OAuth providers
|
||||||
|
#[serde(default)]
|
||||||
|
pub upstream_oauth2: UpstreamOAuth2Config,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@ -118,6 +124,7 @@ impl ConfigurationSection<'_> for RootConfig {
|
|||||||
secrets: SecretsConfig::generate(&mut rng).await?,
|
secrets: SecretsConfig::generate(&mut rng).await?,
|
||||||
matrix: MatrixConfig::generate(&mut rng).await?,
|
matrix: MatrixConfig::generate(&mut rng).await?,
|
||||||
policy: PolicyConfig::generate(&mut rng).await?,
|
policy: PolicyConfig::generate(&mut rng).await?,
|
||||||
|
upstream_oauth2: UpstreamOAuth2Config::generate(&mut rng).await?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,6 +141,7 @@ impl ConfigurationSection<'_> for RootConfig {
|
|||||||
secrets: SecretsConfig::test(),
|
secrets: SecretsConfig::test(),
|
||||||
matrix: MatrixConfig::test(),
|
matrix: MatrixConfig::test(),
|
||||||
policy: PolicyConfig::test(),
|
policy: PolicyConfig::test(),
|
||||||
|
upstream_oauth2: UpstreamOAuth2Config::test(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
144
crates/config/src/sections/upstream_oauth2.rs
Normal file
144
crates/config/src/sections/upstream_oauth2.rs
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
// Copyright 2023 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 async_trait::async_trait;
|
||||||
|
use rand::Rng;
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_with::skip_serializing_none;
|
||||||
|
use ulid::Ulid;
|
||||||
|
|
||||||
|
use crate::ConfigurationSection;
|
||||||
|
|
||||||
|
/// Upstream OAuth 2.0 providers configuration
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
|
||||||
|
pub struct UpstreamOAuth2Config {
|
||||||
|
/// List of OAuth 2.0 providers
|
||||||
|
pub providers: Vec<Provider>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<'a> ConfigurationSection<'a> for UpstreamOAuth2Config {
|
||||||
|
fn path() -> &'static str {
|
||||||
|
"upstream_oauth2"
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn generate<R>(_rng: R) -> anyhow::Result<Self>
|
||||||
|
where
|
||||||
|
R: Rng + Send,
|
||||||
|
{
|
||||||
|
Ok(Self::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Authentication methods used against the OAuth 2.0 provider
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||||
|
#[serde(tag = "token_endpoint_auth_method", rename_all = "snake_case")]
|
||||||
|
pub enum TokenAuthMethod {
|
||||||
|
/// `none`: No authentication
|
||||||
|
None,
|
||||||
|
|
||||||
|
/// `client_secret_basic`: `client_id` and `client_secret` used as basic
|
||||||
|
/// authorization credentials
|
||||||
|
ClientSecretBasic { client_secret: String },
|
||||||
|
|
||||||
|
/// `client_secret_post`: `client_id` and `client_secret` sent in the
|
||||||
|
/// request body
|
||||||
|
ClientSecretPost { client_secret: String },
|
||||||
|
|
||||||
|
/// `client_secret_basic`: a `client_assertion` sent in the request body and
|
||||||
|
/// signed using the `client_secret`
|
||||||
|
ClientSecretJwt {
|
||||||
|
client_secret: String,
|
||||||
|
token_endpoint_auth_signing_alg: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// `client_secret_basic`: a `client_assertion` sent in the request body and
|
||||||
|
/// signed by an asymmetric key
|
||||||
|
PrivateKeyJwt {
|
||||||
|
token_endpoint_auth_signing_alg: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum ImportAction {
|
||||||
|
/// Ignore the claim
|
||||||
|
#[default]
|
||||||
|
Ignore,
|
||||||
|
|
||||||
|
/// Suggest the claim value, but allow the user to change it
|
||||||
|
Suggest,
|
||||||
|
|
||||||
|
/// Force the claim value, but don't fail if it is missing
|
||||||
|
Force,
|
||||||
|
|
||||||
|
/// Force the claim value, and fail if it is missing
|
||||||
|
Require,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
|
||||||
|
pub struct ImportPreference {
|
||||||
|
/// How to handle the claim
|
||||||
|
#[serde(default)]
|
||||||
|
pub action: ImportAction,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
|
||||||
|
pub struct ClaimsImports {
|
||||||
|
/// Import the localpart of the MXID based on the `preferred_username` claim
|
||||||
|
#[serde(default)]
|
||||||
|
pub localpart: Option<ImportPreference>,
|
||||||
|
|
||||||
|
/// Import the displayname of the user based on the `name` claim
|
||||||
|
#[serde(default)]
|
||||||
|
pub displayname: Option<ImportPreference>,
|
||||||
|
|
||||||
|
/// Import the email address of the user based on the `email` and
|
||||||
|
/// `email_verified` claims
|
||||||
|
#[serde(default)]
|
||||||
|
pub email: Option<ImportPreference>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[skip_serializing_none]
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct Provider {
|
||||||
|
/// An internal unique identifier for this provider
|
||||||
|
#[schemars(
|
||||||
|
with = "String",
|
||||||
|
regex(pattern = r"^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$"),
|
||||||
|
description = "A ULID as per https://github.com/ulid/spec"
|
||||||
|
)]
|
||||||
|
pub id: Ulid,
|
||||||
|
|
||||||
|
/// The OIDC issuer URL
|
||||||
|
pub issuer: String,
|
||||||
|
|
||||||
|
/// The client ID to use when authenticating with the provider
|
||||||
|
pub client_id: String,
|
||||||
|
|
||||||
|
/// The scopes to request from the provider
|
||||||
|
pub scope: String,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub token_auth_method: TokenAuthMethod,
|
||||||
|
|
||||||
|
/// How claims should be imported from the `id_token` provided by the
|
||||||
|
/// provider
|
||||||
|
pub claims_imports: ClaimsImports,
|
||||||
|
}
|
@ -162,7 +162,7 @@ pub(crate) async fn post(
|
|||||||
) => {
|
) => {
|
||||||
// Let's generate a random client secret
|
// Let's generate a random client secret
|
||||||
let client_secret = Alphanumeric.sample_string(&mut rng, 20);
|
let client_secret = Alphanumeric.sample_string(&mut rng, 20);
|
||||||
let encrypted_client_secret = encrypter.encryt_to_string(client_secret.as_bytes())?;
|
let encrypted_client_secret = encrypter.encrypt_to_string(client_secret.as_bytes())?;
|
||||||
(Some(client_secret), Some(encrypted_client_secret))
|
(Some(client_secret), Some(encrypted_client_secret))
|
||||||
}
|
}
|
||||||
_ => (None, None),
|
_ => (None, None),
|
||||||
|
@ -81,7 +81,7 @@ impl Encrypter {
|
|||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Will return `Err` when the payload failed to encrypt
|
/// Will return `Err` when the payload failed to encrypt
|
||||||
pub fn encryt_to_string(&self, decrypted: &[u8]) -> Result<String, aead::Error> {
|
pub fn encrypt_to_string(&self, decrypted: &[u8]) -> Result<String, aead::Error> {
|
||||||
let nonce = rand::random();
|
let nonce = rand::random();
|
||||||
let encrypted = self.encrypt(&nonce, decrypted)?;
|
let encrypted = self.encrypt(&nonce, decrypted)?;
|
||||||
let encrypted = [&nonce[..], &encrypted].concat();
|
let encrypted = [&nonce[..], &encrypted].concat();
|
||||||
|
@ -197,6 +197,17 @@
|
|||||||
"$ref": "#/definitions/TemplatesConfig"
|
"$ref": "#/definitions/TemplatesConfig"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"upstream_oauth2": {
|
||||||
|
"description": "Configuration related to upstream OAuth providers",
|
||||||
|
"default": {
|
||||||
|
"providers": []
|
||||||
|
},
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/UpstreamOAuth2Config"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
@ -278,6 +289,38 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"ClaimsImports": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"displayname": {
|
||||||
|
"description": "Import the displayname of the user based on the `name` claim",
|
||||||
|
"default": null,
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/ImportPreference"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"description": "Import the email address of the user based on the `email` and `email_verified` claims",
|
||||||
|
"default": null,
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/ImportPreference"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"localpart": {
|
||||||
|
"description": "Import the localpart of the MXID based on the `preferred_username` claim",
|
||||||
|
"default": null,
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/ImportPreference"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"ClientConfig": {
|
"ClientConfig": {
|
||||||
"description": "An OAuth 2.0 client configuration",
|
"description": "An OAuth 2.0 client configuration",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@ -358,7 +401,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "`client_secret_basic`: a `client_assertion` sent in the request body and signed by an asymetric key",
|
"description": "`client_secret_basic`: a `client_assertion` sent in the request body and signed by an asymmetric key",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
{
|
{
|
||||||
@ -758,6 +801,52 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ImportAction": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"description": "Ignore the claim",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"ignore"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Suggest the claim value, but allow the user to change it",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"suggest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Force the claim value, but don't fail if it is missing",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"force"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Force the claim value, and fail if it is missing",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"require"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ImportPreference": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"action": {
|
||||||
|
"description": "How to handle the claim",
|
||||||
|
"default": "ignore",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/ImportAction"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"JsonWebKeyEcEllipticCurve": {
|
"JsonWebKeyEcEllipticCurve": {
|
||||||
"description": "JSON Web Key EC Elliptic Curve",
|
"description": "JSON Web Key EC Elliptic Curve",
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
@ -1305,6 +1394,139 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"Provider": {
|
||||||
|
"description": "Authentication methods used against the OAuth 2.0 provider",
|
||||||
|
"type": "object",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"description": "`none`: No authentication",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"token_endpoint_auth_method"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"token_endpoint_auth_method": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"none"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "`client_secret_basic`: `client_id` and `client_secret` used as basic authorization credentials",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"client_secret",
|
||||||
|
"token_endpoint_auth_method"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"client_secret": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"token_endpoint_auth_method": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"client_secret_basic"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "`client_secret_post`: `client_id` and `client_secret` sent in the request body",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"client_secret",
|
||||||
|
"token_endpoint_auth_method"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"client_secret": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"token_endpoint_auth_method": {
|
||||||
|
"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": "object",
|
||||||
|
"required": [
|
||||||
|
"client_secret",
|
||||||
|
"token_endpoint_auth_method"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"client_secret": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"token_endpoint_auth_method": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"client_secret_jwt"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"token_endpoint_auth_signing_alg": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "`client_secret_basic`: a `client_assertion` sent in the request body and signed by an asymmetric key",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"token_endpoint_auth_method"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"token_endpoint_auth_method": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"private_key_jwt"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"token_endpoint_auth_signing_alg": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"required": [
|
||||||
|
"claims_imports",
|
||||||
|
"client_id",
|
||||||
|
"id",
|
||||||
|
"issuer",
|
||||||
|
"scope"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"claims_imports": {
|
||||||
|
"description": "How claims should be imported from the `id_token` provided by the provider",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/ClaimsImports"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"client_id": {
|
||||||
|
"description": "The client ID to use when authenticating with the provider",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"description": "A ULID as per https://github.com/ulid/spec",
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$"
|
||||||
|
},
|
||||||
|
"issuer": {
|
||||||
|
"description": "The OIDC issuer URL",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"description": "The scopes to request from the provider",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Resource": {
|
"Resource": {
|
||||||
"description": "HTTP resources to mount",
|
"description": "HTTP resources to mount",
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
@ -1785,6 +2007,22 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"UpstreamOAuth2Config": {
|
||||||
|
"description": "Upstream OAuth 2.0 providers configuration",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"providers"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"providers": {
|
||||||
|
"description": "List of OAuth 2.0 providers",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/Provider"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user