Add authentication from upstream providers #107

Merged
pierre merged 25 commits from feat-upstream-providers into master 2023-04-27 10:10:29 +00:00
4 changed files with 198 additions and 6 deletions
Showing only changes of commit 0fdc8b2e4b - Show all commits

View File

@ -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;

View File

@ -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

View File

@ -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:?}"),
} }
} }
} }

View File

@ -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) {