Add authentication from upstream providers #107
@ -1,3 +1,4 @@
|
|||||||
pub mod bruteforce_actor;
|
pub mod bruteforce_actor;
|
||||||
pub mod openid_sessions_actor;
|
pub mod openid_sessions_actor;
|
||||||
|
pub mod providers_states_actor;
|
||||||
pub mod users_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 attempts
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -71,3 +71,9 @@ pub const OPEN_ID_REFRESH_TOKEN_TIMEOUT: u64 = 360000;
|
|||||||
/// Webauthn constants
|
/// Webauthn constants
|
||||||
pub const WEBAUTHN_REGISTER_CHALLENGE_EXPIRE: u64 = 3600;
|
pub const WEBAUTHN_REGISTER_CHALLENGE_EXPIRE: u64 = 3600;
|
||||||
pub const WEBAUTHN_LOGIN_CHALLENGE_EXPIRE: u64 = 3600;
|
pub const WEBAUTHN_LOGIN_CHALLENGE_EXPIRE: u64 = 3600;
|
||||||
|
|
||||||
|
/// OpenID provider login constants
|
||||||
|
pub const OIDC_STATES_CLEANUP_INTERVAL: Duration = Duration::from_secs(60);
|
||||||
|
pub const MAX_OIDC_PROVIDERS_STATES: usize = 10;
|
||||||
|
pub const OIDC_PROVIDERS_STATE_LEN: usize = 40;
|
||||||
|
pub const OIDC_PROVIDERS_STATE_DURATION: u64 = 60 * 15;
|
||||||
|
@ -5,6 +5,7 @@ pub mod base_controller;
|
|||||||
pub mod login_api;
|
pub mod login_api;
|
||||||
pub mod login_controller;
|
pub mod login_controller;
|
||||||
pub mod openid_controller;
|
pub mod openid_controller;
|
||||||
|
pub mod providers_controller;
|
||||||
pub mod settings_controller;
|
pub mod settings_controller;
|
||||||
pub mod two_factor_api;
|
pub mod two_factor_api;
|
||||||
pub mod two_factors_controller;
|
pub mod two_factors_controller;
|
||||||
|
55
src/controllers/providers_controller.rs
Normal file
55
src/controllers/providers_controller.rs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
use crate::actors::providers_states_actor;
|
||||||
|
use crate::actors::providers_states_actor::{ProviderLoginState, ProvidersStatesActor};
|
||||||
|
use crate::controllers::base_controller::build_fatal_error_page;
|
||||||
|
use crate::data::action_logger::{Action, ActionLogger};
|
||||||
|
use crate::data::login_redirect::LoginRedirect;
|
||||||
|
use crate::data::provider::{ProviderID, ProvidersManager};
|
||||||
|
use crate::data::remote_ip::RemoteIP;
|
||||||
|
use actix::Addr;
|
||||||
|
use actix_web::{web, HttpResponse, Responder};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
pub struct StartLoginQuery {
|
||||||
|
#[serde(default)]
|
||||||
|
redirect: LoginRedirect,
|
||||||
|
id: ProviderID,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start user authentication using a provider
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub async fn start_login(
|
||||||
|
remote_ip: RemoteIP,
|
||||||
|
providers: web::Data<Arc<ProvidersManager>>,
|
||||||
|
states: web::Data<Addr<ProvidersStatesActor>>,
|
||||||
|
query: web::Query<StartLoginQuery>,
|
||||||
|
logger: ActionLogger,
|
||||||
|
) -> impl Responder {
|
||||||
|
// Get provider information
|
||||||
|
let provider = match providers.find_by_id(&query.id) {
|
||||||
|
None => {
|
||||||
|
return HttpResponse::NotFound()
|
||||||
|
.body(build_fatal_error_page("Login provider not found!"))
|
||||||
|
}
|
||||||
|
Some(p) => p,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate & save state
|
||||||
|
let state = ProviderLoginState::new(&provider.id, query.redirect.clone());
|
||||||
|
states
|
||||||
|
.send(providers_states_actor::RecordState {
|
||||||
|
ip: remote_ip.0,
|
||||||
|
state: state.clone(),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
logger.log(Action::StartLoginAttemptWithOpenIDProvider {
|
||||||
|
provider_id: &provider.id,
|
||||||
|
state: &state.state_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
HttpResponse::Ok().body(state.state_id)
|
||||||
|
|
||||||
|
// Redirect user
|
||||||
|
}
|
@ -10,6 +10,7 @@ use actix_web::{web, Error, FromRequest, HttpRequest};
|
|||||||
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::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};
|
||||||
@ -23,7 +24,14 @@ pub enum Action<'a> {
|
|||||||
AdminSetAuthorizedAuthenticationSources(&'a User, &'a AuthorizedAuthenticationSources),
|
AdminSetAuthorizedAuthenticationSources(&'a User, &'a AuthorizedAuthenticationSources),
|
||||||
AdminSetNewGrantedClientsList(&'a User, &'a GrantedClients),
|
AdminSetNewGrantedClientsList(&'a User, &'a GrantedClients),
|
||||||
AdminClear2FAHistory(&'a User),
|
AdminClear2FAHistory(&'a User),
|
||||||
LoginWebauthnAttempt { success: bool, user_id: UserID },
|
LoginWebauthnAttempt {
|
||||||
|
success: bool,
|
||||||
|
user_id: UserID,
|
||||||
|
},
|
||||||
|
StartLoginAttemptWithOpenIDProvider {
|
||||||
|
provider_id: &'a ProviderID,
|
||||||
|
state: &'a str,
|
||||||
|
},
|
||||||
Signout,
|
Signout,
|
||||||
UserNeed2FAOnLogin(&'a User),
|
UserNeed2FAOnLogin(&'a User),
|
||||||
UserSuccessfullyAuthenticated(&'a User),
|
UserSuccessfullyAuthenticated(&'a User),
|
||||||
@ -32,12 +40,19 @@ pub enum Action<'a> {
|
|||||||
TryLocalLoginFromUnauthorizedAccount(&'a str),
|
TryLocalLoginFromUnauthorizedAccount(&'a str),
|
||||||
FailedLoginWithBadCredentials(&'a str),
|
FailedLoginWithBadCredentials(&'a str),
|
||||||
UserChangedPasswordOnLogin(&'a UserID),
|
UserChangedPasswordOnLogin(&'a UserID),
|
||||||
OTPLoginAttempt { user: &'a User, success: bool },
|
OTPLoginAttempt {
|
||||||
NewOpenIDSession { client: &'a Client },
|
user: &'a User,
|
||||||
|
success: bool,
|
||||||
|
},
|
||||||
|
NewOpenIDSession {
|
||||||
|
client: &'a Client,
|
||||||
|
},
|
||||||
ChangedHisPassword,
|
ChangedHisPassword,
|
||||||
ClearedHisLoginHistory,
|
ClearedHisLoginHistory,
|
||||||
AddNewFactor(&'a TwoFactor),
|
AddNewFactor(&'a TwoFactor),
|
||||||
Removed2FAFactor { factor_id: &'a FactorID },
|
Removed2FAFactor {
|
||||||
|
factor_id: &'a FactorID,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Action<'a> {
|
impl<'a> Action<'a> {
|
||||||
@ -80,6 +95,9 @@ impl<'a> Action<'a> {
|
|||||||
true => format!("successfully performed webauthn attempt for user {user_id:?}"),
|
true => format!("successfully performed webauthn attempt for user {user_id:?}"),
|
||||||
false => format!("performed FAILED webauthn attempt for user {user_id:?}"),
|
false => format!("performed FAILED webauthn attempt for user {user_id:?}"),
|
||||||
},
|
},
|
||||||
|
Action::StartLoginAttemptWithOpenIDProvider { provider_id, state } => format!(
|
||||||
|
"started new authentication attempt through an OpenID provider (prov={} / state={state})", provider_id.0
|
||||||
|
),
|
||||||
Action::Signout => "signed out".to_string(),
|
Action::Signout => "signed out".to_string(),
|
||||||
Action::UserNeed2FAOnLogin(user) => {
|
Action::UserNeed2FAOnLogin(user) => {
|
||||||
format!(
|
format!(
|
||||||
|
12
src/main.rs
12
src/main.rs
@ -12,6 +12,7 @@ use actix_web::{get, middleware, web, App, HttpResponse, HttpServer};
|
|||||||
|
|
||||||
use basic_oidc::actors::bruteforce_actor::BruteForceActor;
|
use basic_oidc::actors::bruteforce_actor::BruteForceActor;
|
||||||
use basic_oidc::actors::openid_sessions_actor::OpenIDSessionsActor;
|
use basic_oidc::actors::openid_sessions_actor::OpenIDSessionsActor;
|
||||||
|
use basic_oidc::actors::providers_states_actor::ProvidersStatesActor;
|
||||||
use basic_oidc::actors::users_actor::{UsersActor, UsersSyncBackend};
|
use basic_oidc::actors::users_actor::{UsersActor, UsersSyncBackend};
|
||||||
use basic_oidc::constants::*;
|
use basic_oidc::constants::*;
|
||||||
use basic_oidc::controllers::assets_controller::assets_route;
|
use basic_oidc::controllers::assets_controller::assets_route;
|
||||||
@ -69,6 +70,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
|
|
||||||
let users_actor = UsersActor::new(users).start();
|
let users_actor = UsersActor::new(users).start();
|
||||||
let bruteforce_actor = BruteForceActor::default().start();
|
let bruteforce_actor = BruteForceActor::default().start();
|
||||||
|
let providers_states_actor = ProvidersStatesActor::default().start();
|
||||||
let openid_sessions_actor = OpenIDSessionsActor::default().start();
|
let openid_sessions_actor = OpenIDSessionsActor::default().start();
|
||||||
let jwt_signer = JWTSigner::gen_from_memory().expect("Failed to generate JWKS key");
|
let jwt_signer = JWTSigner::gen_from_memory().expect("Failed to generate JWKS key");
|
||||||
let webauthn_manager = Arc::new(WebAuthManager::init(config));
|
let webauthn_manager = Arc::new(WebAuthManager::init(config));
|
||||||
@ -105,6 +107,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
App::new()
|
App::new()
|
||||||
.app_data(web::Data::new(users_actor.clone()))
|
.app_data(web::Data::new(users_actor.clone()))
|
||||||
.app_data(web::Data::new(bruteforce_actor.clone()))
|
.app_data(web::Data::new(bruteforce_actor.clone()))
|
||||||
|
.app_data(web::Data::new(providers_states_actor.clone()))
|
||||||
.app_data(web::Data::new(openid_sessions_actor.clone()))
|
.app_data(web::Data::new(openid_sessions_actor.clone()))
|
||||||
.app_data(web::Data::new(clients.clone()))
|
.app_data(web::Data::new(clients.clone()))
|
||||||
.app_data(web::Data::new(providers.clone()))
|
.app_data(web::Data::new(providers.clone()))
|
||||||
@ -117,7 +120,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.wrap(AuthMiddleware {})
|
.wrap(AuthMiddleware {})
|
||||||
.wrap(identity_middleware)
|
.wrap(identity_middleware)
|
||||||
.wrap(session_mw)
|
.wrap(session_mw)
|
||||||
// main route
|
// Main route
|
||||||
.route(
|
.route(
|
||||||
"/",
|
"/",
|
||||||
web::get().to(|| async {
|
web::get().to(|| async {
|
||||||
@ -127,7 +130,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.route("/robots.txt", web::get().to(assets_controller::robots_txt))
|
.route("/robots.txt", web::get().to(assets_controller::robots_txt))
|
||||||
// health route
|
// Health route
|
||||||
.service(health)
|
.service(health)
|
||||||
// Assets serving
|
// Assets serving
|
||||||
.route("/assets/{path:.*}", web::get().to(assets_route))
|
.route("/assets/{path:.*}", web::get().to(assets_route))
|
||||||
@ -158,6 +161,11 @@ async fn main() -> std::io::Result<()> {
|
|||||||
"/login/api/auth_webauthn",
|
"/login/api/auth_webauthn",
|
||||||
web::post().to(login_api::auth_webauthn),
|
web::post().to(login_api::auth_webauthn),
|
||||||
)
|
)
|
||||||
|
// Providers controller
|
||||||
|
.route(
|
||||||
|
"/login_with_prov",
|
||||||
|
web::get().to(providers_controller::start_login),
|
||||||
|
)
|
||||||
// Settings routes
|
// Settings routes
|
||||||
.route(
|
.route(
|
||||||
"/settings",
|
"/settings",
|
||||||
|
Loading…
Reference in New Issue
Block a user