From 4f1b201c7491126202ab2f7b70f03c9d23dbbb07 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 22 Jun 2023 18:28:34 +0200 Subject: [PATCH] Define upstream OAuth providers in the config And adds CLI tool to sync them with the database (WIP) --- crates/cli/src/commands/config.rs | 80 +++++- crates/cli/src/commands/database.rs | 2 +- crates/cli/src/commands/debug.rs | 15 +- crates/cli/src/commands/manage.rs | 28 +- crates/cli/src/commands/mod.rs | 20 +- crates/cli/src/commands/server.rs | 2 +- crates/cli/src/commands/templates.rs | 6 +- crates/cli/src/commands/worker.rs | 2 +- crates/config/src/sections/clients.rs | 2 +- crates/config/src/sections/mod.rs | 8 + crates/config/src/sections/upstream_oauth2.rs | 144 +++++++++++ crates/handlers/src/oauth2/registration.rs | 2 +- crates/keystore/src/encrypter.rs | 2 +- docs/config.schema.json | 240 +++++++++++++++++- 14 files changed, 503 insertions(+), 50 deletions(-) create mode 100644 crates/config/src/sections/upstream_oauth2.rs diff --git a/crates/cli/src/commands/config.rs b/crates/cli/src/commands/config.rs index f8e3aaa5..8d622cdf 100644 --- a/crates/cli/src/commands/config.rs +++ b/crates/cli/src/commands/config.rs @@ -12,10 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::collections::HashSet; + use clap::Parser; use mas_config::{ConfigurationSection, RootConfig}; +use mas_storage::{upstream_oauth2::UpstreamOAuthProviderRepository, Repository, RepositoryAccess}; +use mas_storage_pg::PgRepository; use rand::SeedableRng; -use tracing::{info, info_span}; +use tracing::{info, info_span, warn}; + +use crate::util::database_from_config; #[derive(Parser, Debug)] pub(super) struct Options { @@ -33,27 +39,36 @@ enum Subcommand { /// Generate a new config file 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 { - pub async fn run(&self, root: &super::Options) -> anyhow::Result<()> { + pub async fn run(self, root: &super::Options) -> anyhow::Result<()> { use Subcommand as SC; - match &self.subcommand { + match self.subcommand { SC::Dump => { let _span = info_span!("cli.config.dump").entered(); let config: RootConfig = root.load_config()?; serde_yaml::to_writer(std::io::stdout(), &config)?; - - Ok(()) } SC::Check => { let _span = info_span!("cli.config.check").entered(); let _config: RootConfig = root.load_config()?; info!(path = ?root.config, "Configuration file looks good"); - Ok(()) } SC::Generate => { let _span = info_span!("cli.config.generate").entered(); @@ -63,9 +78,60 @@ impl Options { let config = RootConfig::load_and_generate(rng).await?; 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::>(); + let config_ids = config + .upstream_oauth2 + .providers + .iter() + .map(|p| p.id) + .collect::>(); + + let needs_pruning = existing_ids.difference(&config_ids).collect::>(); + 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(()) } } diff --git a/crates/cli/src/commands/database.rs b/crates/cli/src/commands/database.rs index 0e4d68af..0277a09d 100644 --- a/crates/cli/src/commands/database.rs +++ b/crates/cli/src/commands/database.rs @@ -33,7 +33,7 @@ enum Subcommand { } 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 config: DatabaseConfig = root.load_config()?; let pool = database_from_config(&config).await?; diff --git a/crates/cli/src/commands/debug.rs b/crates/cli/src/commands/debug.rs index 6f5d9a1a..127a541b 100644 --- a/crates/cli/src/commands/debug.rs +++ b/crates/cli/src/commands/debug.rs @@ -65,10 +65,10 @@ fn print_headers(parts: &hyper::http::response::Parts) { impl Options { #[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; let http_client_factory = HttpClientFactory::new(10); - match &self.subcommand { + match self.subcommand { SC::Http { show_headers, json: false, @@ -83,15 +83,13 @@ impl Options { let response = client.ready().await?.call(request).await?; let (parts, body) = response.into_parts(); - if *show_headers { + if show_headers { print_headers(&parts); } let mut body = hyper::body::aggregate(body).await?; let mut stdout = tokio::io::stdout(); stdout.write_all_buf(&mut body).await?; - - Ok(()) } SC::Http { @@ -113,14 +111,12 @@ impl Options { client.ready().await?.call(request).await?; let (parts, body) = response.into_parts(); - if *show_headers { + if show_headers { print_headers(&parts); } let body = serde_json::to_string_pretty(&body)?; println!("{body}"); - - Ok(()) } SC::Policy => { @@ -130,8 +126,9 @@ impl Options { let policy_factory = policy_factory_from_config(&config).await?; let _instance = policy_factory.instantiate().await?; - Ok(()) } } + + Ok(()) } } diff --git a/crates/cli/src/commands/manage.rs b/crates/cli/src/commands/manage.rs index 9c8cd30f..eb6e22b7 100644 --- a/crates/cli/src/commands/manage.rs +++ b/crates/cli/src/commands/manage.rs @@ -202,13 +202,13 @@ enum Subcommand { impl Options { #[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; let clock = SystemClock::default(); // XXX: we should disallow SeedableRng::from_entropy let mut rng = rand_chacha::ChaChaRng::from_entropy(); - match &self.subcommand { + match self.subcommand { SC::SetPassword { username, password } => { let _span = 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 user = repo .user() - .find_by_username(username) + .find_by_username(&username) .await? .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?; @@ -254,13 +254,13 @@ impl Options { let user = repo .user() - .find_by_username(username) + .find_by_username(&username) .await? .context("User not found")?; let email = repo .user_email() - .find(&user, email) + .find(&user, &email) .await? .context("Email not found")?; let email = repo.user_email().mark_as_verified(&clock, email).await?; @@ -302,7 +302,7 @@ impl Options { // TODO: should be moved somewhere else 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()?; repo.oauth2_client() @@ -361,7 +361,7 @@ impl Options { let encrypted_client_secret = client_secret .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()?; let provider = repo @@ -369,11 +369,11 @@ impl Options { .add( &mut rng, &clock, - issuer.clone(), - scope.clone(), + issuer, + scope, token_endpoint_auth_method, token_endpoint_signing_alg, - client_id.clone(), + client_id, encrypted_client_secret, UpstreamOAuthProviderClaimsImports::default(), ) @@ -404,19 +404,19 @@ impl Options { let user = repo .user() - .find_by_username(username) + .find_by_username(&username) .await? .context("User not found")?; let device = if let Some(device_id) = device_id { - device_id.clone().try_into()? + device_id.try_into()? } else { Device::generate(&mut rng) }; let compat_session = repo .compat_session() - .add(&mut rng, &clock, &user, device, *admin) + .add(&mut rng, &clock, &user, device, admin) .await?; let token = TokenType::CompatAccessToken.generate(&mut rng); diff --git a/crates/cli/src/commands/mod.rs b/crates/cli/src/commands/mod.rs index 8054b171..6f27124a 100644 --- a/crates/cli/src/commands/mod.rs +++ b/crates/cli/src/commands/mod.rs @@ -60,17 +60,17 @@ pub struct Options { } impl Options { - pub async fn run(&self) -> anyhow::Result<()> { + pub async fn run(mut self) -> anyhow::Result<()> { use Subcommand as S; - match &self.subcommand { - Some(S::Config(c)) => c.run(self).await, - Some(S::Database(c)) => c.run(self).await, - Some(S::Server(c)) => c.run(self).await, - Some(S::Worker(c)) => c.run(self).await, - Some(S::Manage(c)) => c.run(self).await, - Some(S::Templates(c)) => c.run(self).await, - Some(S::Debug(c)) => c.run(self).await, - None => self::server::Options::default().run(self).await, + match self.subcommand.take() { + Some(S::Config(c)) => c.run(&self).await, + Some(S::Database(c)) => c.run(&self).await, + Some(S::Server(c)) => c.run(&self).await, + Some(S::Worker(c)) => c.run(&self).await, + Some(S::Manage(c)) => c.run(&self).await, + Some(S::Templates(c)) => c.run(&self).await, + Some(S::Debug(c)) => c.run(&self).await, + None => self::server::Options::default().run(&self).await, } } diff --git a/crates/cli/src/commands/server.rs b/crates/cli/src/commands/server.rs index 37e19c0d..bced9e98 100644 --- a/crates/cli/src/commands/server.rs +++ b/crates/cli/src/commands/server.rs @@ -52,7 +52,7 @@ pub(super) struct Options { impl Options { #[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 config: RootConfig = root.load_config()?; diff --git a/crates/cli/src/commands/templates.rs b/crates/cli/src/commands/templates.rs index 6f09b751..4c00e496 100644 --- a/crates/cli/src/commands/templates.rs +++ b/crates/cli/src/commands/templates.rs @@ -35,9 +35,9 @@ enum Subcommand { } 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; - match &self.subcommand { + match self.subcommand { SC::Check { path } => { let _span = info_span!("cli.templates.check").entered(); @@ -45,7 +45,7 @@ impl Options { // XXX: we should disallow SeedableRng::from_entropy let mut rng = rand_chacha::ChaChaRng::from_entropy(); 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?; Ok(()) diff --git a/crates/cli/src/commands/worker.rs b/crates/cli/src/commands/worker.rs index 510f04e4..1a993cf0 100644 --- a/crates/cli/src/commands/worker.rs +++ b/crates/cli/src/commands/worker.rs @@ -29,7 +29,7 @@ use crate::util::{database_from_config, mailer_from_config, templates_from_confi pub(super) struct 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 config: RootConfig = root.load_config()?; diff --git a/crates/config/src/sections/clients.rs b/crates/config/src/sections/clients.rs index 344bf899..bcaed40b 100644 --- a/crates/config/src/sections/clients.rs +++ b/crates/config/src/sections/clients.rs @@ -69,7 +69,7 @@ pub enum ClientAuthMethodConfig { }, /// `client_secret_basic`: a `client_assertion` sent in the request body and - /// signed by an asymetric key + /// signed by an asymmetric key PrivateKeyJwt(JwksOrJwksUri), } diff --git a/crates/config/src/sections/mod.rs b/crates/config/src/sections/mod.rs index 96e8cbc9..91a033b5 100644 --- a/crates/config/src/sections/mod.rs +++ b/crates/config/src/sections/mod.rs @@ -28,6 +28,7 @@ mod policy; mod secrets; mod telemetry; mod templates; +mod upstream_oauth2; pub use self::{ clients::{ClientAuthMethodConfig, ClientConfig, ClientsConfig}, @@ -47,6 +48,7 @@ pub use self::{ TelemetryConfig, TracingConfig, TracingExporterConfig, }, templates::TemplatesConfig, + upstream_oauth2::UpstreamOAuth2Config, }; use crate::util::ConfigurationSection; @@ -94,6 +96,10 @@ pub struct RootConfig { /// Configuration related to the OPA policies #[serde(default)] pub policy: PolicyConfig, + + /// Configuration related to upstream OAuth providers + #[serde(default)] + pub upstream_oauth2: UpstreamOAuth2Config, } #[async_trait] @@ -118,6 +124,7 @@ impl ConfigurationSection<'_> for RootConfig { secrets: SecretsConfig::generate(&mut rng).await?, matrix: MatrixConfig::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(), matrix: MatrixConfig::test(), policy: PolicyConfig::test(), + upstream_oauth2: UpstreamOAuth2Config::test(), } } } diff --git a/crates/config/src/sections/upstream_oauth2.rs b/crates/config/src/sections/upstream_oauth2.rs new file mode 100644 index 00000000..5e7d954e --- /dev/null +++ b/crates/config/src/sections/upstream_oauth2.rs @@ -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, +} + +#[async_trait] +impl<'a> ConfigurationSection<'a> for UpstreamOAuth2Config { + fn path() -> &'static str { + "upstream_oauth2" + } + + async fn generate(_rng: R) -> anyhow::Result + 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, + }, + + /// `client_secret_basic`: a `client_assertion` sent in the request body and + /// signed by an asymmetric key + PrivateKeyJwt { + token_endpoint_auth_signing_alg: Option, + }, +} + +#[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, + + /// Import the displayname of the user based on the `name` claim + #[serde(default)] + pub displayname: Option, + + /// Import the email address of the user based on the `email` and + /// `email_verified` claims + #[serde(default)] + pub email: Option, +} + +#[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, +} diff --git a/crates/handlers/src/oauth2/registration.rs b/crates/handlers/src/oauth2/registration.rs index 17373fc0..40608374 100644 --- a/crates/handlers/src/oauth2/registration.rs +++ b/crates/handlers/src/oauth2/registration.rs @@ -162,7 +162,7 @@ pub(crate) async fn post( ) => { // Let's generate a random client secret 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)) } _ => (None, None), diff --git a/crates/keystore/src/encrypter.rs b/crates/keystore/src/encrypter.rs index fe95fc55..c57763de 100644 --- a/crates/keystore/src/encrypter.rs +++ b/crates/keystore/src/encrypter.rs @@ -81,7 +81,7 @@ impl Encrypter { /// # Errors /// /// Will return `Err` when the payload failed to encrypt - pub fn encryt_to_string(&self, decrypted: &[u8]) -> Result { + pub fn encrypt_to_string(&self, decrypted: &[u8]) -> Result { let nonce = rand::random(); let encrypted = self.encrypt(&nonce, decrypted)?; let encrypted = [&nonce[..], &encrypted].concat(); diff --git a/docs/config.schema.json b/docs/config.schema.json index 1fab6557..443f12dc 100644 --- a/docs/config.schema.json +++ b/docs/config.schema.json @@ -197,6 +197,17 @@ "$ref": "#/definitions/TemplatesConfig" } ] + }, + "upstream_oauth2": { + "description": "Configuration related to upstream OAuth providers", + "default": { + "providers": [] + }, + "allOf": [ + { + "$ref": "#/definitions/UpstreamOAuth2Config" + } + ] } }, "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": { "description": "An OAuth 2.0 client configuration", "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", "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": { "description": "JSON Web Key EC Elliptic Curve", "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": { "description": "HTTP resources to mount", "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" + } + } + } } } } \ No newline at end of file