Start to implement OpenID authentication
This commit is contained in:
		@@ -33,9 +33,9 @@ pub struct AppConfig {
 | 
			
		||||
    )]
 | 
			
		||||
    pub oidc_configuration_url: String,
 | 
			
		||||
 | 
			
		||||
    /// Disable OpenID authentication
 | 
			
		||||
    /// Disable authentication (for development purposes ONLY)
 | 
			
		||||
    #[arg(long, env)]
 | 
			
		||||
    pub disable_oidc: bool,
 | 
			
		||||
    pub unsecure_disable_login: bool,
 | 
			
		||||
 | 
			
		||||
    /// OpenID client ID
 | 
			
		||||
    #[arg(long, env, default_value = "foo")]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								remote_backend/src/constants.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								remote_backend/src/constants.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
pub const ROUTES_WITHOUT_AUTH: [&str; 3] = [
 | 
			
		||||
    "/api/server/config",
 | 
			
		||||
    "/api/auth/start_oidc",
 | 
			
		||||
    "/api/auth/finish_oidc",
 | 
			
		||||
];
 | 
			
		||||
							
								
								
									
										97
									
								
								remote_backend/src/controllers/auth_controller.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								remote_backend/src/controllers/auth_controller.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,97 @@
 | 
			
		||||
use actix_remote_ip::RemoteIP;
 | 
			
		||||
use actix_web::web::Data;
 | 
			
		||||
use actix_web::{web, HttpResponse, Responder};
 | 
			
		||||
use light_openid::basic_state_manager::BasicStateManager;
 | 
			
		||||
 | 
			
		||||
use crate::app_config::AppConfig;
 | 
			
		||||
use crate::controllers::HttpResult;
 | 
			
		||||
use crate::extractors::auth_extractor::AuthExtractor;
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize)]
 | 
			
		||||
struct StartOIDCResponse {
 | 
			
		||||
    url: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Start OIDC authentication
 | 
			
		||||
pub async fn start_oidc(sm: Data<BasicStateManager>, ip: RemoteIP) -> HttpResult {
 | 
			
		||||
    let prov = AppConfig::get().openid_provider();
 | 
			
		||||
 | 
			
		||||
    let conf =
 | 
			
		||||
        light_openid::primitives::OpenIDConfig::load_from_url(prov.configuration_url).await?;
 | 
			
		||||
 | 
			
		||||
    Ok(HttpResponse::Ok().json(StartOIDCResponse {
 | 
			
		||||
        url: conf.gen_authorization_url(
 | 
			
		||||
            prov.client_id,
 | 
			
		||||
            &sm.gen_state(ip.0)?,
 | 
			
		||||
            &AppConfig::get().oidc_redirect_url(),
 | 
			
		||||
        ),
 | 
			
		||||
    }))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize)]
 | 
			
		||||
pub struct FinishOpenIDLoginQuery {
 | 
			
		||||
    code: String,
 | 
			
		||||
    state: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Finish OIDC authentication
 | 
			
