Add authentication from upstream providers #107
@ -1,6 +1,6 @@
|
|||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
|
||||||
use crate::data::provider::ProviderID;
|
use crate::data::provider::{Provider, ProviderID};
|
||||||
use actix::{Actor, Context, Handler, Message, MessageResult};
|
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};
|
||||||
@ -9,6 +9,7 @@ use crate::utils::err::Res;
|
|||||||
/// User storage interface
|
/// User storage interface
|
||||||
pub trait UsersSyncBackend {
|
pub trait UsersSyncBackend {
|
||||||
fn find_by_username_or_email(&self, u: &str) -> Res<Option<User>>;
|
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 find_by_user_id(&self, id: &UserID) -> Res<Option<User>>;
|
||||||
fn get_entire_users_list(&self) -> Res<Vec<User>>;
|
fn get_entire_users_list(&self) -> Res<Vec<User>>;
|
||||||
fn create_user_account(&mut self, settings: GeneralSettings) -> Res<UserID>;
|
fn create_user_account(&mut self, settings: GeneralSettings) -> Res<UserID>;
|
||||||
@ -35,6 +36,7 @@ pub enum LoginResult {
|
|||||||
InvalidPassword,
|
InvalidPassword,
|
||||||
AccountDisabled,
|
AccountDisabled,
|
||||||
LocalAuthForbidden,
|
LocalAuthForbidden,
|
||||||
|
AuthFromProviderForbidden,
|
||||||
Success(Box<User>),
|
Success(Box<User>),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,6 +47,13 @@ pub struct LocalLoginRequest {
|
|||||||
pub password: String,
|
pub password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Message)]
|
||||||
|
#[rtype(LoginResult)]
|
||||||
|
pub struct ProviderLoginRequest {
|
||||||
|
pub email: String,
|
||||||
|
pub provider: Provider,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
#[rtype(GetUserResult)]
|
#[rtype(GetUserResult)]
|
||||||
pub struct GetUserRequest(pub UserID);
|
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 {
|
impl Handler<CreateAccount> for UsersActor {
|
||||||
type Result = <CreateAccount as actix::Message>::Result;
|
type Result = <CreateAccount as actix::Message>::Result;
|
||||||
|
|
||||||
|
@ -6,7 +6,8 @@ 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::{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::constants::{APP_NAME, MAX_FAILED_LOGIN_ATTEMPTS};
|
||||||
use crate::controllers::base_controller::{build_fatal_error_page, redirect_user};
|
use crate::controllers::base_controller::{build_fatal_error_page, redirect_user};
|
||||||
use crate::controllers::login_controller::BaseLoginPage;
|
use crate::controllers::login_controller::BaseLoginPage;
|
||||||
@ -127,6 +128,7 @@ pub struct FinishLoginQuery {
|
|||||||
pub async fn finish_login(
|
pub async fn finish_login(
|
||||||
remote_ip: RemoteIP,
|
remote_ip: RemoteIP,
|
||||||
providers: web::Data<Arc<ProvidersManager>>,
|
providers: web::Data<Arc<ProvidersManager>>,
|
||||||
|
users: web::Data<Addr<UsersActor>>,
|
||||||
states: web::Data<Addr<ProvidersStatesActor>>,
|
states: web::Data<Addr<ProvidersStatesActor>>,
|
||||||
bruteforce: web::Data<Addr<BruteForceActor>>,
|
bruteforce: web::Data<Addr<BruteForceActor>>,
|
||||||
query: web::Query<FinishLoginQuery>,
|
query: web::Query<FinishLoginQuery>,
|
||||||
@ -226,10 +228,119 @@ pub async fn finish_login(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Use access token to get user information
|
// Use access token to get user information
|
||||||
let info = provider_config.get_userinfo(&token).await;
|
let user_info = match provider_config.get_userinfo(&token).await {
|
||||||
println!("info: {:?}", info);
|
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 : check if 2FA is enabled
|
||||||
// TODO : redirect user to login route
|
// TODO : redirect user to login route
|
||||||
// TODO : add proper logging
|
// 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;
|
||||||
use crate::actors::users_actor::{AuthorizedAuthenticationSources, UsersActor};
|
use crate::actors::users_actor::{AuthorizedAuthenticationSources, UsersActor};
|
||||||
use crate::data::client::Client;
|
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::remote_ip::RemoteIP;
|
||||||
use crate::data::session_identity::SessionIdentity;
|
use crate::data::session_identity::SessionIdentity;
|
||||||
use crate::data::user::{FactorID, GrantedClients, TwoFactor, User, UserID};
|
use crate::data::user::{FactorID, GrantedClients, TwoFactor, User, UserID};
|
||||||
@ -44,6 +44,32 @@ pub enum Action<'a> {
|
|||||||
state: &'a ProviderLoginState,
|
state: &'a ProviderLoginState,
|
||||||
code: &'a str,
|
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,
|
Signout,
|
||||||
UserNeed2FAOnLogin(&'a User),
|
UserNeed2FAOnLogin(&'a User),
|
||||||
UserSuccessfullyAuthenticated(&'a User),
|
UserSuccessfullyAuthenticated(&'a User),
|
||||||
@ -116,6 +142,17 @@ impl<'a> Action<'a> {
|
|||||||
format!("provided invalid callback state after provider authentication: '{state}'"),
|
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::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::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::Signout => "signed out".to_string(),
|
||||||
Action::UserNeed2FAOnLogin(user) => {
|
Action::UserNeed2FAOnLogin(user) => {
|
||||||
format!(
|
format!(
|
||||||
@ -162,6 +199,7 @@ impl<'a> Action<'a> {
|
|||||||
factor.quick_description(),
|
factor.quick_description(),
|
||||||
),
|
),
|
||||||
Action::Removed2FAFactor { factor_id } => format!("Removed his factor {factor_id:?}"),
|
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> {
|
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>> {
|
fn find_by_username_or_email(&self, u: &str) -> Res<Option<User>> {
|
||||||
for entry in self.iter() {
|
for entry in self.iter() {
|
||||||
if entry.username.eq(u) || entry.email.eq(u) {
|
if entry.username.eq(u) || entry.email.eq(u) {
|
||||||
|
Loading…
Reference in New Issue
Block a user