support OIDC private clients

This commit is contained in:
lafleur 2025-08-09 01:00:34 +02:00
commit 511e60b41d
7 changed files with 46 additions and 11 deletions

View file

@ -2,7 +2,7 @@ use axum::{Json, extract::State};
use conduwuit::{Result, err}; use conduwuit::{Result, err};
use oxide_auth::primitives::prelude::Client; use oxide_auth::primitives::prelude::Client;
use reqwest::Url; use reqwest::Url;
use ruma::DeviceId; use ruma::{DeviceId, identifiers_validation};
/// The required parameters to register a new client for OAuth2 application. /// The required parameters to register a new client for OAuth2 application.
#[derive(serde::Deserialize, Clone, Debug)] #[derive(serde::Deserialize, Clone, Debug)]
@ -35,6 +35,8 @@ pub(crate) struct ClientQuery {
#[derive(serde::Serialize, Debug)] #[derive(serde::Serialize, Debug)]
pub(crate) struct ClientResponse { pub(crate) struct ClientResponse {
client_id: String, client_id: String,
client_secret: Option<String>,
client_secret_expires_at: Option<u32>,
client_name: String, client_name: String,
client_uri: Url, client_uri: Url,
logo_uri: Option<Url>, logo_uri: Option<Url>,
@ -68,18 +70,38 @@ pub(crate) async fn register_client(
let scope = format!( let scope = format!(
"urn:matrix:org.matrix.msc2967.client:api:* \ "urn:matrix:org.matrix.msc2967.client:api:* \
urn:matrix:org.matrix.msc2967.client:device:{device_id}" urn:matrix:org.matrix.msc2967.client:device:{device_id}"
); ).parse().expect("parseable default Matrix scope");
// TODO check if the users service needs an update. // TODO check if the users service needs an update.
//services.users.update_device_metadata(); //services.users.update_device_metadata();
services.oidc.register_client(&Client::public(
device_id.as_ref(), // If the client cannot authenticate itself at the token endpoint, then
redirect_uri.into(), // it's a public client.
scope let is_private = client.token_endpoint_auth_method != "none";
.parse() // TODO generate a device secret.
.expect("device ID should parse in Matrix scope"), 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(&registerable)?;
let client_response = ClientResponse { let client_response = ClientResponse {
client_id: device_id.to_string(), 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_name: client.client_name.clone(),
client_uri: client.client_uri.clone(), client_uri: client.client_uri.clone(),
redirect_uris: client.redirect_uris.clone(), redirect_uris: client.redirect_uris.clone(),

View file

@ -26,6 +26,7 @@ pub(crate) struct LoginPageTemplate<'a> {
hostname: &'a str, hostname: &'a str,
route: &'a str, route: &'a str,
client_id: &'a str, client_id: &'a str,
client_secret: Option<&'a str>,
redirect_uri: &'a str, redirect_uri: &'a str,
scope: &'a str, scope: &'a str,
state: &'a str, state: &'a str,
@ -43,6 +44,7 @@ pub(crate) struct ConsentPageTemplate<'a> {
hostname: &'a str, hostname: &'a str,
route: &'a str, route: &'a str,
client_id: &'a str, client_id: &'a str,
client_secret: Option<&'a str>,
redirect_uri: &'a str, redirect_uri: &'a str,
scope: &'a str, scope: &'a str,
state: &'a str, state: &'a str,

View file

@ -6,6 +6,7 @@ use super::LoginQuery;
#[derive(serde::Deserialize, Debug)] #[derive(serde::Deserialize, Debug)]
pub struct AuthorizationQuery { pub struct AuthorizationQuery {
pub client_id: String, pub client_id: String,
pub client_secret: Option<String>,
pub redirect_uri: Url, pub redirect_uri: Url,
pub scope: String, pub scope: String,
pub state: String, pub state: String,
@ -19,6 +20,7 @@ impl From<LoginQuery> for AuthorizationQuery {
fn from(value: LoginQuery) -> Self { fn from(value: LoginQuery) -> Self {
let LoginQuery { let LoginQuery {
client_id, client_id,
client_secret,
redirect_uri, redirect_uri,
scope, scope,
state, state,
@ -31,6 +33,7 @@ impl From<LoginQuery> for AuthorizationQuery {
Self { Self {
client_id, client_id,
client_secret,
redirect_uri, redirect_uri,
scope, scope,
state, state,

View file

@ -37,6 +37,7 @@ fn consent_page(hostname: &str, query: &AuthorizationQuery, route: &str, nonce:
hostname, hostname,
route, route,
client_id: &encode(query.client_id.as_str()), client_id: &encode(query.client_id.as_str()),
client_secret: query.client_secret.as_deref(),
redirect_uri: &encode(query.redirect_uri.as_str()), redirect_uri: &encode(query.redirect_uri.as_str()),
scope: &encode(query.scope.as_str()), scope: &encode(query.scope.as_str()),
state: &encode(query.state.as_str()), state: &encode(query.state.as_str()),

View file

@ -13,6 +13,7 @@ pub struct LoginQuery {
pub username: String, pub username: String,
pub password: String, pub password: String,
pub client_id: String, pub client_id: String,
pub client_secret: Option<String>,
pub redirect_uri: Url, pub redirect_uri: Url,
pub scope: String, pub scope: String,
pub state: String, pub state: String,
@ -68,11 +69,13 @@ impl TryFrom<OidcRequest> for LoginQuery {
| "https" => Cow::Borrowed("fragment"), | "https" => Cow::Borrowed("fragment"),
| _ => Cow::Borrowed("query") | _ => Cow::Borrowed("query")
}); });
let client_secret = body.unique_value("client_secret").map(|s| s.to_string());
Ok(Self { Ok(Self {
username: username.to_string(), username: username.to_string(),
password: password.to_string(), password: password.to_string(),
client_id: client_id.to_string(), client_id: client_id.to_string(),
client_secret,
redirect_uri, redirect_uri,
scope: scope.to_string(), scope: scope.to_string(),
state: state.to_string(), state: state.to_string(),
@ -116,6 +119,7 @@ fn login_page(hostname: &str, query: &AuthorizationQuery, route: &str, nonce: &s
hostname, hostname,
route, route,
client_id: query.client_id.as_str(), client_id: query.client_id.as_str(),
client_secret: query.client_secret.as_deref(),
redirect_uri: query.redirect_uri.as_str(), redirect_uri: query.redirect_uri.as_str(),
scope: query.scope.as_str(), scope: query.scope.as_str(),
state: query.state.as_str(), state: query.state.as_str(),

View file

@ -6,8 +6,8 @@
'{{ client_id }}' (at {{ redirect_uri }}) is requesting permission for '{{ scope }}' '{{ client_id }}' (at {{ redirect_uri }}) is requesting permission for '{{ scope }}'
</p> </p>
<form method="post"> <form method="post">
<input type="submit" value="Accept" formaction="{{ route }}?client_id={{ client_id }}&redirect_uri={{ redirect_uri }}&scope={{scope }}&state={{ state }}&code_challenge={{ code_challenge }}&code_challenge_method={{ code_challenge_method }}&response_type={{ response_type }}&response_mode={{ response_mode }}&allow=true"> <input type="submit" value="Accept" formaction="{{ route }}?client_id={{ client_id }}{%- if let Some(secret) = client_secret -%}&client_secret={{ secret }}{%- endif -%}&redirect_uri={{ redirect_uri }}&scope={{ scope }}&state={{ state }}&code_challenge={{ code_challenge }}&code_challenge_method={{ code_challenge_method }}&response_type={{ response_type }}&response_mode={{ response_mode }}&allow=true">
<input type="submit" value="Deny" formaction="{{ route }}?client_id={{ client_id }}&redirect_uri={{ redirect_uri }}&scope={{scope }}&state={{ state }}&code_challenge={{ code_challenge }}&code_challenge_method={{ code_challenge_method }}&response_type={{ response_type }}&response_mode={{ response_mode }}&deny=true"> <input type="submit" value="Deny" formaction="{{ route }}?client_id={{ client_id }}{%- if let Some(secret) = client_secret -%}&client_secret={{ secret }}{%- endif -%}&redirect_uri={{ redirect_uri }}&scope={{scope }}&state={{ state }}&code_challenge={{ code_challenge }}&code_challenge_method={{ code_challenge_method }}&response_type={{ response_type }}&response_mode={{ response_mode }}&deny=true">
</form> </form>
</div> </div>
{%- endblock content -%} {%- endblock content -%}

View file

@ -6,6 +6,9 @@
<input type="text" name="username" placeholder="Username" required> <input type="text" name="username" placeholder="Username" required>
<input type="password" name="password" placeholder="Password" required> <input type="password" name="password" placeholder="Password" required>
<input type="hidden" name="client_id" value="{{ client_id }}"> <input type="hidden" name="client_id" value="{{ client_id }}">
{%- if let Some(secret) = client_secret -%}
<input type="hidden" name="client_secret" value="{{ secret }}">
{%- endif -%}
<input type="hidden" name="redirect_uri" value="{{ redirect_uri }}"> <input type="hidden" name="redirect_uri" value="{{ redirect_uri }}">
<input type="hidden" name="scope" value="{{ scope }}"> <input type="hidden" name="scope" value="{{ scope }}">
<input type="hidden" name="state" value="{{ state }}"> <input type="hidden" name="state" value="{{ state }}">