save sessions in database

This commit is contained in:
Quentin Gliech
2021-07-09 22:49:23 +02:00
parent 8bbc8c809a
commit adb2234b31
13 changed files with 196 additions and 57 deletions

5
Cargo.lock generated
View File

@@ -1490,6 +1490,7 @@ dependencies = [
"anyhow",
"async-std",
"async-trait",
"chrono",
"csrf",
"data-encoding",
"figment",
@@ -1502,7 +1503,6 @@ dependencies = [
"thiserror",
"tide",
"tide-tracing",
"time 0.2.27",
"tracing",
"tracing-subscriber",
"url",
@@ -2151,6 +2151,7 @@ version = "1.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad9fdbb69badc8916db738c25efd04f0a65297d26c2f8de4b62e57b8c12bc72"
dependencies = [
"chrono",
"hex",
"rustversion",
"serde",
@@ -2334,6 +2335,7 @@ dependencies = [
"bitflags",
"byteorder",
"bytes",
"chrono",
"crc",
"crossbeam-channel",
"crossbeam-queue",
@@ -2652,7 +2654,6 @@ checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242"
dependencies = [
"const_fn",
"libc",
"serde",
"standback",
"stdweb",
"time-macros",

View File

@@ -20,8 +20,8 @@ tera = "1.12.0"
anyhow = "1.0.41"
csrf = "0.4.0"
data-encoding = "2.3.2"
time = { version = "0.2.27", features = ["serde"] }
chrono = { version = "0.4.19", features = ["serde"] }
tide-tracing = "0.0.11"
mime = "0.3.16"
sqlx = { version = "0.5.5", features = ["runtime-async-std-rustls", "postgres", "migrate"] }
serde_with = { version = "1.9.4", features = ["hex"] }
sqlx = { version = "0.5.5", features = ["runtime-async-std-rustls", "postgres", "migrate", "chrono"] }
serde_with = { version = "1.9.4", features = ["hex", "chrono"] }

View File

@@ -1 +0,0 @@
-- Add migration script here

View File

@@ -0,0 +1,15 @@
-- 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.
DROP TABLE sessions;

View File

@@ -0,0 +1,19 @@
-- 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.
CREATE TABLE sessions (
"id" VARCHAR NOT NULL PRIMARY KEY,
"expires" TIMESTAMP WITH TIME ZONE NULL,
"session" JSONB NOT NULL
);

View File

@@ -12,16 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use csrf::AesGcmCsrfProtection;
use std::time::Duration;
use csrf::{AesGcmCsrfProtection, CsrfProtection};
use serde::Deserialize;
use serde_with::serde_as;
use tide::Middleware;
use time::Duration;
use crate::middlewares::CsrfMiddleware;
fn default_ttl() -> Duration {
Duration::hour()
Duration::from_secs(3600)
}
fn default_cookie_name() -> String {
@@ -38,11 +39,12 @@ pub struct Config {
cookie_name: String,
#[serde(default = "default_ttl")]
#[serde_as(as = "serde_with::DurationSeconds<u64>")]
ttl: Duration,
}
impl Config {
pub fn into_protection(self) -> AesGcmCsrfProtection {
pub fn into_protection(self) -> impl CsrfProtection {
AesGcmCsrfProtection::from_key(self.key)
}

View File

@@ -23,12 +23,14 @@ mod csrf;
mod database;
mod http;
mod oauth2;
mod session;
pub use self::{
csrf::Config as CsrfConfig,
database::Config as DatabaseConfig,
http::Config as HttpConfig,
oauth2::{ClientConfig as OAuth2ClientConfig, Config as OAuth2Config},
session::Config as SessionConfig,
};
#[derive(Debug, Deserialize)]
@@ -43,6 +45,8 @@ pub struct RootConfig {
pub database: DatabaseConfig,
pub csrf: CsrfConfig,
pub session: SessionConfig,
}
impl RootConfig {

View File

@@ -0,0 +1,36 @@
// 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 serde::Deserialize;
use serde_with::serde_as;
use tide::{
sessions::{SessionMiddleware, SessionStore},
Middleware,
};
#[serde_as]
#[derive(Debug, Deserialize)]
pub struct Config {
#[serde_as(as = "serde_with::hex::Hex")]
secret: Vec<u8>,
}
impl Config {
pub fn to_middleware<State: Clone + Send + Sync + 'static>(
&self,
store: impl SessionStore,
) -> impl Middleware<State> {
SessionMiddleware::new(store, &self.secret)
}
}

View File

@@ -111,14 +111,12 @@ pub fn install(app: &mut Server<State>) {
.allow_origin(Origin::from("*"))
.allow_credentials(false);
let csrf = state.config().csrf.clone().into_middleware();
app.at("/health").get(self::health::get);
app.at("/").nest({
let mut views = tide::with_state(state.clone());
views.with(state.session_middleware());
views.with(csrf);
views.with(state.csrf_middleware());
views.with(crate::middlewares::errors);
views.at("/").get(self::views::index::get);
views

View File

@@ -12,13 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::convert::TryInto;
use std::{convert::TryInto, time::Duration};
use async_trait::async_trait;
use csrf::CsrfProtection;
use data_encoding::BASE64;
use tide::http::Cookie;
use time::Duration;
#[derive(Debug, Clone)]
pub struct Middleware<T> {
@@ -28,10 +27,10 @@ pub struct Middleware<T> {
}
impl<T: CsrfProtection> Middleware<T> {
pub fn new(protection: T, cookie_name: String, ttl: Duration) -> Self {
pub fn new<D: Into<Duration>>(protection: T, cookie_name: String, ttl: D) -> Self {
Self {
protection,
ttl,
ttl: ttl.into(),
cookie_name,
}
}
@@ -53,9 +52,11 @@ where
.and_then(|cookie| BASE64.decode(cookie.value().as_bytes()).ok())
.and_then(|decoded| self.protection.parse_cookie(&decoded).ok())
.and_then(|parsed| parsed.value().try_into().ok());
let (token, cookie) = self
.protection
.generate_token_pair(previous_token_value.as_ref(), self.ttl.whole_seconds())?;
let (token, cookie) = self.protection.generate_token_pair(
previous_token_value.as_ref(),
self.ttl.as_secs().try_into()?,
)?;
request.set_ext(token);
@@ -63,7 +64,7 @@ where
response.insert_cookie(
Cookie::build(self.cookie_name.clone(), cookie.b64_string())
.http_only(true)
.max_age(Duration::seconds(self.ttl.whole_seconds()))
.max_age(self.ttl.try_into()?)
.same_site(tide::http::cookies::SameSite::Strict)
.finish(),
);

View File

@@ -14,13 +14,9 @@
use std::sync::Arc;
use async_trait::async_trait;
use sqlx::PgPool;
use tera::Tera;
use tide::{
sessions::{MemoryStore, SessionMiddleware, SessionStore},
Middleware,
};
use tide::Middleware;
use url::Url;
use crate::{config::RootConfig, storage::Storage};
@@ -30,7 +26,6 @@ pub struct State {
config: Arc<RootConfig>,
templates: Arc<Tera>,
storage: Arc<Storage<PgPool>>,
session_store: Arc<MemoryStore>,
}
impl std::fmt::Debug for State {
@@ -45,7 +40,6 @@ impl State {
config: Arc::new(config),
templates: Arc::new(templates),
storage: Arc::new(Storage::new(pool)),
session_store: Arc::new(MemoryStore::new()),
}
}
@@ -62,10 +56,13 @@ impl State {
}
pub fn session_middleware(&self) -> impl Middleware<Self> {
SessionMiddleware::new(
self.clone(),
b"some random value that we will figure out later",
)
self.config
.session
.to_middleware(self.storage.session_store())
}
pub fn csrf_middleware(&self) -> impl Middleware<Self> {
self.config.csrf.clone().into_middleware()
}
fn base(&self) -> Url {
@@ -88,28 +85,3 @@ impl State {
self.base().join(".well-known/jwks.json").ok()
}
}
#[async_trait]
impl SessionStore for State {
async fn load_session(
&self,
cookie_value: String,
) -> anyhow::Result<Option<tide::sessions::Session>> {
self.session_store.load_session(cookie_value).await
}
async fn store_session(
&self,
session: tide::sessions::Session,
) -> anyhow::Result<Option<String>> {
self.session_store.store_session(session).await
}
async fn destroy_session(&self, session: tide::sessions::Session) -> anyhow::Result<()> {
self.session_store.destroy_session(session).await
}
async fn clear_store(&self) -> anyhow::Result<()> {
self.session_store.clear_store().await
}
}

View File

@@ -18,6 +18,7 @@ use async_std::sync::RwLock;
use sqlx::migrate::Migrator;
mod client;
mod session;
mod user;
pub use self::{

View File

@@ -0,0 +1,91 @@
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(),
}
}
}