1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-08-07 17:03:01 +03:00

Handle cookies better by setting the right flags & expiration

This commit is contained in:
Quentin Gliech
2023-08-24 17:38:33 +02:00
parent 2405a3c061
commit a39f71c181
31 changed files with 242 additions and 167 deletions

1
Cargo.lock generated
View File

@@ -2909,7 +2909,6 @@ dependencies = [
"base64ct", "base64ct",
"chacha20poly1305", "chacha20poly1305",
"const-oid", "const-oid",
"cookie",
"der", "der",
"ecdsa", "ecdsa",
"elliptic-curve", "elliptic-curve",

View File

@@ -8,7 +8,7 @@ license = "Apache-2.0"
[dependencies] [dependencies]
async-trait = "0.1.73" async-trait = "0.1.73"
axum = { version = "0.6.20", features = ["headers"] } axum = { version = "0.6.20", features = ["headers"] }
axum-extra = { version = "0.7.7", features = ["cookie-private"] } axum-extra = { version = "0.7.7", features = ["cookie-private", "cookie-key-expansion"] }
chrono.workspace = true chrono.workspace = true
data-encoding = "2.4.0" data-encoding = "2.4.0"
futures-util = "0.3.28" futures-util = "0.3.28"

View File

@@ -14,8 +14,18 @@
//! Private (encrypted) cookie jar, based on axum-extra's cookie jar //! Private (encrypted) cookie jar, based on axum-extra's cookie jar
use std::convert::Infallible;
use async_trait::async_trait;
use axum::{
extract::{FromRef, FromRequestParts},
response::{IntoResponseParts, ResponseParts},
};
use axum_extra::extract::cookie::{Cookie, Key, PrivateCookieJar, SameSite};
use http::request::Parts;
use serde::{de::DeserializeOwned, Serialize}; use serde::{de::DeserializeOwned, Serialize};
use thiserror::Error; use thiserror::Error;
use url::Url;
#[derive(Debug, Error)] #[derive(Debug, Error)]
#[error("could not decode cookie")] #[error("could not decode cookie")]
@@ -23,32 +33,113 @@ pub enum CookieDecodeError {
Deserialize(#[from] serde_json::Error), Deserialize(#[from] serde_json::Error),
} }
pub trait CookieExt { /// Manages cookie options and encryption key
fn decode<T>(&self) -> Result<T, CookieDecodeError> ///
where /// This is meant to be accessible through axum's state via the [`FromRef`]
T: DeserializeOwned; /// trait
#[derive(Clone)]
pub struct CookieManager {
options: CookieOption,
key: Key,
}
impl CookieManager {
#[must_use]
pub const fn new(base_url: Url, key: Key) -> Self {
let options = CookieOption::new(base_url);
Self { options, key }
}
#[must_use] #[must_use]
fn encode<T>(self, t: &T) -> Self pub fn derive_from(base_url: Url, key: &[u8]) -> Self {
where let key = Key::derive_from(key);
T: Serialize; Self::new(base_url, key)
}
} }
impl<'a> CookieExt for axum_extra::extract::cookie::Cookie<'a> { #[async_trait]
fn decode<T>(&self) -> Result<T, CookieDecodeError> impl<S> FromRequestParts<S> for CookieJar
where where
T: DeserializeOwned, CookieManager: FromRef<S>,
{ S: Send + Sync,
let decoded = serde_json::from_str(self.value())?; {
Ok(decoded) type Rejection = Infallible;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let cookie_manager = CookieManager::from_ref(state);
let inner = PrivateCookieJar::from_headers(&parts.headers, cookie_manager.key.clone());
let options = cookie_manager.options.clone();
Ok(CookieJar { inner, options })
}
}
#[derive(Debug, Clone)]
struct CookieOption {
base_url: Url,
}
impl CookieOption {
const fn new(base_url: Url) -> Self {
Self { base_url }
} }
fn encode<T>(mut self, t: &T) -> Self fn secure(&self) -> bool {
where self.base_url.scheme() == "https"
T: Serialize, }
{
let encoded = serde_json::to_string(t).unwrap(); fn path(&self) -> &str {
self.set_value(encoded); self.base_url.path()
}
fn apply<'a>(&self, mut cookie: Cookie<'a>) -> Cookie<'a> {
cookie.set_http_only(true);
cookie.set_secure(self.secure());
cookie.set_path(self.path().to_owned());
cookie.set_same_site(SameSite::Lax);
cookie
}
}
/// A cookie jar which encrypts cookies & sets secure options
pub struct CookieJar {
inner: PrivateCookieJar<Key>,
options: CookieOption,
}
impl CookieJar {
#[must_use]
pub fn save<T: Serialize>(mut self, key: &str, payload: &T, permanent: bool) -> Self {
let serialized =
serde_json::to_string(payload).expect("failed to serialize cookie payload");
let cookie = Cookie::new(key.to_owned(), serialized);
let mut cookie = self.options.apply(cookie);
if permanent {
// XXX: this should use a clock
cookie.make_permanent();
}
self.inner = self.inner.add(cookie);
self self
} }
pub fn load<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>, CookieDecodeError> {
let Some(cookie) = self.inner.get(key) else {
return Ok(None);
};
let decoded = serde_json::from_str(cookie.value())?;
Ok(Some(decoded))
}
}
impl IntoResponseParts for CookieJar {
type Error = Infallible;
fn into_response_parts(self, res: ResponseParts) -> Result<ResponseParts, Self::Error> {
self.inner.into_response_parts(res)
}
} }

View File

