Add authentication from upstream providers #107
@ -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<Option<User>>;
|
||||
fn find_by_email(&self, u: &str) -> Res<Option<User>>;
|
||||
fn find_by_user_id(&self, id: &UserID) -> Res<Option<User>>;
|
||||
fn get_entire_users_list(&self) -> Res<Vec<User>>;
|
||||
fn create_user_account(&mut self, settings: GeneralSettings) -> Res<UserID>;
|
||||
@ -35,6 +36,7 @@ pub enum LoginResult {
|
||||
InvalidPassword,
|
||||
AccountDisabled,
|
||||
LocalAuthForbidden,
|
||||
AuthFromProviderForbidden,
|
||||
Success(Box<User>),
|
||||
}
|
||||
|
||||
@ -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<LocalLoginRequest> for UsersActor {
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<ProviderLoginRequest> for UsersActor {
|
||||
type Result = MessageResult<ProviderLoginRequest>;
|
||||
|
||||
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<CreateAccount> for UsersActor {
|
||||
type Result = <CreateAccount as actix::Message>::Result;
|
||||
|
||||
|
@ -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<Arc<ProvidersManager>>,
|
||||
users: web::Data<Addr<UsersActor>>,
|
||||
states: web::Data<Addr<ProvidersStatesActor>>,
|
||||
bruteforce: web::Data<Addr<BruteForceActor>>,
|
||||
query: web::Query<FinishLoginQuery>,
|
||||
@ -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
|
||||
|
@ -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:?}"),
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +41,15 @@ fn verify_password<P: AsRef<[u8]>>(pwd: P, hash: &str) -> bool {
|
||||
}
|
||||
|
||||
impl UsersSyncBackend for EntityManager<User> {
|
||||
fn find_by_email(&self, u: &str) -> Res<Option<User>> {
|
||||
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<Option<User>> {
|
||||
for entry in self.iter() {
|
||||
if entry.username.eq(u) || entry.email.eq(u) {
|
||||
|
Loading…
Reference in New Issue
Block a user