Implement padlock proxy
This commit is contained in:
parent
fe7cea89c5
commit
aecf9509f5
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
|
- [user token generation](https://wiki.factorio.com/Web_authentication_API) and storage (`POST
|
||||||
/api-login`)
|
/api-login`)
|
||||||
- LDAP authentication backend
|
- LDAP authentication backend
|
||||||
|
- server padlock proxying (to allow e.g. factorio.com users to join servers using a custom auth
|
||||||
|
server)
|
||||||
|
|
||||||
### Planned
|
### Planned
|
||||||
|
|
||||||
- more authentication backends: user file, PAM(?)
|
- 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
|
### Unplanned
|
||||||
|
|
||||||
|
@ -42,9 +42,20 @@ these custom users are properly authenticated.
|
||||||
|
|
||||||
### Configuring factoriauth
|
### Configuring factoriauth
|
||||||
|
|
||||||
Copy `config.toml.example` to `config.toml` and adjust as necessary. `padlock-secret` needs to be a
|
Copy `config.toml.example` to `config.toml` and adjust as necessary.
|
||||||
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.
|
#### 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
|
#### LDAP authentication backend notes
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
padlock-secret = ""
|
|
||||||
listen = "[::]:80"
|
listen = "[::]:80"
|
||||||
|
|
||||||
|
[padlock]
|
||||||
|
secret = ""
|
||||||
|
#proxy = "https://some-other-auth-server.example"
|
||||||
|
|
||||||
[database]
|
[database]
|
||||||
connection-string = "sqlite://sqlite.db"
|
connection-string = "sqlite://sqlite.db"
|
||||||
|
|
||||||
|
|
145
src/auth/mod.rs
145
src/auth/mod.rs
|
@ -8,14 +8,16 @@ use rand::{
|
||||||
thread_rng,
|
thread_rng,
|
||||||
};
|
};
|
||||||
use secrecy::ExposeSecret;
|
use secrecy::ExposeSecret;
|
||||||
|
use serde::Deserialize;
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use time::{macros::format_description, OffsetDateTime};
|
use time::{macros::format_description, OffsetDateTime};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tracing::{event, instrument, Level};
|
use tracing::{event, instrument, Level};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::AuthBackendConfig,
|
config::{AuthBackendConfig, PadlockConfig},
|
||||||
db::{Database, UserTokenEntry},
|
db::{Database, UserTokenEntry},
|
||||||
secrets::{
|
secrets::{
|
||||||
PadlockGenerationSecret, Password, ServerHash, ServerPadlock, UserServerKey, UserToken,
|
PadlockGenerationSecret, Password, ServerHash, ServerPadlock, UserServerKey, UserToken,
|
||||||
|
@ -38,6 +40,8 @@ pub enum AuthenticationError {
|
||||||
Database(#[from] sqlx::Error),
|
Database(#[from] sqlx::Error),
|
||||||
#[error("Authentication backend error")]
|
#[error("Authentication backend error")]
|
||||||
Backend(#[from] ldap3::LdapError),
|
Backend(#[from] ldap3::LdapError),
|
||||||
|
#[error("Padlock proxy error")]
|
||||||
|
PadlockProxy(#[from] PadlockProxyError),
|
||||||
#[error("No authentication backends available")]
|
#[error("No authentication backends available")]
|
||||||
NoBackends,
|
NoBackends,
|
||||||
}
|
}
|
||||||
|
@ -112,17 +116,18 @@ impl UserAuthenticator {
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut db = self.db.lock().await;
|
let mut db = self.db.lock().await;
|
||||||
let token =
|
let token = if let Some(UserTokenEntry::Valid(old_token, _, _)) =
|
||||||
if let Some(UserTokenEntry::Valid(old_token, _, _)) = db.get_user_token(&username).await? {
|
db.get_user_token(&username).await?
|
||||||
old_token
|
{
|
||||||
} else {
|
old_token
|
||||||
let new_token =
|
} else {
|
||||||
UserToken::from(Alphanumeric.sample_string(&mut thread_rng(), Self::TOKEN_LEN));
|
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))
|
Ok((username, token))
|
||||||
}
|
}
|
||||||
|
@ -150,11 +155,11 @@ impl UserAuthenticator {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ServerPadlockGenerator {
|
pub struct ServerPadlockSecret {
|
||||||
secret: PadlockGenerationSecret,
|
secret: PadlockGenerationSecret,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerPadlockGenerator {
|
impl ServerPadlockSecret {
|
||||||
const HASH_LEN: usize = 32;
|
const HASH_LEN: usize = 32;
|
||||||
|
|
||||||
pub fn new(secret: PadlockGenerationSecret) -> Self {
|
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)]
|
#[derive(Debug)]
|
||||||
pub struct UserServerKeyGenerator {
|
pub struct UserServerKeyGenerator {
|
||||||
user_authenticator: Arc<UserAuthenticator>,
|
user_authenticator: Arc<UserAuthenticator>,
|
||||||
|
@ -206,7 +325,7 @@ impl UserServerKeyGenerator {
|
||||||
.verify_user_token(username, token)
|
.verify_user_token(username, token)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let padlock = self.padlock_generator.generate_padlock(server_hash);
|
let padlock = self.padlock_generator.generate_padlock(server_hash).await?;
|
||||||
|
|
||||||
#[allow(clippy::expect_used)]
|
#[allow(clippy::expect_used)]
|
||||||
let timestamp = OffsetDateTime::now_utc()
|
let timestamp = OffsetDateTime::now_utc()
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::net::{Ipv6Addr, SocketAddr, SocketAddrV6};
|
use std::net::{Ipv6Addr, SocketAddr, SocketAddrV6};
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use crate::secrets::PadlockGenerationSecret;
|
use crate::secrets::PadlockGenerationSecret;
|
||||||
|
|
||||||
|
@ -11,16 +12,23 @@ fn default_listen_addr() -> SocketAddr {
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
#[serde(with = "hex::serde")]
|
|
||||||
pub padlock_secret: PadlockGenerationSecret,
|
|
||||||
#[serde(default = "default_listen_addr")]
|
#[serde(default = "default_listen_addr")]
|
||||||
pub listen: SocketAddr,
|
pub listen: SocketAddr,
|
||||||
|
|
||||||
|
pub padlock: PadlockConfig,
|
||||||
pub database: DatabaseConfig,
|
pub database: DatabaseConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub auth_backends: Vec<AuthBackendConfig>,
|
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)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
#[allow(clippy::module_name_repetitions)]
|
#[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 sqlx::{query, query_as, sqlite::SqliteConnectOptions, Connection, SqliteConnection};
|
||||||
use tracing::instrument;
|
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!
|
// TODO: check again if it's possible to pass this as a parameter to query!
|
||||||
// const TABLE_USER_TOKENS: &str = "user_tokens";
|
// const TABLE_USER_TOKENS: &str = "user_tokens";
|
||||||
|
@ -41,6 +41,17 @@ pub trait Database: Debug {
|
||||||
username: &str,
|
username: &str,
|
||||||
token: &UserToken,
|
token: &UserToken,
|
||||||
) -> Result<(), sqlx::Error>;
|
) -> 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)]
|
#[derive(Debug)]
|
||||||
|
@ -77,6 +88,15 @@ impl SqliteDatabase {
|
||||||
.execute(&mut self.conn)
|
.execute(&mut self.conn)
|
||||||
.await?;
|
.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -160,4 +180,41 @@ impl Database for SqliteDatabase {
|
||||||
|
|
||||||
Ok(())
|
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 user_authenticator = Arc::new(UserAuthenticator::new(Arc::clone(&database), auth_backends));
|
||||||
let padlock_generator = Arc::new(ServerPadlockGenerator::new(config.padlock_secret));
|
let padlock_generator = Arc::new(ServerPadlockGenerator::new(config.padlock, database)?);
|
||||||
let user_server_key_generator = Arc::new(UserServerKeyGenerator::new(
|
let user_server_key_generator = Arc::new(UserServerKeyGenerator::new(
|
||||||
Arc::clone(&user_authenticator),
|
Arc::clone(&user_authenticator),
|
||||||
Arc::clone(&padlock_generator),
|
Arc::clone(&padlock_generator),
|
||||||
|
|
|
@ -180,8 +180,14 @@ async fn generate_server_padlock_2(
|
||||||
) -> ApiResult<Json<ServerPadlockResponse>> {
|
) -> ApiResult<Json<ServerPadlockResponse>> {
|
||||||
event!(Level::INFO, "Creating server padlock");
|
event!(Level::INFO, "Creating server padlock");
|
||||||
|
|
||||||
let server_hash = ServerPadlockGenerator::generate_hash();
|
let server_hash = server_padlock_generator
|
||||||
let server_padlock = server_padlock_generator.generate_padlock(&server_hash);
|
.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 {
|
Ok(Json(ServerPadlockResponse {
|
||||||
server_hash,
|
server_hash,
|
||||||
|
|
Loading…
Reference in a new issue