Add authentication layer
This commit is contained in:
97
central_backend/src/server/auth_middleware.rs
Normal file
97
central_backend/src/server/auth_middleware.rs
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
@ -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>;
|
||||
|
@ -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(())
|
||||
}
|
||||
|
134
central_backend/src/server/servers.rs
Normal file
134
central_backend/src/server/servers.rs
Normal 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(())
|
||||
}
|
2
central_backend/src/server/unsecure_server/mod.rs
Normal file
2
central_backend/src/server/unsecure_server/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod unsecure_pki_controller;
|
||||
pub mod unsecure_server_controller;
|
@ -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")
|
||||
}
|
52
central_backend/src/server/web_api/auth_controller.rs
Normal file
52
central_backend/src/server/web_api/auth_controller.rs
Normal 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())
|
||||
}
|
3
central_backend/src/server/web_api/mod.rs
Normal file
3
central_backend/src/server/web_api/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod auth_controller;
|
||||
pub mod energy_controller;
|
||||
pub mod server_controller;
|
25
central_backend/src/server/web_api/server_controller.rs
Normal file
25
central_backend/src/server/web_api/server_controller.rs
Normal 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())
|
||||
}
|
Reference in New Issue
Block a user