From 9a599fdde26ae9974af0a66b2ce95ae3b3426615 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 29 Oct 2025 11:30:45 +0100 Subject: [PATCH] Can create accounts automatically for a given upstream provider --- README.md | 1 + src/actors/users_actor.rs | 89 ++++++++++++++++++++++++- src/controllers/providers_controller.rs | 21 ++++-- src/controllers/two_factor_api.rs | 5 +- src/data/action_logger.rs | 9 +++ src/data/provider.rs | 4 ++ src/data/user.rs | 14 +++- src/data/users_file_entity.rs | 2 +- 8 files changed, 130 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 46e10bb..e7ab93f 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ You can add as much upstream provider as you want, using the following syntax in client_id: CLIENT_ID_GIVEN_BY_PROVIDER client_secret: CLIENT_SECRET_GIVEN_BY_PROVIDER configuration_url: https://gitlab.com/.well-known/openid-configuration + allow_auto_account_creation: true ``` diff --git a/src/actors/users_actor.rs b/src/actors/users_actor.rs index 3bc11c0..1b57b07 100644 --- a/src/actors/users_actor.rs +++ b/src/actors/users_actor.rs @@ -1,10 +1,11 @@ use std::net::IpAddr; use crate::data::provider::{Provider, ProviderID}; -use actix::{Actor, Context, Handler, Message, MessageResult}; - use crate::data::user::{FactorID, GeneralSettings, GrantedClients, TwoFactor, User, UserID}; use crate::utils::err::Res; +use crate::utils::string_utils::is_acceptable_login; +use actix::{Actor, Context, Handler, Message, MessageResult}; +use light_openid::primitives::OpenIDUserInfo; /// User storage interface pub trait UsersSyncBackend { @@ -38,6 +39,8 @@ pub enum LoginResult { LocalAuthForbidden, AuthFromProviderForbidden, Success(Box), + AccountAutoCreated(Box), + CannotAutoCreateAccount(String), } #[derive(Message)] @@ -51,6 +54,7 @@ pub struct LocalLoginRequest { #[rtype(LoginResult)] pub struct ProviderLoginRequest { pub email: String, + pub user_info: OpenIDUserInfo, pub provider: Provider, } @@ -187,7 +191,86 @@ impl Handler for UsersActor { log::error!("Failed to find user! {e}"); MessageResult(LoginResult::Error) } - Ok(None) => MessageResult(LoginResult::AccountNotFound), + Ok(None) => { + // Check if automatic account creation is enabled for this provider + if !msg.provider.allow_auto_account_creation { + return MessageResult(LoginResult::AccountNotFound); + } + + // Extract username for account creation + let mut username = msg + .user_info + .preferred_username + .unwrap_or(msg.email.to_string()); + + // Determine username from email, if necessary + if !is_acceptable_login(&username) + || matches!( + self.manager.find_by_username_or_email(&username), + Ok(Some(_)) + ) + { + username = msg.email.clone(); + } + + // Check if username is already taken + if matches!( + self.manager.find_by_username_or_email(&username), + Ok(Some(_)) + ) { + return MessageResult(LoginResult::CannotAutoCreateAccount(format!( + "username {username} is already taken!" + ))); + } + + if !is_acceptable_login(&username) { + return MessageResult(LoginResult::CannotAutoCreateAccount( + "could not determine acceptable login for user!".to_string(), + )); + } + + // Automatic account creation + let user_id = match self.manager.create_user_account(GeneralSettings { + uid: UserID::random(), + username, + first_name: msg.user_info.given_name.unwrap_or_default(), + last_name: msg.user_info.family_name.unwrap_or_default(), + email: msg.email.to_string(), + enabled: true, + two_factor_exemption_after_successful_login: false, + is_admin: false, + }) { + Ok(u) => u, + Err(e) => { + log::error!("Failed to create user account! {e}"); + return MessageResult(LoginResult::CannotAutoCreateAccount( + "missing some user information".to_string(), + )); + } + }; + + // Mark the provider as the only authorized source + if let Err(e) = self.manager.set_authorized_authentication_sources( + &user_id, + AuthorizedAuthenticationSources { + local: false, + upstream: vec![msg.provider.id], + }, + ) { + log::error!( + "Failed to set authorized authentication sources for newly created account! {e}" + ); + } + + // Extract user information to return them + let Ok(Some(user)) = self.manager.find_by_user_id(&user_id) else { + return MessageResult(LoginResult::CannotAutoCreateAccount( + "failed to get created user information".to_string(), + )); + }; + + MessageResult(LoginResult::AccountAutoCreated(Box::new(user))) + } Ok(Some(user)) => { if !user.can_login_from_provider(&msg.provider) { return MessageResult(LoginResult::AuthFromProviderForbidden); diff --git a/src/controllers/providers_controller.rs b/src/controllers/providers_controller.rs index fde5e05..9edf921 100644 --- a/src/controllers/providers_controller.rs +++ b/src/controllers/providers_controller.rs @@ -1,11 +1,5 @@ use std::sync::Arc; -use actix::Addr; -use actix_identity::Identity; -use actix_remote_ip::RemoteIP; -use actix_web::{HttpRequest, HttpResponse, Responder, web}; -use askama::Template; - use crate::actors::bruteforce_actor::BruteForceActor; use crate::actors::providers_states_actor::{ProviderLoginState, ProvidersStatesActor}; use crate::actors::users_actor::{LoginResult, UsersActor}; @@ -18,6 +12,11 @@ use crate::data::login_redirect::LoginRedirect; use crate::data::provider::{ProviderID, ProvidersManager}; use crate::data::provider_configuration::ProviderConfigurationHelper; use crate::data::session_identity::{SessionIdentity, SessionStatus}; +use actix::Addr; +use actix_identity::Identity; +use actix_remote_ip::RemoteIP; +use actix_web::{HttpRequest, HttpResponse, Responder, web}; +use askama::Template; #[derive(askama::Template)] #[template(path = "login/prov_login_error.html")] @@ -273,7 +272,7 @@ pub async fn finish_login( } // Check if email was provided by the userinfo endpoint - let email = match user_info.email { + let email = match &user_info.email { Some(e) => e, None => { logger.log(Action::ProviderMissingEmailInResponse { @@ -293,6 +292,7 @@ pub async fn finish_login( let result: LoginResult = users .send(users_actor::ProviderLoginRequest { email: email.clone(), + user_info: user_info.clone(), provider: provider.clone(), }) .await @@ -300,6 +300,13 @@ pub async fn finish_login( let user = match result { LoginResult::Success(u) => u, + LoginResult::AccountAutoCreated(u) => { + logger.log(Action::ProviderAccountAutoCreated { + provider: &provider, + user: u.loggable(), + }); + u + } LoginResult::AccountNotFound => { logger.log(Action::ProviderAccountNotFound { provider: &provider, diff --git a/src/controllers/two_factor_api.rs b/src/controllers/two_factor_api.rs index c3624c7..84d5c8b 100644 --- a/src/controllers/two_factor_api.rs +++ b/src/controllers/two_factor_api.rs @@ -1,6 +1,5 @@ use actix::Addr; use actix_web::{HttpResponse, Responder, web}; -use uuid::Uuid; use webauthn_rs::prelude::RegisterPublicKeyCredential; use crate::actors::users_actor; @@ -53,7 +52,7 @@ pub async fn save_totp_factor( } let factor = TwoFactor { - id: FactorID(Uuid::new_v4().to_string()), + id: FactorID::random(), name: factor_name, kind: TwoFactorType::TOTP(key), }; @@ -102,7 +101,7 @@ pub async fn save_webauthn_factor( }; let factor = TwoFactor { - id: FactorID(Uuid::new_v4().to_string()), + id: FactorID::random(), name: factor_name, kind: TwoFactorType::WEBAUTHN(Box::new(key)), }; diff --git a/src/data/action_logger.rs b/src/data/action_logger.rs index a720c5d..448c2b3 100644 --- a/src/data/action_logger.rs +++ b/src/data/action_logger.rs @@ -150,6 +150,10 @@ pub enum Action<'a> { provider: &'a Provider, email: &'a str, }, + ProviderAccountAutoCreated { + provider: &'a Provider, + user: LoggableUser, + }, ProviderAccountDisabled { provider: &'a Provider, email: &'a str, @@ -282,6 +286,11 @@ impl Action<'_> { "could not login using provider {} because the email {email} could not be associated to any account!", &provider.id.0 ), + Action::ProviderAccountAutoCreated { provider, user } => format!( + "triggered automatic account creation for {} from provider {} because it was not found in local accounts list!", + user.quick_identity(), + &provider.id.0 + ), Action::ProviderAccountDisabled { provider, email } => format!( "could not login using provider {} because the account associated to the email {email} is disabled!", &provider.id.0 diff --git a/src/data/provider.rs b/src/data/provider.rs index be1c5c4..d699c26 100644 --- a/src/data/provider.rs +++ b/src/data/provider.rs @@ -26,6 +26,10 @@ pub struct Provider { /// /// (.well-known/openid-configuration endpoint) pub configuration_url: String, + + /// Set to true if accounts on BasicOIDC should be automatically created from this provider + #[serde(default)] + pub allow_auto_account_creation: bool, } impl Provider { diff --git a/src/data/user.rs b/src/data/user.rs index 680d22e..7911245 100644 --- a/src/data/user.rs +++ b/src/data/user.rs @@ -14,6 +14,12 @@ use crate::utils::time::{fmt_time, time}; #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, Encode, Decode)] pub struct UserID(pub String); +impl UserID { + pub fn random() -> Self { + Self(uuid::Uuid::new_v4().to_string()) + } +} + #[derive(Debug, Clone)] pub struct GeneralSettings { pub uid: UserID, @@ -46,6 +52,12 @@ impl GrantedClients { #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub struct FactorID(pub String); +impl FactorID { + pub fn random() -> Self { + Self(uuid::Uuid::new_v4().to_string()) + } +} + #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum TwoFactorType { TOTP(TotpKey), @@ -295,7 +307,7 @@ impl Eq for User {} impl Default for User { fn default() -> Self { Self { - uid: UserID(uuid::Uuid::new_v4().to_string()), + uid: UserID::random(), first_name: "".to_string(), last_name: "".to_string(), username: "".to_string(), diff --git a/src/data/users_file_entity.rs b/src/data/users_file_entity.rs index f0a4fd6..137ebb5 100644 --- a/src/data/users_file_entity.rs +++ b/src/data/users_file_entity.rs @@ -71,7 +71,7 @@ impl UsersSyncBackend for EntityManager { fn create_user_account(&mut self, settings: GeneralSettings) -> Res { let mut user = User { - uid: UserID(uuid::Uuid::new_v4().to_string()), + uid: UserID::random(), ..Default::default() }; user.update_general_settings(settings);