Add authentication layer

This commit is contained in:
2024-06-29 14:43:56 +02:00
parent 738c53c8b9
commit e1739d9818
26 changed files with 1038 additions and 90 deletions

View File

@ -0,0 +1,97 @@
use actix_identity::Identity;
use std::future::{ready, Ready};
use std::rc::Rc;
use crate::app_config::AppConfig;
use crate::constants;
use actix_web::body::EitherBody;
use actix_web::dev::Payload;
use actix_web::{
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
Error, FromRequest, HttpResponse,
};
use futures_util::future::LocalBoxFuture;
// There are two steps in middleware processing.
// 1. Middleware initialization, middleware factory gets called with
// next service in chain as parameter.
// 2. Middleware's call method gets called with normal request.
#[derive(Default)]
pub struct AuthChecker;
// Middleware factory is `Transform` trait
// `S` - type of the next service
// `B` - type of response's body
impl<S, B> Transform<S, ServiceRequest> for AuthChecker
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = Error;
type Transform = AuthMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(AuthMiddleware {
service: Rc::new(service),
}))
}
}
pub struct AuthMiddleware<S> {
service: Rc<S>,
}
impl<S, B> Service<ServiceRequest> for AuthMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
let service = Rc::clone(&self.service);
Box::pin(async move {
// Check if no authentication is required
if constants::ROUTES_WITHOUT_AUTH.contains(&req.path())
|| !req.path().starts_with("/web_api/")
{
log::trace!("No authentication is required")
}
// Dev only, check for auto login
else if AppConfig::get().unsecure_disable_login {
log::trace!("Authentication is disabled")
}
// Check cookie authentication
else {
let identity: Option<Identity> =
Identity::from_request(req.request(), &mut Payload::None)
.into_inner()
.ok();
if identity.is_none() {
log::error!(
"Missing identity information in request, user is not authenticated!"
);
return Ok(req
.into_response(HttpResponse::PreconditionFailed().finish())
.map_into_right_body());
};
}
service
.call(req)
.await
.map(ServiceResponse::map_into_left_body)
})
}
}

View File

@ -91,9 +91,22 @@ impl From<actix::MailboxError> for HttpErr {
}
}
impl From<actix_identity::error::GetIdentityError> for HttpErr {
fn from(value: actix_identity::error::GetIdentityError) -> Self {
HttpErr::Err(std::io::Error::new(ErrorKind::Other, value.to_string()).into())
}
}
impl From<actix_identity::error::LoginError> for HttpErr {
fn from(value: actix_identity::error::LoginError) -> Self {
HttpErr::Err(std::io::Error::new(ErrorKind::Other, value.to_string()).into())
}
}
impl From<HttpResponse> for HttpErr {
fn from(value: HttpResponse) -> Self {
HttpErr::HTTPResponse(value)
}
}
pub type HttpResult = Result<HttpResponse, HttpErr>;

View File

@ -1,70 +1,11 @@
use actix_web::middleware::Logger;
use actix_web::{web, App, HttpServer};
use openssl::ssl::{SslAcceptor, SslMethod};
use actix_web::web;
use crate::app_config::AppConfig;
use crate::crypto::pki;
use crate::energy::energy_actor::EnergyActorAddr;
pub mod auth_middleware;
pub mod custom_error;
pub mod energy_controller;
pub mod pki_controller;
pub mod server_controller;
pub mod servers;
pub mod unsecure_server;
pub mod web_api;
pub type WebEnergyActor = web::Data<EnergyActorAddr>;
/// Start unsecure (HTTP) server
pub async fn unsecure_server() -> anyhow::Result<()> {
log::info!(
"Unsecure server starting to listen on {} for {}",
AppConfig::get().unsecure_listen_address,
AppConfig::get().unsecure_origin()
);
HttpServer::new(|| {
App::new()
.wrap(Logger::default())
.route("/", web::get().to(server_controller::unsecure_home))
.route("/pki/{file}", web::get().to(pki_controller::serve_pki_file))
})
.bind(&AppConfig::get().unsecure_listen_address)?
.run()
.await?;
Ok(())
}
/// Start secure (HTTPS) server
pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()> {
let web_ca = pki::CertData::load_web_ca()?;
let server_cert = pki::CertData::load_server()?;
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
builder.set_private_key(&server_cert.key)?;
builder.set_certificate(&server_cert.cert)?;
builder.add_extra_chain_cert(web_ca.cert)?;
log::info!(
"Secure server starting to listen on {} for {}",
AppConfig::get().listen_address,
AppConfig::get().secure_origin()
);
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(energy_actor.clone()))
.wrap(Logger::default())
.route("/", web::get().to(server_controller::secure_home))
.route(
"/api/energy/curr_consumption",
web::get().to(energy_controller::curr_consumption),
)
.route(
"/api/energy/cached_consumption",
web::get().to(energy_controller::cached_consumption),
)
})
.bind_openssl(&AppConfig::get().listen_address, builder)?
.run()
.await?;
Ok(())
}

