Compare commits

...

2 commits

Author SHA1 Message Date
35f706b537 add PAM auth backend 2024-10-31 18:37:19 +01:00
74f12d0b55 move LdapBackend to new submodule 2024-10-31 17:15:38 +01:00
10 changed files with 274 additions and 44 deletions

140
Cargo.lock generated
View file

@ -252,6 +252,26 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bindgen"
version = "0.69.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"itertools",
"lazy_static",
"lazycell",
"proc-macro2",
"quote",
"regex",
"rustc-hash 1.1.0",
"shlex",
"syn 2.0.79",
]
[[package]]
name = "bitflags"
version = "2.6.0"
@ -297,12 +317,31 @@ dependencies = [
"shlex",
]
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
]
[[package]]
name = "clap"
version = "4.5.20"
@ -496,6 +535,17 @@ dependencies = [
"powerfmt",
]
[[package]]
name = "derive-debug"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e53ef7e1cf756fd5a8e74b9a0a9504ec446eddde86c3063a76ff26a13b7773b1"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "digest"
version = "0.10.7"
@ -543,6 +593,18 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "enum_dispatch"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd"
dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.79",
]
[[package]]
name = "equivalent"
version = "1.0.1"
@ -600,10 +662,13 @@ dependencies = [
"base64 0.22.1",
"clap",
"color-eyre",
"derive-debug",
"enum_dispatch",
"hex",
"hmac",
"ldap3",
"md-5",
"pam",
"rand",
"reqwest",
"secrecy",
@ -779,6 +844,12 @@ version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "glob"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "h2"
version = "0.4.6"
@ -1013,6 +1084,15 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itertools"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.11"
@ -1037,6 +1117,12 @@ dependencies = [
"spin 0.9.8",
]
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "lber"
version = "0.4.2"
@ -1308,6 +1394,40 @@ version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
[[package]]
name = "pam"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ab553c52103edb295d8f7d6a3b593dc22a30b1fb99643c777a8f36915e285ba"
dependencies = [
"libc",
"memchr",
"pam-macros",
"pam-sys",
"users",
]
[[package]]
name = "pam-macros"
version = "0.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c94f3b9b97df3c6d4e51a14916639b24e02c7d15d1dba686ce9b1118277cb811"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "pam-sys"
version = "1.0.0-alpha5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce9484729b3e52c0bacdc5191cb6a6a5f31ef4c09c5e4ab1209d3340ad9e997b"
dependencies = [
"bindgen",
"libc",
]
[[package]]
name = "parking"
version = "2.2.1"
@ -1431,7 +1551,7 @@ dependencies = [
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustc-hash 2.0.0",
"rustls 0.23.14",
"socket2",
"thiserror",
@ -1448,7 +1568,7 @@ dependencies = [
"bytes",
"rand",
"ring 0.17.8",
"rustc-hash",
"rustc-hash 2.0.0",
"rustls 0.23.14",
"slab",
"thiserror",
@ -1662,6 +1782,12 @@ version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc-hash"
version = "2.0.0"
@ -2690,6 +2816,16 @@ dependencies = [
"serde",
]
[[package]]
name = "users"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa4227e95324a443c9fcb06e03d4d85e91aabe9a5a02aa818688b6918b6af486"
dependencies = [
"libc",
"log",
]
[[package]]
name = "utf8parse"
version = "0.2.2"

View file

@ -21,10 +21,13 @@ axum = "0.7.4"
base64 = "0.22.0"
clap = { version = "4.5.0", features = ["derive"] }
color-eyre = { version = "0.6.2" }
derive-debug = "0.1.2"
enum_dispatch = "0.3.13"
hex = { version = "0.4.3", features = ["serde"] }
hmac = "0.12.1"
ldap3 = { version = "0.11.3", default-features = false, features = ["tls-rustls"] }
md-5 = "0.10.6"
pam = "0.8.0"
rand = "0.8.5"
reqwest = { version = "0.12.0", default-features = false, features = ["charset", "http2", "macos-system-configuration", "json", "rustls-tls-native-roots"] }
secrecy = { version = "0.10.0", features = ["serde"] }

View file

@ -13,8 +13,8 @@ which is no good, especially for PvP scenarios! Wouldn't it be great if you coul
authentication server?
factoriauth is exactly that. It allows clients to log in as custom users provided by one of several
authentication backends (e.g. LDAP or a passwd-style file), and allows servers to validate that
these custom users are properly authenticated.
authentication backends (e.g. local users via PAM, LDAP, or a passwd-style file), and allows servers
to validate that these custom users are properly authenticated.
## Roadmap
@ -25,12 +25,13 @@ these custom users are properly authenticated.
- [user token generation](https://wiki.factorio.com/Web_authentication_API) and storage (`POST
/api-login`)
- LDAP authentication backend
- PAM authentication backend
- server padlock proxying (to allow e.g. factorio.com users to join servers using a custom auth
server)
### Planned
- more authentication backends: user file, PAM(?)
- more authentication backends: user file
### Unplanned

View file

@ -7,8 +7,14 @@ secret = ""
[database]
connection-string = "sqlite://sqlite.db"
[[auth-backends]]
type = "LDAP"
server-address = "ldap://ldap.example.com"
search-base = "ou=users,dc=example,dc=com"
user-filter = "(|(uid=%s)(mail=%s))"
# uncomment to allow login as local user via PAM
#[[auth-backends]]
#type = "PAM"
#service-name = "system-auth"
# uncomment and configure to allow login via LDAP
#[[auth-backends]]
#type = "LDAP"
#server-address = "ldap://ldap.example.com"
#search-base = "ou=users,dc=example,dc=com"
#user-filter = "(|(uid=%s)(mail=%s))"

View file

@ -5,19 +5,9 @@ use secrecy::ExposeSecret;
use tokio::{sync::Mutex, time::sleep};
use tracing::{event, instrument, Level};
use crate::{config::LdapBackendConfig, secrets::Password};
use crate::{auth::AuthenticationError, config::LdapBackendConfig, secrets::Password};
use super::AuthenticationError;
pub trait ValidateLogin {
/// Validates that the given username and password combination is correct, and returns the
/// (normalized) user ID.
async fn validate_login(
&self,
username: &str,
password: &Password,
) -> Result<String, AuthenticationError>;
}
use super::ValidateLogin;
#[instrument]
async fn start_ldap_connection(

14
src/auth/backends/mod.rs Normal file
View file

@ -0,0 +1,14 @@
use crate::{auth::AuthenticationError, secrets::Password};
pub mod ldap_backend;
pub mod pam_backend;
pub trait ValidateLogin {
/// Validates that the given username and password combination is correct, and returns the
/// (normalized) user ID.
async fn validate_login(
&self,
username: &str,
password: &Password,
) -> Result<String, AuthenticationError>;
}

View file

@ -0,0 +1,57 @@
use derive_debug::Dbg;
use secrecy::ExposeSecret;
use std::sync::Arc;
use tokio::sync::Mutex;
use tracing::{event, Level};
use pam::{Client, PasswordConv};
use crate::{auth::AuthenticationError, config::PamBackendConfig, secrets::Password};
use super::ValidateLogin;
#[derive(Dbg)]
pub struct PamBackend<'a> {
#[dbg(skip)]
pam: Arc<Mutex<pam::Client<'a, PasswordConv>>>,
config: PamBackendConfig,
}
impl PamBackend<'_> {
pub fn new(config: PamBackendConfig) -> Result<Arc<Self>, AuthenticationError> {
let pam = Client::with_password(&config.service_name)?;
event!(Level::INFO, "PAM client initialized");
Ok(Arc::new(Self {
pam: Arc::new(Mutex::new(pam)),
config,
}))
}
}
impl ValidateLogin for PamBackend<'_> {
async fn validate_login(
&self,
username: &str,
password: &Password,
) -> Result<String, AuthenticationError> {
let mut pam = self.pam.lock().await;
pam.conversation_mut()
.set_credentials(username, password.0.expose_secret());
if let Err(pam_error) = pam.authenticate() {
event!(
Level::INFO,
username,
pam_error = pam_error.to_string(),
"PAM authentication failed"
);
return Err(AuthenticationError::InvalidUserOrPassword);
}
event!(Level::INFO, username, "PAM authentication succeeded");
Ok(username.to_string())
}
}

View file

@ -1,5 +1,6 @@
use std::sync::Arc;
use backends::pam_backend::PamBackend;
use base64::{prelude::BASE64_STANDARD, Engine};
use hmac::{Hmac, Mac};
use md5::Md5;
@ -24,7 +25,7 @@ use crate::{
},
};
use self::backends::{LdapBackend, ValidateLogin};
use self::backends::{ldap_backend::LdapBackend, ValidateLogin};
pub mod backends;
@ -38,8 +39,10 @@ pub enum AuthenticationError {
InvalidServerHash,
#[error("Database error")]
Database(#[from] sqlx::Error),
#[error("Authentication backend error")]
Backend(#[from] ldap3::LdapError),
#[error("LDAP backend error")]
LdapBackend(#[from] ldap3::LdapError),
#[error("PAM backend error")]
PamBackend(#[from] pam::PamError),
#[error("Padlock proxy error")]
PadlockProxy(#[from] PadlockProxyError),
#[error("No authentication backends available")]
@ -47,19 +50,21 @@ pub enum AuthenticationError {
}
#[derive(Debug)]
pub enum AuthenticationBackend {
pub enum AuthenticationBackend<'a> {
Ldap(Arc<LdapBackend>),
Pam(Arc<PamBackend<'a>>),
}
impl AuthenticationBackend {
impl AuthenticationBackend<'_> {
pub async fn new(config: AuthBackendConfig) -> Result<Self, AuthenticationError> {
match config {
AuthBackendConfig::Ldap(c) => Ok(Self::Ldap(LdapBackend::new(c).await?)),
AuthBackendConfig::Pam(c) => Ok(Self::Pam(PamBackend::new(c)?)),
}
}
}
impl ValidateLogin for AuthenticationBackend {
impl ValidateLogin for AuthenticationBackend<'_> {
async fn validate_login(
&self,
username: &str,
@ -67,22 +72,23 @@ impl ValidateLogin for AuthenticationBackend {
) -> Result<String, AuthenticationError> {
match self {
AuthenticationBackend::Ldap(b) => b.validate_login(username, password).await,
AuthenticationBackend::Pam(b) => b.validate_login(username, password).await,
}
}
}
#[derive(Debug)]
pub struct UserAuthenticator {
pub struct UserAuthenticator<'a> {
db: Arc<Mutex<Box<dyn Database + Send>>>,
backends: Vec<AuthenticationBackend>,
backends: Vec<AuthenticationBackend<'a>>,
}
impl UserAuthenticator {
impl<'a> UserAuthenticator<'a> {
const TOKEN_LEN: usize = 30;
pub fn new(
db: Arc<Mutex<Box<dyn Database + Send>>>,
backends: Vec<AuthenticationBackend>,
backends: Vec<AuthenticationBackend<'a>>,
) -> Self {
Self { db, backends }
}
@ -298,14 +304,14 @@ impl ServerPadlockGenerator {
}
#[derive(Debug)]
pub struct UserServerKeyGenerator {
user_authenticator: Arc<UserAuthenticator>,
pub struct UserServerKeyGenerator<'a> {
user_authenticator: Arc<UserAuthenticator<'a>>,
padlock_generator: Arc<ServerPadlockGenerator>,
}
impl UserServerKeyGenerator {
impl<'a> UserServerKeyGenerator<'a> {
pub fn new(
user_authenticator: Arc<UserAuthenticator>,
user_authenticator: Arc<UserAuthenticator<'a>>,
padlock_generator: Arc<ServerPadlockGenerator>,
) -> Self {
Self {

View file

@ -42,6 +42,8 @@ pub struct DatabaseConfig {
pub enum AuthBackendConfig {
#[serde(rename = "LDAP")]
Ldap(LdapBackendConfig),
#[serde(rename = "PAM")]
Pam(PamBackendConfig)
}
#[derive(Debug, Clone, Deserialize)]
@ -53,3 +55,18 @@ pub struct LdapBackendConfig {
/// User filter template. All occurences of `%s` will be replaced with the username.
pub user_filter: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[allow(clippy::module_name_repetitions)]
pub struct PamBackendConfig {
pub service_name: String,
}
impl Default for PamBackendConfig {
fn default() -> Self {
Self {
service_name: "system-auth".to_string()
}
}
}

View file

@ -18,18 +18,18 @@ use crate::auth::{
use crate::secrets::{Password, ServerHash, UserToken};
#[derive(Debug, Clone)]
struct AppState {
user_authenticator: Arc<UserAuthenticator>,
struct AppState<'a> {
user_authenticator: Arc<UserAuthenticator<'a>>,
server_padlock_generator: Arc<ServerPadlockGenerator>,
user_server_key_generator: Arc<UserServerKeyGenerator>,
user_server_key_generator: Arc<UserServerKeyGenerator<'a>>,
}
#[instrument]
pub async fn run(
listen: SocketAddr,
user_authenticator: Arc<UserAuthenticator>,
user_authenticator: Arc<UserAuthenticator<'static>>,
server_padlock_generator: Arc<ServerPadlockGenerator>,
user_server_key_generator: Arc<UserServerKeyGenerator>,
user_server_key_generator: Arc<UserServerKeyGenerator<'static>>,
) -> color_eyre::Result<()> {
let app_state = AppState {
user_authenticator,
@ -110,7 +110,7 @@ struct LoginResponse {
async fn api_login(
State(AppState {
user_authenticator, ..
}): State<AppState>,
}): State<AppState<'_>>,
Query(ApiVersion { api_version }): Query<ApiVersion>,
Form(LoginRequest { username, password }): Form<LoginRequest>,
) -> ApiResult<Json<LoginResponse>> {
@ -144,7 +144,7 @@ async fn generate_user_server_key_2(
State(AppState {
user_server_key_generator,
..
}): State<AppState>,
}): State<AppState<'_>>,
Query(ApiVersion { api_version }): Query<ApiVersion>,
Form(UserServerKeyRequest {
username,
@ -175,7 +175,7 @@ async fn generate_server_padlock_2(
State(AppState {
server_padlock_generator,
..
}): State<AppState>,
}): State<AppState<'_>>,
Query(ApiVersion { api_version }): Query<ApiVersion>,
) -> ApiResult<Json<ServerPadlockResponse>> {
event!(Level::INFO, "Creating server padlock");