diff --git a/src/actors/mod.rs b/src/actors/mod.rs index fc4d21e..352c944 100644 --- a/src/actors/mod.rs +++ b/src/actors/mod.rs @@ -1,2 +1,3 @@ pub mod users_actor; -pub mod bruteforce_actor; \ No newline at end of file +pub mod bruteforce_actor; +pub mod openid_sessions_actor; \ No newline at end of file diff --git a/src/actors/openid_sessions_actor.rs b/src/actors/openid_sessions_actor.rs new file mode 100644 index 0000000..1a818b2 --- /dev/null +++ b/src/actors/openid_sessions_actor.rs @@ -0,0 +1,74 @@ +use actix::{Actor, AsyncContext, Context, Handler}; +use actix::Message; + +use crate::constants::*; +use crate::data::client::ClientID; +use crate::data::user::UserID; +use crate::utils::time::time; + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq)] +pub struct SessionID(pub String); + +#[derive(Clone, Debug)] +pub struct Session { + pub session_id: SessionID, + pub client: ClientID, + pub user: UserID, + pub redirect_uri: String, + + pub authorization_code: String, + pub code_expire_on: u64, + + pub token: String, + pub token_expire_at: u64, + + pub nonce: Option, + pub code_challenge: Option<(String, String)>, +} + +impl Session { + pub fn is_expired(&self) -> bool { + self.code_expire_on < time() || self.token_expire_at < time() + } +} + +#[derive(Message)] +#[rtype(result = "()")] +pub struct PushNewSession(pub Session); + + +pub struct OpenIDSessionsActor { + session: Vec, +} + +impl OpenIDSessionsActor { + pub fn clean_old_sessions(&mut self) { + self.session.retain(|s| !s.is_expired()); + } +} + +impl Default for OpenIDSessionsActor { + fn default() -> Self { + Self { session: vec![] } + } +} + +impl Actor for OpenIDSessionsActor { + type Context = Context; + + fn started(&mut self, ctx: &mut Self::Context) { + // Clean up at a regular interval failed attempts + ctx.run_interval(OPEN_ID_SESSION_CLEANUP_INTERVAL, |act, _ctx| { + log::trace!("Cleaning up old login sessions"); + act.clean_old_sessions(); + }); + } +} + +impl Handler for OpenIDSessionsActor { + type Result = (); + + fn handle(&mut self, msg: PushNewSession, _ctx: &mut Self::Context) -> Self::Result { + self.session.push(msg.0) + } +} \ No newline at end of file diff --git a/src/constants.rs b/src/constants.rs index 66866e7..bb165a4 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -43,4 +43,12 @@ pub const FAIL_LOGIN_ATTEMPT_CLEANUP_INTERVAL: Duration = Duration::from_secs(60 pub const TEMPORARY_PASSWORDS_LEN: usize = 20; /// Open ID routes -pub const AUTHORIZE_URI: &str = "/openid/authorize"; \ No newline at end of file +pub const AUTHORIZE_URI: &str = "/openid/authorize"; + +/// Open ID constants +pub const OPEN_ID_SESSION_CLEANUP_INTERVAL: Duration = Duration::from_secs(60); +pub const OPEN_ID_SESSION_LEN: usize = 40; +pub const OPEN_ID_AUTHORIZATION_CODE_LEN: usize = 120; +pub const OPEN_ID_AUTHORIZATION_CODE_TIMEOUT: u64 = 300; +pub const OPEN_ID_TOKEN_LEN: usize = 120; +pub const OPEN_ID_TOKEN_TIMEOUT: u64 = 3600; \ No newline at end of file diff --git a/src/controllers/openid_controller.rs b/src/controllers/openid_controller.rs index 36b748a..5654264 100644 --- a/src/controllers/openid_controller.rs +++ b/src/controllers/openid_controller.rs @@ -1,12 +1,17 @@ +use actix::Addr; use actix_web::{HttpResponse, Responder, web}; use askama::Template; -use crate::constants::AUTHORIZE_URI; +use crate::actors::openid_sessions_actor; +use crate::actors::openid_sessions_actor::{OpenIDSessionsActor, Session, SessionID}; +use crate::constants::{AUTHORIZE_URI, OPEN_ID_AUTHORIZATION_CODE_LEN, OPEN_ID_AUTHORIZATION_CODE_TIMEOUT, OPEN_ID_SESSION_LEN, OPEN_ID_TOKEN_LEN, OPEN_ID_TOKEN_TIMEOUT}; use crate::controllers::base_controller::FatalErrorPage; use crate::data::app_config::AppConfig; use crate::data::client::{ClientID, ClientManager}; use crate::data::current_user::CurrentUser; use crate::data::openid_config::OpenIDConfig; +use crate::utils::string_utils::rand_str; +use crate::utils::time::time; pub async fn get_configuration(app_conf: web::Data) -> impl Responder { HttpResponse::Ok().json(OpenIDConfig { @@ -23,7 +28,7 @@ pub async fn get_configuration(app_conf: web::Data) -> impl Responder }) } -#[derive(serde::Deserialize)] +#[derive(serde::Deserialize, Debug)] pub struct AuthorizeQuery { /// REQUIRED. OpenID Connect requests MUST contain the openid scope value. If the openid scope value is not present, the behavior is entirely unspecified. Other scope values MAY be present. Scope values used that are not understood by an implementation SHOULD be ignored. See Sections 5.4 and 11 for additional scope values defined by this specification. scope: String, @@ -49,6 +54,7 @@ pub struct AuthorizeQuery { } fn error_redirect(query: &AuthorizeQuery, error: &str, description: &str) -> HttpResponse { + log::warn!("Failed to process sign in request ({} => {}): {:?}", error, description, query); HttpResponse::Found() .append_header( ("Location", format!( @@ -63,7 +69,8 @@ fn error_redirect(query: &AuthorizeQuery, error: &str, description: &str) -> Htt } pub async fn authorize(user: CurrentUser, query: web::Query, - clients: web::Data) -> impl Responder { + clients: web::Data, + sessions: web::Data>) -> impl Responder { let client = match clients.find_by_id(&query.client_id) { None => { return HttpResponse::BadRequest().body(FatalErrorPage { @@ -104,5 +111,30 @@ pub async fn authorize(user: CurrentUser, query: web::Query, (_, _) => None }; - HttpResponse::Ok().json("You did it") + // TODO : Check if user is authorized to access the application + + // Save all authentication information in memory + let session = Session { + session_id: SessionID(rand_str(OPEN_ID_SESSION_LEN)), + client: client.id, + user: user.uid.clone(), + redirect_uri, + authorization_code: rand_str(OPEN_ID_AUTHORIZATION_CODE_LEN), + code_expire_on: time() + OPEN_ID_AUTHORIZATION_CODE_TIMEOUT, + token: rand_str(OPEN_ID_TOKEN_LEN), + token_expire_at: time() + OPEN_ID_TOKEN_TIMEOUT, + nonce: query.0.nonce, + code_challenge, + }; + sessions.send(openid_sessions_actor::PushNewSession(session.clone())).await.unwrap(); + + log::trace!("New OpenID session: {:#?}", session); + + HttpResponse::Found() + .append_header(("Location", format!( + "{}?state={}&session_sate=&code={}", + session.redirect_uri, + urlencoding::encode(&query.0.state), + urlencoding::encode(&session.authorization_code) + ))).finish() } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index dfc2b61..49ca2ff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ use actix_web::middleware::Logger; use clap::Parser; use basic_oidc::actors::bruteforce_actor::BruteForceActor; +use basic_oidc::actors::openid_sessions_actor::OpenIDSessionsActor; use basic_oidc::actors::users_actor::UsersActor; use basic_oidc::constants::*; use basic_oidc::controllers::*; @@ -64,6 +65,7 @@ async fn main() -> std::io::Result<()> { let users_actor = UsersActor::new(users).start(); let bruteforce_actor = BruteForceActor::default().start(); + let openid_sessions_actor = OpenIDSessionsActor::default().start(); log::info!("Server will listen on {}", config.listen_address); let listen_address = config.listen_address.to_string(); @@ -77,11 +79,12 @@ async fn main() -> std::io::Result<()> { .secure(config.secure_cookie()) .visit_deadline(Duration::seconds(MAX_INACTIVITY_DURATION)) .login_deadline(Duration::seconds(MAX_SESSION_DURATION)) - .same_site(SameSite::Strict); + .same_site(SameSite::Lax); App::new() .app_data(web::Data::new(users_actor.clone())) .app_data(web::Data::new(bruteforce_actor.clone())) + .app_data(web::Data::new(openid_sessions_actor.clone())) .app_data(web::Data::new(config.clone())) .app_data(web::Data::new(clients))