Add conditional UIAA flows for SSO and password. (#314)

Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
Jason Volk
2026-03-04 20:34:55 +00:00
parent 449b80de1d
commit 3127eca67c
2 changed files with 61 additions and 16 deletions

View File

@@ -5,7 +5,13 @@ use ruma::{
client::uiaa::{AuthData, AuthFlow, AuthType, Jwt, UiaaInfo}, client::uiaa::{AuthData, AuthFlow, AuthType, Jwt, UiaaInfo},
}, },
}; };
use tuwunel_core::{Err, Error, Result, err, is_equal_to, utils}; use tuwunel_core::{
Err, Error, Result, err, utils,
utils::{
OptionExt,
future::{OptionFutureExt, TryExtExt},
},
};
use tuwunel_service::{Services, uiaa::SESSION_ID_LENGTH}; use tuwunel_service::{Services, uiaa::SESSION_ID_LENGTH};
use crate::{Ruma, client::jwt}; use crate::{Ruma, client::jwt};
@@ -15,14 +21,46 @@ where
T: IncomingRequest + Send + Sync, T: IncomingRequest + Send + Sync,
{ {
let sender_device = body.sender_device()?; let sender_device = body.sender_device()?;
let sender_user = body.sender_user.as_deref();
let flows = [ let password_flow = [AuthType::Password];
AuthFlow::new([AuthType::Password].into()), let has_password = sender_user
AuthFlow::new([AuthType::Jwt].into()), .map_async(|sender_user| {
]; services
.users
.has_password(sender_user)
.unwrap_or(false)
})
.unwrap_or(false)
.await;
//TODO: UIAA for SSO.
let sso_flow = [AuthType::Sso];
let has_sso = false;
let _has_sso = sender_user
.map_async(|sender_user| {
services
.oauth
.sessions
.exists_for_user(sender_user)
})
.unwrap_or(false)
.await;
//NOTE: Not implemented as a fallback/web for now.
let has_jwt = false;
let jwt_flow = [AuthType::Jwt];
let mut uiaainfo = UiaaInfo { let mut uiaainfo = UiaaInfo {
flows: flows.into(), flows: has_password
.then_some(password_flow)
.into_iter()
.chain(has_sso.then_some(sso_flow))
.chain(has_jwt.then_some(jwt_flow))
.map(Vec::from)
.map(AuthFlow::new)
.collect(),
..Default::default() ..Default::default()
}; };
@@ -70,16 +108,6 @@ where
.as_deref() .as_deref()
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?; .ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
// Skip UIAA for SSO/OIDC users.
if services
.users
.origin(sender_user)
.await
.is_ok_and(is_equal_to!("sso"))
{
return Ok(sender_user.to_owned());
}
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH)); uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services services
.uiaa .uiaa

View File

@@ -16,6 +16,8 @@ use tuwunel_core::{
}; };
use tuwunel_database::{Deserialized, Json, Map}; use tuwunel_database::{Deserialized, Json, Map};
use crate::users::PASSWORD_SENTINEL;
pub struct Service { pub struct Service {
userdevicesessionid_uiaarequest: RwLock<RequestMap>, userdevicesessionid_uiaarequest: RwLock<RequestMap>,
db: Data, db: Data,
@@ -107,9 +109,11 @@ pub async fn try_auth(
// Check if password is correct // Check if password is correct
let user_id = user_id_from_username; let user_id = user_id_from_username;
let mut password_verified = false; let mut password_verified = false;
let mut password_sentinel = false;
// First try local password hash verification // First try local password hash verification
if let Ok(hash) = self.services.users.password_hash(&user_id).await { if let Ok(hash) = self.services.users.password_hash(&user_id).await {
password_sentinel = hash == PASSWORD_SENTINEL;
password_verified = hash::verify_password(password, &hash).is_ok(); password_verified = hash::verify_password(password, &hash).is_ok();
} }
@@ -130,6 +134,19 @@ pub async fn try_auth(
} }
} }
// For SSO users that have never set a password, allow.
if !password_verified
&& password_sentinel
&& self
.services
.oauth
.sessions
.exists_for_user(&user_id)
.await
{
return Ok((true, uiaainfo));
}
if !password_verified { if !password_verified {
uiaainfo.auth_error = Some(StandardErrorBody { uiaainfo.auth_error = Some(StandardErrorBody {
kind: ErrorKind::forbidden(), kind: ErrorKind::forbidden(),