Implement padlock proxy
This commit is contained in:
parent
fe7cea89c5
commit
aecf9509f5
7 changed files with 230 additions and 26 deletions
21
README.md
21
README.md
|
@ -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
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
padlock-secret = ""
|
||||
listen = "[::]:80"
|
||||
|
||||
[padlock]
|
||||
secret = ""
|
||||
#proxy = "https://some-other-auth-server.example"
|
||||
|
||||
[database]
|
||||
connection-string = "sqlite://sqlite.db"
|
||||
|
||||
|
|
145
src/auth/mod.rs
145
src/auth/mod.rs
|
@ -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()
|
||||
|
|
|
@ -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)]
|
||||
|
|
59
src/db.rs
59
src/db.rs
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue