mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-08-27 22:35:18 +00:00
feat: Speedbump when logging in with OIDC with no next target
This commit is contained in:
+15
-7
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user