Per-provider registration token requirement (#5605)

This commit is contained in:
Quentin Gliech
2026-06-10 10:05:46 +02:00
committed by GitHub
34 changed files with 269 additions and 91 deletions
+1
View File
@@ -341,6 +341,7 @@ pub async fn config_sync(
forward_login_hint: provider.forward_login_hint,
ui_order,
on_backchannel_logout,
registration_token_required: provider.registration_token_required,
},
)
.await?;
+9
View File
@@ -218,6 +218,14 @@ pub fn site_config_from_config(
user_session_inactivity_ttl: c.expire_user_sessions.then_some(c.ttl),
});
if account_config.registration_token_required {
tracing::warn!(
"`account.registration_token_required` is deprecated. use \
`account.password_registration_token_required` and per-provider \
`registration_token_required` instead"
);
}
Ok(SiteConfig {
access_token_ttl: experimental_config.access_token_ttl,
compat_token_ttl: experimental_config.compat_token_ttl,
@@ -229,6 +237,7 @@ pub fn site_config_from_config(
password_registration_enabled: password_config.enabled()
&& account_config.password_registration_enabled,
password_registration_email_required: account_config.password_registration_email_required,
password_registration_token_required: account_config.password_registration_token_required,
registration_token_required: account_config.registration_token_required,
email_change_allowed: account_config.email_change_allowed,
displayname_change_allowed: account_config.displayname_change_allowed,
+15 -8
View File
@@ -69,6 +69,15 @@ pub struct AccountConfig {
#[serde(default = "default_false", skip_serializing_if = "is_default_false")]
pub password_recovery_enabled: bool,
/// Whether registration tokens are required for password registrations.
/// Defaults to `false`.
///
/// When enabled, users must provide a valid registration token during
/// password registration. This has no effect if password registration
/// is disabled.
#[serde(default = "default_false", skip_serializing_if = "is_default_false")]
pub password_registration_token_required: bool,
/// Whether users are allowed to delete their own account. Defaults to
/// `true`.
#[serde(default = "default_true", skip_serializing_if = "is_default_true")]
@@ -80,12 +89,8 @@ pub struct AccountConfig {
#[serde(default = "default_false", skip_serializing_if = "is_default_false")]
pub login_with_email_allowed: bool,
/// Whether registration tokens are required for password registrations.
/// Defaults to `false`.
///
/// When enabled, users must provide a valid registration token during
/// password registration. This has no effect if password registration
/// is disabled.
/// Whether registration tokens are required for password registrations
/// This is deprecated in favor of `password_registration_token_required`
#[serde(default = "default_false", skip_serializing_if = "is_default_false")]
pub registration_token_required: bool,
}
@@ -99,9 +104,10 @@ impl Default for AccountConfig {
password_registration_email_required: default_true(),
password_change_allowed: default_true(),
password_recovery_enabled: default_false(),
password_registration_token_required: default_false(),
registration_token_required: default_false(),
account_deactivation_allowed: default_true(),
login_with_email_allowed: default_false(),
registration_token_required: default_false(),
}
}
}
@@ -114,9 +120,10 @@ impl AccountConfig {
&& is_default_true(&self.displayname_change_allowed)
&& is_default_true(&self.password_change_allowed)
&& is_default_false(&self.password_recovery_enabled)
&& is_default_false(&self.password_registration_token_required)
&& is_default_false(&self.registration_token_required)
&& is_default_true(&self.account_deactivation_allowed)
&& is_default_false(&self.login_with_email_allowed)
&& is_default_false(&self.registration_token_required)
}
}
@@ -511,6 +511,7 @@ impl OnBackchannelLogout {
#[serde_as]
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[allow(clippy::struct_excessive_bools)]
pub struct Provider {
/// Whether this provider is enabled.
///
@@ -691,6 +692,12 @@ pub struct Provider {
/// Defaults to `do_nothing`.
#[serde(default, skip_serializing_if = "OnBackchannelLogout::is_default")]
pub on_backchannel_logout: OnBackchannelLogout,
/// Whether or not to require a registration token on `OAuth2` auth
///
/// Defaults to `false`
#[serde(default)]
pub registration_token_required: bool,
}
impl Provider {
+5
View File
@@ -80,6 +80,11 @@ pub struct SiteConfig {
pub password_registration_email_required: bool,
/// Whether registration tokens are required for password registrations.
pub password_registration_token_required: bool,
/// Whether registration tokens are required globally for password
/// registrations. Deprecated in favor of
/// `password_registration_token_required`
pub registration_token_required: bool,
/// Whether users can change their email.
@@ -285,6 +285,7 @@ pub struct UpstreamOAuthProvider {
pub additional_authorization_parameters: Vec<(String, String)>,
pub forward_login_hint: bool,
pub on_backchannel_logout: OnBackchannelLogout,
pub registration_token_required: bool,
}
impl PartialOrd for UpstreamOAuthProvider {
+8
View File
@@ -723,6 +723,10 @@ pub struct UpstreamOAuthProvider {
/// When the provider was disabled. If null, the provider is enabled.
disabled_at: Option<DateTime<Utc>>,
/// Whether a registration token is required for registrations via this
/// provider.
registration_token_required: bool,
}
impl From<mas_data_model::UpstreamOAuthProvider> for UpstreamOAuthProvider {
@@ -734,6 +738,7 @@ impl From<mas_data_model::UpstreamOAuthProvider> for UpstreamOAuthProvider {
brand_name: provider.brand_name,
created_at: provider.created_at,
disabled_at: provider.disabled_at,
registration_token_required: provider.registration_token_required,
}
}
}
@@ -758,6 +763,7 @@ impl UpstreamOAuthProvider {
brand_name: Some("google".to_owned()),
created_at: DateTime::default(),
disabled_at: None,
registration_token_required: false,
},
Self {
id: Ulid::from_bytes([0x02; 16]),
@@ -766,6 +772,7 @@ impl UpstreamOAuthProvider {
brand_name: Some("apple".to_owned()),
created_at: DateTime::default(),
disabled_at: Some(DateTime::default()),
registration_token_required: false,
},
Self {
id: Ulid::from_bytes([0x03; 16]),
@@ -774,6 +781,7 @@ impl UpstreamOAuthProvider {
brand_name: None,
created_at: DateTime::default(),
disabled_at: None,
registration_token_required: true,
},
]
}
+10 -1
View File
@@ -25,7 +25,12 @@ pub struct SiteConfig {
/// Whether a valid email address is required for password registrations.
pub password_registration_email_required: bool,
pub password_registration_token_required: bool,
/// Whether registration tokens are required for password registrations.
/// Deprecated in favor of `password_registration_token_required`
#[deprecated = "use `password_registration_token_required` instead"]
#[allow(deprecated)]
pub registration_token_required: bool,
/// Whether users can change their email.
@@ -52,6 +57,7 @@ pub struct SiteConfig {
pub minimum_password_complexity: u8,
}
#[allow(deprecated)]
pub fn doc(operation: TransformOperation) -> TransformOperation {
operation
.id("siteConfig")
@@ -63,6 +69,7 @@ pub fn doc(operation: TransformOperation) -> TransformOperation {
password_login_enabled: true,
password_registration_enabled: true,
password_registration_email_required: true,
password_registration_token_required: true,
registration_token_required: true,
email_change_allowed: true,
displayname_change_allowed: true,
@@ -76,6 +83,7 @@ pub fn doc(operation: TransformOperation) -> TransformOperation {
}
#[tracing::instrument(name = "handler.admin.v1.site_config", skip_all)]
#[allow(deprecated)]
pub async fn handler(
_: CallContext,
State(site_config): State<mas_data_model::SiteConfig>,
@@ -85,7 +93,8 @@ pub async fn handler(
password_login_enabled: site_config.password_login_enabled,
password_registration_enabled: site_config.password_registration_enabled,
password_registration_email_required: site_config.password_registration_email_required,
registration_token_required: site_config.registration_token_required,
password_registration_token_required: site_config.password_registration_token_required,
registration_token_required: site_config.password_registration_token_required,
email_change_allowed: site_config.email_change_allowed,
displayname_change_allowed: site_config.displayname_change_allowed,
password_change_allowed: site_config.password_change_allowed,
@@ -51,6 +51,7 @@ mod test_utils {
forward_login_hint: false,
ui_order: 0,
on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
registration_token_required: false,
}
}
}
@@ -119,6 +119,7 @@ mod tests {
forward_login_hint: false,
on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
ui_order: 0,
registration_token_required: false,
};
let provider = repo
@@ -164,7 +165,8 @@ mod tests {
"human_name": "Google",
"brand_name": "google",
"created_at": "2022-01-16T14:40:00Z",
"disabled_at": null
"disabled_at": null,
"registration_token_required": false
},
"links": {
"self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG0MZAA6S4AF7CTV32E"
@@ -191,6 +191,7 @@ mod tests {
forward_login_hint: false,
on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
ui_order: 0,
registration_token_required: false,
};
repo.upstream_oauth_provider()
@@ -223,6 +224,7 @@ mod tests {
forward_login_hint: false,
on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
ui_order: 1,
registration_token_required: false,
};
let disabled_provider = repo
@@ -262,6 +264,7 @@ mod tests {
forward_login_hint: false,
on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
ui_order: 2,
registration_token_required: false,
};
repo.upstream_oauth_provider()
@@ -304,7 +307,8 @@ mod tests {
"human_name": "Apple ID",
"brand_name": "apple",
"created_at": "2022-01-16T14:40:00Z",
"disabled_at": "2022-01-16T14:40:00Z"
"disabled_at": "2022-01-16T14:40:00Z",
"registration_token_required": false
},
"links": {
"self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG07HNEZXNQM2KNBNF6"
@@ -323,7 +327,8 @@ mod tests {
"human_name": "Microsoft",
"brand_name": "microsoft",
"created_at": "2022-01-16T14:40:00Z",
"disabled_at": null
"disabled_at": null,
"registration_token_required": false
},
"links": {
"self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG09AVTNSQFMSR34AJC"
@@ -342,7 +347,8 @@ mod tests {
"human_name": "Google",
"brand_name": "google",
"created_at": "2022-01-16T14:40:00Z",
"disabled_at": null
"disabled_at": null,
"registration_token_required": false
},
"links": {
"self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG0MZAA6S4AF7CTV32E"
@@ -392,7 +398,8 @@ mod tests {
"human_name": "Microsoft",
"brand_name": "microsoft",
"created_at": "2022-01-16T14:40:00Z",
"disabled_at": null
"disabled_at": null,
"registration_token_required": false
},
"links": {
"self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG09AVTNSQFMSR34AJC"
@@ -411,7 +418,8 @@ mod tests {
"human_name": "Google",
"brand_name": "google",
"created_at": "2022-01-16T14:40:00Z",
"disabled_at": null
"disabled_at": null,
"registration_token_required": false
},
"links": {
"self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG0MZAA6S4AF7CTV32E"
@@ -461,7 +469,8 @@ mod tests {
"human_name": "Apple ID",
"brand_name": "apple",
"created_at": "2022-01-16T14:40:00Z",
"disabled_at": "2022-01-16T14:40:00Z"
"disabled_at": "2022-01-16T14:40:00Z",
"registration_token_required": false
},
"links": {
"self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG07HNEZXNQM2KNBNF6"
@@ -512,7 +521,8 @@ mod tests {
"human_name": "Apple ID",
"brand_name": "apple",
"created_at": "2022-01-16T14:40:00Z",
"disabled_at": "2022-01-16T14:40:00Z"
"disabled_at": "2022-01-16T14:40:00Z",
"registration_token_required": false
},
"links": {
"self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG07HNEZXNQM2KNBNF6"
@@ -531,7 +541,8 @@ mod tests {
"human_name": "Microsoft",
"brand_name": "microsoft",
"created_at": "2022-01-16T14:40:00Z",
"disabled_at": null
"disabled_at": null,
"registration_token_required": false
},
"links": {
"self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG09AVTNSQFMSR34AJC"
@@ -578,7 +589,8 @@ mod tests {
"human_name": "Google",
"brand_name": "google",
"created_at": "2022-01-16T14:40:00Z",
"disabled_at": null
"disabled_at": null,
"registration_token_required": false
},
"links": {
"self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG0MZAA6S4AF7CTV32E"
@@ -640,7 +652,8 @@ mod tests {
"human_name": "Apple ID",
"brand_name": "apple",
"created_at": "2022-01-16T14:40:00Z",
"disabled_at": "2022-01-16T14:40:00Z"
"disabled_at": "2022-01-16T14:40:00Z",
"registration_token_required": false
},
"links": {
"self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG07HNEZXNQM2KNBNF6"
@@ -659,7 +672,8 @@ mod tests {
"human_name": "Microsoft",
"brand_name": "microsoft",
"created_at": "2022-01-16T14:40:00Z",
"disabled_at": null
"disabled_at": null,
"registration_token_required": false
},
"links": {
"self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG09AVTNSQFMSR34AJC"
@@ -678,7 +692,8 @@ mod tests {
"human_name": "Google",
"brand_name": "google",
"created_at": "2022-01-16T14:40:00Z",
"disabled_at": null
"disabled_at": null,
"registration_token_required": false
},
"links": {
"self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG0MZAA6S4AF7CTV32E"
@@ -737,7 +752,8 @@ mod tests {
"human_name": "Microsoft",
"brand_name": "microsoft",
"created_at": "2022-01-16T14:40:00Z",
"disabled_at": null
"disabled_at": null,
"registration_token_required": false
},
"links": {
"self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG09AVTNSQFMSR34AJC"
@@ -756,7 +772,8 @@ mod tests {
"human_name": "Google",
"brand_name": "google",
"created_at": "2022-01-16T14:40:00Z",
"disabled_at": null
"disabled_at": null,
"registration_token_required": false
},
"links": {
"self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG0MZAA6S4AF7CTV32E"
+1
View File
@@ -200,6 +200,7 @@ async fn create_session_hierarchy(
forward_login_hint: false,
ui_order: 0,
on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::LogoutAll,
registration_token_required: false,
};
let provider = repo
+1
View File
@@ -137,6 +137,7 @@ pub fn test_site_config() -> SiteConfig {
imprint: None,
password_login_enabled: true,
password_registration_enabled: true,
password_registration_token_required: false,
registration_token_required: false,
email_change_allowed: true,
displayname_change_allowed: true,
@@ -433,6 +433,7 @@ mod tests {
additional_authorization_parameters: Vec::new(),
forward_login_hint: false,
on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
registration_token_required: false,
};
// Without any override, it should just use discovery
@@ -1345,6 +1345,7 @@ mod tests {
ui_order: 0,
on_backchannel_logout:
mas_data_model::UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
registration_token_required: false,
},
)
.await
@@ -1542,6 +1543,7 @@ mod tests {
ui_order: 0,
on_backchannel_logout:
mas_data_model::UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
registration_token_required: false,
},
)
.await
@@ -1703,6 +1705,7 @@ mod tests {
on_backchannel_logout:
mas_data_model::UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
ui_order: 0,
registration_token_required: false,
},
)
.await
@@ -1819,6 +1822,7 @@ mod tests {
on_backchannel_logout:
mas_data_model::UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
ui_order: 0,
registration_token_required: false,
},
)
.await
@@ -1988,6 +1992,7 @@ mod tests {
on_backchannel_logout:
mas_data_model::UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
ui_order: 0,
registration_token_required: false,
},
)
.await
@@ -2136,6 +2141,7 @@ mod tests {
on_backchannel_logout:
mas_data_model::UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
ui_order: 0,
registration_token_required: false,
},
)
.await
@@ -2255,6 +2261,7 @@ mod tests {
on_backchannel_logout:
mas_data_model::UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
ui_order: 0,
registration_token_required: false,
},
)
.await
+2
View File
@@ -525,6 +525,7 @@ mod test {
forward_login_hint: false,
ui_order: 0,
on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
registration_token_required: false,
},
)
.await
@@ -568,6 +569,7 @@ mod test {
forward_login_hint: false,
ui_order: 1,
on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
registration_token_required: false,
},
)
.await
@@ -120,8 +120,29 @@ pub(crate) async fn get(
)));
}
// Check if the registration token is required and was provided
let registration_token = if site_config.registration_token_required {
let token_required = if let Some(session_id) =
registration.upstream_oauth_authorization_session_id
{
let session = repo
.upstream_oauth_session()
.lookup(session_id)
.await?
.context("Could not load the upstream OAuth authorization session")
.map_err(InternalError::from_anyhow)?;
let provider = repo
.upstream_oauth_provider()
.lookup(session.provider_id)
.await?
.context("Could not load the upstream OAuth provider")
.map_err(InternalError::from_anyhow)?;
provider.registration_token_required || site_config.registration_token_required
} else {
site_config.password_registration_token_required || site_config.registration_token_required
};
let registration_token = if token_required {
if let Some(registration_token_id) = registration.user_registration_token_id {
let registration_token = repo
.user_registration_token()
@@ -1,46 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO upstream_oauth_providers (\n upstream_oauth_provider_id,\n issuer,\n human_name,\n brand_name,\n scope,\n token_endpoint_auth_method,\n token_endpoint_signing_alg,\n id_token_signed_response_alg,\n fetch_userinfo,\n userinfo_signed_response_alg,\n client_id,\n encrypted_client_secret,\n claims_imports,\n authorization_endpoint_override,\n token_endpoint_override,\n userinfo_endpoint_override,\n jwks_uri_override,\n discovery_mode,\n pkce_mode,\n response_mode,\n additional_parameters,\n forward_login_hint,\n ui_order,\n on_backchannel_logout,\n created_at\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10,\n $11, $12, $13, $14, $15, $16, $17, $18, $19, $20,\n $21, $22, $23, $24, $25)\n ON CONFLICT (upstream_oauth_provider_id)\n DO UPDATE\n SET\n issuer = EXCLUDED.issuer,\n human_name = EXCLUDED.human_name,\n brand_name = EXCLUDED.brand_name,\n scope = EXCLUDED.scope,\n token_endpoint_auth_method = EXCLUDED.token_endpoint_auth_method,\n token_endpoint_signing_alg = EXCLUDED.token_endpoint_signing_alg,\n id_token_signed_response_alg = EXCLUDED.id_token_signed_response_alg,\n fetch_userinfo = EXCLUDED.fetch_userinfo,\n userinfo_signed_response_alg = EXCLUDED.userinfo_signed_response_alg,\n disabled_at = NULL,\n client_id = EXCLUDED.client_id,\n encrypted_client_secret = EXCLUDED.encrypted_client_secret,\n claims_imports = EXCLUDED.claims_imports,\n authorization_endpoint_override = EXCLUDED.authorization_endpoint_override,\n token_endpoint_override = EXCLUDED.token_endpoint_override,\n userinfo_endpoint_override = EXCLUDED.userinfo_endpoint_override,\n jwks_uri_override = EXCLUDED.jwks_uri_override,\n discovery_mode = EXCLUDED.discovery_mode,\n pkce_mode = EXCLUDED.pkce_mode,\n response_mode = EXCLUDED.response_mode,\n additional_parameters = EXCLUDED.additional_parameters,\n forward_login_hint = EXCLUDED.forward_login_hint,\n ui_order = EXCLUDED.ui_order,\n on_backchannel_logout = EXCLUDED.on_backchannel_logout\n RETURNING created_at\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "created_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid",
"Text",
"Text",
"Text",
"Text",
"Text",
"Text",
"Text",
"Bool",
"Text",
"Text",
"Text",
"Jsonb",
"Text",
"Text",
"Text",
"Text",
"Text",
"Text",
"Text",
"Jsonb",
"Bool",
"Int4",
"Text",
"Timestamptz"
]
},
"nullable": [
false
]
},
"hash": "0f2ea548e00b080502edc04ee97ea304d43c336ce80723789ff3e66c0dd4d86c"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO upstream_oauth_providers (\n upstream_oauth_provider_id,\n issuer,\n human_name,\n brand_name,\n scope,\n token_endpoint_auth_method,\n token_endpoint_signing_alg,\n id_token_signed_response_alg,\n fetch_userinfo,\n userinfo_signed_response_alg,\n client_id,\n encrypted_client_secret,\n claims_imports,\n authorization_endpoint_override,\n token_endpoint_override,\n userinfo_endpoint_override,\n jwks_uri_override,\n discovery_mode,\n pkce_mode,\n response_mode,\n forward_login_hint,\n on_backchannel_logout,\n created_at\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11,\n $12, $13, $14, $15, $16, $17, $18, $19, $20,\n $21, $22, $23)\n ",
"query": "\n INSERT INTO upstream_oauth_providers (\n upstream_oauth_provider_id,\n issuer,\n human_name,\n brand_name,\n scope,\n token_endpoint_auth_method,\n token_endpoint_signing_alg,\n id_token_signed_response_alg,\n fetch_userinfo,\n userinfo_signed_response_alg,\n client_id,\n encrypted_client_secret,\n claims_imports,\n authorization_endpoint_override,\n token_endpoint_override,\n userinfo_endpoint_override,\n jwks_uri_override,\n discovery_mode,\n pkce_mode,\n response_mode,\n forward_login_hint,\n on_backchannel_logout,\n registration_token_required,\n created_at\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11,\n $12, $13, $14, $15, $16, $17, $18, $19, $20,\n $21, $22, $23, $24)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -27,10 +27,11 @@
"Text",
"Bool",
"Text",
"Bool",
"Timestamptz"
]
},
"nullable": []
},
"hash": "3312f901f70c3b69e0d315206c31ffe11da64835ae297c9277271b8971d5de81"
"hash": "212e0ce75c5e8b73e95b3a9505365192f1b5c133fcda65894add35dc5ae62295"
}
@@ -0,0 +1,47 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO upstream_oauth_providers (\n upstream_oauth_provider_id,\n issuer,\n human_name,\n brand_name,\n scope,\n token_endpoint_auth_method,\n token_endpoint_signing_alg,\n id_token_signed_response_alg,\n fetch_userinfo,\n userinfo_signed_response_alg,\n client_id,\n encrypted_client_secret,\n claims_imports,\n authorization_endpoint_override,\n token_endpoint_override,\n userinfo_endpoint_override,\n jwks_uri_override,\n discovery_mode,\n pkce_mode,\n response_mode,\n additional_parameters,\n forward_login_hint,\n ui_order,\n on_backchannel_logout,\n registration_token_required,\n created_at\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10,\n $11, $12, $13, $14, $15, $16, $17, $18, $19, $20,\n $21, $22, $23, $24, $25, $26)\n ON CONFLICT (upstream_oauth_provider_id)\n DO UPDATE\n SET\n issuer = EXCLUDED.issuer,\n human_name = EXCLUDED.human_name,\n brand_name = EXCLUDED.brand_name,\n scope = EXCLUDED.scope,\n token_endpoint_auth_method = EXCLUDED.token_endpoint_auth_method,\n token_endpoint_signing_alg = EXCLUDED.token_endpoint_signing_alg,\n id_token_signed_response_alg = EXCLUDED.id_token_signed_response_alg,\n fetch_userinfo = EXCLUDED.fetch_userinfo,\n userinfo_signed_response_alg = EXCLUDED.userinfo_signed_response_alg,\n disabled_at = NULL,\n client_id = EXCLUDED.client_id,\n encrypted_client_secret = EXCLUDED.encrypted_client_secret,\n claims_imports = EXCLUDED.claims_imports,\n authorization_endpoint_override = EXCLUDED.authorization_endpoint_override,\n token_endpoint_override = EXCLUDED.token_endpoint_override,\n userinfo_endpoint_override = EXCLUDED.userinfo_endpoint_override,\n jwks_uri_override = EXCLUDED.jwks_uri_override,\n discovery_mode = EXCLUDED.discovery_mode,\n pkce_mode = EXCLUDED.pkce_mode,\n response_mode = EXCLUDED.response_mode,\n additional_parameters = EXCLUDED.additional_parameters,\n forward_login_hint = EXCLUDED.forward_login_hint,\n ui_order = EXCLUDED.ui_order,\n on_backchannel_logout = EXCLUDED.on_backchannel_logout,\n registration_token_required = EXCLUDED.registration_token_required\n RETURNING created_at\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "created_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid",
"Text",
"Text",
"Text",
"Text",
"Text",
"Text",
"Text",
"Bool",
"Text",
"Text",
"Text",
"Jsonb",
"Text",
"Text",
"Text",
"Text",
"Text",
"Text",
"Text",
"Jsonb",
"Bool",
"Int4",
"Text",
"Bool",
"Timestamptz"
]
},
"nullable": [
false
]
},
"hash": "82aaee10fac6db827cc60f8b2571462c443e686349cca3e9788dd93e21aa025e"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n upstream_oauth_provider_id,\n issuer,\n human_name,\n brand_name,\n scope,\n client_id,\n encrypted_client_secret,\n token_endpoint_signing_alg,\n token_endpoint_auth_method,\n id_token_signed_response_alg,\n fetch_userinfo,\n userinfo_signed_response_alg,\n created_at,\n disabled_at,\n claims_imports as \"claims_imports: Json<UpstreamOAuthProviderClaimsImports>\",\n jwks_uri_override,\n authorization_endpoint_override,\n token_endpoint_override,\n userinfo_endpoint_override,\n discovery_mode,\n pkce_mode,\n response_mode,\n additional_parameters as \"additional_parameters: Json<Vec<(String, String)>>\",\n forward_login_hint,\n on_backchannel_logout\n FROM upstream_oauth_providers\n WHERE upstream_oauth_provider_id = $1\n ",
"query": "\n SELECT\n upstream_oauth_provider_id,\n issuer,\n human_name,\n brand_name,\n scope,\n client_id,\n encrypted_client_secret,\n token_endpoint_signing_alg,\n token_endpoint_auth_method,\n id_token_signed_response_alg,\n fetch_userinfo,\n userinfo_signed_response_alg,\n created_at,\n disabled_at,\n claims_imports as \"claims_imports: Json<UpstreamOAuthProviderClaimsImports>\",\n jwks_uri_override,\n authorization_endpoint_override,\n token_endpoint_override,\n userinfo_endpoint_override,\n discovery_mode,\n pkce_mode,\n response_mode,\n additional_parameters as \"additional_parameters: Json<Vec<(String, String)>>\",\n forward_login_hint,\n on_backchannel_logout,\n registration_token_required\n FROM upstream_oauth_providers\n WHERE upstream_oauth_provider_id = $1\n ",
"describe": {
"columns": [
{
@@ -127,6 +127,11 @@
"ordinal": 24,
"name": "on_backchannel_logout",
"type_info": "Text"
},
{
"ordinal": 25,
"name": "registration_token_required",
"type_info": "Bool"
}
],
"parameters": {
@@ -159,8 +164,9 @@
true,
true,
false,
false,
false
]
},
"hash": "6589987e88fa9dbbd2bd48acd910e08bab57721007c64ef2597cb09a62100792"
"hash": "c80f6e8169a4b0e76168d851bcb1b7b05caef229399ea0f61e0d7a5788034309"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n upstream_oauth_provider_id,\n issuer,\n human_name,\n brand_name,\n scope,\n client_id,\n encrypted_client_secret,\n token_endpoint_signing_alg,\n token_endpoint_auth_method,\n id_token_signed_response_alg,\n fetch_userinfo,\n userinfo_signed_response_alg,\n created_at,\n disabled_at,\n claims_imports as \"claims_imports: Json<UpstreamOAuthProviderClaimsImports>\",\n jwks_uri_override,\n authorization_endpoint_override,\n token_endpoint_override,\n userinfo_endpoint_override,\n discovery_mode,\n pkce_mode,\n response_mode,\n additional_parameters as \"additional_parameters: Json<Vec<(String, String)>>\",\n forward_login_hint,\n on_backchannel_logout\n FROM upstream_oauth_providers\n WHERE disabled_at IS NULL\n ORDER BY ui_order ASC, upstream_oauth_provider_id ASC\n ",
"query": "\n SELECT\n upstream_oauth_provider_id,\n issuer,\n human_name,\n brand_name,\n scope,\n client_id,\n encrypted_client_secret,\n token_endpoint_signing_alg,\n token_endpoint_auth_method,\n id_token_signed_response_alg,\n fetch_userinfo,\n userinfo_signed_response_alg,\n created_at,\n disabled_at,\n claims_imports as \"claims_imports: Json<UpstreamOAuthProviderClaimsImports>\",\n jwks_uri_override,\n authorization_endpoint_override,\n token_endpoint_override,\n userinfo_endpoint_override,\n discovery_mode,\n pkce_mode,\n response_mode,\n additional_parameters as \"additional_parameters: Json<Vec<(String, String)>>\",\n forward_login_hint,\n on_backchannel_logout,\n registration_token_required\n\n FROM upstream_oauth_providers\n WHERE disabled_at IS NULL\n ORDER BY ui_order ASC, upstream_oauth_provider_id ASC\n ",
"describe": {
"columns": [
{
@@ -127,6 +127,11 @@
"ordinal": 24,
"name": "on_backchannel_logout",
"type_info": "Text"
},
{
"ordinal": 25,
"name": "registration_token_required",
"type_info": "Bool"
}
],
"parameters": {
@@ -157,8 +162,9 @@
true,
true,
false,
false,
false
]
},
"hash": "99394fbd9c07d6d24429934b3f7344dfab024b42e47ddc7bd9e551897ba6e9b8"
"hash": "ec53297b6bdd23bc15bda823869fa1b3cc6e0991897274636afefcd1a62bdbff"
}
@@ -0,0 +1,9 @@
-- Copyright 2026 Element Creations Ltd.
--
-- SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
-- Please see LICENSE files in the repository root for full details.
-- Adds a `registration_token_required` column to the UpstreamOauthProvider table
ALTER TABLE upstream_oauth_providers
ADD COLUMN registration_token_required BOOLEAN NOT NULL DEFAULT FALSE;
+1
View File
@@ -156,6 +156,7 @@ pub enum UpstreamOAuthProviders {
AuthorizationEndpointOverride,
UserinfoEndpointOverride,
OnBackchannelLogout,
RegistrationTokenRequired,
}
#[derive(sea_query::Iden)]
@@ -79,6 +79,7 @@ mod tests {
forward_login_hint: false,
ui_order: 0,
on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
registration_token_required: false,
},
)
.await
@@ -359,6 +360,7 @@ mod tests {
forward_login_hint: false,
ui_order: 0,
on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
registration_token_required: false,
},
)
.await
@@ -497,6 +499,7 @@ mod tests {
forward_login_hint: false,
ui_order: 0,
on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
registration_token_required: false,
},
)
.await
@@ -73,6 +73,7 @@ struct ProviderLookup {
additional_parameters: Option<Json<Vec<(String, String)>>>,
forward_login_hint: bool,
on_backchannel_logout: String,
registration_token_required: bool,
}
impl Node<Ulid> for ProviderLookup {
@@ -234,6 +235,7 @@ impl TryFrom<ProviderLookup> for UpstreamOAuthProvider {
additional_authorization_parameters,
forward_login_hint: value.forward_login_hint,
on_backchannel_logout,
registration_token_required: value.registration_token_required,
})
}
}
@@ -293,7 +295,8 @@ impl UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<'_> {
response_mode,
additional_parameters as "additional_parameters: Json<Vec<(String, String)>>",
forward_login_hint,
on_backchannel_logout
on_backchannel_logout,
registration_token_required
FROM upstream_oauth_providers
WHERE upstream_oauth_provider_id = $1
"#,
@@ -357,10 +360,11 @@ impl UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<'_> {
response_mode,
forward_login_hint,
on_backchannel_logout,
registration_token_required,
created_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11,
$12, $13, $14, $15, $16, $17, $18, $19, $20,
$21, $22, $23)
$21, $22, $23, $24)
"#,
Uuid::from(id),
params.issuer.as_deref(),
@@ -399,6 +403,7 @@ impl UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<'_> {
params.response_mode.as_ref().map(ToString::to_string),
params.forward_login_hint,
params.on_backchannel_logout.as_str(),
params.registration_token_required,
created_at,
)
.traced()
@@ -431,6 +436,7 @@ impl UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<'_> {
additional_authorization_parameters: params.additional_authorization_parameters,
on_backchannel_logout: params.on_backchannel_logout,
forward_login_hint: params.forward_login_hint,
registration_token_required: params.registration_token_required,
})
}
@@ -546,10 +552,11 @@ impl UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<'_> {
forward_login_hint,
ui_order,
on_backchannel_logout,
registration_token_required,
created_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
$11, $12, $13, $14, $15, $16, $17, $18, $19, $20,
$21, $22, $23, $24, $25)
$21, $22, $23, $24, $25, $26)
ON CONFLICT (upstream_oauth_provider_id)
DO UPDATE
SET
@@ -576,7 +583,8 @@ impl UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<'_> {
additional_parameters = EXCLUDED.additional_parameters,
forward_login_hint = EXCLUDED.forward_login_hint,
ui_order = EXCLUDED.ui_order,
on_backchannel_logout = EXCLUDED.on_backchannel_logout
on_backchannel_logout = EXCLUDED.on_backchannel_logout,
registration_token_required = EXCLUDED.registration_token_required
RETURNING created_at
"#,
Uuid::from(id),
@@ -618,6 +626,7 @@ impl UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<'_> {
params.forward_login_hint,
params.ui_order,
params.on_backchannel_logout.as_str(),
params.registration_token_required,
created_at,
)
.traced()
@@ -650,6 +659,7 @@ impl UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<'_> {
additional_authorization_parameters: params.additional_authorization_parameters,
forward_login_hint: params.forward_login_hint,
on_backchannel_logout: params.on_backchannel_logout,
registration_token_required: params.registration_token_required,
})
}
@@ -874,6 +884,13 @@ impl UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<'_> {
)),
ProviderLookupIden::OnBackchannelLogout,
)
.expr_as(
Expr::col((
UpstreamOAuthProviders::Table,
UpstreamOAuthProviders::RegistrationTokenRequired,
)),
ProviderLookupIden::RegistrationTokenRequired,
)
.from(UpstreamOAuthProviders::Table)
.apply_filter(filter)
.generate_pagination(
@@ -968,7 +985,9 @@ impl UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<'_> {
response_mode,
additional_parameters as "additional_parameters: Json<Vec<(String, String)>>",
forward_login_hint,
on_backchannel_logout
on_backchannel_logout,
registration_token_required
FROM upstream_oauth_providers
WHERE disabled_at IS NULL
ORDER BY ui_order ASC, upstream_oauth_provider_id ASC
@@ -993,6 +993,7 @@ mod tests {
forward_login_hint: false,
ui_order: 0,
on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
registration_token_required: false,
},
)
.await
+1
View File
@@ -772,6 +772,7 @@ async fn test_user_session(pool: PgPool) {
ui_order: 0,
on_backchannel_logout:
mas_data_model::UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
registration_token_required: false,
},
)
.await
@@ -105,6 +105,9 @@ pub struct UpstreamOAuthProviderParams {
/// The behavior when receiving a backchannel logout notification
pub on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout,
/// Whether or not to require a registration token on `OAuth2` auth
pub registration_token_required: bool,
}
/// Filter parameters for listing upstream OAuth 2.0 providers
@@ -27,6 +27,7 @@ upstream_oauth_providers:
jwks_uri_override: ~
on_backchannel_logout: do_nothing
pkce_mode: auto
registration_token_required: "false"
response_mode: query
scope: openid
token_endpoint_auth_method: client_secret_basic
@@ -347,6 +347,7 @@ impl OidcProvider {
additional_authorization_parameters,
forward_login_hint: self.forward_login_hint,
on_backchannel_logout,
registration_token_required: false,
})
}
}
+1
View File
@@ -1754,6 +1754,7 @@ impl TemplateContext for UpstreamRegister {
created_at: now,
disabled_at: None,
on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
registration_token_required: false,
},
)])
}
+22 -7
View File
@@ -36,6 +36,7 @@
"password_login_enabled": true,
"password_registration_enabled": true,
"password_registration_email_required": true,
"password_registration_token_required": true,
"registration_token_required": true,
"email_change_allowed": true,
"displayname_change_allowed": true,
@@ -4685,7 +4686,8 @@
"human_name": "Google",
"brand_name": "google",
"created_at": "1970-01-01T00:00:00Z",
"disabled_at": null
"disabled_at": null,
"registration_token_required": false
},
"links": {
"self": "/api/admin/v1/upstream-oauth-providers/01040G2081040G2081040G2081"
@@ -4704,7 +4706,8 @@
"human_name": "Apple ID",
"brand_name": "apple",
"created_at": "1970-01-01T00:00:00Z",
"disabled_at": "1970-01-01T00:00:00Z"
"disabled_at": "1970-01-01T00:00:00Z",
"registration_token_required": false
},
"links": {
"self": "/api/admin/v1/upstream-oauth-providers/02081040G2081040G2081040G2"
@@ -4723,7 +4726,8 @@
"human_name": "Custom OAuth Provider",
"brand_name": null,
"created_at": "1970-01-01T00:00:00Z",
"disabled_at": null
"disabled_at": null,
"registration_token_required": true
},
"links": {
"self": "/api/admin/v1/upstream-oauth-providers/030C1G60R30C1G60R30C1G60R3"
@@ -4784,7 +4788,8 @@
"human_name": "Google",
"brand_name": "google",
"created_at": "1970-01-01T00:00:00Z",
"disabled_at": null
"disabled_at": null,
"registration_token_required": false
},
"links": {
"self": "/api/admin/v1/upstream-oauth-providers/01040G2081040G2081040G2081"
@@ -4859,10 +4864,14 @@
"description": "Whether a valid email address is required for password registrations.",
"type": "boolean"
},
"registration_token_required": {
"description": "Whether registration tokens are required for password registrations.",
"password_registration_token_required": {
"type": "boolean"
},
"registration_token_required": {
"description": "Whether registration tokens are required for password registrations.\n Deprecated in favor of `password_registration_token_required`",
"type": "boolean",
"deprecated": true
},
"email_change_allowed": {
"description": "Whether users can change their email.",
"type": "boolean"
@@ -4900,6 +4909,7 @@
"password_login_enabled",
"password_registration_enabled",
"password_registration_email_required",
"password_registration_token_required",
"registration_token_required",
"email_change_allowed",
"displayname_change_allowed",
@@ -7382,10 +7392,15 @@
"null"
],
"format": "date-time"
},
"registration_token_required": {
"description": "Whether a registration token is required for registrations via this\n provider.",
"type": "boolean"
}
},
"required": [
"created_at"
"created_at",
"registration_token_required"
]
},
"SingleResponse_for_UpstreamOAuthProvider": {
+10 -1
View File
@@ -2338,6 +2338,11 @@
"$ref": "#/definitions/OnBackchannelLogout"
}
]
},
"registration_token_required": {
"description": "Whether or not to require a registration token on `OAuth2` auth\n\n Defaults to `false`",
"type": "boolean",
"default": false
}
},
"required": [
@@ -2807,6 +2812,10 @@
"description": "Whether email-based password recovery is enabled. Defaults to `false`.\n\n This has no effect if password login is disabled.",
"type": "boolean"
},
"password_registration_token_required": {
"description": "Whether registration tokens are required for password registrations.\n Defaults to `false`.\n\n When enabled, users must provide a valid registration token during\n password registration. This has no effect if password registration\n is disabled.",
"type": "boolean"
},
"account_deactivation_allowed": {
"description": "Whether users are allowed to delete their own account. Defaults to\n `true`.",
"type": "boolean"
@@ -2816,7 +2825,7 @@
"type": "boolean"
},
"registration_token_required": {
"description": "Whether registration tokens are required for password registrations.\n Defaults to `false`.\n\n When enabled, users must provide a valid registration token during\n password registration. This has no effect if password registration\n is disabled.",
"description": "Whether registration tokens are required for password registrations\n This is deprecated in favor of `password_registration_token_required`",
"type": "boolean"
}
}