@@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use axum_extra::extract::cookie::{Cookie, PrivateCookieJar};
use chrono::{DateTime, Duration, Utc}; use chrono::{DateTime, Duration, Utc};
use data_encoding::{DecodeError, BASE64URL_NOPAD}; use data_encoding::{DecodeError, BASE64URL_NOPAD};
use mas_storage::Clock; use mas_storage::Clock;
@@ -21,7 +20,7 @@ use serde::{Deserialize, Serialize};
use serde_with::{serde_as, TimestampSeconds}; use serde_with::{serde_as, TimestampSeconds};
use thiserror::Error; use thiserror::Error;
use crate::{cookies::CookieDecodeError, CookieExt}; use crate::cookies::{CookieDecodeError, CookieJar};
/// Failed to validate CSRF token /// Failed to validate CSRF token
#[derive(Debug, Error)] #[derive(Debug, Error)]
@@ -118,36 +117,41 @@ pub trait CsrfExt {
C: Clock; C: Clock;
} }
impl<K> CsrfExt for PrivateCookieJar<K> { impl CsrfExt for CookieJar {
fn csrf_token<C, R>(self, clock: &C, rng: R) -> (CsrfToken, Self) fn csrf_token<C, R>(self, clock: &C, rng: R) -> (CsrfToken, Self)
where where
R: RngCore, R: RngCore,
C: Clock, C: Clock,
{ {
let jar = self;
let mut cookie = jar.get("csrf").unwrap_or_else(|| Cookie::new("csrf", ""));
cookie.set_path("/");
cookie.set_http_only(true);
let now = clock.now(); let now = clock.now();
let new_token = cookie let maybe_token = match self.load::<CsrfToken>("csrf") {
.decode() Ok(Some(token)) => {
.ok() let token = token.verify_expiration(now);
.and_then(|token: CsrfToken| token.verify_expiration(now).ok())
.unwrap_or_else(|| CsrfToken::generate(now, rng, Duration::hours(1)))
.refresh(now, Duration::hours(1));
let cookie = cookie.encode(&new_token); // If the token is expired, just ignore it
let jar = jar.add(cookie); token.ok()
(new_token, jar) }
Ok(None) => None,
Err(e) => {
tracing::warn!("Failed to decode CSRF cookie: {}", e);
None
}
};
let token = maybe_token.map_or_else(
|| CsrfToken::generate(now, rng, Duration::hours(1)),
|token| token.refresh(now, Duration::hours(1)),
);
let jar = self.save("csrf", &token, false);
(token, jar)
} }
fn verify_form<C, T>(&self, clock: &C, form: ProtectedForm<T>) -> Result<T, CsrfError> fn verify_form<C, T>(&self, clock: &C, form: ProtectedForm<T>) -> Result<T, CsrfError>
where where
C: Clock, C: Clock,
{ {
let cookie = self.get("csrf").ok_or(CsrfError::Missing)?; let token: CsrfToken = self.load("csrf")?.ok_or(CsrfError::Missing)?;
let token: CsrfToken = cookie.decode()?;
let token = token.verify_expiration(clock.now())?; let token = token.verify_expiration(clock.now())?;
token.verify_form_value(&form.csrf)?; token.verify_form_value(&form.csrf)?;
Ok(form.inner) Ok(form.inner)

View File

@@ -34,7 +34,6 @@ pub mod user_authorization;
pub use axum; pub use axum;
pub use self::{ pub use self::{
cookies::CookieExt,
fancy_error::FancyError, fancy_error::FancyError,
session::{SessionInfo, SessionInfoExt}, session::{SessionInfo, SessionInfoExt},
}; };

View File

@@ -12,13 +12,12 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use axum_extra::extract::cookie::{Cookie, PrivateCookieJar};
use mas_data_model::BrowserSession; use mas_data_model::BrowserSession;
use mas_storage::{user::BrowserSessionRepository, RepositoryAccess}; use mas_storage::{user::BrowserSessionRepository, RepositoryAccess};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ulid::Ulid; use ulid::Ulid;
use crate::CookieExt; use crate::cookies::CookieJar;
/// An encrypted cookie to save the session ID /// An encrypted cookie to save the session ID
#[derive(Serialize, Deserialize, Debug, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
@@ -79,26 +78,22 @@ pub trait SessionInfoExt {
} }
} }
impl<K> SessionInfoExt for PrivateCookieJar<K> { impl SessionInfoExt for CookieJar {
fn session_info(self) -> (SessionInfo, Self) { fn session_info(self) -> (SessionInfo, Self) {
let jar = self; let info = match self.load("session") {
let mut cookie = jar Ok(Some(s)) => s,
.get("session") Ok(None) => SessionInfo::default(),
.unwrap_or_else(|| Cookie::new("session", "")); Err(e) => {
cookie.set_path("/"); tracing::error!("failed to load session cookie: {}", e);
cookie.set_http_only(true); SessionInfo::default()
let session_info = cookie.decode().unwrap_or_default(); }
};
let cookie = cookie.encode(&session_info); let jar = self.update_session_info(&info);
let jar = jar.add(cookie); (info, jar)
(session_info, jar)
} }
fn update_session_info(self, info: &SessionInfo) -> Self { fn update_session_info(self, info: &SessionInfo) -> Self {
let mut cookie = Cookie::new("session", ""); self.save("session", info, true)
cookie.set_path("/");
cookie.set_http_only(true);
let cookie = cookie.encode(&info);
self.add(cookie)
} }
} }

View File

@@ -18,7 +18,7 @@ use anyhow::Context;
use clap::Parser; use clap::Parser;
use itertools::Itertools; use itertools::Itertools;
use mas_config::AppConfig; use mas_config::AppConfig;
use mas_handlers::{AppState, HttpClientFactory, MatrixHomeserver}; use mas_handlers::{AppState, CookieManager, HttpClientFactory, MatrixHomeserver};
use mas_listener::{server::Server, shutdown::ShutdownStream}; use mas_listener::{server::Server, shutdown::ShutdownStream};
use mas_matrix_synapse::SynapseConnection; use mas_matrix_synapse::SynapseConnection;
use mas_router::UrlBuilder; use mas_router::UrlBuilder;
@@ -52,6 +52,11 @@ impl Options {
let span = info_span!("cli.run.init").entered(); let span = info_span!("cli.run.init").entered();
let config: AppConfig = root.load_config()?; let config: AppConfig = root.load_config()?;
// XXX: there should be a generic config verification step
if config.http.public_base.path() != "/" {
anyhow::bail!("The http.public_base path is not set to /, this is not supported");
}
// Connect to the database // Connect to the database
info!("Connecting to the database"); info!("Connecting to the database");
let pool = database_from_config(&config.database).await?; let pool = database_from_config(&config.database).await?;
@@ -73,6 +78,8 @@ impl Options {
.context("could not import keys from config")?; .context("could not import keys from config")?;
let encrypter = config.secrets.encrypter(); let encrypter = config.secrets.encrypter();
let cookie_manager =
CookieManager::derive_from(config.http.public_base.clone(), &config.secrets.encryption);
// Load and compile the WASM policies (and fallback to the default embedded one) // Load and compile the WASM policies (and fallback to the default embedded one)
info!("Loading and compiling the policy module"); info!("Loading and compiling the policy module");
@@ -140,6 +147,7 @@ impl Options {
pool, pool,
templates, templates,
key_store, key_store,
cookie_manager,
encrypter, encrypter,
url_builder, url_builder,
homeserver, homeserver,

View File

@@ -73,7 +73,7 @@ pub struct SecretsConfig {
example = "example_secret" example = "example_secret"
)] )]
#[serde_as(as = "serde_with::hex::Hex")] #[serde_as(as = "serde_with::hex::Hex")]
encryption: [u8; 32], pub encryption: [u8; 32],
/// List of private keys to use for signing and encrypting payloads /// List of private keys to use for signing and encrypting payloads
#[serde(default)] #[serde(default)]

View File

@@ -20,7 +20,7 @@ use axum::{
response::IntoResponse, response::IntoResponse,
}; };
use hyper::StatusCode; use hyper::StatusCode;
use mas_axum_utils::http_client_factory::HttpClientFactory; use mas_axum_utils::{cookies::CookieManager, http_client_factory::HttpClientFactory};
use mas_keystore::{Encrypter, Keystore}; use mas_keystore::{Encrypter, Keystore};
use mas_policy::PolicyFactory; use mas_policy::PolicyFactory;
use mas_router::UrlBuilder; use mas_router::UrlBuilder;
@@ -42,6 +42,7 @@ pub struct AppState {
pub pool: PgPool, pub pool: PgPool,
pub templates: Templates, pub templates: Templates,
pub key_store: Keystore, pub key_store: Keystore,
pub cookie_manager: CookieManager,
pub encrypter: Encrypter, pub encrypter: Encrypter,
pub url_builder: UrlBuilder, pub url_builder: UrlBuilder,
pub homeserver: MatrixHomeserver, pub homeserver: MatrixHomeserver,
@@ -161,6 +162,12 @@ impl FromRef<AppState> for PasswordManager {
} }
} }
impl FromRef<AppState> for CookieManager {
fn from_ref(input: &AppState) -> Self {
input.cookie_manager.clone()
}
}
#[async_trait] #[async_trait]
impl FromRequestParts<AppState> for BoxClock { impl FromRequestParts<AppState> for BoxClock {
type Rejection = Infallible; type Rejection = Infallible;

View File

@@ -20,14 +20,13 @@ use axum::{
extract::{Form, Path, Query, State}, extract::{Form, Path, Query, State},
response::{Html, IntoResponse, Redirect, Response}, response::{Html, IntoResponse, Redirect, Response},
}; };
use axum_extra::extract::PrivateCookieJar;
use chrono::Duration; use chrono::Duration;
use mas_axum_utils::{ use mas_axum_utils::{
cookies::CookieJar,
csrf::{CsrfExt, ProtectedForm}, csrf::{CsrfExt, ProtectedForm},
FancyError, SessionInfoExt, FancyError, SessionInfoExt,
}; };
use mas_data_model::Device; use mas_data_model::Device;
use mas_keystore::Encrypter;
use mas_router::{CompatLoginSsoAction, PostAuthAction, Route}; use mas_router::{CompatLoginSsoAction, PostAuthAction, Route};
use mas_storage::{ use mas_storage::{
compat::{CompatSessionRepository, CompatSsoLoginRepository}, compat::{CompatSessionRepository, CompatSsoLoginRepository},
@@ -63,7 +62,7 @@ pub async fn get(
clock: BoxClock, clock: BoxClock,
mut repo: BoxRepository, mut repo: BoxRepository,
State(templates): State<Templates>, State(templates): State<Templates>,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: CookieJar,
Path(id): Path<Ulid>, Path(id): Path<Ulid>,
Query(params): Query<Params>, Query(params): Query<Params>,
) -> Result<Response, FancyError> { ) -> Result<Response, FancyError> {
@@ -129,7 +128,7 @@ pub async fn post(
clock: BoxClock, clock: BoxClock,
mut repo: BoxRepository, mut repo: BoxRepository,
State(templates): State<Templates>, State(templates): State<Templates>,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: CookieJar,
Path(id): Path<Ulid>, Path(id): Path<Ulid>,
Query(params): Query<Params>, Query(params): Query<Params>,
Form(form): Form<ProtectedForm<()>>, Form(form): Form<ProtectedForm<()>>,

View File

@@ -25,13 +25,11 @@ use axum::{
response::{Html, IntoResponse, Response}, response::{Html, IntoResponse, Response},
Json, TypedHeader, Json, TypedHeader,
}; };
use axum_extra::extract::PrivateCookieJar;
use futures_util::TryStreamExt; use futures_util::TryStreamExt;
use headers::{authorization::Bearer, Authorization, ContentType, HeaderValue}; use headers::{authorization::Bearer, Authorization, ContentType, HeaderValue};
use hyper::header::CACHE_CONTROL; use hyper::header::CACHE_CONTROL;
use mas_axum_utils::{FancyError, SessionInfo, SessionInfoExt}; use mas_axum_utils::{cookies::CookieJar, FancyError, SessionInfo, SessionInfoExt};
use mas_graphql::{Requester, Schema}; use mas_graphql::{Requester, Schema};
use mas_keystore::Encrypter;
use mas_matrix::HomeserverConnection; use mas_matrix::HomeserverConnection;
use mas_storage::{ use mas_storage::{
BoxClock, BoxRepository, BoxRng, Clock, Repository, RepositoryError, SystemClock, BoxClock, BoxRepository, BoxRng, Clock, Repository, RepositoryError, SystemClock,
@@ -228,7 +226,7 @@ pub async fn post(
State(schema): State<Schema>, State(schema): State<Schema>,
clock: BoxClock, clock: BoxClock,
repo: BoxRepository, repo: BoxRepository,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: CookieJar,
content_type: Option<TypedHeader<ContentType>>, content_type: Option<TypedHeader<ContentType>>,
authorization: Option<TypedHeader<Authorization<Bearer>>>, authorization: Option<TypedHeader<Authorization<Bearer>>>,
body: BodyStream, body: BodyStream,
@@ -268,7 +266,7 @@ pub async fn get(
State(schema): State<Schema>, State(schema): State<Schema>,
clock: BoxClock, clock: BoxClock,
repo: BoxRepository, repo: BoxRepository,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: CookieJar,
authorization: Option<TypedHeader<Authorization<Bearer>>>, authorization: Option<TypedHeader<Authorization<Bearer>>>,
RawQuery(query): RawQuery, RawQuery(query): RawQuery,
) -> Result<impl IntoResponse, FancyError> { ) -> Result<impl IntoResponse, FancyError> {

View File

@@ -47,7 +47,7 @@ use hyper::{
}, },
StatusCode, Version, StatusCode, Version,
}; };
use mas_axum_utils::FancyError; use mas_axum_utils::{cookies::CookieJar, FancyError};
use mas_http::CorsLayerExt; use mas_http::CorsLayerExt;
use mas_keystore::{Encrypter, Keystore}; use mas_keystore::{Encrypter, Keystore};
use mas_policy::PolicyFactory; use mas_policy::PolicyFactory;
@@ -87,7 +87,7 @@ macro_rules! impl_from_error_for_route {
}; };
} }
pub use mas_axum_utils::http_client_factory::HttpClientFactory; pub use mas_axum_utils::{cookies::CookieManager, http_client_factory::HttpClientFactory};
pub use self::{app_state::AppState, compat::MatrixHomeserver, graphql::schema as graphql_schema}; pub use self::{app_state::AppState, compat::MatrixHomeserver, graphql::schema as graphql_schema};
@@ -110,6 +110,7 @@ where
BoxRepository: FromRequestParts<S>, BoxRepository: FromRequestParts<S>,
BoxClock: FromRequestParts<S>, BoxClock: FromRequestParts<S>,
Encrypter: FromRef<S>, Encrypter: FromRef<S>,
CookieJar: FromRequestParts<S>,
{ {
let mut router = Router::new().route( let mut router = Router::new().route(
"/graphql", "/graphql",
@@ -267,6 +268,7 @@ where
UrlBuilder: FromRef<S>, UrlBuilder: FromRef<S>,
Arc<PolicyFactory>: FromRef<S>, Arc<PolicyFactory>: FromRef<S>,
BoxRepository: FromRequestParts<S>, BoxRepository: FromRequestParts<S>,
CookieJar: FromRequestParts<S>,
Encrypter: FromRef<S>, Encrypter: FromRef<S>,
Templates: FromRef<S>, Templates: FromRef<S>,
Keystore: FromRef<S>, Keystore: FromRef<S>,

View File

@@ -18,11 +18,10 @@ use axum::{
extract::{Path, State}, extract::{Path, State},
response::{Html, IntoResponse, Response}, response::{Html, IntoResponse, Response},
}; };
use axum_extra::extract::PrivateCookieJar;
use hyper::StatusCode; use hyper::StatusCode;
use mas_axum_utils::{csrf::CsrfExt, SessionInfoExt}; use mas_axum_utils::{cookies::CookieJar, csrf::CsrfExt, SessionInfoExt};
use mas_data_model::{AuthorizationGrant, BrowserSession, Client, Device}; use mas_data_model::{AuthorizationGrant, BrowserSession, Client, Device};
use mas_keystore::{Encrypter, Keystore}; use mas_keystore::Keystore;
use mas_policy::{EvaluationResult, PolicyFactory}; use mas_policy::{EvaluationResult, PolicyFactory};
use mas_router::{PostAuthAction, Route, UrlBuilder}; use mas_router::{PostAuthAction, Route, UrlBuilder};
use mas_storage::{ use mas_storage::{
@@ -96,7 +95,7 @@ pub(crate) async fn get(
State(url_builder): State<UrlBuilder>, State(url_builder): State<UrlBuilder>,
State(key_store): State<Keystore>, State(key_store): State<Keystore>,
mut repo: BoxRepository, mut repo: BoxRepository,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: CookieJar,
Path(grant_id): Path<Ulid>, Path(grant_id): Path<Ulid>,
) -> Result<Response, RouteError> { ) -> Result<Response, RouteError> {
let (session_info, cookie_jar) = cookie_jar.session_info(); let (session_info, cookie_jar) = cookie_jar.session_info();

View File

@@ -18,11 +18,10 @@ use axum::{
extract::{Form, State}, extract::{Form, State},
response::{Html, IntoResponse, Response}, response::{Html, IntoResponse, Response},
}; };
use axum_extra::extract::PrivateCookieJar;
use hyper::StatusCode; use hyper::StatusCode;
use mas_axum_utils::{csrf::CsrfExt, SessionInfoExt}; use mas_axum_utils::{cookies::CookieJar, csrf::CsrfExt, SessionInfoExt};
use mas_data_model::{AuthorizationCode, Pkce}; use mas_data_model::{AuthorizationCode, Pkce};
use mas_keystore::{Encrypter, Keystore}; use mas_keystore::Keystore;
use mas_policy::PolicyFactory; use mas_policy::PolicyFactory;
use mas_router::{PostAuthAction, Route, UrlBuilder}; use mas_router::{PostAuthAction, Route, UrlBuilder};
use mas_storage::{ use mas_storage::{
@@ -146,7 +145,7 @@ pub(crate) async fn get(
State(key_store): State<Keystore>, State(key_store): State<Keystore>,
State(url_builder): State<UrlBuilder>, State(url_builder): State<UrlBuilder>,
mut repo: BoxRepository, mut repo: BoxRepository,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: CookieJar,
Form(params): Form<Params>, Form(params): Form<Params>,
) -> Result<Response, RouteError> { ) -> Result<Response, RouteError> {
// First, figure out what client it is // First, figure out what client it is

View File

@@ -18,14 +18,13 @@ use axum::{
extract::{Form, Path, State}, extract::{Form, Path, State},
response::{Html, IntoResponse, Response}, response::{Html, IntoResponse, Response},
}; };
use axum_extra::extract::PrivateCookieJar;
use hyper::StatusCode; use hyper::StatusCode;
use mas_axum_utils::{ use mas_axum_utils::{
cookies::CookieJar,
csrf::{CsrfExt, ProtectedForm}, csrf::{CsrfExt, ProtectedForm},
SessionInfoExt, SessionInfoExt,
}; };
use mas_data_model::{AuthorizationGrantStage, Device}; use mas_data_model::{AuthorizationGrantStage, Device};
use mas_keystore::Encrypter;
use mas_policy::PolicyFactory; use mas_policy::PolicyFactory;
use mas_router::{PostAuthAction, Route}; use mas_router::{PostAuthAction, Route};
use mas_storage::{ use mas_storage::{
@@ -84,7 +83,7 @@ pub(crate) async fn get(
State(policy_factory): State<Arc<PolicyFactory>>, State(policy_factory): State<Arc<PolicyFactory>>,
State(templates): State<Templates>, State(templates): State<Templates>,
mut repo: BoxRepository, mut repo: BoxRepository,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: CookieJar,
Path(grant_id): Path<Ulid>, Path(grant_id): Path<Ulid>,
) -> Result<Response, RouteError> { ) -> Result<Response, RouteError> {
let (session_info, cookie_jar) = cookie_jar.session_info(); let (session_info, cookie_jar) = cookie_jar.session_info();
@@ -149,7 +148,7 @@ pub(crate) async fn post(
clock: BoxClock, clock: BoxClock,
State(policy_factory): State<Arc<PolicyFactory>>, State(policy_factory): State<Arc<PolicyFactory>>,
mut repo: BoxRepository, mut repo: BoxRepository,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: CookieJar,
Path(grant_id): Path<Ulid>, Path(grant_id): Path<Ulid>,
Form(form): Form<ProtectedForm<()>>, Form(form): Form<ProtectedForm<()>>,
) -> Result<Response, RouteError> { ) -> Result<Response, RouteError> {

View File

@@ -24,7 +24,7 @@ use axum::{
}; };
use headers::{Authorization, ContentType, HeaderMapExt, HeaderName}; use headers::{Authorization, ContentType, HeaderMapExt, HeaderName};
use hyper::{header::CONTENT_TYPE, Request, Response, StatusCode}; use hyper::{header::CONTENT_TYPE, Request, Response, StatusCode};
use mas_axum_utils::http_client_factory::HttpClientFactory; use mas_axum_utils::{cookies::CookieManager, http_client_factory::HttpClientFactory};
use mas_keystore::{Encrypter, JsonWebKey, JsonWebKeySet, Keystore, PrivateKey}; use mas_keystore::{Encrypter, JsonWebKey, JsonWebKeySet, Keystore, PrivateKey};
use mas_matrix::{HomeserverConnection, MockHomeserverConnection}; use mas_matrix::{HomeserverConnection, MockHomeserverConnection};
use mas_policy::PolicyFactory; use mas_policy::PolicyFactory;
@@ -59,6 +59,7 @@ pub(crate) struct TestState {
pub pool: PgPool, pub pool: PgPool,
pub templates: Templates, pub templates: Templates,
pub key_store: Keystore, pub key_store: Keystore,
pub cookie_manager: CookieManager,
pub encrypter: Encrypter, pub encrypter: Encrypter,
pub url_builder: UrlBuilder, pub url_builder: UrlBuilder,
pub homeserver: MatrixHomeserver, pub homeserver: MatrixHomeserver,
@@ -95,6 +96,8 @@ impl TestState {
let key_store = Keystore::new(jwks); let key_store = Keystore::new(jwks);
let encrypter = Encrypter::new(&[0x42; 32]); let encrypter = Encrypter::new(&[0x42; 32]);
let cookie_manager =
CookieManager::derive_from("https://example.com".parse()?, &[0x42; 32]);
let password_manager = PasswordManager::new([(1, Hasher::argon2id(None))])?; let password_manager = PasswordManager::new([(1, Hasher::argon2id(None))])?;
@@ -135,6 +138,7 @@ impl TestState {
pool, pool,
templates, templates,
key_store, key_store,
cookie_manager,
encrypter, encrypter,
url_builder, url_builder,
homeserver, homeserver,
@@ -317,6 +321,12 @@ impl FromRef<TestState> for PasswordManager {
} }
} }
impl FromRef<TestState> for CookieManager {
fn from_ref(input: &TestState) -> Self {
input.cookie_manager.clone()
}
}
#[async_trait] #[async_trait]
impl FromRequestParts<TestState> for BoxClock { impl FromRequestParts<TestState> for BoxClock {
type Rejection = Infallible; type Rejection = Infallible;

View File

@@ -16,10 +16,8 @@ use axum::{
extract::{Path, Query, State}, extract::{Path, Query, State},
response::{IntoResponse, Redirect}, response::{IntoResponse, Redirect},
}; };
use axum_extra::extract::PrivateCookieJar;
use hyper::StatusCode; use hyper::StatusCode;
use mas_axum_utils::http_client_factory::HttpClientFactory; use mas_axum_utils::{cookies::CookieJar, http_client_factory::HttpClientFactory};
use mas_keystore::Encrypter;
use mas_oidc_client::requests::authorization_code::AuthorizationRequestData; use mas_oidc_client::requests::authorization_code::AuthorizationRequestData;
use mas_router::UrlBuilder; use mas_router::UrlBuilder;
use mas_storage::{ use mas_storage::{
@@ -68,7 +66,7 @@ pub(crate) async fn get(
State(http_client_factory): State<HttpClientFactory>, State(http_client_factory): State<HttpClientFactory>,
mut repo: BoxRepository, mut repo: BoxRepository,
State(url_builder): State<UrlBuilder>, State(url_builder): State<UrlBuilder>,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: CookieJar,
Path(provider_id): Path<Ulid>, Path(provider_id): Path<Ulid>,
Query(query): Query<OptionalPostAuthAction>, Query(query): Query<OptionalPostAuthAction>,
) -> Result<impl IntoResponse, RouteError> { ) -> Result<impl IntoResponse, RouteError> {

View File

@@ -16,9 +16,8 @@ use axum::{
extract::{Path, Query, State}, extract::{Path, Query, State},
response::IntoResponse, response::IntoResponse,
}; };
use axum_extra::extract::PrivateCookieJar;
use hyper::StatusCode; use hyper::StatusCode;
use mas_axum_utils::http_client_factory::HttpClientFactory; use mas_axum_utils::{cookies::CookieJar, http_client_factory::HttpClientFactory};
use mas_jose::claims::ClaimError; use mas_jose::claims::ClaimError;
use mas_keystore::{Encrypter, Keystore}; use mas_keystore::{Encrypter, Keystore};
use mas_oidc_client::requests::{ use mas_oidc_client::requests::{
@@ -133,7 +132,7 @@ pub(crate) async fn get(
State(url_builder): State<UrlBuilder>, State(url_builder): State<UrlBuilder>,
State(encrypter): State<Encrypter>, State(encrypter): State<Encrypter>,
State(keystore): State<Keystore>, State(keystore): State<Keystore>,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: CookieJar,
Path(provider_id): Path<Ulid>, Path(provider_id): Path<Ulid>,
Query(params): Query<QueryParams>, Query(params): Query<QueryParams>,
) -> Result<impl IntoResponse, RouteError> { ) -> Result<impl IntoResponse, RouteError> {

View File

@@ -14,14 +14,12 @@
// TODO: move that to a standalone cookie manager // TODO: move that to a standalone cookie manager
use axum_extra::extract::{cookie::Cookie, PrivateCookieJar};
use chrono::{DateTime, Duration, NaiveDateTime, Utc}; use chrono::{DateTime, Duration, NaiveDateTime, Utc};
use mas_axum_utils::CookieExt; use mas_axum_utils::cookies::CookieJar;
use mas_router::PostAuthAction; use mas_router::PostAuthAction;
use mas_storage::Clock; use mas_storage::Clock;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use time::OffsetDateTime;
use ulid::Ulid; use ulid::Ulid;
/// Name of the cookie /// Name of the cookie
@@ -62,30 +60,24 @@ pub struct UpstreamSessionNotFound;
impl UpstreamSessions { impl UpstreamSessions {
/// Load the upstreams sessions cookie /// Load the upstreams sessions cookie
pub fn load<K>(cookie_jar: &PrivateCookieJar<K>) -> Self { pub fn load(cookie_jar: &CookieJar) -> Self {
cookie_jar match cookie_jar.load(COOKIE_NAME) {
.get(COOKIE_NAME) Ok(Some(sessions)) => sessions,
.and_then(|c| c.decode().ok()) Ok(None) => Self::default(),
.unwrap_or_default() Err(e) => {
tracing::warn!("Invalid upstream sessions cookie: {}", e);
Self::default()
}
}
} }
/// Save the upstreams sessions to the cookie jar /// Save the upstreams sessions to the cookie jar
pub fn save<K, C>(self, cookie_jar: PrivateCookieJar<K>, clock: &C) -> PrivateCookieJar<K> pub fn save<C>(self, cookie_jar: CookieJar, clock: &C) -> CookieJar
where where
C: Clock, C: Clock,
{ {
let now = clock.now(); let this = self.expire(clock.now());
let this = self.expire(now); cookie_jar.save(COOKIE_NAME, &this, false)
let mut cookie = Cookie::named(COOKIE_NAME).encode(&this);
cookie.set_path("/");
cookie.set_http_only(true);
let expiration = now + Duration::seconds(SESSION_MAX_TIME_SECS);
let expiration = OffsetDateTime::from_unix_timestamp(expiration.timestamp())
.expect("invalid unix timestamp");
cookie.set_expires(expiration);
cookie_jar.add(cookie)
} }
fn expire(mut self, now: DateTime<Utc>) -> Self { fn expire(mut self, now: DateTime<Utc>) -> Self {

View File

@@ -17,15 +17,14 @@ use axum::{
response::{Html, IntoResponse}, response::{Html, IntoResponse},
Form, Form,
}; };
use axum_extra::extract::PrivateCookieJar;
use hyper::StatusCode; use hyper::StatusCode;
use mas_axum_utils::{ use mas_axum_utils::{
cookies::CookieJar,
csrf::{CsrfExt, ProtectedForm}, csrf::{CsrfExt, ProtectedForm},
SessionInfoExt, SessionInfoExt,
}; };
use mas_data_model::{UpstreamOAuthProviderImportPreference, User}; use mas_data_model::{UpstreamOAuthProviderImportPreference, User};
use mas_jose::jwt::Jwt; use mas_jose::jwt::Jwt;
use mas_keystore::Encrypter;
use mas_storage::{ use mas_storage::{
job::{JobRepositoryExt, ProvisionUserJob}, job::{JobRepositoryExt, ProvisionUserJob},
upstream_oauth2::{UpstreamOAuthLinkRepository, UpstreamOAuthSessionRepository}, upstream_oauth2::{UpstreamOAuthLinkRepository, UpstreamOAuthSessionRepository},
@@ -170,7 +169,7 @@ pub(crate) async fn get(
clock: BoxClock, clock: BoxClock,
mut repo: BoxRepository, mut repo: BoxRepository,
State(templates): State<Templates>, State(templates): State<Templates>,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: CookieJar,
Path(link_id): Path<Ulid>, Path(link_id): Path<Ulid>,
) -> Result<impl IntoResponse, RouteError> { ) -> Result<impl IntoResponse, RouteError> {
let sessions_cookie = UpstreamSessionsCookie::load(&cookie_jar); let sessions_cookie = UpstreamSessionsCookie::load(&cookie_jar);
@@ -350,7 +349,7 @@ pub(crate) async fn post(
mut rng: BoxRng, mut rng: BoxRng,
clock: BoxClock, clock: BoxClock,
mut repo: BoxRepository, mut repo: BoxRepository,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: CookieJar,
Path(link_id): Path<Ulid>, Path(link_id): Path<Ulid>,
Form(form): Form<ProtectedForm<FormData>>, Form(form): Form<ProtectedForm<FormData>>,
) -> Result<impl IntoResponse, RouteError> { ) -> Result<impl IntoResponse, RouteError> {

View File

@@ -16,12 +16,11 @@ use axum::{
extract::{Form, Query, State}, extract::{Form, Query, State},
response::{Html, IntoResponse, Response}, response::{Html, IntoResponse, Response},
}; };
use axum_extra::extract::PrivateCookieJar;
use mas_axum_utils::{ use mas_axum_utils::{
cookies::CookieJar,
csrf::{CsrfExt, ProtectedForm}, csrf::{CsrfExt, ProtectedForm},
FancyError, SessionInfoExt, FancyError, SessionInfoExt,
}; };
use mas_keystore::Encrypter;
use mas_router::Route; use mas_router::Route;
use mas_storage::{ use mas_storage::{
job::{JobRepositoryExt, VerifyEmailJob}, job::{JobRepositoryExt, VerifyEmailJob},
@@ -44,7 +43,7 @@ pub(crate) async fn get(
clock: BoxClock, clock: BoxClock,
State(templates): State<Templates>, State(templates): State<Templates>,
mut repo: BoxRepository, mut repo: BoxRepository,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: CookieJar,
) -> Result<Response, FancyError> { ) -> Result<Response, FancyError> {
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng); let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let (session_info, cookie_jar) = cookie_jar.session_info(); let (session_info, cookie_jar) = cookie_jar.session_info();
@@ -70,7 +69,7 @@ pub(crate) async fn post(
mut rng: BoxRng, mut rng: BoxRng,
clock: BoxClock, clock: BoxClock,
mut repo: BoxRepository, mut repo: BoxRepository,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: CookieJar,
Query(query): Query<OptionalPostAuthAction>, Query(query): Query<OptionalPostAuthAction>,
Form(form): Form<ProtectedForm<EmailForm>>, Form(form): Form<ProtectedForm<EmailForm>>,
) -> Result<Response, FancyError> { ) -> Result<Response, FancyError> {

View File

@@ -17,12 +17,11 @@ use axum::{
extract::{Form, Path, Query, State}, extract::{Form, Path, Query, State},
response::{Html, IntoResponse, Response}, response::{Html, IntoResponse, Response},
}; };
use axum_extra::extract::PrivateCookieJar;
use mas_axum_utils::{ use mas_axum_utils::{
cookies::CookieJar,
csrf::{CsrfExt, ProtectedForm}, csrf::{CsrfExt, ProtectedForm},
FancyError, SessionInfoExt, FancyError, SessionInfoExt,
}; };
use mas_keystore::Encrypter;
use mas_router::Route; use mas_router::Route;
use mas_storage::{ use mas_storage::{
job::{JobRepositoryExt, ProvisionUserJob}, job::{JobRepositoryExt, ProvisionUserJob},
@@ -53,7 +52,7 @@ pub(crate) async fn get(
mut repo: BoxRepository, mut repo: BoxRepository,
Query(query): Query<OptionalPostAuthAction>, Query(query): Query<OptionalPostAuthAction>,
Path(id): Path<Ulid>, Path(id): Path<Ulid>,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: CookieJar,
) -> Result<Response, FancyError> { ) -> Result<Response, FancyError> {
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng); let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let (session_info, cookie_jar) = cookie_jar.session_info(); let (session_info, cookie_jar) = cookie_jar.session_info();
@@ -96,7 +95,7 @@ pub(crate) async fn get(
pub(crate) async fn post( pub(crate) async fn post(
clock: BoxClock, clock: BoxClock,
mut repo: BoxRepository, mut repo: BoxRepository,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: CookieJar,
Query(query): Query<OptionalPostAuthAction>, Query(query): Query<OptionalPostAuthAction>,
Path(id): Path<Ulid>, Path(id): Path<Ulid>,
Form(form): Form<ProtectedForm<CodeForm>>, Form(form): Form<ProtectedForm<CodeForm>>,

View File

@@ -18,13 +18,12 @@ use axum::{
http::StatusCode, http::StatusCode,
response::{Html, IntoResponse, Response}, response::{Html, IntoResponse, Response},
}; };
use axum_extra::extract::PrivateCookieJar;
use mas_axum_utils::{ use mas_axum_utils::{
cookies::CookieJar,
csrf::{CsrfExt, ProtectedForm}, csrf::{CsrfExt, ProtectedForm},
FancyError, SessionInfoExt, FancyError, SessionInfoExt,
}; };
use mas_data_model::BrowserSession; use mas_data_model::BrowserSession;
use mas_keystore::Encrypter;
use mas_router::Route; use mas_router::Route;
use mas_storage::{ use mas_storage::{
user::{BrowserSessionRepository, UserPasswordRepository}, user::{BrowserSessionRepository, UserPasswordRepository},
@@ -51,7 +50,7 @@ pub(crate) async fn get(
State(templates): State<Templates>, State(templates): State<Templates>,
State(password_manager): State<PasswordManager>, State(password_manager): State<PasswordManager>,
mut repo: BoxRepository, mut repo: BoxRepository,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: CookieJar,
) -> Result<Response, FancyError> { ) -> Result<Response, FancyError> {
// If the password manager is disabled, we can go back to the account page. // If the password manager is disabled, we can go back to the account page.
if !password_manager.is_enabled() { if !password_manager.is_enabled() {
@@ -75,7 +74,7 @@ async fn render(
clock: &impl Clock, clock: &impl Clock,
templates: Templates, templates: Templates,
session: BrowserSession, session: BrowserSession,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: CookieJar,
) -> Result<Response, FancyError> { ) -> Result<Response, FancyError> {
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(clock, rng); let (csrf_token, cookie_jar) = cookie_jar.csrf_token(clock, rng);
@@ -95,7 +94,7 @@ pub(crate) async fn post(
State(password_manager): State<PasswordManager>, State(password_manager): State<PasswordManager>,
State(templates): State<Templates>, State(templates): State<Templates>,
mut repo: BoxRepository, mut repo: BoxRepository,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: CookieJar,
Form(form): Form<ProtectedForm<ChangeForm>>, Form(form): Form<ProtectedForm<ChangeForm>>,
) -> Result<Response, FancyError> { ) -> Result<Response, FancyError> {
if !password_manager.is_enabled() { if !password_manager.is_enabled() {

View File

@@ -16,9 +16,7 @@ use axum::{
extract::State, extract::State,
response::{Html, IntoResponse}, response::{Html, IntoResponse},
}; };
use axum_extra::extract::PrivateCookieJar; use mas_axum_utils::{cookies::CookieJar, FancyError, SessionInfoExt};
use mas_axum_utils::{FancyError, SessionInfoExt};
use mas_keystore::Encrypter;
use mas_router::{PostAuthAction, Route}; use mas_router::{PostAuthAction, Route};
use mas_storage::BoxRepository; use mas_storage::BoxRepository;
use mas_templates::{AppContext, Templates}; use mas_templates::{AppContext, Templates};
@@ -27,7 +25,7 @@ use mas_templates::{AppContext, Templates};
pub async fn get( pub async fn get(
State(templates): State<Templates>, State(templates): State<Templates>,
mut repo: BoxRepository, mut repo: BoxRepository,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: CookieJar,
) -> Result<impl IntoResponse, FancyError> { ) -> Result<impl IntoResponse, FancyError> {
let (session_info, cookie_jar) = cookie_jar.session_info(); let (session_info, cookie_jar) = cookie_jar.session_info();
let session = session_info.load_session(&mut repo).await?; let session = session_info.load_session(&mut repo).await?;

View File

@@ -16,9 +16,7 @@ use axum::{
extract::State, extract::State,
response::{Html, IntoResponse}, response::{Html, IntoResponse},
}; };
use axum_extra::extract::PrivateCookieJar; use mas_axum_utils::{cookies::CookieJar, csrf::CsrfExt, FancyError, SessionInfoExt};
use mas_axum_utils::{csrf::CsrfExt, FancyError, SessionInfoExt};
use mas_keystore::Encrypter;
use mas_router::UrlBuilder; use mas_router::UrlBuilder;
use mas_storage::{BoxClock, BoxRepository, BoxRng}; use mas_storage::{BoxClock, BoxRepository, BoxRng};
use mas_templates::{IndexContext, TemplateContext, Templates}; use mas_templates::{IndexContext, TemplateContext, Templates};
@@ -30,7 +28,7 @@ pub async fn get(
State(templates): State<Templates>, State(templates): State<Templates>,
State(url_builder): State<UrlBuilder>, State(url_builder): State<UrlBuilder>,
mut repo: BoxRepository, mut repo: BoxRepository,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: CookieJar,
) -> Result<impl IntoResponse, FancyError> { ) -> Result<impl IntoResponse, FancyError> {
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng); let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let (session_info, cookie_jar) = cookie_jar.session_info(); let (session_info, cookie_jar) = cookie_jar.session_info();

View File

@@ -16,14 +16,13 @@ use axum::{
extract::{Form, Query, State}, extract::{Form, Query, State},
response::{Html, IntoResponse, Response}, response::{Html, IntoResponse, Response},
}; };
use axum_extra::extract::PrivateCookieJar;
use hyper::StatusCode; use hyper::StatusCode;
use mas_axum_utils::{ use mas_axum_utils::{
cookies::CookieJar,
csrf::{CsrfExt, CsrfToken, ProtectedForm}, csrf::{CsrfExt, CsrfToken, ProtectedForm},
FancyError, SessionInfoExt, FancyError, SessionInfoExt,
}; };
use mas_data_model::BrowserSession; use mas_data_model::BrowserSession;
use mas_keystore::Encrypter;
use mas_router::{Route, UpstreamOAuth2Authorize}; use mas_router::{Route, UpstreamOAuth2Authorize};
use mas_storage::{ use mas_storage::{
upstream_oauth2::UpstreamOAuthProviderRepository, upstream_oauth2::UpstreamOAuthProviderRepository,
@@ -58,7 +57,7 @@ pub(crate) async fn get(
State(templates): State<Templates>, State(templates): State<Templates>,
mut repo: BoxRepository, mut repo: BoxRepository,
Query(query): Query<OptionalPostAuthAction>, Query(query): Query<OptionalPostAuthAction>,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: CookieJar,
) -> Result<Response, FancyError> { ) -> Result<Response, FancyError> {
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng); let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let (session_info, cookie_jar) = cookie_jar.session_info(); let (session_info, cookie_jar) = cookie_jar.session_info();
@@ -109,7 +108,7 @@ pub(crate) async fn post(
State(templates): State<Templates>, State(templates): State<Templates>,
mut repo: BoxRepository, mut repo: BoxRepository,
Query(query): Query<OptionalPostAuthAction>, Query(query): Query<OptionalPostAuthAction>,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: CookieJar,
Form(form): Form<ProtectedForm<LoginForm>>, Form(form): Form<ProtectedForm<LoginForm>>,
) -> Result<Response, FancyError> { ) -> Result<Response, FancyError> {
if !password_manager.is_enabled() { if !password_manager.is_enabled() {

View File

@@ -13,12 +13,11 @@
// limitations under the License. // limitations under the License.
use axum::{extract::Form, response::IntoResponse}; use axum::{extract::Form, response::IntoResponse};
use axum_extra::extract::PrivateCookieJar;
use mas_axum_utils::{ use mas_axum_utils::{
cookies::CookieJar,
csrf::{CsrfExt, ProtectedForm}, csrf::{CsrfExt, ProtectedForm},
FancyError, SessionInfoExt, FancyError, SessionInfoExt,
}; };
use mas_keystore::Encrypter;
use mas_router::{PostAuthAction, Route}; use mas_router::{PostAuthAction, Route};
use mas_storage::{user::BrowserSessionRepository, BoxClock, BoxRepository}; use mas_storage::{user::BrowserSessionRepository, BoxClock, BoxRepository};
@@ -26,7 +25,7 @@ use mas_storage::{user::BrowserSessionRepository, BoxClock, BoxRepository};
pub(crate) async fn post( pub(crate) async fn post(
clock: BoxClock, clock: BoxClock,
mut repo: BoxRepository, mut repo: BoxRepository,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: CookieJar,
Form(form): Form<ProtectedForm<Option<PostAuthAction>>>, Form(form): Form<ProtectedForm<Option<PostAuthAction>>>,
) -> Result<impl IntoResponse, FancyError> { ) -> Result<impl IntoResponse, FancyError> {
let form = cookie_jar.verify_form(&clock, form)?; let form = cookie_jar.verify_form(&clock, form)?;

View File

@@ -17,13 +17,12 @@ use axum::{
extract::{Form, Query, State}, extract::{Form, Query, State},
response::{Html, IntoResponse, Response}, response::{Html, IntoResponse, Response},
}; };
use axum_extra::extract::PrivateCookieJar;
use hyper::StatusCode; use hyper::StatusCode;
use mas_axum_utils::{ use mas_axum_utils::{
cookies::CookieJar,
csrf::{CsrfExt, ProtectedForm}, csrf::{CsrfExt, ProtectedForm},
FancyError, SessionInfoExt, FancyError, SessionInfoExt,
}; };
use mas_keystore::Encrypter;
use mas_router::Route; use mas_router::Route;
use mas_storage::{ use mas_storage::{
user::{BrowserSessionRepository, UserPasswordRepository}, user::{BrowserSessionRepository, UserPasswordRepository},
@@ -49,7 +48,7 @@ pub(crate) async fn get(
State(templates): State<Templates>, State(templates): State<Templates>,
mut repo: BoxRepository, mut repo: BoxRepository,
Query(query): Query<OptionalPostAuthAction>, Query(query): Query<OptionalPostAuthAction>,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: CookieJar,
) -> Result<Response, FancyError> { ) -> Result<Response, FancyError> {
if !password_manager.is_enabled() { if !password_manager.is_enabled() {
// XXX: do something better here // XXX: do something better here
@@ -89,7 +88,7 @@ pub(crate) async fn post(
State(password_manager): State<PasswordManager>, State(password_manager): State<PasswordManager>,
mut repo: BoxRepository, mut repo: BoxRepository,
Query(query): Query<OptionalPostAuthAction>, Query(query): Query<OptionalPostAuthAction>,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: CookieJar,
Form(form): Form<ProtectedForm<ReauthForm>>, Form(form): Form<ProtectedForm<ReauthForm>>,
) -> Result<Response, FancyError> { ) -> Result<Response, FancyError> {
if !password_manager.is_enabled() { if !password_manager.is_enabled() {

View File

@@ -18,14 +18,13 @@ use axum::{
extract::{Form, Query, State}, extract::{Form, Query, State},
response::{Html, IntoResponse, Response}, response::{Html, IntoResponse, Response},
}; };
use axum_extra::extract::PrivateCookieJar;
use hyper::StatusCode; use hyper::StatusCode;
use lettre::Address; use lettre::Address;
use mas_axum_utils::{ use mas_axum_utils::{
cookies::CookieJar,
csrf::{CsrfExt, CsrfToken, ProtectedForm}, csrf::{CsrfExt, CsrfToken, ProtectedForm},
FancyError, SessionInfoExt, FancyError, SessionInfoExt,
}; };
use mas_keystore::Encrypter;
use mas_policy::PolicyFactory; use mas_policy::PolicyFactory;
use mas_router::Route; use mas_router::Route;
use mas_storage::{ use mas_storage::{
@@ -63,7 +62,7 @@ pub(crate) async fn get(
State(password_manager): State<PasswordManager>, State(password_manager): State<PasswordManager>,
mut repo: BoxRepository, mut repo: BoxRepository,
Query(query): Query<OptionalPostAuthAction>, Query(query): Query<OptionalPostAuthAction>,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: CookieJar,
) -> Result<Response, FancyError> { ) -> Result<Response, FancyError> {
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng); let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let (session_info, cookie_jar) = cookie_jar.session_info(); let (session_info, cookie_jar) = cookie_jar.session_info();
@@ -104,7 +103,7 @@ pub(crate) async fn post(
State(templates): State<Templates>, State(templates): State<Templates>,
mut repo: BoxRepository, mut repo: BoxRepository,
Query(query): Query<OptionalPostAuthAction>, Query(query): Query<OptionalPostAuthAction>,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: CookieJar,
Form(form): Form<ProtectedForm<RegisterForm>>, Form(form): Form<ProtectedForm<RegisterForm>>,
) -> Result<Response, FancyError> { ) -> Result<Response, FancyError> {
if !password_manager.is_enabled() { if !password_manager.is_enabled() {

View File

@@ -8,7 +8,6 @@ license = "Apache-2.0"
[dependencies] [dependencies]
aead = { version = "0.5.2", features = ["std"] } aead = { version = "0.5.2", features = ["std"] }
const-oid = { version = "0.9.5", features = ["std"] } const-oid = { version = "0.9.5", features = ["std"] }
cookie = { version = "0.17.0", features = ["key-expansion", "private"] }
der = { version = "0.7.8", features = ["std"] } der = { version = "0.7.8", features = ["std"] }
ecdsa = { version = "0.16.8", features = ["std"] } ecdsa = { version = "0.16.8", features = ["std"] }
elliptic-curve = { version = "0.13.5", features = ["std", "pem", "sec1"] } elliptic-curve = { version = "0.13.5", features = ["std", "pem", "sec1"] }

View File

@@ -17,23 +17,15 @@ use std::sync::Arc;
use aead::Aead; use aead::Aead;
use base64ct::{Base64, Encoding}; use base64ct::{Base64, Encoding};
use chacha20poly1305::{ChaCha20Poly1305, KeyInit}; use chacha20poly1305::{ChaCha20Poly1305, KeyInit};
use cookie::Key;
use generic_array::GenericArray; use generic_array::GenericArray;
use thiserror::Error; use thiserror::Error;
/// Helps encrypting and decrypting data /// Helps encrypting and decrypting data
#[derive(Clone)] #[derive(Clone)]
pub struct Encrypter { pub struct Encrypter {
cookie_key: Arc<Key>,
aead: Arc<ChaCha20Poly1305>, aead: Arc<ChaCha20Poly1305>,
} }
impl From<Encrypter> for Key {
fn from(e: Encrypter) -> Self {
e.cookie_key.as_ref().clone()
}
}
#[derive(Debug, Error)] #[derive(Debug, Error)]
#[error("Decryption error")] #[error("Decryption error")]
pub enum DecryptError { pub enum DecryptError {
@@ -46,12 +38,10 @@ impl Encrypter {
/// Creates an [`Encrypter`] out of an encryption key /// Creates an [`Encrypter`] out of an encryption key
#[must_use] #[must_use]
pub fn new(key: &[u8; 32]) -> Self { pub fn new(key: &[u8; 32]) -> Self {
let cookie_key = Key::derive_from(&key[..]);
let cookie_key = Arc::new(cookie_key);
let key = GenericArray::from_slice(key); let key = GenericArray::from_slice(key);
let aead = ChaCha20Poly1305::new(key); let aead = ChaCha20Poly1305::new(key);
let aead = Arc::new(aead); let aead = Arc::new(aead);
Self { cookie_key, aead } Self { aead }
} }
/// Encrypt a payload /// Encrypt a payload