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 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<String>,
client_secret_expires_at: Option<u32>,
client_name: String,
client_uri: Url,
logo_uri: Option<Url>,
@ -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(
// 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.into(),
scope
.parse()
.expect("device ID should parse in Matrix scope"),
))?;
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 {
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(),

View file

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

View file

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

View file

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

View file

@ -13,6 +13,7 @@ pub struct LoginQuery {
pub username: String,
pub password: String,
pub client_id: String,
pub client_secret: Option<String>,
pub redirect_uri: Url,
pub scope: String,
pub state: String,
@ -68,11 +69,13 @@ impl TryFrom<OidcRequest> 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(),

View file

@ -6,8 +6,8 @@
'{{ client_id }}' (at {{ redirect_uri }}) is requesting permission for '{{ scope }}'
</p>
<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="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="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 }}{%- 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>
</div>
{%- endblock content -%}

View file

@ -6,6 +6,9 @@
<input type="text" name="username" placeholder="Username" required>
<input type="password" name="password" placeholder="Password" required>
<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="scope" value="{{ scope }}">
<input type="hidden" name="state" value="{{ state }}">