diff --git a/src/service/oidc/mod.rs b/src/service/oidc/mod.rs index 70ebb823e..2adeb2259 100644 --- a/src/service/oidc/mod.rs +++ b/src/service/oidc/mod.rs @@ -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) -> (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)) } diff --git a/src/web/pages/account/login.rs b/src/web/pages/account/login.rs index ca66bae2d..5f1db34ff 100644 --- a/src/web/pages/account/login.rs +++ b/src/web/pages/account/login.rs @@ -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 { template! { struct Login use "login.html.j2" { body: LoginBody, + login_type: LoginType, login_error: Option } } @@ -45,7 +48,6 @@ struct Login use "login.html.j2" { enum LoginBody { Unauthenticated { server_name: String, - registration_available: bool, next: Option, }, 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, @@ -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) { diff --git a/src/web/pages/account/password/change.rs b/src/web/pages/account/password/change.rs index a8d4adc80..74a5a5f64 100644 --- a/src/web/pages/account/password/change.rs +++ b/src/web/pages/account/password/change.rs @@ -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, ) -> 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; diff --git a/src/web/pages/account/password/reset.rs b/src/web/pages/account/password/reset.rs index b263060b7..b95601fc9 100644 --- a/src/web/pages/account/password/reset.rs +++ b/src/web/pages/account/password/reset.rs @@ -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( diff --git a/src/web/pages/templates/login.html.j2 b/src/web/pages/templates/login.html.j2 index a2ade2e04..9f118e0bc 100644 --- a/src/web/pages/templates/login.html.j2 +++ b/src/web/pages/templates/login.html.j2 @@ -9,9 +9,14 @@ Log in {%- endblock -%} {%- block content -%} +{% match login_type %} + {% when LoginType::Interactive { .. } %}
+ {% when LoginType::Oidc { .. } %} +
+{% endmatch %} {% match body %} - {% when LoginBody::Unauthenticated { server_name, registration_available, next } %} + {% when LoginBody::Unauthenticated { server_name, next } %}

{% if next.is_some() %} Log in to continue @@ -25,39 +30,49 @@ Log in

You're about to log in to your account on {{ server_name }}

-
-
-

- - -

-

- - -

- -
- + {% match login_type %} + {% when LoginType::Interactive { registration_available } %} +
+
+

+ + +

+

+ + +

+ +
+ + {% when LoginType::Oidc { redirect_url } %} + Continue + {% endmatch %} {% when LoginBody::Authenticated { user_card } %}

Confirm your identity

{{ user_card }} -

Enter your password to continue.

-
-

- - -

- -
- + {% match login_type %} + {% when LoginType::Interactive { .. } %} +

Enter your password to continue.

+
+

+ + +

+ +
+ + {% when LoginType::Oidc { redirect_url } %} + Continue + {% endmatch %} {% endmatch %} {% if let Some(error) = login_error %} {{ error }}