feat: Speedbump when logging in with OIDC with no next target

This commit is contained in:
Ginger
2026-07-02 16:34:38 -04:00
parent 4ee0bb7533
commit e2fe166d63
5 changed files with 113 additions and 63 deletions
+15 -7
View File
@@ -6,7 +6,10 @@
use openidconnect::{
AuthorizationCode, CsrfToken, EndpointMaybeSet, EndpointNotSet, EndpointSet, IssuerUrl,
Nonce, PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, TokenResponse,
core::{CoreAuthenticationFlow, CoreClient, CoreIdTokenClaims, CoreProviderMetadata},
core::{
CoreAuthPrompt, CoreAuthenticationFlow, CoreClient, CoreIdTokenClaims,
CoreProviderMetadata,
},
reqwest,
};
use ruma::{OwnedUserId, UserId};
@@ -133,20 +136,25 @@ impl Service {
pub fn enabled(&self) -> bool { self.client.is_some() }
pub async fn begin_session(&self) -> (PendingSession, Url) {
pub async fn begin_session(&self, prompt: Option<CoreAuthPrompt>) -> (PendingSession, Url) {
let OidcClient { machine, .. } = self.client.as_ref().expect("oidc should be configured");
let machine = machine.wait().await;
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
let (auth_url, csrf_token, nonce) = machine
let mut auth_url = machine
.authorize_url(
CoreAuthenticationFlow::AuthorizationCode,
CsrfToken::new_random,
Nonce::new_random,
)
.set_pkce_challenge(pkce_challenge)
.url();
.set_pkce_challenge(pkce_challenge);
if let Some(prompt) = prompt {
auth_url = auth_url.add_prompt(prompt);
}
let (auth_url, csrf_token, nonce) = auth_url.url();
(PendingSession { pkce_verifier, nonce, csrf_token }, auth_url)
}
@@ -253,8 +261,6 @@ pub async fn complete_session(
Self::SERVER_MISCONFIGURED
})?;
self.link_user(&user_id, subject);
info!(?subject, ?user_id, "Shadow user created for {user_id}");
},
| AccountStatus::Deactivated => {
@@ -262,6 +268,8 @@ pub async fn complete_session(
},
}
self.link_user(&user_id, subject);
Ok(SessionCompletionStatus::Complete(user_id))
}
+38 -25
View File
@@ -7,12 +7,14 @@
routing::{get, on},
};
use conduwuit_api::client::handle_login;
use openidconnect::core::CoreAuthPrompt;
use ruma::{
OwnedUserId,
api::client::uiaa::{EmailUserIdentifier, MatrixUserIdentifier, UserIdentifier},
};
use serde::Deserialize;
use tower_sessions::Session;
use url::Url;
use crate::{
ROUTE_PREFIX, WebError,
@@ -37,6 +39,7 @@ pub(crate) fn build() -> Router<crate::State> {
template! {
struct Login use "login.html.j2" {
body: LoginBody,
login_type: LoginType,
login_error: Option<String>
}
}
@@ -45,7 +48,6 @@ struct Login use "login.html.j2" {
enum LoginBody {
Unauthenticated {
server_name: String,
registration_available: bool,
next: Option<LoginTarget>,
},
Authenticated {
@@ -53,6 +55,16 @@ enum LoginBody {
},
}
#[derive(Debug)]
enum LoginType {
Interactive {
registration_available: bool,
},
Oidc {
redirect_url: Url,
},
}
#[derive(Deserialize)]
struct LoginForm {
identifier: Option<String>,
@@ -69,38 +81,39 @@ async fn route_login(
) -> Result {
let user_id = user.into_session().map(|session| session.user_id);
if services.oidc.enabled() {
if user_id.is_some() && !reauthenticate {
return response!(Redirect::to(&next.unwrap_or_default().target_path()));
}
let (session, redirect_url) = services.oidc.begin_session().await;
let login_type = if services.oidc.enabled() {
let (session, redirect_url) = services
.oidc
.begin_session(reauthenticate.then(|| CoreAuthPrompt::Consent))
.await;
session_store
.insert(OIDC_SESSION_ID_KEY, OidcSession {
next: next.unwrap_or_default(),
state: OidcSessionState::CodeExchange { expected_user: user_id, session },
next: next.clone().unwrap_or_default(),
state: OidcSessionState::CodeExchange { expected_user: user_id.clone(), session },
})
.await
.expect("should be able to serialize OIDC session");
return response!(Redirect::to(redirect_url.as_str()));
}
if next.is_some() {
return response!(Redirect::to(redirect_url.as_str()));
}
LoginType::Oidc { redirect_url }
} else {
let (trusted_flow_status, untrusted_flow_status) =
registration_flow_status(&services).await;
let registration_available = matches!(trusted_flow_status, TrustedFlowStatus::Available)
|| matches!(untrusted_flow_status, UntrustedFlowStatus::Available { .. });
LoginType::Interactive { registration_available }
};
let body = match &user_id {
| None => {
let (trusted_flow_status, untrusted_flow_status) =
registration_flow_status(&services).await;
let registration_available =
matches!(trusted_flow_status, TrustedFlowStatus::Available)
|| matches!(untrusted_flow_status, UntrustedFlowStatus::Available { .. });
LoginBody::Unauthenticated {
server_name: services.globals.server_name().to_string(),
registration_available,
next: next.clone(),
}
| None => LoginBody::Unauthenticated {
server_name: services.globals.server_name().to_string(),
next: next.clone(),
},
| Some(user_id) => {
if !reauthenticate {
@@ -113,7 +126,7 @@ async fn route_login(
},
};
let mut template = Login::new(context, body, None);
let mut template = Login::new(context, body, login_type, None);
if let Some(form) = form {
let login_result = match (user_id, form.identifier) {
+7
View File
@@ -4,6 +4,7 @@
use validator::{Validate, ValidationError, ValidationErrors};
use crate::{
WebError,
extract::PostForm,
form,
pages::{
@@ -65,6 +66,12 @@ async fn route_change_password(
user: User,
PostForm(form): PostForm<ChangePasswordForm>,
) -> Result {
if services.oidc.enabled() {
return Err(WebError::BadRequest(
"Password changing is not available on this server".to_owned(),
));
}
let user_id = user.expect(LoginTarget::ChangePassword)?;
let user_card = UserCard::for_local_user(&services, user_id.clone()).await;
+7
View File
@@ -65,6 +65,13 @@ async fn route_reset_password(
return response!(ResetPassword::new(context, ResetPasswordBody::Unavailable));
}
// Check if OIDC is enabled
if services.oidc.enabled() {
return Err(WebError::BadRequest(
"Password resets are not available on this server".to_owned(),
));
}
let Some(form) = form else {
// For GET requests return the reset request form
return response!(ResetPassword::new(
+46 -31
View File
@@ -9,9 +9,14 @@ Log in
{%- endblock -%}
{%- block content -%}
{% match login_type %}
{% when LoginType::Interactive { .. } %}
<div class="panel narrow">
{% when LoginType::Oidc { .. } %}
<div class="panel narrow middle"/>
{% endmatch %}
{% match body %}
{% when LoginBody::Unauthenticated { server_name, registration_available, next } %}
{% when LoginBody::Unauthenticated { server_name, next } %}
<h1 class="with-matrix-icon">
{% if next.is_some() %}
Log in to continue
@@ -25,39 +30,49 @@ Log in
<p>
You're about to log in to your account on <em>{{ server_name }}</em>
</p>
<hr>
<form method="post">
<p>
<label for="identifier">Username or email address</label>
<input type="text" id="identifier" name="identifier" autocomplete="username">
</p>
<p>
<label for="password">Password</label>
<input type="password" id="password" name="password" autocomplete="current-password">
</p>
<button type="submit">Log in</button>
</form>
<div class="centered-links">
{% if registration_available %}
{% let query = next.as_ref().map(serde_urlencoded::to_string).transpose().unwrap().unwrap_or_default() %}
<a href="{{ crate::ROUTE_PREFIX }}/account/register/?{{ query }}">Sign up</a>
{% endif %}
<a href="{{ crate::ROUTE_PREFIX }}/account/password/reset/">Forgot your password?</a>
</div>
{% match login_type %}
{% when LoginType::Interactive { registration_available } %}
<hr>
<form method="post">
<p>
<label for="identifier">Username or email address</label>
<input type="text" id="identifier" name="identifier" autocomplete="username">
</p>
<p>
<label for="password">Password</label>
<input type="password" id="password" name="password" autocomplete="current-password">
</p>
<button type="submit">Log in</button>
</form>
<div class="centered-links">
{% if registration_available %}
{% let query = next.as_ref().map(serde_urlencoded::to_string).transpose().unwrap().unwrap_or_default() %}
<a href="{{ crate::ROUTE_PREFIX }}/account/register/?{{ query }}">Sign up</a>
{% endif %}
<a href="{{ crate::ROUTE_PREFIX }}/account/password/reset/">Forgot your password?</a>
</div>
{% when LoginType::Oidc { redirect_url } %}
<a class="button" href="{{ redirect_url }}">Continue</a>
{% endmatch %}
{% when LoginBody::Authenticated { user_card } %}
<h1>Confirm your identity</h1>
{{ user_card }}
<p>Enter your password to continue.</p>
<form method="post">
<p>
<label for="password">Password</label>
<input type="password" id="password" name="password" autocomplete="current-password">
</p>
<button type="submit">Continue</button>
</form>
<div class="centered-links">
<a href="{{ crate::ROUTE_PREFIX }}/account/password/reset/">Forgot your password?</a>
</div>
{% match login_type %}
{% when LoginType::Interactive { .. } %}
<p>Enter your password to continue.</p>
<form method="post">
<p>
<label for="password">Password</label>
<input type="password" id="password" name="password" autocomplete="current-password">
</p>
<button type="submit">Continue</button>
</form>
<div class="centered-links">
<a href="{{ crate::ROUTE_PREFIX }}/account/password/reset/">Forgot your password?</a>
</div>
{% when LoginType::Oidc { redirect_url } %}
<a class="button" href="{{ redirect_url }}">Continue</a>
{% endmatch %}
{% endmatch %}
{% if let Some(error) = login_error %}
<small class="error">{{ error }}</small>