From 511e60b41d78d600cc311c3726a96d88408a7a01 Mon Sep 17 00:00:00 2001
From: lafleur
Date: Sat, 9 Aug 2025 01:00:34 +0200
Subject: [PATCH] support OIDC private clients
---
src/api/client/oidc/register.rs | 40 ++++++++++++++++++++++++-------
src/web/oidc.rs | 2 ++
src/web/oidc/authorize.rs | 3 +++
src/web/oidc/consent.rs | 1 +
src/web/oidc/login.rs | 4 ++++
src/web/templates/consent.html.j2 | 4 ++--
src/web/templates/login.html.j2 | 3 +++
7 files changed, 46 insertions(+), 11 deletions(-)
diff --git a/src/api/client/oidc/register.rs b/src/api/client/oidc/register.rs
index 6b752f5b..d39b6414 100644
--- a/src/api/client/oidc/register.rs
+++ b/src/api/client/oidc/register.rs
@@ -2,7 +2,7 @@ use axum::{Json, extract::State};
use conduwuit::{Result, err};
use oxide_auth::primitives::prelude::Client;
use reqwest::Url;
-use ruma::DeviceId;
+use ruma::{DeviceId, identifiers_validation};
/// The required parameters to register a new client for OAuth2 application.
#[derive(serde::Deserialize, Clone, Debug)]
@@ -35,6 +35,8 @@ pub(crate) struct ClientQuery {
#[derive(serde::Serialize, Debug)]
pub(crate) struct ClientResponse {
client_id: String,
+ client_secret: Option,
+ client_secret_expires_at: Option,
client_name: String,
client_uri: Url,
logo_uri: Option,
@@ -68,18 +70,38 @@ pub(crate) async fn register_client(
let scope = format!(
"urn:matrix:org.matrix.msc2967.client:api:* \
urn:matrix:org.matrix.msc2967.client:device:{device_id}"
- );
+ ).parse().expect("parseable default Matrix scope");
// TODO check if the users service needs an update.
//services.users.update_device_metadata();
- services.oidc.register_client(&Client::public(
- device_id.as_ref(),
- redirect_uri.into(),
- scope
- .parse()
- .expect("device ID should parse in Matrix scope"),
- ))?;
+
+ // If the client cannot authenticate itself at the token endpoint, then
+ // it's a public client.
+ let is_private = client.token_endpoint_auth_method != "none";
+ // TODO generate a device secret.
+ let secret = "cacestdubonsecretmonlouou=--".to_string();
+ if let Err(err) = identifiers_validation::client_secret::validate(&secret) {
+ tracing::warn!("oops, we generated an invalid client_secret: {err}");
+ }
+ let registerable = match is_private {
+ | true => &Client::confidential(
+ device_id.as_ref(),
+ redirect_uri,
+ scope,
+ secret.as_bytes(),
+ ).with_additional_redirect_uris(remaining_uris),
+ | _ => &Client::public(
+ device_id.as_ref(),
+ redirect_uri,
+ scope,
+ ).with_additional_redirect_uris(remaining_uris)
+ };
+ tracing::trace!("registering OIDC device : {registerable:#?}");
+ services.oidc.register_client(®isterable)?;
+
let client_response = ClientResponse {
client_id: device_id.to_string(),
+ client_secret: if is_private { Some(secret) } else { None },
+ client_secret_expires_at: if is_private { Some(0) } else { None },
client_name: client.client_name.clone(),
client_uri: client.client_uri.clone(),
redirect_uris: client.redirect_uris.clone(),
diff --git a/src/web/oidc.rs b/src/web/oidc.rs
index 1abe6f83..84349d0b 100644
--- a/src/web/oidc.rs
+++ b/src/web/oidc.rs
@@ -26,6 +26,7 @@ pub(crate) struct LoginPageTemplate<'a> {
hostname: &'a str,
route: &'a str,
client_id: &'a str,
+ client_secret: Option<&'a str>,
redirect_uri: &'a str,
scope: &'a str,
state: &'a str,
@@ -43,6 +44,7 @@ pub(crate) struct ConsentPageTemplate<'a> {
hostname: &'a str,
route: &'a str,
client_id: &'a str,
+ client_secret: Option<&'a str>,
redirect_uri: &'a str,
scope: &'a str,
state: &'a str,
diff --git a/src/web/oidc/authorize.rs b/src/web/oidc/authorize.rs
index 1e851234..9721bd3f 100644
--- a/src/web/oidc/authorize.rs
+++ b/src/web/oidc/authorize.rs
@@ -6,6 +6,7 @@ use super::LoginQuery;
#[derive(serde::Deserialize, Debug)]
pub struct AuthorizationQuery {
pub client_id: String,
+ pub client_secret: Option,
pub redirect_uri: Url,
pub scope: String,
pub state: String,
@@ -19,6 +20,7 @@ impl From for AuthorizationQuery {
fn from(value: LoginQuery) -> Self {
let LoginQuery {
client_id,
+ client_secret,
redirect_uri,
scope,
state,
@@ -31,6 +33,7 @@ impl From for AuthorizationQuery {
Self {
client_id,
+ client_secret,
redirect_uri,
scope,
state,
diff --git a/src/web/oidc/consent.rs b/src/web/oidc/consent.rs
index fbc2ce05..311117e0 100644
--- a/src/web/oidc/consent.rs
+++ b/src/web/oidc/consent.rs
@@ -37,6 +37,7 @@ fn consent_page(hostname: &str, query: &AuthorizationQuery, route: &str, nonce:
hostname,
route,
client_id: &encode(query.client_id.as_str()),
+ client_secret: query.client_secret.as_deref(),
redirect_uri: &encode(query.redirect_uri.as_str()),
scope: &encode(query.scope.as_str()),
state: &encode(query.state.as_str()),
diff --git a/src/web/oidc/login.rs b/src/web/oidc/login.rs
index 60719f62..9c36268a 100644
--- a/src/web/oidc/login.rs
+++ b/src/web/oidc/login.rs
@@ -13,6 +13,7 @@ pub struct LoginQuery {
pub username: String,
pub password: String,
pub client_id: String,
+ pub client_secret: Option,
pub redirect_uri: Url,
pub scope: String,
pub state: String,
@@ -68,11 +69,13 @@ impl TryFrom for LoginQuery {
| "https" => Cow::Borrowed("fragment"),
| _ => Cow::Borrowed("query")
});
+ let client_secret = body.unique_value("client_secret").map(|s| s.to_string());
Ok(Self {
username: username.to_string(),
password: password.to_string(),
client_id: client_id.to_string(),
+ client_secret,
redirect_uri,
scope: scope.to_string(),
state: state.to_string(),
@@ -116,6 +119,7 @@ fn login_page(hostname: &str, query: &AuthorizationQuery, route: &str, nonce: &s
hostname,
route,
client_id: query.client_id.as_str(),
+ client_secret: query.client_secret.as_deref(),
redirect_uri: query.redirect_uri.as_str(),
scope: query.scope.as_str(),
state: query.state.as_str(),
diff --git a/src/web/templates/consent.html.j2 b/src/web/templates/consent.html.j2
index 09501a74..054a9fb7 100644
--- a/src/web/templates/consent.html.j2
+++ b/src/web/templates/consent.html.j2
@@ -6,8 +6,8 @@
'{{ client_id }}' (at {{ redirect_uri }}) is requesting permission for '{{ scope }}'
{%- endblock content -%}
diff --git a/src/web/templates/login.html.j2 b/src/web/templates/login.html.j2
index 1748b037..5da9fb01 100644
--- a/src/web/templates/login.html.j2
+++ b/src/web/templates/login.html.j2
@@ -6,6 +6,9 @@
+ {%- if let Some(secret) = client_secret -%}
+
+ {%- endif -%}