From 0fdc8b2e4b328b6504c55e5132269b811150315d Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Thu, 27 Apr 2023 10:04:35 +0200 Subject: [PATCH] Map provider given email with local account --- src/actors/users_actor.rs | 36 ++++++- src/controllers/providers_controller.rs | 119 +++++++++++++++++++++++- src/data/action_logger.rs | 40 +++++++- src/data/users_file_entity.rs | 9 ++ 4 files changed, 198 insertions(+), 6 deletions(-) diff --git a/src/actors/users_actor.rs b/src/actors/users_actor.rs index 330991e..1257726 100644 --- a/src/actors/users_actor.rs +++ b/src/actors/users_actor.rs @@ -1,6 +1,6 @@ use std::net::IpAddr; -use crate::data::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}; @@ -9,6 +9,7 @@ use crate::utils::err::Res; /// User storage interface pub trait UsersSyncBackend { fn find_by_username_or_email(&self, u: &str) -> Res>; + fn find_by_email(&self, u: &str) -> Res>; fn find_by_user_id(&self, id: &UserID) -> Res>; fn get_entire_users_list(&self) -> Res>; fn create_user_account(&mut self, settings: GeneralSettings) -> Res; @@ -35,6 +36,7 @@ pub enum LoginResult { InvalidPassword, AccountDisabled, LocalAuthForbidden, + AuthFromProviderForbidden, Success(Box), } @@ -45,6 +47,13 @@ pub struct LocalLoginRequest { pub password: String, } +#[derive(Message)] +#[rtype(LoginResult)] +pub struct ProviderLoginRequest { + pub email: String, + pub provider: Provider, +} + #[derive(Message)] #[rtype(GetUserResult)] pub struct GetUserRequest(pub UserID); @@ -169,6 +178,31 @@ impl Handler for UsersActor { } } +impl Handler for UsersActor { + type Result = MessageResult; + + fn handle(&mut self, msg: ProviderLoginRequest, _ctx: &mut Self::Context) -> Self::Result { + match self.manager.find_by_email(&msg.email) { + Err(e) => { + log::error!("Failed to find user! {}", e); + MessageResult(LoginResult::Error) + } + Ok(None) => MessageResult(LoginResult::AccountNotFound), + Ok(Some(user)) => { + if !user.can_login_from_provider(&msg.provider) { + return MessageResult(LoginResult::AuthFromProviderForbidden); + } + + if !user.enabled { + return MessageResult(LoginResult::AccountDisabled); + } + + MessageResult(LoginResult::Success(Box::new(user))) + } + } + } +} + impl Handler for UsersActor { type Result = ::Result; diff --git a/src/controllers/providers_controller.rs b/src/controllers/providers_controller.rs index 18e21f6..8daebce 100644 --- a/src/controllers/providers_controller.rs +++ b/src/controllers/providers_controller.rs @@ -6,7 +6,8 @@ use askama::Template; use crate::actors::bruteforce_actor::BruteForceActor; use crate::actors::providers_states_actor::{ProviderLoginState, ProvidersStatesActor}; -use crate::actors::{bruteforce_actor, providers_states_actor}; +use crate::actors::users_actor::{LoginResult, UsersActor}; +use crate::actors::{bruteforce_actor, providers_states_actor, users_actor}; use crate::constants::{APP_NAME, MAX_FAILED_LOGIN_ATTEMPTS}; use crate::controllers::base_controller::{build_fatal_error_page, redirect_user}; use crate::controllers::login_controller::BaseLoginPage; @@ -127,6 +128,7 @@ pub struct FinishLoginQuery { pub async fn finish_login( remote_ip: RemoteIP, providers: web::Data>, + users: web::Data>, states: web::Data>, bruteforce: web::Data>, query: web::Query, @@ -226,10 +228,119 @@ pub async fn finish_login( }; // Use access token to get user information - let info = provider_config.get_userinfo(&token).await; - println!("info: {:?}", info); + let user_info = match provider_config.get_userinfo(&token).await { + Ok(info) => info, + Err(e) => { + log::error!("Failed to retrieve user information! {:?}", e); + + logger.log(Action::ProviderFailedGetUserInfo { + provider: &provider, + }); + + return ProviderLoginError::get( + "Failed to retrieve user information from identity provider!", + &state.redirect, + ); + } + }; + + // Check if user email is validated + if user_info.email_verified == Some(false) { + logger.log(Action::ProviderEmailNotValidated { + provider: &provider, + }); + return ProviderLoginError::get( + &format!( + "{} indicated that your email address has not been validated!", + provider.name + ), + &state.redirect, + ); + } + + // Check if email was provided by the userinfo endpoint + let email = match user_info.email { + Some(e) => e, + None => { + logger.log(Action::ProviderMissingEmailInResponse { + provider: &provider, + }); + return ProviderLoginError::get( + &format!( + "{} did not provide your email address in its reply, so we could not identify you!", + provider.name + ), + &state.redirect, + ); + } + }; + + // Get user from local database + let result: LoginResult = users + .send(users_actor::ProviderLoginRequest { + email: email.clone(), + provider: provider.clone(), + }) + .await + .unwrap(); + + let user = match result { + LoginResult::Success(u) => u, + LoginResult::AccountNotFound => { + logger.log(Action::ProviderAccountNotFound { + provider: &provider, + email: email.as_str(), + }); + + return ProviderLoginError::get( + &format!("The email address {email} was not found in the database!"), + &state.redirect, + ); + } + LoginResult::AccountDisabled => { + logger.log(Action::ProviderAccountDisabled { + provider: &provider, + email: email.as_str(), + }); + + return ProviderLoginError::get( + &format!("The account associated with the email address {email} is disabled!"), + &state.redirect, + ); + } + + LoginResult::AuthFromProviderForbidden => { + logger.log(Action::ProviderAccountNotAllowedToLoginWithProvider { + provider: &provider, + email: email.as_str(), + }); + + return ProviderLoginError::get( + &format!( + "The account associated with the email address {email} is not allowed to sign in using this provider!" + ), + &state.redirect, + ); + } + + c => { + log::error!( + "Login from provider {} failed with error {:?}", + provider.id.0, + c + ); + + logger.log(Action::ProviderLoginFailed { + provider: &provider, + email: email.as_str(), + }); + + return ProviderLoginError::get("Failed to complete login!", &state.redirect); + } + }; + + log::info!("user={:#?}", user); - // TODO : check if user is authorized to access application // TODO : check if 2FA is enabled // TODO : redirect user to login route // TODO : add proper logging diff --git a/src/data/action_logger.rs b/src/data/action_logger.rs index be0c538..4439952 100644 --- a/src/data/action_logger.rs +++ b/src/data/action_logger.rs @@ -11,7 +11,7 @@ use crate::actors::providers_states_actor::ProviderLoginState; use crate::actors::users_actor; use crate::actors::users_actor::{AuthorizedAuthenticationSources, UsersActor}; use crate::data::client::Client; -use crate::data::provider::ProviderID; +use crate::data::provider::{Provider, ProviderID}; use crate::data::remote_ip::RemoteIP; use crate::data::session_identity::SessionIdentity; use crate::data::user::{FactorID, GrantedClients, TwoFactor, User, UserID}; @@ -44,6 +44,32 @@ pub enum Action<'a> { state: &'a ProviderLoginState, code: &'a str, }, + ProviderFailedGetUserInfo { + provider: &'a Provider, + }, + ProviderEmailNotValidated { + provider: &'a Provider, + }, + ProviderMissingEmailInResponse { + provider: &'a Provider, + }, + ProviderAccountNotFound { + provider: &'a Provider, + email: &'a str, + }, + ProviderAccountDisabled { + provider: &'a Provider, + email: &'a str, + }, + + ProviderAccountNotAllowedToLoginWithProvider { + provider: &'a Provider, + email: &'a str, + }, + ProviderLoginFailed { + provider: &'a Provider, + email: &'a str, + }, Signout, UserNeed2FAOnLogin(&'a User), UserSuccessfullyAuthenticated(&'a User), @@ -116,6 +142,17 @@ impl<'a> Action<'a> { format!("provided invalid callback state after provider authentication: '{state}'"), Action::ProviderRateLimited => "could not complete OpenID login because it has reached failed attempts rate limit!".to_string(), Action::ProviderFailedGetToken {state, code} => format!("could not complete login from provider because the id_token could not be retrieved! (state={:?} code = {code})",state), + Action::ProviderFailedGetUserInfo {provider} => format!("could not get user information from userinfo endpoint of provider {}!", provider.id.0), + Action::ProviderEmailNotValidated {provider}=>format!("could not login using provider {} because its email was marked as not validated!", provider.id.0), + Action::ProviderMissingEmailInResponse {provider}=>format!("could not login using provider {} because the email was not provided by userinfo endpoint!", provider.id.0), + Action::ProviderAccountNotFound { provider, email } => + format!("could not login using provider {} because the email {email} could not be associated to any account!", &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), + Action::ProviderAccountNotAllowedToLoginWithProvider { provider, email } => + format!("could not login using provider {} because the account associated to the email {email} is not allowed to authenticate using this provider!", &provider.id.0), + Action::ProviderLoginFailed { provider, email } => + format!("could not login using provider {} with the email {email} for an unknown reason!", &provider.id.0), Action::Signout => "signed out".to_string(), Action::UserNeed2FAOnLogin(user) => { format!( @@ -162,6 +199,7 @@ impl<'a> Action<'a> { factor.quick_description(), ), Action::Removed2FAFactor { factor_id } => format!("Removed his factor {factor_id:?}"), + } } } diff --git a/src/data/users_file_entity.rs b/src/data/users_file_entity.rs index 4cf9e34..176664a 100644 --- a/src/data/users_file_entity.rs +++ b/src/data/users_file_entity.rs @@ -41,6 +41,15 @@ fn verify_password>(pwd: P, hash: &str) -> bool { } impl UsersSyncBackend for EntityManager { + fn find_by_email(&self, u: &str) -> Res> { + for entry in self.iter() { + if entry.email.eq(u) { + return Ok(Some(entry.clone())); + } + } + Ok(None) + } + fn find_by_username_or_email(&self, u: &str) -> Res> { for entry in self.iter() { if entry.username.eq(u) || entry.email.eq(u) {