Add authentication from upstream providers (#107)
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Let BasicOIDC delegate authentication to upstream providers (Google, GitHub, GitLab, Keycloak...) Reviewed-on: #107
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
pub mod bruteforce_actor;
|
||||
pub mod openid_sessions_actor;
|
||||
pub mod providers_states_actor;
|
||||
pub mod users_actor;
|
||||
|
||||
130
src/actors/providers_states_actor.rs
Normal file
130
src/actors/providers_states_actor.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
//! # Providers state actor
|
||||
//!
|
||||
//! This actor stores the content of the states
|
||||
//! during authentication with upstream providers
|
||||
|
||||
use crate::constants::{
|
||||
MAX_OIDC_PROVIDERS_STATES, OIDC_PROVIDERS_STATE_DURATION, OIDC_PROVIDERS_STATE_LEN,
|
||||
OIDC_STATES_CLEANUP_INTERVAL,
|
||||
};
|
||||
use actix::{Actor, AsyncContext, Context, Handler, Message};
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::net::IpAddr;
|
||||
|
||||
use crate::data::login_redirect::LoginRedirect;
|
||||
use crate::data::provider::ProviderID;
|
||||
use crate::utils::string_utils::rand_str;
|
||||
use crate::utils::time::time;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProviderLoginState {
|
||||
pub provider_id: ProviderID,
|
||||
pub state_id: String,
|
||||
pub redirect: LoginRedirect,
|
||||
pub expire: u64,
|
||||
}
|
||||
|
||||
impl ProviderLoginState {
|
||||
pub fn new(prov_id: &ProviderID, redirect: LoginRedirect) -> Self {
|
||||
Self {
|
||||
provider_id: prov_id.clone(),
|
||||
state_id: rand_str(OIDC_PROVIDERS_STATE_LEN),
|
||||
redirect,
|
||||
expire: time() + OIDC_PROVIDERS_STATE_DURATION,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "()")]
|
||||
pub struct RecordState {
|
||||
pub ip: IpAddr,
|
||||
pub state: ProviderLoginState,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "Option<ProviderLoginState>")]
|
||||
pub struct ConsumeState {
|
||||
pub ip: IpAddr,
|
||||
pub state_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ProvidersStatesActor {
|
||||
states: HashMap<IpAddr, Vec<ProviderLoginState>>,
|
||||
}
|
||||
|
||||
impl ProvidersStatesActor {
|
||||
/// Clean outdated states
|
||||
fn clean_old_states(&mut self) {
|
||||
#[allow(clippy::map_clone)]
|
||||
let keys = self.states.keys().map(|i| *i).collect::<Vec<_>>();
|
||||
|
||||
for ip in keys {
|
||||
// Remove old states
|
||||
let states = self.states.get_mut(&ip).unwrap();
|
||||
states.retain(|i| i.expire < time());
|
||||
|
||||
// Remove empty entry keys
|
||||
if states.is_empty() {
|
||||
self.states.remove(&ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a new provider login state
|
||||
pub fn insert_state(&mut self, ip: IpAddr, state: ProviderLoginState) {
|
||||
if let Entry::Vacant(e) = self.states.entry(ip) {
|
||||
e.insert(vec![state]);
|
||||
} else {
|
||||
let states = self.states.get_mut(&ip).unwrap();
|
||||
|
||||
// We limit the number of states per IP address
|
||||
if states.len() > MAX_OIDC_PROVIDERS_STATES {
|
||||
states.remove(0);
|
||||
}
|
||||
|
||||
states.push(state);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get & consume a login state
|
||||
pub fn consume_state(&mut self, ip: IpAddr, state_id: &str) -> Option<ProviderLoginState> {
|
||||
let idx = self
|
||||
.states
|
||||
.get(&ip)?
|
||||
.iter()
|
||||
.position(|val| val.state_id.as_str() == state_id)?;
|
||||
|
||||
Some(self.states.get_mut(&ip)?.remove(idx))
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for ProvidersStatesActor {
|
||||
type Context = Context<Self>;
|
||||
|
||||
fn started(&mut self, ctx: &mut Self::Context) {
|
||||
// Clean up at a regular interval failed attempts
|
||||
ctx.run_interval(OIDC_STATES_CLEANUP_INTERVAL, |act, _ctx| {
|
||||
log::trace!("Cleaning up old states");
|
||||
act.clean_old_states();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<RecordState> for ProvidersStatesActor {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, req: RecordState, _ctx: &mut Self::Context) -> Self::Result {
|
||||
self.insert_state(req.ip, req.state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<ConsumeState> for ProvidersStatesActor {
|
||||
type Result = Option<ProviderLoginState>;
|
||||
|
||||
fn handle(&mut self, req: ConsumeState, _ctx: &mut Self::Context) -> Self::Result {
|
||||
self.consume_state(req.ip, &req.state_id)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::net::IpAddr;
|
||||
|
||||
use crate::data::provider::{Provider, ProviderID};
|
||||
use actix::{Actor, Context, Handler, Message, MessageResult};
|
||||
|
||||
use crate::data::user::{FactorID, GeneralSettings, GrantedClients, TwoFactor, User, UserID};
|
||||
@@ -8,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>;
|
||||
@@ -19,6 +21,11 @@ pub trait UsersSyncBackend {
|
||||
fn save_new_successful_2fa_authentication(&mut self, id: &UserID, ip: IpAddr) -> Res;
|
||||
fn clear_2fa_login_history(&mut self, id: &UserID) -> Res;
|
||||
fn delete_account(&mut self, id: &UserID) -> Res;
|
||||
fn set_authorized_authentication_sources(
|
||||
&mut self,
|
||||
id: &UserID,
|
||||
sources: AuthorizedAuthenticationSources,
|
||||
) -> Res;
|
||||
fn set_granted_2fa_clients(&mut self, id: &UserID, clients: GrantedClients) -> Res;
|
||||
}
|
||||
|
||||
@@ -28,16 +35,25 @@ pub enum LoginResult {
|
||||
AccountNotFound,
|
||||
InvalidPassword,
|
||||
AccountDisabled,
|
||||
LocalAuthForbidden,
|
||||
AuthFromProviderForbidden,
|
||||
Success(Box<User>),
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(LoginResult)]
|
||||
pub struct LoginRequest {
|
||||
pub struct LocalLoginRequest {
|
||||
pub login: String,
|
||||
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);
|
||||
@@ -88,6 +104,16 @@ pub struct AddSuccessful2FALogin(pub UserID, pub IpAddr);
|
||||
#[rtype(result = "bool")]
|
||||
pub struct Clear2FALoginHistory(pub UserID);
|
||||
|
||||
#[derive(Eq, PartialEq, Debug, Clone)]
|
||||
pub struct AuthorizedAuthenticationSources {
|
||||
pub local: bool,
|
||||
pub upstream: Vec<ProviderID>,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "bool")]
|
||||
pub struct SetAuthorizedAuthenticationSources(pub UserID, pub AuthorizedAuthenticationSources);
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "bool")]
|
||||
pub struct SetGrantedClients(pub UserID, pub GrantedClients);
|
||||
@@ -119,10 +145,10 @@ impl Actor for UsersActor {
|
||||
type Context = Context<Self>;
|
||||
}
|
||||
|
||||
impl Handler<LoginRequest> for UsersActor {
|
||||
type Result = MessageResult<LoginRequest>;
|
||||
impl Handler<LocalLoginRequest> for UsersActor {
|
||||
type Result = MessageResult<LocalLoginRequest>;
|
||||
|
||||
fn handle(&mut self, msg: LoginRequest, _ctx: &mut Self::Context) -> Self::Result {
|
||||
fn handle(&mut self, msg: LocalLoginRequest, _ctx: &mut Self::Context) -> Self::Result {
|
||||
match self.manager.find_by_username_or_email(&msg.login) {
|
||||
Err(e) => {
|
||||
log::error!("Failed to find user! {}", e);
|
||||
@@ -142,6 +168,35 @@ impl Handler<LoginRequest> for UsersActor {
|
||||
return MessageResult(LoginResult::AccountDisabled);
|
||||
}
|
||||
|
||||
if !user.allow_local_login {
|
||||
return MessageResult(LoginResult::LocalAuthForbidden);
|
||||
}
|
||||
|
||||
MessageResult(LoginResult::Success(Box::new(user)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)))
|
||||
}
|
||||
}
|
||||
@@ -241,6 +296,29 @@ impl Handler<Clear2FALoginHistory> for UsersActor {
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<SetAuthorizedAuthenticationSources> for UsersActor {
|
||||
type Result = <SetAuthorizedAuthenticationSources as actix::Message>::Result;
|
||||
fn handle(
|
||||
&mut self,
|
||||
msg: SetAuthorizedAuthenticationSources,
|
||||
_ctx: &mut Self::Context,
|
||||
) -> Self::Result {
|
||||
match self
|
||||
.manager
|
||||
.set_authorized_authentication_sources(&msg.0, msg.1)
|
||||
{
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Failed to set authorized authentication sources for user! {}",
|
||||
e
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<SetGrantedClients> for UsersActor {
|
||||
type Result = <SetGrantedClients as actix::Message>::Result;
|
||||
fn handle(&mut self, msg: SetGrantedClients, _ctx: &mut Self::Context) -> Self::Result {
|
||||
|
||||
Reference in New Issue
Block a user