save sessions in database
This commit is contained in:
5
Cargo.lock
generated
5
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
-- Add migration script here
|
||||
@@ -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;
|
||||
@@ -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
|
||||
);
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
36
matrix-authentication-service/src/config/session.rs
Normal file
36
matrix-authentication-service/src/config/session.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ use async_std::sync::RwLock;
|
||||
use sqlx::migrate::Migrator;
|
||||
|
||||
mod client;
|
||||
mod session;
|
||||
mod user;
|
||||
|
||||
pub use self::{
|
||||
|
||||
91
matrix-authentication-service/src/storage/session.rs
Normal file
91
matrix-authentication-service/src/storage/session.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user