View File

@ -0,0 +1,134 @@
use crate::app_config::AppConfig;
use crate::constants;
use crate::crypto::pki;
use crate::energy::energy_actor::EnergyActorAddr;
use crate::server::auth_middleware::AuthChecker;
use crate::server::unsecure_server::*;
use crate::server::web_api::*;
use actix_cors::Cors;
use actix_identity::config::LogoutBehaviour;
use actix_identity::IdentityMiddleware;
use actix_remote_ip::RemoteIPConfig;
use actix_session::storage::CookieSessionStore;
use actix_session::SessionMiddleware;
use actix_web::cookie::{Key, SameSite};
use actix_web::middleware::Logger;
use actix_web::{web, App, HttpServer};
use openssl::ssl::{SslAcceptor, SslMethod};
use std::time::Duration;
/// Start unsecure (HTTP) server
pub async fn unsecure_server() -> anyhow::Result<()> {
log::info!(
"Unsecure server starting to listen on {} for {}",
AppConfig::get().unsecure_listen_address,
AppConfig::get().unsecure_origin()
);
HttpServer::new(|| {
App::new()
.wrap(Logger::default())
.route(
"/",
web::get().to(unsecure_server_controller::unsecure_home),
)
.route(
"/pki/{file}",
web::get().to(unsecure_pki_controller::serve_pki_file),
)
})
.bind(&AppConfig::get().unsecure_listen_address)?
.run()
.await?;
Ok(())
}
/// Start secure (HTTPS) server
pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()> {
let web_ca = pki::CertData::load_web_ca()?;
let server_cert = pki::CertData::load_server()?;
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
builder.set_private_key(&server_cert.key)?;
builder.set_certificate(&server_cert.cert)?;
builder.add_extra_chain_cert(web_ca.cert)?;
log::info!(
"Secure server starting to listen on {} for {}",
AppConfig::get().listen_address,
AppConfig::get().secure_origin()
);
HttpServer::new(move || {
let session_mw = SessionMiddleware::builder(
CookieSessionStore::default(),
Key::from(AppConfig::get().secret().as_bytes()),
)
.cookie_name(constants::SESSION_COOKIE_NAME.to_string())
.cookie_secure(AppConfig::get().cookie_secure)
.cookie_same_site(SameSite::Strict)
.cookie_domain(AppConfig::get().cookie_domain())
.cookie_http_only(true)
.build();
let identity_middleware = IdentityMiddleware::builder()
.logout_behaviour(LogoutBehaviour::PurgeSession)
.visit_deadline(Some(Duration::from_secs(
constants::MAX_INACTIVITY_DURATION,
)))
.login_deadline(Some(Duration::from_secs(constants::MAX_SESSION_DURATION)))
.build();
let mut cors = Cors::default()
.allowed_origin(&AppConfig::get().secure_origin())
.allowed_methods(vec!["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"])
.allowed_header("X-Auth-Token")
.allow_any_header()
.supports_credentials()
.max_age(3600);
if cfg!(debug_assertions) {
cors = cors.allow_any_origin();
}
App::new()
.app_data(web::Data::new(energy_actor.clone()))
.wrap(Logger::default())
.wrap(AuthChecker)
.wrap(identity_middleware)
.wrap(session_mw)
.wrap(cors)
.app_data(web::Data::new(RemoteIPConfig {
proxy: AppConfig::get().proxy_ip.clone(),
}))
.route("/", web::get().to(server_controller::secure_home))
.route(
"/web_api/server/config",
web::get().to(server_controller::config),
)
.route(
"/web_api/auth/password_auth",
web::post().to(auth_controller::password_auth),
)
.route(
"/web_api/auth/info",
web::get().to(auth_controller::auth_info),
)
.route(
"/web_api/auth/sign_out",
web::get().to(auth_controller::sign_out),
)
.route(
"/web_api/energy/curr_consumption",
web::get().to(energy_controller::curr_consumption),
)
.route(
"/web_api/energy/cached_consumption",
web::get().to(energy_controller::cached_consumption),
)
})
.bind_openssl(&AppConfig::get().listen_address, builder)?
.run()
.await?;
Ok(())
}

