diff --git a/src/service/oidc/mod.rs b/src/service/oidc/mod.rs index 82710d149..70ebb823e 100644 --- a/src/service/oidc/mod.rs +++ b/src/service/oidc/mod.rs @@ -58,9 +58,8 @@ pub struct PendingSession { } pub enum SessionCompletionStatus { + NeedsUserId, Complete(OwnedUserId), - NeedsLocalpart, - InvalidLocalpart(String), } #[async_trait] @@ -199,7 +198,7 @@ impl Service { pub async fn complete_session( &self, claims: &CoreIdTokenClaims, - supplied_username: Option, + supplied_user_id: Option, ) -> Result { let Some(OidcClient { config, .. }) = self.client.as_ref() else { return Err("Delegated authentication is not enabled on this server."); @@ -217,19 +216,10 @@ impl Service { UserId::parse(format!("@{localpart}:{}", self.services.globals.server_name())) .expect("saved localpart should be valid") } else if config.prompt_for_localpart { - if let Some(supplied_username) = supplied_username { - match self - .services - .users - .determine_registration_user_id(Some(supplied_username), None, None) - .await - { - | Ok(user_id) => user_id, - | Err(err) => - return Ok(SessionCompletionStatus::InvalidLocalpart(err.message())), - } + if let Some(supplied_user_id) = supplied_user_id { + supplied_user_id } else { - return Ok(SessionCompletionStatus::NeedsLocalpart); + return Ok(SessionCompletionStatus::NeedsUserId); } } else if let Some(preferred_username) = claims.preferred_username() { self.services diff --git a/src/web/pages/oidc/complete.rs b/src/web/pages/oidc/complete.rs index fbe75b8ec..62314e48f 100644 --- a/src/web/pages/oidc/complete.rs +++ b/src/web/pages/oidc/complete.rs @@ -8,7 +8,7 @@ use axum::{ }; use conduwuit_service::{oauth::grant::AuthorizationCodeResponse, oidc::SessionCompletionStatus}; use futures::FutureExt; -use ruma::OwnedServerName; +use ruma::{OwnedServerName, UserId}; use serde::{Deserialize, de::IgnoredAny}; use tower_sessions::Session; @@ -17,6 +17,7 @@ use crate::{ extract::{Expect, PostForm}, pages::{ GET_POST, Result, TemplateContext, + components::UserCard, oidc::{OIDC_SESSION_ID_KEY, OidcSession, OidcSessionState}, }, response, @@ -30,14 +31,27 @@ pub(crate) fn build() -> Router { template! { struct OidcComplete use "oidc_complete.html.j2" { - server_name: OwnedServerName, - username_error: Option + body: OidcCompleteBody } } +#[derive(Debug)] +enum OidcCompleteBody { + UsernamePrompt { + server_name: OwnedServerName, + username_error: Option, + }, + PasswordPrompt { + username: String, + user_card: UserCard, + password_error: bool, + }, +} + #[derive(Deserialize)] struct LoginForm { username: String, + password: Option, } async fn route_complete( @@ -89,11 +103,71 @@ async fn route_complete( services.oidc.complete_session(&claims, None).await }, - | OidcSessionState::Authorized { claims } => + | OidcSessionState::Authorized { claims } => { + let supplied_user_id = if let Some(form) = form { + if let Ok(user_id) = UserId::parse(format!( + "@{}:{}", + &form.username, + services.globals.server_name() + )) && services.users.status(&user_id).await.is_active() + { + let user_card = UserCard::for_local_user(&services, user_id.clone()).await; + + if let Some(password) = form.password { + if services + .users + .check_password(&user_id, &password) + .await + .is_ok() + { + Some(user_id) + } else { + return response!(OidcComplete::new( + context, + OidcCompleteBody::PasswordPrompt { + username: form.username, + user_card, + password_error: true + } + )); + } + } else { + return response!(OidcComplete::new( + context, + OidcCompleteBody::PasswordPrompt { + username: form.username, + user_card, + password_error: false, + } + )); + } + } else { + match services + .users + .determine_registration_user_id(Some(form.username), None, None) + .await + { + | Ok(user_id) => Some(user_id), + | Err(err) => { + return response!(OidcComplete::new( + context, + OidcCompleteBody::UsernamePrompt { + server_name: services.globals.server_name().to_owned(), + username_error: Some(err.message()), + } + )); + }, + } + } + } else { + None + }; + services .oidc - .complete_session(&claims, form.map(|form| form.username)) - .await, + .complete_session(&claims, supplied_user_id) + .await + }, } .map_err(|err| WebError::BadRequest(err.to_owned()))?; @@ -112,15 +186,11 @@ async fn route_complete( response!(Redirect::to(&session.next.target_path())) }, - | SessionCompletionStatus::NeedsLocalpart => { - response!(OidcComplete::new(context, services.globals.server_name().to_owned(), None)) - }, - | SessionCompletionStatus::InvalidLocalpart(error) => { - response!(OidcComplete::new( - context, - services.globals.server_name().to_owned(), - Some(error) - )) + | SessionCompletionStatus::NeedsUserId => { + response!(OidcComplete::new(context, OidcCompleteBody::UsernamePrompt { + server_name: services.globals.server_name().to_owned(), + username_error: None + })) }, } } diff --git a/src/web/pages/templates/oidc_complete.html.j2 b/src/web/pages/templates/oidc_complete.html.j2 index e5f322bd1..be58288ad 100644 --- a/src/web/pages/templates/oidc_complete.html.j2 +++ b/src/web/pages/templates/oidc_complete.html.j2 @@ -17,23 +17,43 @@ Link your account Matrix logo -
-

To finish linking your account to Matrix, choose a username.

-

- - - @ - - :{{ server_name }} - - {% if let Some(username_error) = username_error %} - - {{ username_error }} - - {% endif %} - Your username cannot be changed after you create your account. -

- -
+ {% match body %} + {% when OidcCompleteBody::UsernamePrompt { server_name, username_error } %} +
+

+ To finish linking your account to Matrix, choose a username. +
If you have an existing Matrix account, enter its user ID to link it. +

+

+ + + @ + + :{{ server_name }} + + {% if let Some(username_error) = username_error %} + + {{ username_error }} + + {% endif %} + Your username cannot be changed after you link your account. +

+ +
+ {% when OidcCompleteBody::PasswordPrompt { username, user_card, password_error } %} + {{ user_card }} +
+

To link this legacy account, enter the password you used to use when logging in.

+

+ + + {% if password_error %} + Incorrect password + {% endif %} +

+ + +
+ {% endmatch %} {% endblock %}