add PAM auth backend
This commit is contained in:
parent
74f12d0b55
commit
35f706b537
9 changed files with 258 additions and 31 deletions
140
Cargo.lock
generated
140
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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))"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
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
|
||||
|
|
57
src/auth/backends/pam_backend.rs
Normal file
57
src/auth/backends/pam_backend.rs
Normal 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())
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
Loading…
Reference in a new issue