Upgrade packages and make sessions more secure

Sign session cookies with base64 encoded secret from .env
This commit is contained in:
Tyler Hallada 2024-05-09 00:32:08 -04:00
parent 2fab68241e
commit 3f97c0e2ca
6 changed files with 805 additions and 790 deletions

1500
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -12,14 +12,15 @@ path = "src/lib.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
ammonia = "3.3.0" ammonia = "4"
ansi-to-html = "0.2" ansi-to-html = "0.2"
anyhow = "1" anyhow = "1"
async-trait = "0.1" async-trait = "0.1"
axum = { version = "0.7", features = ["form", "multipart", "query"] } axum = { version = "0.7", features = ["form", "multipart", "query"] }
axum-client-ip = "0.5" axum-client-ip = "0.6"
axum-extra = { version = "0.9", features = ["typed-header"] } axum-extra = { version = "0.9", features = ["typed-header"] }
axum-login = "0.10" axum-login = "0.15"
base64 = "0.22"
bytes = "1.4" bytes = "1.4"
# TODO: replace chrono with time # TODO: replace chrono with time
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
@ -39,8 +40,8 @@ notify = "6"
once_cell = "1.18" once_cell = "1.18"
opml = "1.1" opml = "1.1"
password-auth = "1.0" password-auth = "1.0"
readability = "0.2" readability = "0.3"
reqwest = { version = "0.11", features = ["json"] } reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_with = "3" serde_with = "3"
sqlx = { version = "0.7", features = [ sqlx = { version = "0.7", features = [
@ -59,13 +60,14 @@ tokio-stream = { version = "0.1", features = ["sync"] }
tower = "0.4" tower = "0.4"
tower-livereload = "0.9" tower-livereload = "0.9"
tower-http = { version = "0.5", features = ["trace", "fs"] } tower-http = { version = "0.5", features = ["trace", "fs"] }
tower-sessions = { version = "0.7", features = ["redis-store"] } tower-sessions = { version = "0.12", features = ["signed"] }
tower-sessions-redis-store = "0.12"
tracing = { version = "0.1", features = ["valuable", "attributes"] } tracing = { version = "0.1", features = ["valuable", "attributes"] }
tracing-appender = "0.2" tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
uuid = { version = "1.4", features = ["serde"] } uuid = { version = "1.4", features = ["serde"] }
url = "2.4" url = "2.4"
validator = { version = "0.16", features = ["derive"] } validator = { version = "0.18", features = ["derive"] }
[profile.dev.package.sqlx-macros] [profile.dev.package.sqlx-macros]
opt-level = 3 opt-level = 3

View File

@ -9,25 +9,26 @@ algorithm. Pining for the days of Google Reader. An excuse to write more Rust.
Install these requirements to get started developing crawlnicle. Install these requirements to get started developing crawlnicle.
* [rust](https://www.rust-lang.org/) - [rust](https://www.rust-lang.org/)
* [postgres](https://www.postgresql.org/) - [postgres](https://www.postgresql.org/)
* [redis](https://redis.io/) - [redis](https://redis.io/)
* [sqlx-cli](https://crates.io/crates/sqlx-cli) - [sqlx-cli](https://crates.io/crates/sqlx-cli)
* Only postgres needed. Install with:
- Only postgres needed. Install with:
```bash ```bash
cargo install sqlx-cli --no-default-features --features native-tls,postgres cargo install sqlx-cli --no-default-features --features native-tls,postgres
``` ```
* [just](https://github.com/casey/just#installation) - [just](https://github.com/casey/just#installation)
* [bun](https://bun.sh) - [bun](https://bun.sh)
* An [SMTP server for sending - An [SMTP server for sending
emails](https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol) (put emails](https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol) (put
configuration in the `.env` file) configuration in the `.env` file)
* (optional) [cargo-watch](https://github.com/watchexec/cargo-watch#install) for - (optional) [cargo-watch](https://github.com/watchexec/cargo-watch#install) for
auto-recompiling the server in development auto-recompiling the server in development
* (optional) [mold](https://github.com/rui314/mold#installation) for faster - (optional) [mold](https://github.com/rui314/mold#installation) for faster
builds builds
### First-time setup ### First-time setup
@ -69,7 +70,7 @@ builds
SMTP_USER=user SMTP_USER=user
SMTP_PASSWORD=password SMTP_PASSWORD=password
EMAIL_FROM="crawlnicle <no-reply@mail.crawlnicle.com>" EMAIL_FROM="crawlnicle <no-reply@mail.crawlnicle.com>"
SESSION_SECRET=64-bytes-of-secret SESSION_SECRET=64-bytes-of-base64-encoded-secret
IP_SOURCE=ConnectInfo IP_SOURCE=ConnectInfo
``` ```

View File

@ -60,7 +60,7 @@ pub struct Config {
#[clap(long, env, default_value = "crawlnicle <no-reply@mail.crawlnicle.com>")] #[clap(long, env, default_value = "crawlnicle <no-reply@mail.crawlnicle.com>")]
pub email_from: Mailbox, pub email_from: Mailbox,
#[clap(long, env)] #[clap(long, env)]
pub session_secret: String, pub session_secret: String, // base64 encoded
#[clap(long, env, default_value = "ConnectInfo")] #[clap(long, env, default_value = "ConnectInfo")]
pub ip_source: IpSource, pub ip_source: IpSource,
#[clap(long, env, default_value = "100")] #[clap(long, env, default_value = "100")]

View File

@ -7,6 +7,7 @@ use crate::htmx::HXRedirect;
pub async fn get(mut auth: AuthSession) -> Result<Response> { pub async fn get(mut auth: AuthSession) -> Result<Response> {
auth.logout() auth.logout()
.await
.context("failed to logout user from session")?; .context("failed to logout user from session")?;
Ok(HXRedirect::to("/").reload(true).into_response()) Ok(HXRedirect::to("/").reload(true).into_response())
} }

View File

@ -2,19 +2,18 @@ use std::{collections::HashMap, net::SocketAddr, path::Path, sync::Arc};
use anyhow::Result; use anyhow::Result;
use axum::{ use axum::{
error_handling::HandleErrorLayer,
routing::{get, post}, routing::{get, post},
BoxError, Router, Router,
}; };
use axum_login::{ use axum_login::{
login_required, login_required,
tower_sessions::{fred::prelude::*, Expiry, RedisStore, SessionManagerLayer}, tower_sessions::{Expiry, SessionManagerLayer},
AuthManagerLayerBuilder, AuthManagerLayerBuilder,
}; };
use base64::prelude::*;
use bytes::Bytes; use bytes::Bytes;
use clap::Parser; use clap::Parser;
use dotenvy::dotenv; use dotenvy::dotenv;
use http::StatusCode;
use lettre::transport::smtp::authentication::Credentials; use lettre::transport::smtp::authentication::Credentials;
use lettre::SmtpTransport; use lettre::SmtpTransport;
use notify::Watcher; use notify::Watcher;
@ -26,6 +25,8 @@ use tokio::sync::Mutex;
use tower::ServiceBuilder; use tower::ServiceBuilder;
use tower_http::{services::ServeDir, trace::TraceLayer}; use tower_http::{services::ServeDir, trace::TraceLayer};
use tower_livereload::LiveReloadLayer; use tower_livereload::LiveReloadLayer;
use tower_sessions::cookie::Key;
use tower_sessions_redis_store::{fred::prelude::*, RedisStore};
use tracing::debug; use tracing::debug;
use lib::config::Config; use lib::config::Config;
@ -62,9 +63,6 @@ async fn main() -> Result<()> {
let domain_locks = DomainLocks::new(); let domain_locks = DomainLocks::new();
let client = Client::builder().user_agent(USER_AGENT).build()?; let client = Client::builder().user_agent(USER_AGENT).build()?;
// TODO: not needed anymore?
// let secret = config.session_secret.as_bytes();
let pool = PgPoolOptions::new() let pool = PgPoolOptions::new()
.max_connections(config.database_max_connections) .max_connections(config.database_max_connections)
.acquire_timeout(std::time::Duration::from_secs(3)) .acquire_timeout(std::time::Duration::from_secs(3))
@ -72,34 +70,25 @@ async fn main() -> Result<()> {
.await?; .await?;
let redis_config = RedisConfig::from_url(&config.redis_url)?; let redis_config = RedisConfig::from_url(&config.redis_url)?;
// TODO: https://github.com/maxcountryman/tower-sessions/issues/92 let redis_pool = RedisPool::new(redis_config, None, None, None, config.redis_pool_size)?;
// let redis_pool = RedisPool::new(redis_config, None, None, config.redis_pool_size)?; redis_pool.init().await?;
// redis_pool.connect();
// redis_pool.wait_for_connect().await?;
let redis_client = RedisClient::new(redis_config, None, None, None);
redis_client.connect();
redis_client.wait_for_connect().await?;
let session_store = RedisStore::new(redis_client); let session_store = RedisStore::new(redis_pool);
let session_layer = SessionManagerLayer::new(session_store) let session_layer = SessionManagerLayer::new(session_store)
.with_secure(!cfg!(debug_assertions)) .with_secure(!cfg!(debug_assertions))
.with_expiry(Expiry::OnInactivity(Duration::days( .with_expiry(Expiry::OnInactivity(Duration::days(
config.session_duration_days, config.session_duration_days,
))); )))
.with_signed(Key::from(&BASE64_STANDARD.decode(&config.session_secret)?));
let backend = Backend::new(pool.clone()); let backend = Backend::new(pool.clone());
let auth_service = ServiceBuilder::new() let auth_layer = AuthManagerLayerBuilder::new(backend, session_layer).build();
.layer(HandleErrorLayer::new(|_: BoxError| async {
StatusCode::BAD_REQUEST
}))
.layer(AuthManagerLayerBuilder::new(backend, session_layer).build());
let creds = Credentials::new(config.smtp_user.clone(), config.smtp_password.clone());
let smtp_creds = Credentials::new(config.smtp_user.clone(), config.smtp_password.clone());
// Open a remote connection to gmail // Open a remote connection to gmail
let mailer = SmtpTransport::relay(&config.smtp_server) let mailer = SmtpTransport::relay(&config.smtp_server)
.unwrap() .unwrap()
.credentials(creds) .credentials(smtp_creds)
.build(); .build();
sqlx::migrate!().run(&pool).await?; sqlx::migrate!().run(&pool).await?;
@ -163,7 +152,7 @@ async fn main() -> Result<()> {
mailer, mailer,
}) })
.layer(ServiceBuilder::new().layer(TraceLayer::new_for_http())) .layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()))
.layer(auth_service) .layer(auth_layer)
.layer(ip_source_extension); .layer(ip_source_extension);
if cfg!(debug_assertions) { if cfg!(debug_assertions) {