feat(oidc_provider) use askama templates

Implements a custom OidcResponse with CSP headers and oxide-auth processing
compatibility.
This commit is contained in:
lafleur authored and Ginger committed 2025-11-11 11:34:26 -05:00
1 parent dba528a5e0
commit a586ea390c
18 files changed
+621 -225

No files matched your search

+16 -130
View File
@@ -1,29 +1,13 @@
use conduwuit_web::oidc::{oidc_consent_form, oidc_login_form, AuthorizationQuery, OidcRequest, OidcResponse};
use oxide_auth_axum::{OAuthResponse, OAuthRequest};
use oxide_auth::{
endpoint::{OwnerConsent, Solicitation},
frontends::simple::endpoint::FnSolicitor,
primitives::registrar::PreGrant,
};
use axum::extract::{Query, State};
use serde_html_form;
use conduwuit::{Result, err};
use reqwest::Url;
use percent_encoding::percent_decode_str;
/// The set of query parameters a client needs to get authorization.
#[derive(serde::Deserialize, Debug)]
pub(crate) struct OAuthQuery {
client_id: String,
redirect_uri: Url,
scope: String,
state: String,
code_challenge: String,
code_challenge_method: String,
response_type: String,
response_mode: String,
}
/// # `GET /_matrix/client/unstable/org.matrix.msc2964/authorize`
///
/// Authenticate a user and device, and solicit the user's consent.
@@ -34,9 +18,9 @@ pub(crate) struct OAuthQuery {
/// access to stage two, [authorize_consent].
pub(crate) async fn authorize(
State(services): State<crate::State>,
Query(query): Query<OAuthQuery>,
oauth: OAuthRequest,
) -> Result<OAuthResponse> {
Query(query): Query<AuthorizationQuery>,
oauth: OidcRequest,
) -> Result<OidcResponse> {
tracing::trace!("processing OAuth request: {query:?}");
// Enforce MSC2964's restrictions on OAuth2 flow.
let Ok(scope) = percent_decode_str(&query.scope).decode_utf8() else {
@@ -60,16 +44,13 @@ pub(crate) async fn authorize(
.client
.as_ref()
.map(|s| s.domain().expect("well-known client should be a domain"));
let login_redirect = OAuthResponse::default()
.body(&login_form(hostname, &query))
.content_type("text/html")
.expect("should set Content-Type on OAuth response");
let hostname = hostname.unwrap_or("Continuwuity");
match oauth.authorization_header() {
| None => {
return Ok(login_redirect);
return Ok(oidc_login_form(hostname, &query));
},
| Some(token) => if services.users.find_from_token(token).await.is_err() {
return Ok(login_redirect);
return Ok(oidc_login_form(hostname, &query));
}
}
// TODO register the device ID ?
@@ -77,18 +58,7 @@ pub(crate) async fn authorize(
services
.oidc
.endpoint()
.with_solicitor(FnSolicitor(move |_: &mut _, solicitation: Solicitation<'_>|
OwnerConsent::InProgress(
OAuthResponse::default()
.body(&consent_page_html(
"/_matrix/client/unstable/org.matrix.msc2964/authorize",
solicitation,
hostname.unwrap_or("conduwuit"),
))
.content_type("text/html")
.expect("set content type on consent form")
)
))
.with_solicitor(oidc_consent_form(hostname, &query))
.authorization_flow()
.execute(oauth)
.map_err(|err| err!("authorization failed: {err:?}"))
@@ -116,99 +86,15 @@ pub(crate) async fn authorize_consent(
services
.oidc
.endpoint()
.with_solicitor(FnSolicitor(move |_: &mut _, solicitation: Solicitation<'_>|
match allowed {
| false => OwnerConsent::Denied,
| true => OwnerConsent::Authorized(solicitation.pre_grant().client_id.clone())
}
))
.with_solicitor(
FnSolicitor(move |_: &mut _, solicitation: Solicitation<'_>|
match allowed {
| false => OwnerConsent::Denied,
| true => OwnerConsent::Authorized(solicitation.pre_grant().client_id.clone())
}
)
)
.authorization_flow()
.execute(oauth)
.map_err(|err| err!(Request(Unknown("consent request failed: {err:?}"))))
}
fn login_form(
hostname: Option<&str>,
OAuthQuery {
client_id,
redirect_uri,
scope,
state,
code_challenge,
code_challenge_method,
response_type,
response_mode,
}: &OAuthQuery,
) -> String {
let hostname = hostname.unwrap_or("");
format!(
r#"
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<center>
<h1>{hostname} login</h1>
<form action="/_matrix/client/unstable/org.matrix.msc2964/login" method="post">
<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}">
<input type="hidden" name="redirect_uri" value="{redirect_uri}">
<input type="hidden" name="scope" value="{scope}">
<input type="hidden" name="state" value="{state}">
<input type="hidden" name="code_challenge" value="{code_challenge}">
<input type="hidden" name="code_challenge_method" value="{code_challenge_method}">
<input type="hidden" name="response_type" value="{response_type}">
<input type="hidden" name="response_mode" value="{response_mode}">
<button type="submit">Login</button>
</form>
</center>
</body>
</html>
"#
)
}
pub(crate) fn consent_page_html(
route: &str,
solicitation: Solicitation<'_>,
hostname: &str,
) -> String {
let state = solicitation.state();
let grant = solicitation.pre_grant();
let PreGrant { client_id, redirect_uri, scope } = grant;
let mut args = vec![
("response_type", "code"),
("client_id", client_id.as_str()),
("redirect_uri", redirect_uri.as_str()),
];
if let Some(state) = state {
args.push(("state", state));
}
let args = serde_html_form::to_string(args).unwrap();
format!(
r#"
<html>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
</head>
<body>
<center>
<h1>{hostname} login</h1>
'{client_id}' (at {redirect_uri}) is requesting permission for '{scope}'
<form method="post">
<input type="submit" value="Accept" formaction="{route}?{args}&allow=true">
<input type="submit" value="Deny" formaction="{route}?{args}&deny=true">
</form>
</center>
</body>
</html>
"#,
)
}