Can create accounts automatically for a given upstream provider
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				continuous-integration/drone/push Build is passing
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	continuous-integration/drone/push Build is passing
				
			This commit is contained in:
		| @@ -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_id: CLIENT_ID_GIVEN_BY_PROVIDER | ||||||
|   client_secret: CLIENT_SECRET_GIVEN_BY_PROVIDER |   client_secret: CLIENT_SECRET_GIVEN_BY_PROVIDER | ||||||
|   configuration_url: https://gitlab.com/.well-known/openid-configuration |   configuration_url: https://gitlab.com/.well-known/openid-configuration | ||||||
|  |   allow_auto_account_creation: true | ||||||
|  |  | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,10 +1,11 @@ | |||||||
| use std::net::IpAddr; | use std::net::IpAddr; | ||||||
|  |  | ||||||
| use crate::data::provider::{Provider, ProviderID}; | 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::data::user::{FactorID, GeneralSettings, GrantedClients, TwoFactor, User, UserID}; | ||||||
| use crate::utils::err::Res; | 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 | /// User storage interface | ||||||
| pub trait UsersSyncBackend { | pub trait UsersSyncBackend { | ||||||
| @@ -38,6 +39,8 @@ pub enum LoginResult { | |||||||
|     LocalAuthForbidden, |     LocalAuthForbidden, | ||||||
|     AuthFromProviderForbidden, |     AuthFromProviderForbidden, | ||||||
|     Success(Box<User>), |     Success(Box<User>), | ||||||
|  |     AccountAutoCreated(Box<User>), | ||||||
|  |     CannotAutoCreateAccount(String), | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Message)] | #[derive(Message)] | ||||||
| @@ -51,6 +54,7 @@ pub struct LocalLoginRequest { | |||||||
| #[rtype(LoginResult)] | #[rtype(LoginResult)] | ||||||
| pub struct ProviderLoginRequest { | pub struct ProviderLoginRequest { | ||||||
|     pub email: String, |     pub email: String, | ||||||
|  |     pub user_info: OpenIDUserInfo, | ||||||
|     pub provider: Provider, |     pub provider: Provider, | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -187,7 +191,86 @@ impl Handler<ProviderLoginRequest> for UsersActor { | |||||||
|                 log::error!("Failed to find user! {e}"); |                 log::error!("Failed to find user! {e}"); | ||||||
|                 MessageResult(LoginResult::Error) |                 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)) => { |             Ok(Some(user)) => { | ||||||
|                 if !user.can_login_from_provider(&msg.provider) { |                 if !user.can_login_from_provider(&msg.provider) { | ||||||
|                     return MessageResult(LoginResult::AuthFromProviderForbidden); |                     return MessageResult(LoginResult::AuthFromProviderForbidden); | ||||||
|   | |||||||
| @@ -1,11 +1,5 @@ | |||||||
| use std::sync::Arc; | 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::bruteforce_actor::BruteForceActor; | ||||||
| use crate::actors::providers_states_actor::{ProviderLoginState, ProvidersStatesActor}; | use crate::actors::providers_states_actor::{ProviderLoginState, ProvidersStatesActor}; | ||||||
| use crate::actors::users_actor::{LoginResult, UsersActor}; | 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::{ProviderID, ProvidersManager}; | ||||||
| use crate::data::provider_configuration::ProviderConfigurationHelper; | use crate::data::provider_configuration::ProviderConfigurationHelper; | ||||||
| use crate::data::session_identity::{SessionIdentity, SessionStatus}; | 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)] | #[derive(askama::Template)] | ||||||
| #[template(path = "login/prov_login_error.html")] | #[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 |     // Check if email was provided by the userinfo endpoint | ||||||
|     let email = match user_info.email { |     let email = match &user_info.email { | ||||||
|         Some(e) => e, |         Some(e) => e, | ||||||
|         None => { |         None => { | ||||||
|             logger.log(Action::ProviderMissingEmailInResponse { |             logger.log(Action::ProviderMissingEmailInResponse { | ||||||
| @@ -293,6 +292,7 @@ pub async fn finish_login( | |||||||
|     let result: LoginResult = users |     let result: LoginResult = users | ||||||
|         .send(users_actor::ProviderLoginRequest { |         .send(users_actor::ProviderLoginRequest { | ||||||
|             email: email.clone(), |             email: email.clone(), | ||||||
|  |             user_info: user_info.clone(), | ||||||
|             provider: provider.clone(), |             provider: provider.clone(), | ||||||
|         }) |         }) | ||||||
|         .await |         .await | ||||||
| @@ -300,6 +300,13 @@ pub async fn finish_login( | |||||||
|  |  | ||||||
|     let user = match result { |     let user = match result { | ||||||
|         LoginResult::Success(u) => u, |         LoginResult::Success(u) => u, | ||||||
|  |         LoginResult::AccountAutoCreated(u) => { | ||||||
|  |             logger.log(Action::ProviderAccountAutoCreated { | ||||||
|  |                 provider: &provider, | ||||||
|  |                 user: u.loggable(), | ||||||
|  |             }); | ||||||
|  |             u | ||||||
|  |         } | ||||||
|         LoginResult::AccountNotFound => { |         LoginResult::AccountNotFound => { | ||||||
|             logger.log(Action::ProviderAccountNotFound { |             logger.log(Action::ProviderAccountNotFound { | ||||||
|                 provider: &provider, |                 provider: &provider, | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| use actix::Addr; | use actix::Addr; | ||||||
| use actix_web::{HttpResponse, Responder, web}; | use actix_web::{HttpResponse, Responder, web}; | ||||||
| use uuid::Uuid; |  | ||||||
| use webauthn_rs::prelude::RegisterPublicKeyCredential; | use webauthn_rs::prelude::RegisterPublicKeyCredential; | ||||||
|  |  | ||||||
| use crate::actors::users_actor; | use crate::actors::users_actor; | ||||||
| @@ -53,7 +52,7 @@ pub async fn save_totp_factor( | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     let factor = TwoFactor { |     let factor = TwoFactor { | ||||||
|         id: FactorID(Uuid::new_v4().to_string()), |         id: FactorID::random(), | ||||||
|         name: factor_name, |         name: factor_name, | ||||||
|         kind: TwoFactorType::TOTP(key), |         kind: TwoFactorType::TOTP(key), | ||||||
|     }; |     }; | ||||||
| @@ -102,7 +101,7 @@ pub async fn save_webauthn_factor( | |||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     let factor = TwoFactor { |     let factor = TwoFactor { | ||||||
|         id: FactorID(Uuid::new_v4().to_string()), |         id: FactorID::random(), | ||||||
|         name: factor_name, |         name: factor_name, | ||||||
|         kind: TwoFactorType::WEBAUTHN(Box::new(key)), |         kind: TwoFactorType::WEBAUTHN(Box::new(key)), | ||||||
|     }; |     }; | ||||||
|   | |||||||
| @@ -150,6 +150,10 @@ pub enum Action<'a> { | |||||||
|         provider: &'a Provider, |         provider: &'a Provider, | ||||||
|         email: &'a str, |         email: &'a str, | ||||||
|     }, |     }, | ||||||
|  |     ProviderAccountAutoCreated { | ||||||
|  |         provider: &'a Provider, | ||||||
|  |         user: LoggableUser, | ||||||
|  |     }, | ||||||
|     ProviderAccountDisabled { |     ProviderAccountDisabled { | ||||||
|         provider: &'a Provider, |         provider: &'a Provider, | ||||||
|         email: &'a str, |         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!", |                 "could not login using provider {} because the email {email} could not be associated to any account!", | ||||||
|                 &provider.id.0 |                 &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!( |             Action::ProviderAccountDisabled { provider, email } => format!( | ||||||
|                 "could not login using provider {} because the account associated to the email {email} is disabled!", |                 "could not login using provider {} because the account associated to the email {email} is disabled!", | ||||||
|                 &provider.id.0 |                 &provider.id.0 | ||||||
|   | |||||||
| @@ -26,6 +26,10 @@ pub struct Provider { | |||||||
|     /// |     /// | ||||||
|     /// (.well-known/openid-configuration endpoint) |     /// (.well-known/openid-configuration endpoint) | ||||||
|     pub configuration_url: String, |     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 { | impl Provider { | ||||||
|   | |||||||
| @@ -14,6 +14,12 @@ use crate::utils::time::{fmt_time, time}; | |||||||
| #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, Encode, Decode)] | #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, Encode, Decode)] | ||||||
| pub struct UserID(pub String); | pub struct UserID(pub String); | ||||||
|  |  | ||||||
|  | impl UserID { | ||||||
|  |     pub fn random() -> Self { | ||||||
|  |         Self(uuid::Uuid::new_v4().to_string()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| #[derive(Debug, Clone)] | #[derive(Debug, Clone)] | ||||||
| pub struct GeneralSettings { | pub struct GeneralSettings { | ||||||
|     pub uid: UserID, |     pub uid: UserID, | ||||||
| @@ -46,6 +52,12 @@ impl GrantedClients { | |||||||
| #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] | #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] | ||||||
| pub struct FactorID(pub String); | 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)] | #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] | ||||||
| pub enum TwoFactorType { | pub enum TwoFactorType { | ||||||
|     TOTP(TotpKey), |     TOTP(TotpKey), | ||||||
| @@ -295,7 +307,7 @@ impl Eq for User {} | |||||||
| impl Default for User { | impl Default for User { | ||||||
|     fn default() -> Self { |     fn default() -> Self { | ||||||
|         Self { |         Self { | ||||||
|             uid: UserID(uuid::Uuid::new_v4().to_string()), |             uid: UserID::random(), | ||||||
|             first_name: "".to_string(), |             first_name: "".to_string(), | ||||||
|             last_name: "".to_string(), |             last_name: "".to_string(), | ||||||
|             username: "".to_string(), |             username: "".to_string(), | ||||||
|   | |||||||
| @@ -71,7 +71,7 @@ impl UsersSyncBackend for EntityManager<User> { | |||||||
|  |  | ||||||
|     fn create_user_account(&mut self, settings: GeneralSettings) -> Res<UserID> { |     fn create_user_account(&mut self, settings: GeneralSettings) -> Res<UserID> { | ||||||
|         let mut user = User { |         let mut user = User { | ||||||
|             uid: UserID(uuid::Uuid::new_v4().to_string()), |             uid: UserID::random(), | ||||||
|             ..Default::default() |             ..Default::default() | ||||||
|         }; |         }; | ||||||
|         user.update_general_settings(settings); |         user.update_general_settings(settings); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user