mirror of
				https://github.com/nitnelave/lldap.git
				synced 2023-04-12 14:25:13 +00:00 
			
		
		
		
	Implement server-side JWT generation and checks
This commit is contained in:
		
							parent
							
								
									ccaa610b3c
								
							
						
					
					
						commit
						7e76d3aae2
					
				@ -8,6 +8,7 @@ version = "0.1.0"
 | 
			
		||||
actix-files = { git = "https://github.com/actix/actix-web", rev = "a9dc1586a0935c48c3f841761bf81c43ca9e2651" }
 | 
			
		||||
actix-http = { git = "https://github.com/actix/actix-web", rev = "a9dc1586a0935c48c3f841761bf81c43ca9e2651" }
 | 
			
		||||
actix-web = { git = "https://github.com/actix/actix-web", rev = "a9dc1586a0935c48c3f841761bf81c43ca9e2651" }
 | 
			
		||||
actix-web-httpauth = { git = "https://github.com/nhruo123/actix-extras", rev = "b4e8db446843a99b06c7ec40f18ef7b59ee7e955" }
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
actix = "0.11.1"
 | 
			
		||||
@ -17,6 +18,7 @@ actix-rt = "2.2"
 | 
			
		||||
actix-server = "2.0.0-beta.5"
 | 
			
		||||
actix-service = "2.0.0"
 | 
			
		||||
actix-web = "4.0.0-beta.6"
 | 
			
		||||
actix-web-httpauth = "0.6.0-beta.1"
 | 
			
		||||
anyhow = "*"
 | 
			
		||||
clap = "3.0.0-beta.2"
 | 
			
		||||
chrono = { version = "*", features = [ "serde" ]}
 | 
			
		||||
@ -39,6 +41,10 @@ sea-query = { version = "0.9.4", features = [ "with-chrono" ] }
 | 
			
		||||
lldap_model = { path = "model" }
 | 
			
		||||
lldap_app = { path = "app" }
 | 
			
		||||
serde_json = "1.0.64"
 | 
			
		||||
jwt = "0.13.0"
 | 
			
		||||
hmac = "0.10"
 | 
			
		||||
sha2 = "0.9.5"
 | 
			
		||||
time = "0.2.26"
 | 
			
		||||
 | 
			
		||||
[dependencies.figment]
 | 
			
		||||
features = ["toml", "env"]
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@ pub struct Configuration {
 | 
			
		||||
    pub ldaps_port: u16,
 | 
			
		||||
    pub http_port: u16,
 | 
			
		||||
    pub secret_pepper: String,
 | 
			
		||||
    pub jwt_secret: String,
 | 
			
		||||
    pub ldap_base_dn: String,
 | 
			
		||||
    pub ldap_user_dn: String,
 | 
			
		||||
    pub ldap_user_pass: String,
 | 
			
		||||
@ -27,6 +28,7 @@ impl Default for Configuration {
 | 
			
		||||
            ldaps_port: 6360,
 | 
			
		||||
            http_port: 17170,
 | 
			
		||||
            secret_pepper: String::from("secretsecretpepper"),
 | 
			
		||||
            jwt_secret: String::from("secretjwtsecret"),
 | 
			
		||||
            ldap_base_dn: String::from("dc=example,dc=com"),
 | 
			
		||||
            ldap_user_dn: String::from("cn=admin,dc=example,dc=com"),
 | 
			
		||||
            ldap_user_pass: String::from("password"),
 | 
			
		||||
 | 
			
		||||
@ -3,10 +3,36 @@ use crate::infra::configuration::Configuration;
 | 
			
		||||
use actix_files::{Files, NamedFile};
 | 
			
		||||
use actix_http::HttpServiceBuilder;
 | 
			
		||||
use actix_server::ServerBuilder;
 | 
			
		||||
use actix_service::map_config;
 | 
			
		||||
use actix_web::{dev::AppConfig, web, App, HttpRequest, HttpResponse};
 | 
			
		||||
use actix_service::{map_config, Service};
 | 
			
		||||
use actix_web::{
 | 
			
		||||
    cookie::Cookie,
 | 
			
		||||
    dev::{AppConfig, ServiceRequest},
 | 
			
		||||
    error::{ErrorBadRequest, ErrorUnauthorized},
 | 
			
		||||
    web, App, HttpRequest, HttpResponse,
 | 
			
		||||
};
 | 
			
		||||
use actix_web_httpauth::{extractors::bearer::BearerAuth, middleware::HttpAuthentication};
 | 
			
		||||
use anyhow::{Context, Result};
 | 
			
		||||
use chrono::prelude::*;
 | 
			
		||||
use futures_util::FutureExt;
 | 
			
		||||
use futures_util::TryFutureExt;
 | 
			
		||||
use hmac::{Hmac, NewMac};
 | 
			
		||||
use jwt::{SignWithKey, VerifyWithKey};
 | 
			
		||||
use log::*;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use sha2::Sha512;
 | 
			
		||||
use std::collections::HashSet;
 | 
			
		||||
use std::path::PathBuf;
 | 
			
		||||
use time::ext::NumericalDuration;
 | 
			
		||||
 | 
			
		||||
type Token<S> = jwt::Token<jwt::Header, JWTClaims, S>;
 | 
			
		||||
type SignedToken = Token<jwt::token::Signed>;
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize)]
 | 
			
		||||