		||||
pub async fn finish_oidc(
 | 
			
		||||
    sm: Data<BasicStateManager>,
 | 
			
		||||
    remote_ip: RemoteIP,
 | 
			
		||||
    req: web::Json<FinishOpenIDLoginQuery>,
 | 
			
		||||
    auth: AuthExtractor,
 | 
			
		||||
) -> HttpResult {
 | 
			
		||||
    if let Err(e) = sm.validate_state(remote_ip.0, &req.state) {
 | 
			
		||||
        log::error!("Failed to validate OIDC CB state! {e}");
 | 
			
		||||
        return Ok(HttpResponse::BadRequest().json("Invalid state!"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let prov = AppConfig::get().openid_provider();
 | 
			
		||||
 | 
			
		||||
    let conf =
 | 
			
		||||
        light_openid::primitives::OpenIDConfig::load_from_url(prov.configuration_url).await?;
 | 
			
		||||
 | 
			
		||||
    let (token, _) = conf
 | 
			
		||||
        .request_token(
 | 
			
		||||
            prov.client_id,
 | 
			
		||||
            prov.client_secret,
 | 
			
		||||
            &req.code,
 | 
			
		||||
            &AppConfig::get().oidc_redirect_url(),
 | 
			
		||||
        )
 | 
			
		||||
        .await?;
 | 
			
		||||
    let (user_info, _) = conf.request_user_info(&token).await?;
 | 
			
		||||
 | 
			
		||||
    if user_info.email_verified != Some(true) {
 | 
			
		||||
        log::error!("Email is not verified!");
 | 
			
		||||
        return Ok(HttpResponse::Unauthorized().json("Email unverified by IDP!"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let mail = match user_info.email {
 | 
			
		||||
        Some(m) => m,
 | 
			
		||||
        None => {
 | 
			
		||||
            return Ok(HttpResponse::Unauthorized().json("Email not provided by the IDP!"));
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    auth.authenticate(mail);
 | 
			
		||||
 | 
			
		||||
    Ok(HttpResponse::Ok().finish())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize)]
 | 
			
		||||
struct CurrentUser {
 | 
			
		||||
    id: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Get current authenticated user
 | 
			
		||||
pub async fn current_user(auth: AuthExtractor) -> impl Responder {
 | 
			
		||||
    HttpResponse::Ok().json(CurrentUser {
 | 
			
		||||
        id: auth.id().unwrap_or_else(|| "Anonymous".to_string()),
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Sign out
 | 
			
		||||
pub async fn sign_out(auth: AuthExtractor) -> impl Responder {
 | 
			
		||||
    auth.sign_out();
 | 
			
		||||
    HttpResponse::Ok().finish()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										89
									
								
								remote_backend/src/controllers/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								remote_backend/src/controllers/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,89 @@
 | 
			
		||||
use actix_web::body::BoxBody;
 | 
			
		||||
use actix_web::http::StatusCode;
 | 
			
		||||
use actix_web::HttpResponse;
 | 
			
		||||
use std::error::Error;
 | 
			
		||||
use std::fmt::{Display, Formatter};
 | 
			
		||||
use std::io::ErrorKind;
 | 
			
		||||
 | 
			
		||||
pub mod auth_controller;
 | 
			
		||||
 | 
			
		||||
/// Custom error to ease controller writing
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub enum HttpErr {
 | 
			
		||||
    Err(anyhow::Error),
 | 
			
		||||
    HTTPResponse(HttpResponse),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Display for HttpErr {
 | 
			
		||||
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
 | 
			
		||||
        match self {
 | 
			
		||||
            HttpErr::Err(err) => Display::fmt(err, f),
 | 
			
		||||
            HttpErr::HTTPResponse(res) => {
 | 
			
		||||
                Display::fmt(&format!("HTTP RESPONSE {}", res.status().as_str()), f)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl actix_web::error::ResponseError for HttpErr {
 | 
			
		||||
    fn status_code(&self) -> StatusCode {
 | 
			
		||||
        match self {
 | 
			
		||||
            HttpErr::Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
 | 
			
		||||
            HttpErr::HTTPResponse(r) => r.status(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    fn error_response(&self) -> HttpResponse<BoxBody> {
 | 
			
		||||
        log::error!("Error while processing request! {}", self);
 | 
			
		||||
 | 
			
		||||
        HttpResponse::InternalServerError().body("Failed to execute request!")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<anyhow::Error> for HttpErr {
 | 
			
		||||
    fn from(err: anyhow::Error) -> HttpErr {
 | 
			
		||||
        HttpErr::Err(err)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<Box<dyn Error>> for HttpErr {
 | 
			
		||||
    fn from(value: Box<dyn Error>) -> Self {
 | 
			
		||||
        HttpErr::Err(std::io::Error::new(ErrorKind::Other, value.to_string()).into())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<std::io::Error> for HttpErr {
 | 
			
		||||
    fn from(value: std::io::Error) -> Self {
 | 
			
		||||
        HttpErr::Err(value.into())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<std::num::ParseIntError> for HttpErr {
 | 
			
		||||
    fn from(value: std::num::ParseIntError) -> Self {
 | 
			
		||||
        HttpErr::Err(value.into())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<reqwest::Error> for HttpErr {
 | 
			
		||||
    fn from(value: reqwest::Error) -> Self {
 | 
			
		||||
        HttpErr::Err(value.into())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<reqwest::header::ToStrError> for HttpErr {
 | 
			
		||||
    fn from(value: reqwest::header::ToStrError) -> Self {
 | 
			
		||||
        HttpErr::Err(value.into())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<actix_web::Error> for HttpErr {
 | 
			
		||||
    fn from(value: actix_web::Error) -> 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>;
 | 
			
		||||
							
								
								
									
										47
									
								
								remote_backend/src/extractors/auth_extractor.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								remote_backend/src/extractors/auth_extractor.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
use actix_identity::Identity;
 | 
			
		||||
use actix_web::dev::Payload;
 | 
			
		||||
use actix_web::{Error, FromRequest, HttpMessage, HttpRequest};
 | 
			
		||||
use futures_util::future::{ready, Ready};
 | 
			
		||||
use std::fmt::Display;
 | 
			
		||||
 | 
			
		||||
pub struct AuthExtractor {
 | 
			
		||||
    identity: Option<Identity>,
 | 
			
		||||
    request: HttpRequest,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl AuthExtractor {
 | 
			
		||||
    /// Check whether the user is authenticated or not
 | 
			
		||||
    pub fn is_authenticated(&self) -> bool {
 | 
			
		||||
        self.identity.is_some()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Authenticate the user
 | 
			
		||||
    pub fn authenticate(&self, id: impl Display) {
 | 
			
		||||
        Identity::login(&self.request.extensions(), id.to_string())
 | 
			
		||||
            .expect("Unable to set authentication!");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn id(&self) -> Option<String> {
 | 
			
		||||
        self.identity.as_ref().map(|i| i.id().unwrap())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn sign_out(self) {
 | 
			
		||||
        if let Some(i) = self.identity {
 | 
			
		||||
            i.logout()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl FromRequest for AuthExtractor {
 | 
			
		||||
    type Error = Error;
 | 
			
		||||
    type Future = Ready<Result<Self, Error>>;
 | 
			
		||||
 | 
			
		||||
    fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
 | 
			
		||||
        let identity: Option<Identity> = Identity::from_request(req, payload).into_inner().ok();
 | 
			
		||||
 | 
			
		||||
        ready(Ok(Self {
 | 
			
		||||
            identity,
 | 
			
		||||
            request: req.clone(),
 | 
			
		||||
        }))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								remote_backend/src/extractors/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								remote_backend/src/extractors/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
pub mod auth_extractor;
 | 
			
		||||
@@ -1,3 +1,7 @@
 | 
			
		||||
pub mod app_config;
 | 
			
		||||
pub mod constants;
 | 
			
		||||
pub mod controllers;
 | 
			
		||||
pub mod extractors;
 | 
			
		||||
pub mod middlewares;
 | 
			
		||||
pub mod utils;
 | 
			
		||||
pub mod virtweb_client;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,10 @@
 | 
			
		||||
use actix_remote_ip::RemoteIPConfig;
 | 
			
		||||
use actix_web::middleware::Logger;
 | 
			
		||||
use actix_web::web::Data;
 | 
			
		||||
use actix_web::{App, HttpServer};
 | 
			
		||||
use actix_web::{web, App, HttpServer};
 | 
			
		||||
use light_openid::basic_state_manager::BasicStateManager;
 | 
			
		||||
use remote_backend::app_config::AppConfig;
 | 
			
		||||
use remote_backend::controllers::auth_controller;
 | 
			
		||||
use remote_backend::virtweb_client;
 | 
			
		||||
 | 
			
		||||
#[actix_web::main]
 | 
			
		||||
@@ -21,6 +22,22 @@ async fn main() -> std::io::Result<()> {
 | 
			
		||||
            .app_data(Data::new(RemoteIPConfig {
 | 
			
		||||
                proxy: AppConfig::get().proxy_ip.clone(),
 | 
			
		||||
            }))
 | 
			
		||||
            .route(
 | 
			
		||||
                "/api/auth/start_oidc",
 | 
			
		||||
                web::get().to(auth_controller::start_oidc),
 | 
			
		||||
            )
 | 
			
		||||
            .route(
 | 
			
		||||
                "/api/auth/finish_oidc",
 | 
			
		||||
                web::post().to(auth_controller::finish_oidc),
 | 
			
		||||
            )
 | 
			
		||||
            .route(
 | 
			
		||||
                "/api/auth/user",
 | 
			
		||||
                web::get().to(auth_controller::current_user),
 | 
			
		||||
            )
 | 
			
		||||
            .route(
 | 
			
		||||
                "/api/auth/sign_out",
 | 
			
		||||
                web::get().to(auth_controller::sign_out),
 | 
			
		||||
            )
 | 
			
		||||
    })
 | 
			
		||||
    .bind(&AppConfig::get().listen_address)?
 | 
			
		||||
    .run()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										111
									
								
								remote_backend/src/middlewares/auth_middleware.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								remote_backend/src/middlewares/auth_middleware.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,111 @@
 | 
			
		||||
use std::future::{ready, Ready};
 | 
			
		||||
use std::rc::Rc;
 | 
			
		||||
 | 
			
		||||
use crate::app_config::AppConfig;
 | 
			
		||||
use crate::constants;
 | 
			
		||||
use crate::extractors::auth_extractor::AuthExtractor;
 | 
			
		||||
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 {
 | 
			
		||||
            let remote_ip =
 | 
			
		||||
                actix_remote_ip::RemoteIP::from_request(req.request(), &mut Payload::None)
 | 
			
		||||
                    .await
 | 
			
		||||
                    .unwrap();
 | 
			
		||||
 | 
			
		||||
            // Check if no authentication is required
 | 
			
		||||
            if constants::ROUTES_WITHOUT_AUTH.contains(&req.path())
 | 
			
		||||
                || !req.path().starts_with("/api/")
 | 
			
		||||
            {
 | 
			
		||||
                log::trace!("No authentication is required")
 | 
			
		||||
            }
 | 
			
		||||
            // Check cookie authentication
 | 
			
		||||
            else if !&AppConfig::get().unsecure_disable_login {
 | 
			
		||||
                let auth = match AuthExtractor::from_request(req.request(), &mut Payload::None)
 | 
			
		||||
                    .into_inner()
 | 
			
		||||
                {
 | 
			
		||||
                    Ok(auth) => auth,
 | 
			
		||||
                    Err(e) => {
 | 
			
		||||
                        log::error!(
 | 
			
		||||
                            "Failed to extract authentication information from request! {e}"
 | 
			
		||||
                        );
 | 
			
		||||
                        return Ok(req
 | 
			
		||||
                            .into_response(HttpResponse::PreconditionFailed().finish())
 | 
			
		||||
                            .map_into_right_body());
 | 
			
		||||
                    }
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                if !auth.is_authenticated() {
 | 
			
		||||
                    log::error!(
 | 
			
		||||
                        "User attempted to access privileged route without authentication!"
 | 
			
		||||
                    );
 | 
			
		||||
                    return Ok(req
 | 
			
		||||
                        .into_response(
 | 
			
		||||
                            HttpResponse::PreconditionFailed().json("Please authenticate!"),
 | 
			
		||||
                        )
 | 
			
		||||
                        .map_into_right_body());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            log::info!("{} - {}", remote_ip.0, req.path());
 | 
			
		||||
 | 
			
		||||
            service
 | 
			
		||||
                .call(req)
 | 
			
		||||
                .await
 | 
			
		||||
                .map(ServiceResponse::map_into_left_body)
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								remote_backend/src/middlewares/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								remote_backend/src/middlewares/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
pub mod auth_middleware;
 | 
			
		||||
		Reference in New Issue
	
	Block a user