Add OpenID routes
This commit is contained in:
		@@ -8,4 +8,10 @@ pub const MAX_INACTIVITY_DURATION: u64 = 60 * 30;
 | 
			
		||||
pub const MAX_SESSION_DURATION: u64 = 3600 * 6;
 | 
			
		||||
 | 
			
		||||
/// The routes that can be accessed without authentication
 | 
			
		||||
pub const ROUTES_WITHOUT_AUTH: [&str; 3] = ["/", "/api/server/static_config", "/api/auth/local"];
 | 
			
		||||
pub const ROUTES_WITHOUT_AUTH: [&str; 5] = [
 | 
			
		||||
    "/",
 | 
			
		||||
    "/api/server/static_config",
 | 
			
		||||
    "/api/auth/local",
 | 
			
		||||
    "/api/auth/start_oidc",
 | 
			
		||||
    "/api/auth/finish_oidc",
 | 
			
		||||
];
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,12 @@
 | 
			
		||||
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;
 | 
			
		||||
use crate::extractors::local_auth_extractor::LocalAuthEnabled;
 | 
			
		||||
use actix_web::{web, HttpResponse, Responder};
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize)]
 | 
			
		||||
pub struct LocalAuthReq {
 | 
			
		||||
@@ -30,6 +35,87 @@ pub async fn local_auth(
 | 
			
		||||
    HttpResponse::Accepted().json("Welcome")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize)]
 | 
			
		||||
struct StartOIDCResponse {
 | 
			
		||||
    url: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Start OIDC authentication
 | 
			
		||||
pub async fn start_oidc(sm: Data<BasicStateManager>, ip: RemoteIP) -> HttpResult {
 | 
			
		||||
    let prov = match AppConfig::get().openid_provider() {
 | 
			
		||||
        None => {
 | 
			
		||||
            return Ok(HttpResponse::UnprocessableEntity().json("OpenID is disabled!"));
 | 
			
		||||
        }
 | 
			
		||||
        Some(conf) => conf,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    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 = match AppConfig::get().openid_provider() {
 | 
			
		||||
        None => {
 | 
			
		||||
            return Ok(HttpResponse::UnprocessableEntity().json("OpenID is disabled!"));
 | 
			
		||||
        }
 | 
			
		||||
        Some(conf) => conf,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    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,
 | 
			
		||||
@@ -41,3 +127,9 @@ pub async fn current_user(auth: AuthExtractor) -> impl Responder {
 | 
			
		||||
        id: auth.id().unwrap(),
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Sign out
 | 
			
		||||
pub async fn sign_out(auth: AuthExtractor) -> impl Responder {
 | 
			
		||||
    auth.sign_out();
 | 
			
		||||
    HttpResponse::Ok().finish()
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,2 +1,61 @@
 | 
			
		||||
use actix_web::body::BoxBody;
 | 
			
		||||
use actix_web::HttpResponse;
 | 
			
		||||
use std::error::Error;
 | 
			
		||||
use std::fmt::{Display, Formatter};
 | 
			
		||||
use std::io::ErrorKind;
 | 
			
		||||
 | 
			
		||||
pub mod auth_controller;
 | 
			
		||||
pub mod server_controller;
 | 
			
		||||
 | 
			
		||||
/// Custom error to ease controller writing
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct HttpErr {
 | 
			
		||||
    err: anyhow::Error,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Display for HttpErr {
 | 
			
		||||
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
 | 
			
		||||
        Display::fmt(&self.err, f)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl actix_web::error::ResponseError for HttpErr {
 | 
			
		||||
    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 }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<serde_json::Error> for HttpErr {
 | 
			
		||||
    fn from(value: serde_json::Error) -> Self {
 | 
			
		||||
        HttpErr { err: value.into() }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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() }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub type HttpResult = Result<HttpResponse, HttpErr>;
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,9 @@ use actix_session::storage::CookieSessionStore;
 | 
			
		||||
use actix_session::SessionMiddleware;
 | 
			
		||||
use actix_web::cookie::{Key, SameSite};
 | 
			
		||||
use actix_web::middleware::Logger;
 | 
			
		||||
use actix_web::web::Data;
 | 
			
		||||
use actix_web::{web, App, HttpServer};
 | 
			
		||||
use light_openid::basic_state_manager::BasicStateManager;
 | 
			
		||||
use std::time::Duration;
 | 
			
		||||
use virtweb_backend::app_config::AppConfig;
 | 
			
		||||
use virtweb_backend::constants::{
 | 
			
		||||
@@ -20,7 +22,9 @@ async fn main() -> std::io::Result<()> {
 | 
			
		||||
 | 
			
		||||
    log::info!("Start to listen on {}", AppConfig::get().listen_address);
 | 
			
		||||
 | 
			
		||||
    HttpServer::new(|| {
 | 
			
		||||
    let state_manager = Data::new(BasicStateManager::new());
 | 
			
		||||
 | 
			
		||||
    HttpServer::new(move || {
 | 
			
		||||
        let session_mw = SessionMiddleware::builder(
 | 
			
		||||
            CookieSessionStore::default(),
 | 
			
		||||
            Key::from(AppConfig::get().secret().as_bytes()),
 | 
			
		||||
@@ -41,7 +45,8 @@ async fn main() -> std::io::Result<()> {
 | 
			
		||||
            .wrap(AuthChecker)
 | 
			
		||||
            .wrap(identity_middleware)
 | 
			
		||||
            .wrap(session_mw)
 | 
			
		||||
            .app_data(web::Data::new(RemoteIPConfig {
 | 
			
		||||
            .app_data(state_manager.clone())
 | 
			
		||||
            .app_data(Data::new(RemoteIPConfig {
 | 
			
		||||
                proxy: AppConfig::get().proxy_ip.clone(),
 | 
			
		||||
            }))
 | 
			
		||||
            // Server controller
 | 
			
		||||
@@ -55,10 +60,22 @@ async fn main() -> std::io::Result<()> {
 | 
			
		||||
                "/api/auth/local",
 | 
			
		||||
                web::post().to(auth_controller::local_auth),
 | 
			
		||||
            )
 | 
			
		||||
            .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()
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user