View File

@ -0,0 +1,2 @@
pub mod unsecure_pki_controller;
pub mod unsecure_server_controller;

View File

@ -5,9 +5,3 @@ pub async fn unsecure_home() -> HttpResponse {
.content_type("text/plain")
.body("SolarEnergy unsecure central backend")
}
pub async fn secure_home() -> HttpResponse {
HttpResponse::Ok()
.content_type("text/plain")
.body("SolarEnergy secure central backend")
}

View File

@ -0,0 +1,52 @@
use crate::app_config::AppConfig;
use crate::server::custom_error::HttpResult;
use actix_identity::Identity;
use actix_remote_ip::RemoteIP;
use actix_web::{web, HttpMessage, HttpRequest, HttpResponse};
#[derive(serde::Deserialize)]
pub struct AuthRequest {
user: String,
password: String,
}
/// Perform password authentication
pub async fn password_auth(
r: web::Json<AuthRequest>,
request: HttpRequest,
remote_ip: RemoteIP,
) -> HttpResult {
if r.user != AppConfig::get().admin_username || r.password != AppConfig::get().admin_password {
log::error!("Failed login attempt from {}!", remote_ip.0.to_string());
return Ok(HttpResponse::Unauthorized().json("Invalid credentials!"));
}
log::info!("Successful login attempt from {}!", remote_ip.0.to_string());
Identity::login(&request.extensions(), r.user.to_string())?;
Ok(HttpResponse::Ok().finish())
}
#[derive(serde::Serialize)]
struct AuthInfo {
id: String,
}
/// Get current user information
pub async fn auth_info(id: Option<Identity>) -> HttpResult {
if AppConfig::get().unsecure_disable_login {
return Ok(HttpResponse::Ok().json(AuthInfo {
id: "auto login".to_string(),
}));
}
Ok(HttpResponse::Ok().json(AuthInfo {
id: id.unwrap().id()?,
}))
}
/// Sign out user
pub async fn sign_out(id: Identity) -> HttpResult {
id.logout();
Ok(HttpResponse::NoContent().finish())
}

View File

@ -0,0 +1,3 @@
pub mod auth_controller;
pub mod energy_controller;
pub mod server_controller;

View File

@ -0,0 +1,25 @@
use crate::app_config::AppConfig;
use actix_web::HttpResponse;
pub async fn secure_home() -> HttpResponse {
HttpResponse::Ok()
.content_type("text/plain")
.body("SolarEnergy secure central backend")
}
#[derive(serde::Serialize)]
struct ServerConfig {
auth_disabled: bool,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
auth_disabled: AppConfig::get().unsecure_disable_login,
}
}
}
pub async fn config() -> HttpResponse {
HttpResponse::Ok().json(ServerConfig::default())
}