diff --git a/Cargo.lock b/Cargo.lock index d63227242..cbe0c182a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,48 +57,12 @@ dependencies = [ "password-hash", ] -[[package]] -name = "arrayref" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" - [[package]] name = "arrayvec" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" -[[package]] -name = "async-lock" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6a8ea61bf9947a1007c5cada31e647dbc77b103c679858150003ba697ea798b" -dependencies = [ - "event-listener", -] - -[[package]] -name = "async-session" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07da4ce523b4e2ebaaf330746761df23a465b951a83d84bbce4233dabedae630" -dependencies = [ - "anyhow", - "async-lock", - "async-trait", - "base64", - "bincode", - "blake3", - "chrono", - "hmac 0.11.0", - "log", - "rand 0.8.4", - "serde", - "serde_json", - "sha2", -] - [[package]] name = "async-trait" version = "0.1.50" @@ -201,21 +165,6 @@ dependencies = [ "opaque-debug 0.3.0", ] -[[package]] -name = "blake3" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b64485778c4f16a6a5a9d335e80d449ac6c70cdd6a06d2af18a6f6f775a125b3" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if 0.1.10", - "constant_time_eq", - "crypto-mac 0.8.0", - "digest 0.9.0", -] - [[package]] name = "block-buffer" version = "0.7.3" @@ -301,12 +250,6 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" - [[package]] name = "cfg-if" version = "1.0.0" @@ -319,7 +262,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea8756167ea0aca10e066cdbe7813bd71d2f24e69b0bc7b50509590cef2ce0b9" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "cipher", "cpufeatures", "zeroize", @@ -409,12 +352,6 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f92cfa0fd5690b3cf8c1ef2cabbd9b7ef22fa53cf5e1f92b05103f6d5d1cf6e7" -[[package]] -name = "constant_time_eq" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" - [[package]] name = "cookie" version = "0.15.1" @@ -449,7 +386,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "crossbeam-utils", ] @@ -459,7 +396,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b10ddc024425c88c2ad148c1b0fd53f4c6d38db9697c9f1588381212fa657c9" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "crossbeam-utils", ] @@ -469,7 +406,7 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "lazy_static", ] @@ -493,16 +430,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "crypto-mac" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" -dependencies = [ - "generic-array 0.14.4", - "subtle", -] - [[package]] name = "darling" version = "0.13.0" @@ -618,12 +545,6 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" -[[package]] -name = "event-listener" -version = "2.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59" - [[package]] name = "fake-simd" version = "0.1.2" @@ -787,7 +708,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "wasi 0.9.0+wasi-snapshot-preview1", ] @@ -798,7 +719,7 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "wasi 0.10.0+wasi-snapshot-preview1", ] @@ -923,16 +844,6 @@ dependencies = [ "digest 0.9.0", ] -[[package]] -name = "hmac" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" -dependencies = [ - "crypto-mac 0.11.1", - "digest 0.9.0", -] - [[package]] name = "http" version = "0.2.4" @@ -1072,7 +983,7 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -1113,7 +1024,7 @@ checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" dependencies = [ "arrayvec", "bitflags", - "cfg-if 1.0.0", + "cfg-if", "ryu", "static_assertions", ] @@ -1145,7 +1056,7 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -1200,7 +1111,6 @@ dependencies = [ "tracing-subscriber", "url", "warp", - "warp-sessions", ] [[package]] @@ -1382,7 +1292,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "instant", "libc", "redox_syscall", @@ -2008,7 +1918,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a0c8611594e2ab4ebbf06ec7cbbf0a99450b8570e96cbf5188b5d5f6ef18d81" dependencies = [ "block-buffer 0.9.0", - "cfg-if 1.0.0", + "cfg-if", "cpufeatures", "digest 0.9.0", "opaque-debug 0.3.0", @@ -2027,7 +1937,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12" dependencies = [ "block-buffer 0.9.0", - "cfg-if 1.0.0", + "cfg-if", "cpufeatures", "digest 0.9.0", "opaque-debug 0.3.0", @@ -2135,7 +2045,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "hmac 0.10.1", + "hmac", "itoa", "libc", "log", @@ -2325,7 +2235,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "rand 0.8.4", "redox_syscall", @@ -2558,7 +2468,7 @@ version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "log", "pin-project-lite", "tracing-attributes", @@ -2887,20 +2797,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "warp-sessions" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c193bbfd203c4fa0b1ce64dce5d3ce9eb78c03f39723e080771e160fd416c145" -dependencies = [ - "async-session", - "async-trait", - "http", - "serde", - "tokio", - "warp", -] - [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -2919,7 +2815,7 @@ version = "0.2.74" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "wasm-bindgen-macro", ] diff --git a/matrix-authentication-service/Cargo.toml b/matrix-authentication-service/Cargo.toml index 529054bd1..d89b5f654 100644 --- a/matrix-authentication-service/Cargo.toml +++ b/matrix-authentication-service/Cargo.toml @@ -46,9 +46,8 @@ url = { version = "2.2.2", features = ["serde"] } mime = "0.3.16" rand = "0.8.4" bincode = "1.3.3" +headers = "0.3.4" cookie = "0.15.1" chacha20poly1305 = { version = "0.8.1", features = ["std"] } oauth2-types = { path = "../oauth2-types" } -headers = "0.3.4" -warp-sessions = "1.0.15" diff --git a/matrix-authentication-service/src/cli/manage.rs b/matrix-authentication-service/src/cli/manage.rs index 205feea15..0962bcd3c 100644 --- a/matrix-authentication-service/src/cli/manage.rs +++ b/matrix-authentication-service/src/cli/manage.rs @@ -12,11 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. +use argon2::Argon2; use clap::Clap; use tracing::info; use super::RootCommand; -use crate::{config::DatabaseConfig, storage::Storage}; +use crate::{config::DatabaseConfig, storage::register_user}; #[derive(Clap, Debug)] pub(super) struct ManageCommand { @@ -37,9 +38,9 @@ impl ManageCommand { SC::Register { username, password } => { let config: DatabaseConfig = root.load_config()?; let pool = config.connect().await?; - let storage = Storage::new(pool); + let hasher = Argon2::default(); - let user = storage.register_user(&username, &password).await?; + let user = register_user(&pool, hasher, username, password).await?; info!(?user, "User registered"); Ok(()) diff --git a/matrix-authentication-service/src/cli/server.rs b/matrix-authentication-service/src/cli/server.rs index 79eae172f..9c6b5c50f 100644 --- a/matrix-authentication-service/src/cli/server.rs +++ b/matrix-authentication-service/src/cli/server.rs @@ -18,8 +18,7 @@ use anyhow::Context; use clap::Clap; use super::RootCommand; -use crate::config::RootConfig; -use crate::templates::Templates; +use crate::{config::RootConfig, templates::Templates}; #[derive(Clap, Debug, Default)] pub(super) struct ServerCommand; @@ -36,7 +35,7 @@ impl ServerCommand { // Start the server let address: SocketAddr = config.http.address.parse()?; - let root = crate::handlers::root(pool, templates, &config); + let root = crate::handlers::root(&pool, &templates, &config); warp::serve(root).run(address).await; Ok(()) diff --git a/matrix-authentication-service/src/config/cookies.rs b/matrix-authentication-service/src/config/cookies.rs new file mode 100644 index 000000000..5f8e0fd8f --- /dev/null +++ b/matrix-authentication-service/src/config/cookies.rs @@ -0,0 +1,43 @@ +// Copyright 2021 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use schemars::{gen::SchemaGenerator, schema::Schema, JsonSchema}; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +use super::ConfigurationSection; + +fn secret_schema(gen: &mut SchemaGenerator) -> Schema { + String::json_schema(gen) +} + +#[serde_as] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct CookiesConfig { + #[schemars(schema_with = "secret_schema")] + #[serde_as(as = "serde_with::hex::Hex")] + pub secret: [u8; 32], +} + +impl ConfigurationSection<'_> for CookiesConfig { + fn path() -> &'static str { + "cookies" + } + + fn generate() -> Self { + Self { + secret: rand::random(), + } + } +} diff --git a/matrix-authentication-service/src/config/csrf.rs b/matrix-authentication-service/src/config/csrf.rs index 52c6efa5d..420e2109a 100644 --- a/matrix-authentication-service/src/config/csrf.rs +++ b/matrix-authentication-service/src/config/csrf.rs @@ -13,27 +13,23 @@ // limitations under the License. use chrono::Duration; +use headers::SetCookie; use schemars::{gen::SchemaGenerator, schema::Schema, JsonSchema}; use serde::{Deserialize, Serialize}; use serde_with::serde_as; -use warp::filters::BoxedFilter; +use warp::{filters::BoxedFilter, Filter, Rejection, Reply}; -use crate::filters::{csrf::extract_or_generate, CsrfToken}; +use crate::filters::{ + cookies::WithTypedHeader, + csrf::{extract_or_generate, CsrfToken}, +}; -use super::ConfigurationSection; +use super::{ConfigurationSection, CookiesConfig}; fn default_ttl() -> Duration { Duration::hours(1) } -fn default_cookie_name() -> String { - "csrf".to_string() -} - -fn key_schema(gen: &mut SchemaGenerator) -> Schema { - String::json_schema(gen) -} - fn ttl_schema(gen: &mut SchemaGenerator) -> Schema { u64::json_schema(gen) } @@ -41,13 +37,6 @@ fn ttl_schema(gen: &mut SchemaGenerator) -> Schema { #[serde_as] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct CsrfConfig { - #[schemars(schema_with = "key_schema")] - #[serde_as(as = "serde_with::hex::Hex")] - pub key: [u8; 32], - - #[serde(default = "default_cookie_name")] - pub cookie_name: String, - #[schemars(schema_with = "ttl_schema")] #[serde(default = "default_ttl")] #[serde_as(as = "serde_with::DurationSeconds")] @@ -55,11 +44,24 @@ pub struct CsrfConfig { } impl CsrfConfig { - pub fn to_extract_filter(&self) -> BoxedFilter<(CsrfToken,)> { - let ttl = self.ttl; - // TODO: we should probably not leak here - let cookie_name = Box::leak(Box::new(self.cookie_name.clone())); - extract_or_generate(self.key, cookie_name, ttl) + pub fn to_extract_filter(&self, cookies_config: &CookiesConfig) -> BoxedFilter<(CsrfToken,)> { + extract_or_generate(cookies_config, "csrf", self.ttl) + } + + pub fn to_save_filter( + &self, + cookies_config: &CookiesConfig, + ) -> impl Fn(F) -> BoxedFilter<(WithTypedHeader,)> + where + F: Filter + Clone + Send + Sync + 'static, + { + crate::filters::cookies::save_encrypted("csrf", cookies_config) + } +} + +impl Default for CsrfConfig { + fn default() -> Self { + Self { ttl: default_ttl() } } } @@ -69,11 +71,7 @@ impl ConfigurationSection<'_> for CsrfConfig { } fn generate() -> Self { - Self { - key: rand::random(), - ttl: default_ttl(), - cookie_name: default_cookie_name(), - } + Self::default() } } @@ -90,22 +88,12 @@ mod tests { "config.yaml", r#" csrf: - key: 0000111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFF ttl: 1800 "#, )?; let config = CsrfConfig::load_from_file("config.yaml")?; - assert_eq!( - config.key, - [ - 0x00, 0x00, 0x11, 0x11, 0x22, 0x22, 0x33, 0x33, 0x44, 0x44, 0x55, 0x55, 0x66, - 0x66, 0x77, 0x77, 0x88, 0x88, 0x99, 0x99, 0xAA, 0xAA, 0xBB, 0xBB, 0xCC, 0xCC, - 0xDD, 0xDD, 0xEE, 0xEE, 0xFF, 0xFF, - ] - ); - assert_eq!(config.ttl, Duration::minutes(30)); Ok(()) diff --git a/matrix-authentication-service/src/config/mod.rs b/matrix-authentication-service/src/config/mod.rs index acb1ab945..d5d86e47b 100644 --- a/matrix-authentication-service/src/config/mod.rs +++ b/matrix-authentication-service/src/config/mod.rs @@ -15,19 +15,19 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +mod cookies; mod csrf; mod database; mod http; mod oauth2; -mod session; mod util; pub use self::{ + cookies::CookiesConfig, csrf::CsrfConfig, database::DatabaseConfig, http::HttpConfig, oauth2::{OAuth2ClientConfig, OAuth2Config}, - session::SessionConfig, util::ConfigurationSection, }; @@ -42,9 +42,10 @@ pub struct RootConfig { #[serde(default)] pub database: DatabaseConfig, - pub csrf: CsrfConfig, + pub cookies: CookiesConfig, - pub session: SessionConfig, + #[serde(default)] + pub csrf: CsrfConfig, } impl ConfigurationSection<'_> for RootConfig { @@ -57,8 +58,8 @@ impl ConfigurationSection<'_> for RootConfig { oauth2: OAuth2Config::generate(), http: HttpConfig::generate(), database: DatabaseConfig::generate(), + cookies: CookiesConfig::generate(), csrf: CsrfConfig::generate(), - session: SessionConfig::generate(), } } } diff --git a/matrix-authentication-service/src/config/session.rs b/matrix-authentication-service/src/config/session.rs deleted file mode 100644 index d8dd01041..000000000 --- a/matrix-authentication-service/src/config/session.rs +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2021 The Matrix.org Foundation C.I.C. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use schemars::{gen::SchemaGenerator, schema::Schema, JsonSchema}; -use serde::{Deserialize, Serialize}; -use serde_with::serde_as; - -use super::ConfigurationSection; - -fn secret_schema(gen: &mut SchemaGenerator) -> Schema { - String::json_schema(gen) -} - -#[serde_as] -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct SessionConfig { - #[schemars(schema_with = "secret_schema")] - #[serde_as(as = "serde_with::hex::Hex")] - secret: Vec, -} - -// impl SessionConfig { -// pub fn to_middleware( -// &self, -// store: impl SessionStore, -// ) -> impl Middleware { -// SessionMiddleware::new(store, &self.secret) -// } -// } - -impl ConfigurationSection<'_> for SessionConfig { - fn path() -> &'static str { - "session" - } - - fn generate() -> Self { - let secret: [u8; 32] = rand::random(); - - Self { - secret: secret.into(), - } - } -} - -#[cfg(test)] -mod tests { - use figment::Jail; - - use super::*; - - #[test] - fn load_config() { - Jail::expect_with(|jail| { - jail.create_file( - "config.yaml", - r#" - session: - secret: 00112233445566778899AABBCCDDEEFF - "#, - )?; - - let config = SessionConfig::load_from_file("config.yaml")?; - - assert_eq!( - config.secret, - [ - 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, - 0xDD, 0xEE, 0xFF, - ] - ); - - Ok(()) - }) - } -} diff --git a/matrix-authentication-service/src/filters/cookies.rs b/matrix-authentication-service/src/filters/cookies.rs new file mode 100644 index 000000000..4dfb2a3eb --- /dev/null +++ b/matrix-authentication-service/src/filters/cookies.rs @@ -0,0 +1,123 @@ +// Copyright 2021 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use chacha20poly1305::{ + aead::{generic_array::GenericArray, Aead, NewAead}, + ChaCha20Poly1305, +}; +use cookie::Cookie; +use data_encoding::BASE64URL_NOPAD; +use headers::{Header, HeaderMapExt, HeaderValue, SetCookie}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use warp::{filters::BoxedFilter, Filter, Rejection, Reply}; + +use crate::{config::CookiesConfig, errors::WrapError}; + +#[derive(Serialize, Deserialize)] +struct EncryptedCookie { + nonce: [u8; 12], + ciphertext: Vec, +} + +impl EncryptedCookie { + /// Encrypt from a given key + fn encrypt(payload: T, key: &[u8; 32]) -> anyhow::Result { + let key = GenericArray::from_slice(key); + let aead = ChaCha20Poly1305::new(key); + let message = bincode::serialize(&payload)?; + let nonce: [u8; 12] = rand::random(); + let ciphertext = aead.encrypt(GenericArray::from_slice(&nonce[..]), &message[..])?; + Ok(Self { nonce, ciphertext }) + } + + /// Decrypt the content of the cookie from a given key + fn decrypt(&self, key: &[u8; 32]) -> anyhow::Result { + let key = GenericArray::from_slice(key); + let aead = ChaCha20Poly1305::new(key); + let message = aead.decrypt( + GenericArray::from_slice(&self.nonce[..]), + &self.ciphertext[..], + )?; + let token = bincode::deserialize(&message)?; + Ok(token) + } + + /// Encode the encrypted cookie to be then saved as a cookie + fn to_cookie_value(&self) -> anyhow::Result { + let raw = bincode::serialize(self)?; + Ok(BASE64URL_NOPAD.encode(&raw)) + } + + fn from_cookie_value(value: &str) -> anyhow::Result { + let raw = BASE64URL_NOPAD.decode(value.as_bytes())?; + let content = bincode::deserialize(&raw)?; + Ok(content) + } +} + +pub fn encrypted(name: &'static str, options: &CookiesConfig) -> BoxedFilter<(Option,)> +where + T: DeserializeOwned + Send + 'static, +{ + let secret = options.secret; + warp::cookie::optional(name) + .map(move |maybe_value: Option| { + maybe_value + .and_then(|value| EncryptedCookie::from_cookie_value(&value).ok()) + .and_then(|encrypted| encrypted.decrypt(&secret).ok()) + }) + .boxed() +} + +pub struct WithTypedHeader { + reply: R, + header: H, +} + +impl Reply for WithTypedHeader +where + R: Reply, + H: Header + Send, +{ + fn into_response(self) -> warp::reply::Response { + let mut res = self.reply.into_response(); + res.headers_mut().typed_insert(self.header); + res + } +} + +pub fn save_encrypted( + name: &'static str, + options: &CookiesConfig, +) -> impl Fn(F) -> BoxedFilter<(WithTypedHeader,)> +where + T: Serialize + Send, + F: Filter + Clone + Send + Sync + 'static, +{ + let secret = options.secret; + move |f: F| { + f.and_then(move |unencrypted: T, reply: R| async move { + let encrypted = EncryptedCookie::encrypt(unencrypted, &secret) + .wrap_error()? + .to_cookie_value() + .wrap_error()?; + let value = Cookie::build(name, encrypted).finish().to_string(); + let header = + SetCookie::decode(&mut [HeaderValue::from_str(&value).wrap_error()?].iter()) + .wrap_error()?; + Ok::<_, Rejection>(WithTypedHeader { reply, header }) + }) + .boxed() + } +} diff --git a/matrix-authentication-service/src/filters/csrf.rs b/matrix-authentication-service/src/filters/csrf.rs index a236a0273..b9740a7bf 100644 --- a/matrix-authentication-service/src/filters/csrf.rs +++ b/matrix-authentication-service/src/filters/csrf.rs @@ -15,31 +15,23 @@ //! Stateless CSRF protection middleware based on a chacha20-poly1305 encrypted //! and signed token -use std::time::SystemTime; - -use chacha20poly1305::{ - aead::{generic_array::GenericArray, Aead, NewAead}, - ChaCha20Poly1305, -}; use chrono::{DateTime, Duration, Utc}; -use cookie::{Cookie, CookieBuilder, SameSite}; use data_encoding::BASE64URL_NOPAD; -use headers::{Header, HeaderMapExt, HeaderValue, SetCookie}; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, TimestampSeconds}; -use warp::{filters::BoxedFilter, Filter, Rejection, Reply}; +use warp::{filters::BoxedFilter, Filter, Rejection}; -use crate::errors::WrapError; +use crate::config::CookiesConfig; #[serde_as] #[derive(Serialize, Deserialize)] -pub struct UnencryptedToken { +pub struct CsrfToken { #[serde_as(as = "TimestampSeconds")] expiration: DateTime, token: [u8; 32], } -impl UnencryptedToken { +impl CsrfToken { /// Create a new token from a defined value valid for a specified duration fn new(token: [u8; 32], ttl: Duration) -> Self { let expiration = Utc::now() + ttl; @@ -57,24 +49,6 @@ impl UnencryptedToken { Self::new(self.token, ttl) } - /// Encrypt the token with the given chacha20-poly1305 key - fn encrypt(&self, key: &[u8; 32]) -> anyhow::Result { - let key = GenericArray::from_slice(key); - let aead = ChaCha20Poly1305::new(key); - - // Serialize the token - let message = bincode::serialize(self)?; - - // Generate a nonce - let nonce: [u8; 12] = rand::random(); - - // And encrypt everything - let ciphertext = aead.encrypt(GenericArray::from_slice(&nonce[..]), &message[..])?; - - // Return the encrypted token + nonce - Ok(EncryptedToken { nonce, ciphertext }) - } - /// Get the value to include in HTML forms pub fn form_value(&self) -> String { BASE64URL_NOPAD.encode(&self.token[..]) @@ -97,90 +71,23 @@ impl UnencryptedToken { Err(anyhow::anyhow!("CSRF token expired")) } } - - fn to_cookie_builder<'c, 'n: 'c>( - &self, - name: &'n str, - key: &[u8; 32], - ) -> anyhow::Result> { - // Converting expiration time from `chrono` to `time` via native `SystemTime` - let expires: SystemTime = self.expiration.into(); - Ok(self - .encrypt(key)? - .to_cookie_builder(name)? - .expires(Some(expires.into()))) - } - - fn from_cookie_value(value: &str, key: &[u8; 32]) -> anyhow::Result { - let encrypted = EncryptedToken::from_cookie_value(value)?; - let token = encrypted.decrypt(key)?; - Ok(token) - } -} - -#[derive(Serialize, Deserialize)] -struct EncryptedToken { - nonce: [u8; 12], - ciphertext: Vec, -} - -impl EncryptedToken { - /// Decrypt the content of the token from a given key - fn decrypt(&self, key: &[u8; 32]) -> anyhow::Result { - let key = GenericArray::from_slice(key); - let aead = ChaCha20Poly1305::new(key); - let message = aead.decrypt( - GenericArray::from_slice(&self.nonce[..]), - &self.ciphertext[..], - )?; - let token = bincode::deserialize(&message)?; - Ok(token) - } - - /// Encode the token to be then saved as a cookie - fn to_cookie_value(&self) -> anyhow::Result { - let raw = bincode::serialize(self)?; - Ok(BASE64URL_NOPAD.encode(&raw)) - } - - /// Extract the encrypted token from a cookie - fn from_cookie_value(value: &str) -> anyhow::Result { - let raw = BASE64URL_NOPAD.decode(value.as_bytes())?; - let content = bincode::deserialize(&raw)?; - Ok(content) - } - - fn to_cookie_builder<'c, 'n: 'c>(&self, name: &'n str) -> anyhow::Result> { - let value = self.to_cookie_value()?; - Ok(Cookie::build(name, value) - .http_only(true) - .same_site(SameSite::Strict)) - } } pub fn extract_or_generate( - key: [u8; 32], + cookies_config: &CookiesConfig, cookie_name: &'static str, ttl: Duration, -) -> BoxedFilter<(UnencryptedToken,)> { - warp::any() - .map(move || (key, ttl)) - .untuple_one() - .and(warp::cookie::optional(cookie_name)) - .and_then(|key, ttl, maybe_cookie: Option| async move { +) -> BoxedFilter<(CsrfToken,)> { + crate::filters::cookies::encrypted(cookie_name, cookies_config) + .and_then(move |maybe_token: Option| async move { // Explicitely specify the "Error" type here to have the `?` operation working Ok::<_, Rejection>( - maybe_cookie - // Try decrypting the cookie - .map(|cookie| UnencryptedToken::from_cookie_value(&cookie, &key)) - // If there was an error decrypting it, bail out here - .transpose() - .wrap_error()? + maybe_token // Verify its TTL (but do not hard-error if it expired) .and_then(|token| token.verify_expiration().ok()) .map_or_else( // Generate a new token if no valid one were found - || UnencryptedToken::generate(ttl), + || CsrfToken::generate(ttl), // Else, refresh the expiration of the token |token| token.refresh(ttl), ), @@ -188,43 +95,3 @@ pub fn extract_or_generate( }) .boxed() } - -pub struct WithTypedHeader { - reply: R, - header: H, -} - -impl Reply for WithTypedHeader -where - R: Reply, - H: Header + Send, -{ - fn into_response(self) -> warp::reply::Response { - let mut res = self.reply.into_response(); - res.headers_mut().typed_insert(self.header); - res - } -} - -pub fn with_csrf( - key: [u8; 32], - cookie_name: &'static str, -) -> impl Fn(F) -> BoxedFilter<(WithTypedHeader,)> -where - F: Filter + Clone + Send + Sync + 'static, -{ - move |f: F| { - f.and_then(move |token: UnencryptedToken, reply: R| async move { - let cookie = token - .to_cookie_builder(cookie_name, &key) - .wrap_error()? - .finish() - .to_string(); - let header = - SetCookie::decode(&mut [HeaderValue::from_str(&cookie).wrap_error()?].iter()) - .wrap_error()?; - Ok::<_, Rejection>(WithTypedHeader { reply, header }) - }) - .boxed() - } -} diff --git a/matrix-authentication-service/src/filters/mod.rs b/matrix-authentication-service/src/filters/mod.rs index b71613853..61ad29643 100644 --- a/matrix-authentication-service/src/filters/mod.rs +++ b/matrix-authentication-service/src/filters/mod.rs @@ -14,18 +14,21 @@ pub mod csrf; // mod errors; +pub mod cookies; +pub mod session; use sqlx::PgPool; use warp::{filters::BoxedFilter, Filter}; +pub use self::csrf::CsrfToken; use crate::templates::Templates; -pub use self::csrf::UnencryptedToken as CsrfToken; - -pub fn with_pool(pool: PgPool) -> BoxedFilter<(PgPool,)> { +pub fn with_pool(pool: &PgPool) -> BoxedFilter<(PgPool,)> { + let pool = pool.clone(); warp::any().map(move || pool.clone()).boxed() } -pub fn with_templates(templates: Templates) -> BoxedFilter<(Templates,)> { +pub fn with_templates(templates: &Templates) -> BoxedFilter<(Templates,)> { + let templates = templates.clone(); warp::any().map(move || templates.clone()).boxed() } diff --git a/matrix-authentication-service/src/filters/session.rs b/matrix-authentication-service/src/filters/session.rs new file mode 100644 index 000000000..a2c616254 --- /dev/null +++ b/matrix-authentication-service/src/filters/session.rs @@ -0,0 +1,81 @@ +// Copyright 2021 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use headers::SetCookie; +use serde::{Deserialize, Serialize}; +use sqlx::{Executor, PgPool, Postgres}; +use warp::{filters::BoxedFilter, Filter, Rejection, Reply}; + +use super::{ + cookies::{encrypted, save_encrypted, WithTypedHeader}, + with_pool, +}; +use crate::{ + config::CookiesConfig, + errors::WrapError, + storage::{lookup_active_session, SessionInfo}, +}; + +#[derive(Serialize, Deserialize)] +pub struct Session { + current: i32, +} + +impl Session { + pub fn from_session_info(info: &SessionInfo) -> Self { + Self { + current: info.key(), + } + } + + pub async fn load_session_info( + &self, + executor: impl Executor<'_, Database = Postgres>, + ) -> anyhow::Result { + lookup_active_session(executor, self.current).await + } +} + +pub fn with_session( + pool: &PgPool, + cookies_config: &CookiesConfig, +) -> BoxedFilter<(Option,)> { + encrypted("session", cookies_config) + .and(with_pool(pool)) + .and_then(|maybe_session: Option, pool: PgPool| async move { + let maybe_session_info = if let Some(session) = maybe_session { + Some(session.load_session_info(&pool).await.wrap_error()?) + } else { + None + }; + Ok::<_, Rejection>(maybe_session_info) + }) + .boxed() +} + +pub fn save_session( + cookies_config: &CookiesConfig, +) -> impl Fn(F) -> BoxedFilter<(WithTypedHeader,)> +where + F: Filter + Clone + Send + Sync + 'static, +{ + // This clone might be avoidable + let cookies_config = cookies_config.clone(); + move |f: F| { + let f = f + .map(|session_info, reply| (Session::from_session_info(&session_info), reply)) + .untuple_one(); + save_encrypted("session", &cookies_config)(f) + } +} diff --git a/matrix-authentication-service/src/handlers/health.rs b/matrix-authentication-service/src/handlers/health.rs index 785705579..24fa5ad5c 100644 --- a/matrix-authentication-service/src/handlers/health.rs +++ b/matrix-authentication-service/src/handlers/health.rs @@ -18,7 +18,7 @@ use warp::{filters::BoxedFilter, Filter, Rejection, Reply}; use crate::{errors::WrapError, filters::with_pool}; -pub fn filter(pool: PgPool) -> BoxedFilter<(impl Reply,)> { +pub fn filter(pool: &PgPool) -> BoxedFilter<(impl Reply,)> { warp::get() .and(warp::path("health")) .and(with_pool(pool)) diff --git a/matrix-authentication-service/src/handlers/mod.rs b/matrix-authentication-service/src/handlers/mod.rs index 31baea2a8..7563f7c66 100644 --- a/matrix-authentication-service/src/handlers/mod.rs +++ b/matrix-authentication-service/src/handlers/mod.rs @@ -24,12 +24,12 @@ mod views; use self::{health::filter as health, oauth2::filter as oauth2, views::filter as views}; pub fn root( - pool: PgPool, - templates: Templates, + pool: &PgPool, + templates: &Templates, config: &RootConfig, ) -> BoxedFilter<(impl warp::Reply,)> { - health(pool.clone()) + health(pool) .or(oauth2(&config.oauth2)) - .or(views(pool, templates, &config.csrf)) + .or(views(pool, templates, &config.csrf, &config.cookies)) .boxed() } diff --git a/matrix-authentication-service/src/handlers/views/index.rs b/matrix-authentication-service/src/handlers/views/index.rs index ba6726512..7f9c19435 100644 --- a/matrix-authentication-service/src/handlers/views/index.rs +++ b/matrix-authentication-service/src/handlers/views/index.rs @@ -16,41 +16,38 @@ use sqlx::PgPool; use warp::{filters::BoxedFilter, reply::with_header, wrap_fn, Filter, Rejection, Reply}; use crate::{ - config::CsrfConfig, + config::{CookiesConfig, CsrfConfig}, errors::WrapError, - filters::{csrf::with_csrf, with_pool, with_templates, CsrfToken}, + filters::{session::with_session, with_templates, CsrfToken}, + storage::SessionInfo, templates::{CommonContext, Templates}, }; pub(super) fn filter( - pool: PgPool, - templates: Templates, + pool: &PgPool, + templates: &Templates, csrf_config: &CsrfConfig, + cookies_config: &CookiesConfig, ) -> BoxedFilter<(impl Reply,)> { - // TODO: this is ugly and leaks - let csrf_cookie_name = Box::leak(Box::new(csrf_config.cookie_name.clone())); - warp::get() .and(warp::path::end()) .and(with_templates(templates)) - .and(csrf_config.to_extract_filter()) - .and(with_pool(pool)) + .and(csrf_config.to_extract_filter(cookies_config)) + .and(with_session(pool, cookies_config)) .and_then(get) .untuple_one() - .with(wrap_fn(with_csrf(csrf_config.key, csrf_cookie_name))) + .with(wrap_fn(csrf_config.to_save_filter(cookies_config))) .boxed() } async fn get( templates: Templates, csrf_token: CsrfToken, - db: PgPool, + session: Option, ) -> Result<(CsrfToken, impl Reply), Rejection> { let ctx = CommonContext::default() .with_csrf_token(&csrf_token) - .load_session(&db) - .await - .wrap_error()? + .maybe_with_session(session) .finish() .wrap_error()?; diff --git a/matrix-authentication-service/src/handlers/views/login.rs b/matrix-authentication-service/src/handlers/views/login.rs index c31674003..5f9918e6f 100644 --- a/matrix-authentication-service/src/handlers/views/login.rs +++ b/matrix-authentication-service/src/handlers/views/login.rs @@ -19,11 +19,14 @@ use warp::{ }; use crate::{ - config::CsrfConfig, + config::{CookiesConfig, CsrfConfig}, csrf::CsrfForm, errors::WrapError, - filters::{csrf::with_csrf, with_pool, with_templates, CsrfToken}, - storage::login, + filters::{ + session::{save_session, with_session}, + with_pool, with_templates, CsrfToken, + }, + storage::{login, SessionInfo}, templates::{CommonContext, Templates}, }; @@ -34,28 +37,26 @@ struct LoginForm { } pub(super) fn filter( - pool: PgPool, - templates: Templates, + pool: &PgPool, + templates: &Templates, csrf_config: &CsrfConfig, + cookies_config: &CookiesConfig, ) -> BoxedFilter<(impl Reply,)> { - // TODO: this is ugly and leaks - let csrf_cookie_name = Box::leak(Box::new(csrf_config.cookie_name.clone())); - let get = warp::get() .and(with_templates(templates)) - .and(csrf_config.to_extract_filter()) - .and(with_pool(pool.clone())) + .and(csrf_config.to_extract_filter(cookies_config)) + .and(with_session(pool, cookies_config)) .and_then(get) .untuple_one() - .with(wrap_fn(with_csrf(csrf_config.key, csrf_cookie_name))); + .with(wrap_fn(csrf_config.to_save_filter(cookies_config))); let post = warp::post() - .and(csrf_config.to_extract_filter()) + .and(csrf_config.to_extract_filter(cookies_config)) .and(with_pool(pool)) .and(warp::body::form()) .and_then(post) .untuple_one() - .with(wrap_fn(with_csrf(csrf_config.key, csrf_cookie_name))); + .with(wrap_fn(save_session(cookies_config))); warp::path("login").and(get.or(post)).boxed() } @@ -63,13 +64,11 @@ pub(super) fn filter( async fn get( templates: Templates, csrf_token: CsrfToken, - db: PgPool, + session: Option, ) -> Result<(CsrfToken, impl Reply), Rejection> { let ctx = CommonContext::default() .with_csrf_token(&csrf_token) - .load_session(&db) - .await - .wrap_error()? + .maybe_with_session(session) .finish() .wrap_error()?; @@ -85,12 +84,12 @@ async fn post( csrf_token: CsrfToken, db: PgPool, form: CsrfForm, -) -> Result<(CsrfToken, impl Reply), Rejection> { +) -> Result<(SessionInfo, impl Reply), Rejection> { let form = form.verify_csrf(&csrf_token).wrap_error()?; - let _session_info = login(&db, &form.username, &form.password) + let session_info = login(&db, &form.username, &form.password) .await .wrap_error()?; - Ok((csrf_token, warp::redirect(Uri::from_static("/")))) + Ok((session_info, warp::redirect(Uri::from_static("/")))) } diff --git a/matrix-authentication-service/src/handlers/views/logout.rs b/matrix-authentication-service/src/handlers/views/logout.rs index d1d186706..23f279748 100644 --- a/matrix-authentication-service/src/handlers/views/logout.rs +++ b/matrix-authentication-service/src/handlers/views/logout.rs @@ -12,28 +12,42 @@ // See the License for the specific language governing permissions and // limitations under the License. +use sqlx::PgPool; use warp::{filters::BoxedFilter, hyper::Uri, wrap_fn, Filter, Rejection, Reply}; use crate::{ - config::CsrfConfig, + config::{CookiesConfig, CsrfConfig}, csrf::CsrfForm, errors::WrapError, - filters::{csrf::with_csrf, CsrfToken}, + filters::{session::with_session, with_pool, CsrfToken}, + storage::SessionInfo, }; -pub(super) fn filter(csrf_config: &CsrfConfig) -> BoxedFilter<(impl Reply,)> { - // TODO: this is ugly and leaks - let csrf_cookie_name = Box::leak(Box::new(csrf_config.cookie_name.clone())); - +pub(super) fn filter( + pool: &PgPool, + csrf_config: &CsrfConfig, + cookies_config: &CookiesConfig, +) -> BoxedFilter<(impl Reply,)> { warp::post() .and(warp::path("logout")) - .and(csrf_config.to_extract_filter()) + .and(csrf_config.to_extract_filter(cookies_config)) + .and(with_session(pool, cookies_config)) + .and(with_pool(pool)) .and(warp::body::form()) - .and_then(|token: CsrfToken, form: CsrfForm<()>| async { - form.verify_csrf(&token).wrap_error()?; - Ok::<_, Rejection>((token, warp::redirect(Uri::from_static("/login")))) - }) + .and_then(post) .untuple_one() - .with(wrap_fn(with_csrf(csrf_config.key, csrf_cookie_name))) + .with(wrap_fn(csrf_config.to_save_filter(cookies_config))) .boxed() } + +async fn post( + token: CsrfToken, + session: Option, + pool: PgPool, + form: CsrfForm<()>, +) -> Result<(CsrfToken, impl Reply), Rejection> { + form.verify_csrf(&token).wrap_error()?; + // TODO: filter with forced active session + session.unwrap().end(&pool).await.wrap_error()?; + Ok::<_, Rejection>((token, warp::redirect(Uri::from_static("/login")))) +} diff --git a/matrix-authentication-service/src/handlers/views/mod.rs b/matrix-authentication-service/src/handlers/views/mod.rs index a67af32ad..e2d909cc6 100644 --- a/matrix-authentication-service/src/handlers/views/mod.rs +++ b/matrix-authentication-service/src/handlers/views/mod.rs @@ -15,26 +15,30 @@ use sqlx::PgPool; use warp::{filters::BoxedFilter, Filter, Reply}; -use crate::{config::CsrfConfig, templates::Templates}; +use crate::{ + config::{CookiesConfig, CsrfConfig}, + templates::Templates, +}; mod index; mod login; mod logout; mod reauth; -use self::index::filter as index; -use self::login::filter as login; -use self::logout::filter as logout; -use self::reauth::filter as reauth; +use self::{ + index::filter as index, login::filter as login, logout::filter as logout, + reauth::filter as reauth, +}; pub(super) fn filter( - pool: PgPool, - templates: Templates, + pool: &PgPool, + templates: &Templates, csrf_config: &CsrfConfig, + cookies_config: &CookiesConfig, ) -> BoxedFilter<(impl Reply,)> { - index(pool.clone(), templates.clone(), csrf_config) - .or(login(pool.clone(), templates.clone(), csrf_config)) - .or(logout(csrf_config)) - .or(reauth(pool, templates, csrf_config)) + index(pool, templates, csrf_config, cookies_config) + .or(login(pool, templates, csrf_config, cookies_config)) + .or(logout(pool, csrf_config, cookies_config)) + .or(reauth(pool, templates, csrf_config, cookies_config)) .boxed() } diff --git a/matrix-authentication-service/src/handlers/views/reauth.rs b/matrix-authentication-service/src/handlers/views/reauth.rs index db69971d5..65cd3b33e 100644 --- a/matrix-authentication-service/src/handlers/views/reauth.rs +++ b/matrix-authentication-service/src/handlers/views/reauth.rs @@ -14,14 +14,16 @@ use serde::Deserialize; use sqlx::PgPool; -use tracing::info; -use warp::{filters::BoxedFilter, reply::with_header, wrap_fn, Filter, Rejection, Reply}; +use warp::{ + filters::BoxedFilter, hyper::Uri, reply::with_header, wrap_fn, Filter, Rejection, Reply, +}; use crate::{ - config::CsrfConfig, + config::{CookiesConfig, CsrfConfig}, csrf::CsrfForm, errors::WrapError, - filters::{csrf::with_csrf, with_pool, with_templates, CsrfToken}, + filters::{session::with_session, with_pool, with_templates, CsrfToken}, + storage::SessionInfo, templates::{CommonContext, Templates}, }; @@ -31,28 +33,27 @@ struct ReauthForm { } pub(super) fn filter( - pool: PgPool, - templates: Templates, + pool: &PgPool, + templates: &Templates, csrf_config: &CsrfConfig, + cookies_config: &CookiesConfig, ) -> BoxedFilter<(impl Reply,)> { - // TODO: this is ugly and leaks - let csrf_cookie_name = Box::leak(Box::new(csrf_config.cookie_name.clone())); - let get = warp::get() .and(with_templates(templates)) - .and(csrf_config.to_extract_filter()) - .and(with_pool(pool.clone())) + .and(csrf_config.to_extract_filter(cookies_config)) + .and(with_session(pool, cookies_config)) .and_then(get) .untuple_one() - .with(wrap_fn(with_csrf(csrf_config.key, csrf_cookie_name))); + .with(wrap_fn(csrf_config.to_save_filter(cookies_config))); let post = warp::post() - .and(csrf_config.to_extract_filter()) + .and(csrf_config.to_extract_filter(cookies_config)) + .and(with_session(pool, cookies_config)) .and(with_pool(pool)) .and(warp::body::form()) .and_then(post) .untuple_one() - .with(wrap_fn(with_csrf(csrf_config.key, csrf_cookie_name))); + .with(wrap_fn(csrf_config.to_save_filter(cookies_config))); warp::path("reauth").and(get.or(post)).boxed() } @@ -60,13 +61,11 @@ pub(super) fn filter( async fn get( templates: Templates, csrf_token: CsrfToken, - db: PgPool, + session: Option, ) -> Result<(CsrfToken, impl Reply), Rejection> { let ctx = CommonContext::default() .with_csrf_token(&csrf_token) - .load_session(&db) - .await - .wrap_error()? + .maybe_with_session(session) .finish() .wrap_error()?; @@ -80,41 +79,17 @@ async fn get( async fn post( csrf_token: CsrfToken, - _db: PgPool, + session: Option, + pool: PgPool, form: CsrfForm, ) -> Result<(CsrfToken, impl Reply), Rejection> { let form = form.verify_csrf(&csrf_token).wrap_error()?; - - info!(?form, "reauth"); - - Ok((csrf_token, "unimplemented")) -} - -/* - let form = form.verify_csrf(&csrf_token).wrap_error()?; - - let _session_info = login(&db, &form.username, &form.password) + // TODO: filter with forced active session + let _session = session + .unwrap() + .reauth(&pool, &form.password) .await .wrap_error()?; Ok((csrf_token, warp::redirect(Uri::from_static("/")))) } - -pub async fn post(mut req: Request) -> tide::Result { - let form: CsrfForm = req.body_form().await?; - let form = form.verify_csrf(&req)?; - let state = req.state(); - let session = req.session(); - - let session_id = session - .get("current_session") - .ok_or_else(|| anyhow::anyhow!("could not find existing session"))?; - - let _session = state - .storage() - .lookup_and_reauth_session(session_id, &form.password) - .await?; - - Ok(Redirect::new("/").into()) -} -*/ diff --git a/matrix-authentication-service/src/main.rs b/matrix-authentication-service/src/main.rs index f92adbce1..90dbd28bb 100644 --- a/matrix-authentication-service/src/main.rs +++ b/matrix-authentication-service/src/main.rs @@ -27,7 +27,6 @@ mod csrf; mod errors; mod filters; mod handlers; -mod state; mod storage; mod templates; diff --git a/matrix-authentication-service/src/state.rs b/matrix-authentication-service/src/state.rs deleted file mode 100644 index e0cc4298e..000000000 --- a/matrix-authentication-service/src/state.rs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2021 The Matrix.org Foundation C.I.C. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::sync::Arc; - -use sqlx::PgPool; -use tera::Tera; - -use crate::{config::RootConfig, storage::Storage}; - -#[derive(Clone)] -pub struct State { - config: Arc, - templates: Arc, - storage: Arc>, -} - -impl std::fmt::Debug for State { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("State").finish() - } -} - -impl State { - pub fn new(config: RootConfig, templates: Tera, storage: Storage) -> Self { - Self { - config: Arc::new(config), - templates: Arc::new(templates), - storage: Arc::new(storage), - } - } - - pub fn config(&self) -> &RootConfig { - &self.config - } - - pub fn storage(&self) -> &Storage { - &self.storage - } - - pub fn templates(&self) -> &Tera { - &self.templates - } -} diff --git a/matrix-authentication-service/src/storage/client.rs b/matrix-authentication-service/src/storage/client.rs deleted file mode 100644 index 986ceeae4..000000000 --- a/matrix-authentication-service/src/storage/client.rs +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2021 The Matrix.org Foundation C.I.C. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::collections::HashSet; - -use serde::Serialize; -use thiserror::Error; -use url::Url; - -use crate::config::OAuth2ClientConfig; - -#[derive(Debug, Clone, Serialize)] -pub struct Client { - pub client_id: String, - redirect_uris: Option>, -} - -#[derive(Debug, Error)] -#[error("Could not find client")] -pub struct ClientLookupError; - -#[derive(Debug, Error)] -#[error("Invalid redirect URI")] -pub struct InvalidRedirectUriError; - -impl Client { - pub fn resolve_redirect_uri<'a>( - &'a self, - suggested_uri: &'a Option, - ) -> Result<&'a Url, InvalidRedirectUriError> { - match (suggested_uri, &self.redirect_uris) { - (None, None) => Err(InvalidRedirectUriError), - (None, Some(redirect_uris)) => { - redirect_uris.iter().next().ok_or(InvalidRedirectUriError) - } - (Some(suggested_uri), None) => Ok(suggested_uri), - (Some(suggested_uri), Some(redirect_uris)) => { - if redirect_uris.contains(&suggested_uri) { - Ok(suggested_uri) - } else { - Err(InvalidRedirectUriError) - } - } - } - } -} - -impl super::Storage { - pub fn with_static_clients(mut self, clients: &[OAuth2ClientConfig]) -> Self { - let storage = &mut self.clients; - for config in clients { - let redirect_uris = config - .redirect_uris - .as_ref() - .map(|uris| uris.iter().cloned().collect()); - let client_id = config.client_id.clone(); - - let client = Client { - client_id: client_id.clone(), - redirect_uris, - }; - - // TODO: we could warn about duplicate clients here - storage.insert(client_id, client); - } - - self - } - - pub async fn lookup_client(&self, client_id: &str) -> Result { - self.clients - .get(client_id) - .cloned() - .ok_or(ClientLookupError) - } -} diff --git a/matrix-authentication-service/src/storage/mod.rs b/matrix-authentication-service/src/storage/mod.rs index 0fbeb1c00..d42c0cb92 100644 --- a/matrix-authentication-service/src/storage/mod.rs +++ b/matrix-authentication-service/src/storage/mod.rs @@ -12,36 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::collections::HashMap; - use sqlx::migrate::Migrator; -mod client; -// mod session; mod user; -pub use self::{ - client::{Client, ClientLookupError, InvalidRedirectUriError}, - user::{login, lookup_session, SessionInfo, User}, -}; +pub use self::user::{login, lookup_active_session, register_user, SessionInfo, User}; pub static MIGRATOR: Migrator = sqlx::migrate!(); - -#[derive(Debug)] -pub struct Storage { - pool: Pool, - clients: HashMap, -} - -impl Storage { - pub fn new(pool: Pool) -> Self { - Self { - pool, - clients: HashMap::default(), - } - } - - pub fn pool(&self) -> &Pool { - &self.pool - } -} diff --git a/matrix-authentication-service/src/storage/session.rs b/matrix-authentication-service/src/storage/session.rs deleted file mode 100644 index 634c267ee..000000000 --- a/matrix-authentication-service/src/storage/session.rs +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright 2021 The Matrix.org Foundation C.I.C. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use async_trait::async_trait; -use sqlx::{types::Json, PgPool}; -use tide::sessions::{Session, SessionStore}; -use tracing::{info_span, Instrument}; - -#[derive(Debug, Clone)] -struct SqlxSessionStore { - pool: PgPool, -} - -#[async_trait] -impl SessionStore for SqlxSessionStore { - async fn load_session(&self, cookie_value: String) -> anyhow::Result> { - let id = Session::id_from_cookie_value(&cookie_value)?; - let mut conn = self.pool.acquire().await?; - - let result: Option<(Json,)> = sqlx::query_as( - r#" - SELECT session - FROM sessions - WHERE id = $1 - AND (expires IS NULL OR expires > $2) - "#, - ) - .bind(&id) - .bind(chrono::Utc::now()) - .fetch_optional(&mut conn) - .instrument(info_span!("Load session")) - .await?; - - Ok(result.map(|(session,)| session.0)) - } - - async fn store_session(&self, session: Session) -> anyhow::Result> { - let id = session.id(); - let expiry = session.expiry(); - let mut conn = self.pool.acquire().await?; - - sqlx::query( - r#" - INSERT INTO sessions - (id, session, expires) SELECT $1, $2, $3 - ON CONFLICT(id) DO UPDATE SET - expires = EXCLUDED.expires, - session = EXCLUDED.session - "#, - ) - .bind(&id) - .bind(&Json(&session)) - .bind(&expiry) - .execute(&mut conn) - .instrument(info_span!("Store session")) - .await?; - - Ok(session.into_cookie_value()) - } - - async fn destroy_session(&self, session: Session) -> anyhow::Result<()> { - let id = session.id(); - let mut conn = self.pool.acquire().await?; - - sqlx::query( - r#" - DELETE FROM sessions WHERE id = $1 - "#, - ) - .bind(&id) - .execute(&mut conn) - .instrument(info_span!("Destroy session")) - .await?; - - Ok(()) - } - - async fn clear_store(&self) -> anyhow::Result<()> { - let mut conn = self.pool.acquire().await?; - sqlx::query("TRUNCATE sessions") - .execute(&mut conn) - .instrument(info_span!("Clear session store")) - .await?; - Ok(()) - } -} - -impl super::Storage { - pub fn session_store(&self) -> impl SessionStore { - SqlxSessionStore { - pool: self.pool().clone(), - } - } -} diff --git a/matrix-authentication-service/src/storage/user.rs b/matrix-authentication-service/src/storage/user.rs index 7ef3d47fe..dc5741d5a 100644 --- a/matrix-authentication-service/src/storage/user.rs +++ b/matrix-authentication-service/src/storage/user.rs @@ -43,34 +43,21 @@ impl SessionInfo { pub fn key(&self) -> i32 { self.id } -} -impl super::Storage { - pub async fn login(&self, username: &str, password: &str) -> anyhow::Result { - login(&self.pool, username, password).await - } - - pub async fn register_user(&self, username: &str, password: &str) -> anyhow::Result { - let mut conn = self.pool.acquire().await?; - let hasher = Argon2::default(); - register_user(&mut conn, hasher, username, password).await - } - - pub async fn lookup_session(&self, id: i32) -> anyhow::Result { - let mut conn = self.pool.acquire().await?; - lookup_session(&mut conn, id).await - } - - pub async fn lookup_and_reauth_session( - &self, - session_id: i32, - password: &str, - ) -> anyhow::Result { - let mut txn = self.pool.begin().await?; - let mut session = lookup_session(&mut txn, session_id).await?; - session.last_authd_at = Some(authenticate_session(&mut txn, session.id, password).await?); + pub async fn reauth(mut self, pool: &PgPool, password: &str) -> anyhow::Result { + let mut txn = pool.begin().await?; + self.last_authd_at = Some(authenticate_session(&mut txn, self.id, password).await?); txn.commit().await?; - Ok(session) + Ok(self) + } + + pub async fn end( + mut self, + executor: impl Executor<'_, Database = Postgres>, + ) -> anyhow::Result { + end_session(executor, self.id).await?; + self.active = false; + Ok(self) } } @@ -83,7 +70,7 @@ pub async fn login(pool: &PgPool, username: &str, password: &str) -> anyhow::Res Ok(session) } -pub async fn lookup_session( +pub async fn lookup_active_session( executor: impl Executor<'_, Database = Postgres>, id: i32, ) -> anyhow::Result { @@ -101,7 +88,7 @@ pub async fn lookup_session( ON s.user_id = u.id LEFT JOIN user_session_authentications a ON a.session_id = s.id - WHERE s.id = $1 + WHERE s.id = $1 AND s.active ORDER BY a.created_at DESC LIMIT 1 "#, @@ -208,6 +195,24 @@ pub async fn register_user( }) } +pub async fn end_session( + executor: impl Executor<'_, Database = Postgres>, + id: i32, +) -> anyhow::Result<()> { + let res = sqlx::query("UPDATE user_sessions SET active = FALSE WHERE id = $1") + .bind(&id) + .execute(executor) + .instrument(info_span!("End session")) + .await + .context("could not end session")?; + + match res.rows_affected() { + 1 => Ok(()), + 0 => Err(anyhow::anyhow!("no row affected")), + _ => Err(anyhow::anyhow!("too many row affected")), + } +} + #[allow(dead_code)] pub async fn lookup_user_by_id( executor: impl Executor<'_, Database = Postgres>, diff --git a/matrix-authentication-service/src/templates.rs b/matrix-authentication-service/src/templates.rs index c7bcdeef1..cc4667b0d 100644 --- a/matrix-authentication-service/src/templates.rs +++ b/matrix-authentication-service/src/templates.rs @@ -16,14 +16,10 @@ use std::{ops::Deref, sync::Arc}; use anyhow::Context as _; use serde::Serialize; -use sqlx::{Executor, Postgres}; use tera::{Context, Tera}; use tracing::info; -use crate::{ - filters::CsrfToken, - storage::{lookup_session, SessionInfo}, -}; +use crate::{filters::CsrfToken, storage::SessionInfo}; #[derive(Clone)] pub struct Templates(Arc); @@ -48,7 +44,7 @@ impl Deref for Templates { #[derive(Serialize, Default)] pub struct CommonContext { csrf_token: Option, - session: Option, + current_session: Option, } impl CommonContext { @@ -59,48 +55,19 @@ impl CommonContext { } } - pub fn with_session(self, session: SessionInfo) -> Self { + pub fn maybe_with_session(self, current_session: Option) -> Self { Self { - session: Some(session), + current_session, ..self } } - pub async fn load_session<'e>( - self, - _executor: impl Executor<'e, Database = Postgres>, - ) -> anyhow::Result { - Ok(self) - /* - let session = lookup_session(executor, 1).await?; - Ok(Self { - session: Some(session), - ..self - }) - */ + #[allow(dead_code)] + pub fn with_session(self, current_session: SessionInfo) -> Self { + self.maybe_with_session(Some(current_session)) } pub fn finish(self) -> anyhow::Result { Context::from_serialize(&self).context("could not serialize common context for templates") } } - -// pub async fn common_context(req: &Request) -> Result { -// let state = req.state(); -// let session = req.session(); -// -// let mut ctx = Context::new(); -// -// let session_id: Option<_> = session.get("current_session"); -// if let Some(session_id) = session_id { -// let user = state.storage().lookup_session(session_id).await?; -// ctx.insert("current_session", &user); -// } -// -// let token: Option<&CsrfToken> = req.ext(); -// if let Some(token) = token { -// ctx.insert("csrf_token", &token.form_value()); -// } -// -// Ok(ctx) -// }