
The EmptyContext is probably dumb or at least goofy.. but I can't work out a better way of making render() happy right now.
213 lines
5.3 KiB
Rust
213 lines
5.3 KiB
Rust
#![deny(warnings)]
|
|
#![feature(proc_macro_hygiene)]
|
|
#[macro_use] extern crate rocket;
|
|
|
|
use log::info;
|
|
use serde_derive::Serialize;
|
|
use std::env;
|
|
use std::fs;
|
|
use std::path::Path;
|
|
|
|
use biscuit::{
|
|
Empty,
|
|
};
|
|
use biscuit::jwa::{
|
|
SignatureAlgorithm,
|
|
Algorithm,
|
|
};
|
|
use biscuit::jwk::{
|
|
RSAKeyParameters,
|
|
CommonParameters,
|
|
AlgorithmParameters,
|
|
JWK,
|
|
JWKSet,
|
|
};
|
|
use num::BigUint;
|
|
use openssl::rsa::Rsa;
|
|
|
|
use ldap3::{ LdapConn, Scope, SearchEntry };
|
|
use rocket::request::Form;
|
|
use rocket_contrib::json::Json;
|
|
use rocket_contrib::templates::Template;
|
|
|
|
#[derive(Debug)]
|
|
struct BasicAuthentication {
|
|
pub username: String,
|
|
pub password: String,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum AuthError {
|
|
Parse,
|
|
Decode,
|
|
LdapBind,
|
|
LdapConfig,
|
|
LdapConnection,
|
|
LdapSearch,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct LdapUser {
|
|
pub dn: String,
|
|
pub groups: Vec<String>,
|
|
pub mail: Vec<String>,
|
|
pub services: Vec<String>,
|
|
}
|
|
|
|
fn auth_user(auth: &BasicAuthentication) -> Result<LdapUser, AuthError> {
|
|
let ldap_server_addr = match env::var("LDAP_SERVER_ADDR") {
|
|
Ok(addr) => addr,
|
|
_ => return Err(AuthError::LdapConfig),
|
|
};
|
|
let ldap = match LdapConn::new(&ldap_server_addr) {
|
|
Ok(conn) => conn,
|
|
Err(_err) => return Err(AuthError::LdapConnection),
|
|
};
|
|
|
|
let base = format!("uid={},ou=people,dc=xeentech,dc=com", auth.username);
|
|
match ldap.simple_bind(&base, &auth.password).unwrap().success() {
|
|
Ok(_ldap) => println!("Connected and authenticated"),
|
|
Err(_err) => return Err(AuthError::LdapBind),
|
|
};
|
|
|
|
let filter = format!("(uid={})", auth.username);
|
|
let s = match ldap.search(&base, Scope::Subtree, &filter, vec!["mail", "enabledService", "memberOf"]) {
|
|
Ok(result) => {
|
|
let (rs, _) = result.success().unwrap();
|
|
rs
|
|
},
|
|
Err(_err) => return Err(AuthError::LdapSearch),
|
|
};
|
|
|
|
// Grab the first, if any, result and discard the rest
|
|
let se = SearchEntry::construct(s.first().unwrap().to_owned());
|
|
let services = match se.attrs.get("enabledService") {
|
|
Some(services) => services.to_vec(),
|
|
None => [].to_vec(),
|
|
};
|
|
let mail = match se.attrs.get("mail") {
|
|
Some(mail) => mail.to_vec(),
|
|
None => [].to_vec(),
|
|
};
|
|
let groups = match se.attrs.get("memberOf") {
|
|
Some(groups) => groups.to_vec(),
|
|
None => [].to_vec(),
|
|
};
|
|
|
|
info!("Authentication success for {:?}", base);
|
|
Ok(LdapUser {
|
|
dn: base,
|
|
groups: groups,
|
|
mail: mail,
|
|
services: services,
|
|
})
|
|
}
|
|
|
|
#[derive(FromForm)]
|
|
struct LoginData {
|
|
username: String,
|
|
password: String,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct EmptyContext {
|
|
}
|
|
|
|
#[get("/login")]
|
|
fn login_form() -> Template {
|
|
let context = EmptyContext {};
|
|
Template::render("login_form", &context)
|
|
}
|
|
|
|
#[post("/login", data = "<form_data>")]
|
|
fn login(form_data: Form<LoginData>) -> String {
|
|
let auth = BasicAuthentication {
|
|
username: form_data.username.to_owned(),
|
|
password: form_data.password.to_owned(),
|
|
};
|
|
match auth_user(&auth) {
|
|
Ok(ldap_user) => format!("OK! {:?}", ldap_user),
|
|
_ => format!("Bad :("),
|
|
}
|
|
}
|
|
|
|
fn jwk_from_pem(file_path: &Path) -> Result<JWK<Empty>, Box<dyn std::error::Error + 'static>> {
|
|
let key_bytes = fs::read(file_path)?;
|
|
let rsa = Rsa::private_key_from_pem(key_bytes.as_slice())?;
|
|
Ok(JWK {
|
|
common: CommonParameters {
|
|
algorithm: Some(Algorithm::Signature(SignatureAlgorithm::RS256)),
|
|
key_id: Some(file_path.file_name().unwrap().to_str().unwrap().to_string()),
|
|
..Default::default()
|
|
},
|
|
algorithm: AlgorithmParameters::RSA(RSAKeyParameters {
|
|
n: BigUint::from_bytes_be(&rsa.n().to_vec()),
|
|
e: BigUint::from_bytes_be(&rsa.e().to_vec()),
|
|
..Default::default()
|
|
}),
|
|
additional: Default::default(),
|
|
})
|
|
}
|
|
|
|
#[get("/oauth2/keys")]
|
|
fn get_keys() -> Json<JWKSet<Empty>> {
|
|
let jwks: Vec<JWK<Empty>> = fs::read_dir("./").unwrap()
|
|
.filter_map(|dir_entry| {
|
|
let path = dir_entry.unwrap().path();
|
|
let ext = match path.extension() {
|
|
Some(ext) => ext.to_str().unwrap().to_owned(),
|
|
None => return None,
|
|
};
|
|
match ext.as_ref() {
|
|
"pem" => match jwk_from_pem(path.as_path()) {
|
|
Ok(jwk) => Some(jwk),
|
|
_ => None,
|
|
},
|
|
_ => None,
|
|
}
|
|
})
|
|
.collect();
|
|
let jwks = JWKSet { keys: jwks };
|
|
Json(jwks)
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct OidcConfig {
|
|
pub jwks_uri: String,
|
|
}
|
|
|
|
#[get("/.well-known/openid-configuration")]
|
|
fn oidc_config() -> Json<OidcConfig> {
|
|
let config = OidcConfig {
|
|
jwks_uri: "https://auth.xeen.dev/oauth2/keys".to_string(),
|
|
};
|
|
Json(config)
|
|
}
|
|
|
|
#[get("/")]
|
|
fn hello() -> Template {
|
|
let config = OidcConfig {
|
|
jwks_uri: "https://auth.xeen.dev/oauth2/keys".to_string(),
|
|
};
|
|
Template::render("hello", &config)
|
|
}
|
|
|
|
fn routes() -> Vec<rocket::Route> {
|
|
routes![
|
|
hello,
|
|
oidc_config,
|
|
get_keys,
|
|
login,
|
|
login_form,
|
|
]
|
|
}
|
|
|
|
fn main() {
|
|
env_logger::init();
|
|
|
|
rocket::ignite()
|
|
.attach(Template::fairing())
|
|
.mount("/", routes())
|
|
.launch();
|
|
}
|