Start to implement OpenID authentication
This commit is contained in:
parent
48f24e6ca1
commit
d8946eb462
47
remote_backend/Cargo.lock
generated
47
remote_backend/Cargo.lock
generated
@ -58,6 +58,22 @@ dependencies = [
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-identity"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2c99b7a5614b72a78f04aa2021e5370fc1aef2475fffeffc0c1266b99007062"
|
||||
dependencies = [
|
||||
"actix-service",
|
||||
"actix-session",
|
||||
"actix-utils",
|
||||
"actix-web",
|
||||
"derive_more",
|
||||
"futures-core",
|
||||
"serde",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-macros"
|
||||
version = "0.2.4"
|
||||
@ -130,6 +146,22 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-session"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b671404ec72194d8af58c2bdaf51e3c477a0595056bd5010148405870dda8df2"
|
||||
dependencies = [
|
||||
"actix-service",
|
||||
"actix-utils",
|
||||
"actix-web",
|
||||
"anyhow",
|
||||
"derive_more",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-utils"
|
||||
version = "3.0.1"
|
||||
@ -360,6 +392,12 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
@ -565,7 +603,14 @@ version = "0.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"base64 0.20.0",
|
||||
"hkdf",
|
||||
"hmac",
|
||||
"percent-encoding",
|
||||
"rand",
|
||||
"sha2",
|
||||
"subtle",
|
||||
"time",
|
||||
"version_check",
|
||||
]
|
||||
@ -1646,12 +1691,14 @@ checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
|
||||
name = "remote_backend"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"actix-identity",
|
||||
"actix-remote-ip",
|
||||
"actix-web",
|
||||
"anyhow",
|
||||
"basic-jwt",
|
||||
"clap",
|
||||
"env_logger",
|
||||
"futures-util",
|
||||
"lazy_static",
|
||||
"light-openid",
|
||||
"log",
|
||||
|
@ -12,10 +12,12 @@ clap = { version = "4.5.4", features = ["derive", "env"] }
|
||||
serde = { version = "1.0.198", features = ["derive"] }
|
||||
light-openid = { version = "1.0.2", features = ["crypto-wrapper"] }
|
||||
basic-jwt = "0.2.0"
|
||||
actix-remote-ip = "0.1.0"
|
||||
lazy_static = "1.4.0"
|
||||
actix-web = "4.5.1"
|
||||
actix-remote-ip = "0.1.0"
|
||||
actix-identity = "0.7.1"
|
||||
lazy_static = "1.4.0"
|
||||
anyhow = "1.0.82"
|
||||
reqwest = { version = "0.12.4", features = ["json"] }
|
||||
thiserror = "1.0.59"
|
||||
uuid = { version = "1.8.0", features = ["v4"] }
|
||||
futures-util = "0.3.30"
|
@ -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;
|
Loading…
Reference in New Issue
Block a user