1
0
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:
Quentin Gliech
2023-06-22 18:28:34 +02:00
parent 9d5c2a40a1
commit 4f1b201c74
14 changed files with 503 additions and 50 deletions

View File

@ -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(())
} }
} }

View File

@ -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?;

View File

@ -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(())
} }
} }

View File

@ -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);

View File

@ -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,
} }
} }

View File

@ -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()?;

View File

@ -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(())

View File

@ -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()?;

View File

@ -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),
} }

View File

@ -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(),
} }
} }
} }

View 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,
}

View File

@ -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),

View File

@ -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();

View File

@ -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"
}
}
}
} }
} }
} }