Implement padlock proxy

This commit is contained in:
Xiretza 2024-02-20 21:57:03 +00:00
parent fe7cea89c5
commit aecf9509f5
7 changed files with 230 additions and 26 deletions

View file

@ -25,12 +25,12 @@ these custom users are properly authenticated.
- [user token generation](https://wiki.factorio.com/Web_authentication_API) and storage (`POST
/api-login`)
- LDAP 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(?)
- server padlock proxying (to allow e.g. factorio.com users to join servers using a custom auth
server)
### Unplanned
@ -42,9 +42,20 @@ these custom users are properly authenticated.
### Configuring factoriauth
Copy `config.toml.example` to `config.toml` and adjust as necessary. `padlock-secret` needs to be a
hex-encoded binary string of at least 32 bytes - either generate your own, or attempt to start
factoriauth once and copy the freshly generated secret from the error message.
Copy `config.toml.example` to `config.toml` and adjust as necessary.
#### Padlock source
There are two possible sources for server padlock: either generated standalone by factoriauth for
completely self-contained setups, or through a padlock proxy.
For the standalone deployment, `padlock.secret` needs to be a hex-encoded binary string of at least
32 bytes - either generate your own, or attempt to start factoriauth once and copy the freshly
generated secret from the error message.
To use the padlock proxy, `padlock.proxy` needs to be set to the base URL of another factorio auth
server, e.g. `https://auth.factorio.com`. Server padlocks will be obtained from this auth server,
allowing users from both factoriauth and the upstream auth server to join the game server.
#### LDAP authentication backend notes

View file

@ -1,6 +1,9 @@
padlock-secret = ""
listen = "[::]:80"
[padlock]
secret = ""
#proxy = "https://some-other-auth-server.example"
[database]
connection-string = "sqlite://sqlite.db"

View file

@ -8,14 +8,16 @@ use rand::{
thread_rng,
};
use secrecy::ExposeSecret;
use serde::Deserialize;
use sha2::Sha256;
use thiserror::Error;
use time::{macros::format_description, OffsetDateTime};
use tokio::sync::Mutex;
use tracing::{event, instrument, Level};
use url::Url;
use crate::{
config::AuthBackendConfig,
config::{AuthBackendConfig, PadlockConfig},
db::{Database, UserTokenEntry},
secrets::{
PadlockGenerationSecret, Password, ServerHash, ServerPadlock, UserServerKey, UserToken,
@ -38,6 +40,8 @@ pub enum AuthenticationError {
Database(#[from] sqlx::Error),
#[error("Authentication backend error")]
Backend(#[from] ldap3::LdapError),
#[error("Padlock proxy error")]
PadlockProxy(#[from] PadlockProxyError),
#[error("No authentication backends available")]
NoBackends,
}
@ -112,17 +116,18 @@ impl UserAuthenticator {
};
let mut db = self.db.lock().await;
let token =
if let Some(UserTokenEntry::Valid(old_token, _, _)) = db.get_user_token(&username).await? {
old_token
} else {
let new_token =
UserToken::from(Alphanumeric.sample_string(&mut thread_rng(), Self::TOKEN_LEN));
let token = if let Some(UserTokenEntry::Valid(old_token, _, _)) =
db.get_user_token(&username).await?
{
old_token
} else {
let new_token =
UserToken::from(Alphanumeric.sample_string(&mut thread_rng(), Self::TOKEN_LEN));
db.save_user_token(&username, &new_token).await?;
db.save_user_token(&username, &new_token).await?;
new_token
};
new_token
};
Ok((username, token))
}
@ -150,11 +155,11 @@ impl UserAuthenticator {
}
#[derive(Debug)]
pub struct ServerPadlockGenerator {
pub struct ServerPadlockSecret {
secret: PadlockGenerationSecret,
}
impl ServerPadlockGenerator {
impl ServerPadlockSecret {
const HASH_LEN: usize = 32;
pub fn new(secret: PadlockGenerationSecret) -> Self {
@ -178,6 +183,120 @@ impl ServerPadlockGenerator {
}
}
#[derive(Debug, Error)]
pub enum PadlockProxyError {
#[error("upstream responded with error")]
Upstream(#[from] reqwest::Error),
#[error("database error")]
Database(#[from] sqlx::Error),
#[error("invalid upstream URL")]
InvalidUrl(#[from] url::ParseError),
#[error("unknown server_hash")]
UnknownHash(ServerHash),
}
#[derive(Debug)]
pub struct ServerPadlockProxy {
client: reqwest::Client,
upstream: Url,
database: Arc<Mutex<Box<dyn Database + Send>>>,
}
impl ServerPadlockProxy {
pub fn new(
upstream: &Url,
database: Arc<Mutex<Box<dyn Database + Send>>>,
) -> Result<Self, PadlockProxyError> {
let client = reqwest::Client::new();
let upstream = upstream.join("generate-server-padlock-2")?;
Ok(Self {
client,
upstream,
database,
})
}
#[instrument]
pub async fn generate_hash(&self) -> Result<ServerHash, PadlockProxyError> {
#[derive(Deserialize)]
struct ServerPadlockResponse {
server_hash: ServerHash,
server_padlock: ServerPadlock,
}
let response: ServerPadlockResponse = self
.client
.post(self.upstream.clone())
.query(&[("api_version", 6)])
.send()
.await?
.json()
.await?;
self.database
.lock()
.await
.save_server_padlock(&response.server_hash, &response.server_padlock)
.await?;
Ok(response.server_hash)
}
#[instrument]
pub async fn generate_padlock(
&self,
server_hash: &ServerHash,
) -> Result<ServerPadlock, PadlockProxyError> {
let Some(padlock) = self
.database
.lock()
.await
.get_server_padlock(server_hash)
.await?
else {
return Err(PadlockProxyError::UnknownHash(server_hash.clone()));
};
Ok(padlock)
}
}
#[derive(Debug)]
pub enum ServerPadlockGenerator {
Secret(ServerPadlockSecret),
Proxy(ServerPadlockProxy),
}
impl ServerPadlockGenerator {
pub fn new(
config: PadlockConfig,
db: Arc<Mutex<Box<dyn Database + Send>>>,
) -> Result<Self, PadlockProxyError> {
match config {
PadlockConfig::Secret(s) => Ok(Self::Secret(ServerPadlockSecret::new(s))),
PadlockConfig::Proxy(u) => ServerPadlockProxy::new(&u, db).map(Self::Proxy),
}
}
pub async fn generate_hash(&self) -> Result<ServerHash, PadlockProxyError> {
match self {
ServerPadlockGenerator::Secret(_) => Ok(ServerPadlockSecret::generate_hash()),
ServerPadlockGenerator::Proxy(p) => p.generate_hash().await,
}
}
pub async fn generate_padlock(
&self,
server_hash: &ServerHash,
) -> Result<ServerPadlock, PadlockProxyError> {
match self {
ServerPadlockGenerator::Secret(s) => Ok(s.generate_padlock(server_hash)),
ServerPadlockGenerator::Proxy(p) => p.generate_padlock(server_hash).await,
}
}
}
#[derive(Debug)]
pub struct UserServerKeyGenerator {
user_authenticator: Arc<UserAuthenticator>,
@ -206,7 +325,7 @@ impl UserServerKeyGenerator {
.verify_user_token(username, token)
.await?;
let padlock = self.padlock_generator.generate_padlock(server_hash);
let padlock = self.padlock_generator.generate_padlock(server_hash).await?;
#[allow(clippy::expect_used)]
let timestamp = OffsetDateTime::now_utc()

View file

@ -1,6 +1,7 @@
use std::net::{Ipv6Addr, SocketAddr, SocketAddrV6};
use serde::Deserialize;
use url::Url;
use crate::secrets::PadlockGenerationSecret;
@ -11,16 +12,23 @@ fn default_listen_addr() -> SocketAddr {
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Config {
#[serde(with = "hex::serde")]
pub padlock_secret: PadlockGenerationSecret,
#[serde(default = "default_listen_addr")]
pub listen: SocketAddr,
pub padlock: PadlockConfig,
pub database: DatabaseConfig,
#[serde(default)]
pub auth_backends: Vec<AuthBackendConfig>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[allow(clippy::module_name_repetitions)]
pub enum PadlockConfig {
Secret(#[serde(with = "hex::serde")] PadlockGenerationSecret),
Proxy(Url),
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[allow(clippy::module_name_repetitions)]

View file

@ -5,7 +5,7 @@ use secrecy::ExposeSecret;
use sqlx::{query, query_as, sqlite::SqliteConnectOptions, Connection, SqliteConnection};
use tracing::instrument;
use crate::secrets::UserToken;
use crate::secrets::{ServerHash, ServerPadlock, UserToken};
// TODO: check again if it's possible to pass this as a parameter to query!
// const TABLE_USER_TOKENS: &str = "user_tokens";
@ -41,6 +41,17 @@ pub trait Database: Debug {
username: &str,
token: &UserToken,
) -> Result<(), sqlx::Error>;
async fn get_server_padlock(
&mut self,
server_hash: &ServerHash,
) -> Result<Option<ServerPadlock>, sqlx::Error>;
async fn save_server_padlock(
&mut self,
server_hash: &ServerHash,
server_padlock: &ServerPadlock,
) -> Result<(), sqlx::Error>;
}
#[derive(Debug)]
@ -77,6 +88,15 @@ impl SqliteDatabase {
.execute(&mut self.conn)
.await?;
query!(
"CREATE TABLE IF NOT EXISTS server_padlocks (
hash TEXT NOT NULL UNIQUE,
padlock TEXT NOT NULL
) STRICT"
)
.execute(&mut self.conn)
.await?;
Ok(())
}
}
@ -160,4 +180,41 @@ impl Database for SqliteDatabase {
Ok(())
}
#[instrument]
async fn get_server_padlock(
&mut self,
server_hash: &ServerHash,
) -> Result<Option<ServerPadlock>, sqlx::Error> {
let server_hash = &server_hash.0;
let padlock = query!(
"SELECT padlock FROM server_padlocks WHERE hash = $1",
server_hash
)
.fetch_optional(&mut self.conn)
.await?;
Ok(padlock.map(|d| ServerPadlock::from(d.padlock)))
}
#[instrument]
async fn save_server_padlock(
&mut self,
server_hash: &ServerHash,
server_padlock: &ServerPadlock,
) -> Result<(), sqlx::Error> {
let server_hash = &server_hash.0;
let server_padlock = server_padlock.0.expose_secret();
query!(
"INSERT INTO server_padlocks (hash, padlock) VALUES ($1, $2)",
server_hash,
server_padlock
)
.execute(&mut self.conn)
.await?;
Ok(())
}
}

View file

@ -129,8 +129,8 @@ async fn main() -> Result<()> {
);
}
let user_authenticator = Arc::new(UserAuthenticator::new(database, auth_backends));
let padlock_generator = Arc::new(ServerPadlockGenerator::new(config.padlock_secret));
let user_authenticator = Arc::new(UserAuthenticator::new(Arc::clone(&database), auth_backends));
let padlock_generator = Arc::new(ServerPadlockGenerator::new(config.padlock, database)?);
let user_server_key_generator = Arc::new(UserServerKeyGenerator::new(
Arc::clone(&user_authenticator),
Arc::clone(&padlock_generator),

View file

@ -180,8 +180,14 @@ async fn generate_server_padlock_2(
) -> ApiResult<Json<ServerPadlockResponse>> {
event!(Level::INFO, "Creating server padlock");
let server_hash = ServerPadlockGenerator::generate_hash();
let server_padlock = server_padlock_generator.generate_padlock(&server_hash);
let server_hash = server_padlock_generator
.generate_hash()
.await
.map_err(AuthenticationError::from)?;
let server_padlock = server_padlock_generator
.generate_padlock(&server_hash)
.await
.map_err(AuthenticationError::from)?;
Ok(Json(ServerPadlockResponse {
server_hash,