struct JWTClaims {
 | 
			
		||||
    exp: DateTime<Utc>,
 | 
			
		||||
    user: String,
 | 
			
		||||
    groups: HashSet<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn index(req: HttpRequest) -> actix_web::Result<NamedFile> {
 | 
			
		||||
    let mut path = PathBuf::new();
 | 
			
		||||
@ -43,6 +69,55 @@ where
 | 
			
		||||
        .unwrap_or_else(error_to_http_response)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn create_jwt(key: &Hmac<Sha512>, user: String, groups: HashSet<String>) -> SignedToken {
 | 
			
		||||
    let claims = JWTClaims {
 | 
			
		||||
        exp: Utc::now() + chrono::Duration::days(1),
 | 
			
		||||
        user,
 | 
			
		||||
        groups,
 | 
			
		||||
    };
 | 
			
		||||
    let header = jwt::Header {
 | 
			
		||||
        algorithm: jwt::AlgorithmType::Hs512,
 | 
			
		||||
        ..Default::default()
 | 
			
		||||
    };
 | 
			
		||||
    jwt::Token::new(header, claims).sign_with_key(key).unwrap()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn post_authorize<Backend>(
 | 
			
		||||
    data: web::Data<AppState<Backend>>,
 | 
			
		||||
    request: web::Json<BindRequest>,
 | 
			
		||||
) -> ApiResult<String>
 | 
			
		||||
where
 | 
			
		||||
    Backend: BackendHandler + 'static,
 | 
			
		||||
{
 | 
			
		||||
    let req: BindRequest = request.clone();
 | 
			
		||||
    data.backend_handler
 | 
			
		||||
        .bind(req)
 | 
			
		||||
        // If the authentication was successful, we need to fetch the groups to create the JWT
 | 
			
		||||
        // token.
 | 
			
		||||
        .and_then(|_| data.backend_handler.get_user_groups(request.name.clone()))
 | 
			
		||||
        .await
 | 
			
		||||
        .map(|groups| {
 | 
			
		||||
            let token = create_jwt(&data.jwt_key, request.name.clone(), groups);
 | 
			
		||||
            ApiResult::Right(
 | 
			
		||||
                HttpResponse::Ok()
 | 
			
		||||
                    .cookie(
 | 
			
		||||
                        Cookie::build("token", token.as_str())
 | 
			
		||||
                            .max_age(1.days())
 | 
			
		||||
                            .path("/api")
 | 
			
		||||
                            .http_only(true)
 | 
			
		||||
                            .finish(),
 | 
			
		||||
                    )
 | 
			
		||||
                    .cookie(
 | 
			
		||||
                        Cookie::build("user_id", &request.name)
 | 
			
		||||
                            .max_age(1.days())
 | 
			
		||||
                            .finish(),
 | 
			
		||||
                    )
 | 
			
		||||
                    .body(token.as_str().to_owned()),
 | 
			
		||||
            )
 | 
			
		||||
        })
 | 
			
		||||
        .unwrap_or_else(error_to_http_response)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn api_config<Backend>(cfg: &mut web::ServiceConfig)
 | 
			
		||||
where
 | 
			
		||||
    Backend: BackendHandler + 'static,
 | 
			
		||||
@ -66,11 +141,79 @@ where
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn token_validator<Backend>(
 | 
			
		||||
    req: ServiceRequest,
 | 
			
		||||
    credentials: BearerAuth,
 | 
			
		||||
) -> Result<ServiceRequest, actix_web::Error>
 | 
			
		||||
where
 | 
			
		||||
    Backend: BackendHandler + 'static,
 | 
			
		||||
{
 | 
			
		||||
    let state = req
 | 
			
		||||
        .app_data::<web::Data<AppState<Backend>>>()
 | 
			
		||||
        .expect("Invalid app config");
 | 
			
		||||
    let token: Token<_> = VerifyWithKey::verify_with_key(credentials.token(), &state.jwt_key)
 | 
			
		||||
        .map_err(|_| ErrorUnauthorized("Invalid JWT"))?;
 | 
			
		||||
    if token.claims().exp.lt(&Utc::now()) {
 | 
			
		||||
        return Err(ErrorUnauthorized("Expired JWT"));
 | 
			
		||||
    }
 | 
			
		||||
    let groups = &token.claims().groups;
 | 
			
		||||
    if groups.contains("lldap_admin") {
 | 
			
		||||
        debug!("Got authorized token for user {}", &token.claims().user);
 | 
			
		||||
        Ok(req)
 | 
			
		||||
    } else {
 | 
			
		||||
        Err(ErrorUnauthorized(
 | 
			
		||||
            "JWT error: User is not in group lldap_admin",
 | 
			
		||||
        ))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn http_config<Backend>(cfg: &mut web::ServiceConfig, backend_handler: Backend, jwt_secret: String)
 | 
			
		||||
where
 | 
			
		||||
    Backend: BackendHandler + 'static,
 | 
			
		||||
{
 | 
			
		||||
    cfg.data(AppState::<Backend> {
 | 
			
		||||
        backend_handler,
 | 
			
		||||
        jwt_key: Hmac::new_varkey(&jwt_secret.as_bytes()).unwrap(),
 | 
			
		||||
    })
 | 
			
		||||
    // Serve index.html and main.js, and default to index.html.
 | 
			
		||||
    .route(
 | 
			
		||||
        "/{filename:(index\\.html|main\\.js)?}",
 | 
			
		||||
        web::get().to(index),
 | 
			
		||||
    )
 | 
			
		||||
    .service(web::resource("/authorize").route(web::post().to(post_authorize::<Backend>)))
 | 
			
		||||
    // API endpoint.
 | 
			
		||||
    .service(
 | 
			
		||||
        web::scope("/api")
 | 
			
		||||
            .wrap(HttpAuthentication::bearer(token_validator::<Backend>))
 | 
			
		||||
            .wrap_fn(|mut req, srv| {
 | 
			
		||||
                if let Some(token_cookie) = req.cookie("token") {
 | 
			
		||||
                    if let Ok(header_value) = actix_http::header::HeaderValue::from_str(&format!(
 | 
			
		||||
                        "Bearer {}",
 | 
			
		||||
                        token_cookie.value()
 | 
			
		||||
                    )) {
 | 
			
		||||
                        req.headers_mut()
 | 
			
		||||
                            .insert(actix_http::header::AUTHORIZATION, header_value);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        return async move {
 | 
			
		||||
                            Ok(req.error_response(ErrorBadRequest("Invalid token cookie")))
 | 
			
		||||
                        }
 | 
			
		||||
                        .boxed_local();
 | 
			
		||||
                    }
 | 
			
		||||
                };
 | 
			
		||||
                Box::pin(srv.call(req))
 | 
			
		||||
            })
 | 
			
		||||
            .configure(api_config::<Backend>),
 | 
			
		||||
    )
 | 
			
		||||
    // Serve the /pkg path with the compiled WASM app.
 | 
			
		||||
    .service(Files::new("/pkg", "./app/pkg"));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct AppState<Backend>
 | 
			
		||||
where
 | 
			
		||||
    Backend: BackendHandler + 'static,
 | 
			
		||||
{
 | 
			
		||||
    pub backend_handler: Backend,
 | 
			
		||||
    pub jwt_key: Hmac<Sha512>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn build_tcp_server<Backend>(
 | 
			
		||||
@ -81,31 +224,18 @@ pub fn build_tcp_server<Backend>(
 | 
			
		||||
where
 | 
			
		||||
    Backend: BackendHandler + 'static,
 | 
			
		||||
{
 | 
			
		||||
    let http_port = config.http_port.clone();
 | 
			
		||||
    let jwt_secret = config.jwt_secret.clone();
 | 
			
		||||
    server_builder
 | 
			
		||||
        .bind("http", ("0.0.0.0", config.http_port), move || {
 | 
			
		||||
            let backend_handler = backend_handler.clone();
 | 
			
		||||
            let jwt_secret = jwt_secret.clone();
 | 
			
		||||
            HttpServiceBuilder::new()
 | 
			
		||||
                .finish(map_config(
 | 
			
		||||
                    App::new()
 | 
			
		||||
                        .data(AppState::<Backend> {
 | 
			
		||||
                            backend_handler: backend_handler.clone(),
 | 
			
		||||
                        })
 | 
			
		||||
                        // Serve index.html and main.js, and default to index.html.
 | 
			
		||||
                        .route(
 | 
			
		||||
                            "/{filename:(index\\.html|main\\.js)?}",
 | 
			
		||||
                            web::get().to(index),
 | 
			
		||||
                        )
 | 
			
		||||
                        // API endpoint.
 | 
			
		||||
                        .service(web::scope("/api").configure(api_config::<Backend>))
 | 
			
		||||
                        // Serve the /pkg path with the compiled WASM app.
 | 
			
		||||
                        .service(Files::new("/pkg", "./app/pkg")),
 | 
			
		||||
                    App::new().configure(move |cfg| http_config(cfg, backend_handler, jwt_secret)),
 | 
			
		||||
                    |_| AppConfig::default(),
 | 
			
		||||
                ))
 | 
			
		||||
                .tcp()
 | 
			
		||||
        })
 | 
			
		||||
        .with_context(|| {
 | 
			
		||||
            format!(
 | 
			
		||||
                "While bringing up the TCP server with port {}",
 | 
			
		||||
                config.http_port
 | 
			
		||||
            )
 | 
			
		||||
        })
 | 
			
		||||
        .with_context(|| format!("While bringing up the TCP server with port {}", http_port))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user