mirror of
https://github.com/element-hq/matrix-authentication-service.git
synced 2026-09-16 11:52:38 +00:00
migrate to warp, last part
This commit is contained in:
Generated
+16
-120
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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>,
|
||||
|
||||
@@ -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)
|
||||
// }
|
||||
|
||||
Reference in New Issue
Block a user