migrate to warp, last part

This commit is contained in:
Quentin Gliech
2021-07-30 15:19:30 +02:00
parent d36069e0fe
commit 0499d1a47f
26 changed files with 459 additions and 857 deletions
Generated
+16 -120
View File
@@ -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",
]
+1 -2
View File
@@ -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"
@@ -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(())
@@ -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(())
@@ -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(),
}
}
}
@@ -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<i64>")]
@@ -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<R: Reply, F>(
&self,
cookies_config: &CookiesConfig,
) -> impl Fn(F) -> BoxedFilter<(WithTypedHeader<R, SetCookie>,)>
where
F: Filter<Extract = (CsrfToken, R), Error = Rejection> + 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(())
@@ -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(),
}
}
}
@@ -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<u8>,
}
// impl SessionConfig {
// pub fn to_middleware<State: Clone + Send + Sync + 'static>(
// &self,
// store: impl SessionStore,
// ) -> impl Middleware<State> {
// 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(())
})
}
}
@@ -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<u8>,
}
impl EncryptedCookie {
/// Encrypt from a given key
fn encrypt<T: Serialize>(payload: T, key: &[u8; 32]) -> anyhow::Result<Self> {
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<T: DeserializeOwned>(&self, key: &[u8; 32]) -> anyhow::Result<T> {
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<String> {
let raw = bincode::serialize(self)?;
Ok(BASE64URL_NOPAD.encode(&raw))
}
fn from_cookie_value(value: &str) -> anyhow::Result<Self> {
let raw = BASE64URL_NOPAD.decode(value.as_bytes())?;
let content = bincode::deserialize(&raw)?;
Ok(content)
}
}
pub fn encrypted<T>(name: &'static str, options: &CookiesConfig) -> BoxedFilter<(Option<T>,)>
where
T: DeserializeOwned + Send + 'static,
{
let secret = options.secret;
warp::cookie::optional(name)
.map(move |maybe_value: Option<String>| {
maybe_value
.and_then(|value| EncryptedCookie::from_cookie_value(&value).ok())
.and_then(|encrypted| encrypted.decrypt(&secret).ok())
})
.boxed()
}
pub struct WithTypedHeader<R, H> {
reply: R,
header: H,
}
impl<R, H> Reply for WithTypedHeader<R, H>
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<T, R: Reply, F>(
name: &'static str,
options: &CookiesConfig,
) -> impl Fn(F) -> BoxedFilter<(WithTypedHeader<R, SetCookie>,)>
where
T: Serialize + Send,
F: Filter<Extract = (T, R), Error = Rejection> + 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()
}
}
+10 -143
View File
@@ -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<i64>")]
expiration: DateTime<Utc>,
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<EncryptedToken> {
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<CookieBuilder<'c>> {
// 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<Self> {
let encrypted = EncryptedToken::from_cookie_value(value)?;
let token = encrypted.decrypt(key)?;
Ok(token)
}
}
#[derive(Serialize, Deserialize)]
struct EncryptedToken {
nonce: [u8; 12],
ciphertext: Vec<u8>,
}
impl EncryptedToken {
/// Decrypt the content of the token from a given key
fn decrypt(&self, key: &[u8; 32]) -> anyhow::Result<UnencryptedToken> {
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<String> {
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<Self> {
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<CookieBuilder<'c>> {
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<String>| async move {
) -> BoxedFilter<(CsrfToken,)> {
crate::filters::cookies::encrypted(cookie_name, cookies_config)
.and_then(move |maybe_token: Option<CsrfToken>| 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<R, H> {
reply: R,
header: H,
}
impl<R, H> Reply for WithTypedHeader<R, H>
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<R: Reply, F>(
key: [u8; 32],
cookie_name: &'static str,
) -> impl Fn(F) -> BoxedFilter<(WithTypedHeader<R, SetCookie>,)>
where
F: Filter<Extract = (UnencryptedToken, R), Error = Rejection> + 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()
}
}
@@ -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()
}
@@ -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<SessionInfo> {
lookup_active_session(executor, self.current).await
}
}
pub fn with_session(
pool: &PgPool,
cookies_config: &CookiesConfig,
) -> BoxedFilter<(Option<SessionInfo>,)> {
encrypted("session", cookies_config)
.and(with_pool(pool))
.and_then(|maybe_session: Option<Session>, 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<R: Reply, F>(
cookies_config: &CookiesConfig,
) -> impl Fn(F) -> BoxedFilter<(WithTypedHeader<R, SetCookie>,)>
where
F: Filter<Extract = (SessionInfo, R), Error = Rejection> + 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)
}
}
@@ -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))
@@ -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()
}
@@ -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<SessionInfo>,
) -> 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()?;
@@ -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<crate::storage::SessionInfo>,
) -> 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<LoginForm>,
) -> 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("/"))))
}
@@ -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<SessionInfo>,
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"))))
}
@@ -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()
}
@@ -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<SessionInfo>,
) -> 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<SessionInfo>,
pool: PgPool,
form: CsrfForm<ReauthForm>,
) -> 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<State>) -> tide::Result {
let form: CsrfForm<ReauthForm> = 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())
}
*/
@@ -27,7 +27,6 @@ mod csrf;
mod errors;
mod filters;
mod handlers;
mod state;
mod storage;
mod templates;
@@ -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<RootConfig>,
templates: Arc<Tera>,
storage: Arc<Storage<PgPool>>,
}
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<PgPool>) -> 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<PgPool> {
&self.storage
}
pub fn templates(&self) -> &Tera {
&self.templates
}
}
@@ -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<HashSet<Url>>,
}
#[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<Url>,
) -> 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<T> super::Storage<T> {
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<Client, ClientLookupError> {
self.clients
.get(client_id)
.cloned()
.ok_or(ClientLookupError)
}
}
@@ -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: Pool,
clients: HashMap<String, Client>,
}
impl<Pool> Storage<Pool> {
pub fn new(pool: Pool) -> Self {
Self {
pool,
clients: HashMap::default(),
}
}
pub fn pool(&self) -> &Pool {
&self.pool
}
}
@@ -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<Option<Session>> {
let id = Session::id_from_cookie_value(&cookie_value)?;
let mut conn = self.pool.acquire().await?;
let result: Option<(Json<Session>,)> = 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<Option<String>> {
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<PgPool> {
pub fn session_store(&self) -> impl SessionStore {
SqlxSessionStore {
pool: self.pool().clone(),
}
}
}
@@ -43,34 +43,21 @@ impl SessionInfo {
pub fn key(&self) -> i32 {
self.id
}
}
impl super::Storage<PgPool> {
pub async fn login(&self, username: &str, password: &str) -> anyhow::Result<SessionInfo> {
login(&self.pool, username, password).await
}
pub async fn register_user(&self, username: &str, password: &str) -> anyhow::Result<User> {
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<SessionInfo> {
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<SessionInfo> {
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<Self> {
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<Self> {
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<SessionInfo> {
@@ -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>,
+7 -40
View File
@@ -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<Tera>);
@@ -48,7 +44,7 @@ impl Deref for Templates {
#[derive(Serialize, Default)]
pub struct CommonContext {
csrf_token: Option<String>,
session: Option<SessionInfo>,
current_session: Option<SessionInfo>,
}
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<SessionInfo>) -> Self {
Self {
session: Some(session),
current_session,
..self
}
}
pub async fn load_session<'e>(
self,
_executor: impl Executor<'e, Database = Postgres>,
) -> anyhow::Result<Self> {
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> {
Context::from_serialize(&self).context("could not serialize common context for templates")
}
}
// pub async fn common_context(req: &Request<State>) -> Result<Context, anyhow::Error> {
// 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)
// }