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

Make the access tokens TTL configurable

This commit is contained in:
Quentin Gliech
2023-08-31 16:30:08 +02:00
parent 73d33dfccb
commit bc04860afb
11 changed files with 155 additions and 93 deletions

View File

@ -18,7 +18,9 @@ use anyhow::Context;
use clap::Parser;
use itertools::Itertools;
use mas_config::AppConfig;
use mas_handlers::{AppState, CookieManager, HttpClientFactory, MatrixHomeserver, MetadataCache};
use mas_handlers::{
AppState, CookieManager, HttpClientFactory, MatrixHomeserver, MetadataCache, SiteConfig,
};
use mas_listener::{server::Server, shutdown::ShutdownStream};
use mas_matrix_synapse::SynapseConnection;
use mas_router::UrlBuilder;
@ -137,6 +139,11 @@ impl Options {
http_client_factory.clone(),
);
let site_config = SiteConfig {
access_token_ttl: config.hack.access_token_ttl,
compat_token_ttl: config.hack.compat_token_ttl,
};
// Explicitly the config to properly zeroize secret keys
drop(config);
@ -159,6 +166,7 @@ impl Options {
graphql_schema,
http_client_factory,
password_manager,
site_config,
conn_acquisition_histogram: None,
};
s.init_metrics()?;

View File

@ -1,4 +1,4 @@
// Copyright 2021, 2022 The Matrix.org Foundation C.I.C.
// Copyright 2023 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.
@ -19,33 +19,42 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use super::ConfigurationSection;
use crate::ConfigurationSection;
fn default_ttl() -> Duration {
Duration::hours(1)
fn default_token_ttl() -> Duration {
Duration::minutes(5)
}
/// Configuration related to Cross-Site Request Forgery protections
/// Configuration sections for miscellaneous options
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct CsrfConfig {
/// Time-to-live of a CSRF token in seconds
#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)]
pub struct HackConfig {
/// Time-to-live of access tokens in seconds
#[schemars(with = "u64", range(min = 60, max = 86400))]
#[serde(default = "default_ttl")]
#[serde(default = "default_token_ttl")]
#[serde_as(as = "serde_with::DurationSeconds<i64>")]
pub ttl: Duration,
pub access_token_ttl: Duration,
/// Time-to-live of compatibility access tokens in seconds
#[schemars(with = "u64", range(min = 60, max = 86400))]
#[serde(default = "default_token_ttl")]
#[serde_as(as = "serde_with::DurationSeconds<i64>")]
pub compat_token_ttl: Duration,
}
impl Default for CsrfConfig {
impl Default for HackConfig {
fn default() -> Self {
Self { ttl: default_ttl() }
Self {
access_token_ttl: default_token_ttl(),
compat_token_ttl: default_token_ttl(),
}
}
}
#[async_trait]
impl ConfigurationSection for CsrfConfig {
impl ConfigurationSection for HackConfig {
fn path() -> &'static str {
"csrf"
"hack"
}
async fn generate<R>(_rng: R) -> anyhow::Result<Self>
@ -59,29 +68,3 @@ impl ConfigurationSection for CsrfConfig {
Self::default()
}
}
#[cfg(test)]
mod tests {
use figment::Jail;
use super::*;
#[test]
fn load_config() {
Jail::expect_with(|jail| {
jail.create_file(
"config.yaml",
r#"
csrf:
ttl: 1800
"#,
)?;
let config = CsrfConfig::load_from_file("config.yaml")?;
assert_eq!(config.ttl, Duration::minutes(30));
Ok(())
});
}
}

View File

@ -18,9 +18,9 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
mod clients;
mod csrf;
mod database;
mod email;
mod hack;
mod http;
mod matrix;
mod passwords;
@ -32,9 +32,9 @@ mod upstream_oauth2;
pub use self::{
clients::{ClientAuthMethodConfig, ClientConfig, ClientsConfig},
csrf::CsrfConfig,
database::{ConnectConfig as DatabaseConnectConfig, DatabaseConfig},
email::{EmailConfig, EmailSmtpMode, EmailTransportConfig},
hack::HackConfig,
http::{
BindConfig as HttpBindConfig, HttpConfig, ListenerConfig as HttpListenerConfig,
Resource as HttpResource, TlsConfig as HttpTlsConfig, UnixOrTcp,
@ -81,10 +81,6 @@ pub struct RootConfig {
#[serde(default)]
pub templates: TemplatesConfig,
/// Configuration related to Cross-Site Request Forgery protections
#[serde(default)]
pub csrf: CsrfConfig,
/// Configuration related to sending emails
#[serde(default)]
pub email: EmailConfig,
@ -106,6 +102,10 @@ pub struct RootConfig {
/// Configuration related to upstream OAuth providers
#[serde(default)]
pub upstream_oauth2: UpstreamOAuth2Config,
/// Miscellaneous configuration options
#[serde(default)]
pub hack: HackConfig,
}
#[async_trait]
@ -124,13 +124,13 @@ impl ConfigurationSection for RootConfig {
database: DatabaseConfig::generate(&mut rng).await?,
telemetry: TelemetryConfig::generate(&mut rng).await?,
templates: TemplatesConfig::generate(&mut rng).await?,
csrf: CsrfConfig::generate(&mut rng).await?,
email: EmailConfig::generate(&mut rng).await?,
passwords: PasswordsConfig::generate(&mut rng).await?,
secrets: SecretsConfig::generate(&mut rng).await?,
matrix: MatrixConfig::generate(&mut rng).await?,
policy: PolicyConfig::generate(&mut rng).await?,
upstream_oauth2: UpstreamOAuth2Config::generate(&mut rng).await?,
hack: HackConfig::generate(&mut rng).await?,
})
}
@ -142,12 +142,12 @@ impl ConfigurationSection for RootConfig {
telemetry: TelemetryConfig::test(),
templates: TemplatesConfig::test(),
passwords: PasswordsConfig::test(),
csrf: CsrfConfig::test(),
email: EmailConfig::test(),
secrets: SecretsConfig::test(),
matrix: MatrixConfig::test(),
policy: PolicyConfig::test(),
upstream_oauth2: UpstreamOAuth2Config::test(),
hack: HackConfig::test(),
}
}
}
@ -165,9 +165,6 @@ pub struct AppConfig {
#[serde(default)]
pub templates: TemplatesConfig,
#[serde(default)]
pub csrf: CsrfConfig,
#[serde(default)]
pub email: EmailConfig,
@ -180,6 +177,9 @@ pub struct AppConfig {
#[serde(default)]
pub policy: PolicyConfig,
#[serde(default)]
pub hack: HackConfig,
}
#[async_trait]
@ -196,12 +196,12 @@ impl ConfigurationSection for AppConfig {
http: HttpConfig::generate(&mut rng).await?,
database: DatabaseConfig::generate(&mut rng).await?,
templates: TemplatesConfig::generate(&mut rng).await?,
csrf: CsrfConfig::generate(&mut rng).await?,
email: EmailConfig::generate(&mut rng).await?,
passwords: PasswordsConfig::generate(&mut rng).await?,
secrets: SecretsConfig::generate(&mut rng).await?,
matrix: MatrixConfig::generate(&mut rng).await?,
policy: PolicyConfig::generate(&mut rng).await?,
hack: HackConfig::generate(&mut rng).await?,
})
}
@ -211,11 +211,11 @@ impl ConfigurationSection for AppConfig {
database: DatabaseConfig::test(),
templates: TemplatesConfig::test(),
passwords: PasswordsConfig::test(),
csrf: CsrfConfig::test(),
email: EmailConfig::test(),
secrets: SecretsConfig::test(),
matrix: MatrixConfig::test(),
policy: PolicyConfig::test(),
hack: HackConfig::test(),
}
}
}

View File

@ -34,7 +34,10 @@ use opentelemetry::{
use rand::SeedableRng;
use sqlx::PgPool;
use crate::{passwords::PasswordManager, upstream_oauth2::cache::MetadataCache, MatrixHomeserver};
use crate::{
passwords::PasswordManager, site_config::SiteConfig, upstream_oauth2::cache::MetadataCache,
MatrixHomeserver,
};
#[derive(Clone)]
pub struct AppState {
@ -50,6 +53,7 @@ pub struct AppState {
pub http_client_factory: HttpClientFactory,
pub password_manager: PasswordManager,
pub metadata_cache: MetadataCache,
pub site_config: SiteConfig,
pub conn_acquisition_histogram: Option<Histogram<u64>>,
}
@ -199,6 +203,12 @@ impl FromRef<AppState> for MetadataCache {
}
}
impl FromRef<AppState> for SiteConfig {
fn from_ref(input: &AppState) -> Self {
input.site_config.clone()
}
}
#[async_trait]
impl FromRequestParts<AppState> for BoxClock {
type Rejection = Infallible;

View File

@ -32,7 +32,7 @@ use thiserror::Error;
use zeroize::Zeroizing;
use super::{MatrixError, MatrixHomeserver};
use crate::{impl_from_error_for_route, passwords::PasswordManager};
use crate::{impl_from_error_for_route, passwords::PasswordManager, site_config::SiteConfig};
#[derive(Debug, Serialize)]
#[serde(tag = "type")]
@ -210,6 +210,7 @@ pub(crate) async fn post(
State(password_manager): State<PasswordManager>,
mut repo: BoxRepository,
State(homeserver): State<MatrixHomeserver>,
State(site_config): State<SiteConfig>,
Json(input): Json<RequestBody>,
) -> Result<impl IntoResponse, RouteError> {
let (session, user) = match (password_manager.is_enabled(), input.credentials) {
@ -242,8 +243,7 @@ pub(crate) async fn post(
// If the client asked for a refreshable token, make it expire
let expires_in = if input.refresh_token {
// TODO: this should be configurable
Some(Duration::minutes(5))
Some(site_config.compat_token_ttl)
} else {
None
};

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use axum::{response::IntoResponse, Json};
use axum::{extract::State, response::IntoResponse, Json};
use chrono::Duration;
use hyper::StatusCode;
use mas_data_model::{TokenFormatError, TokenType};
@ -25,7 +25,7 @@ use serde_with::{serde_as, DurationMilliSeconds};
use thiserror::Error;
use super::MatrixError;
use crate::impl_from_error_for_route;
use crate::{impl_from_error_for_route, site_config::SiteConfig};
#[derive(Debug, Deserialize)]
pub struct RequestBody {
@ -91,6 +91,7 @@ pub(crate) async fn post(
mut rng: BoxRng,
clock: BoxClock,
mut repo: BoxRepository,
State(site_config): State<SiteConfig>,
Json(input): Json<RequestBody>,
) -> Result<impl IntoResponse, RouteError> {
let token_type = TokenType::check(&input.refresh_token)?;
@ -128,7 +129,7 @@ pub(crate) async fn post(
let new_refresh_token_str = TokenType::CompatRefreshToken.generate(&mut rng);
let new_access_token_str = TokenType::CompatAccessToken.generate(&mut rng);
let expires_in = Duration::minutes(5);
let expires_in = site_config.compat_token_ttl;
let new_access_token = repo
.compat_access_token()
.add(

View File

@ -68,6 +68,7 @@ pub mod passwords;
pub mod upstream_oauth2;
mod views;
mod site_config;
#[cfg(test)]
mod test_utils;
@ -89,8 +90,10 @@ macro_rules! impl_from_error_for_route {
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 crate::upstream_oauth2::cache::MetadataCache;
pub use self::{
app_state::AppState, compat::MatrixHomeserver, graphql::schema as graphql_schema,
site_config::SiteConfig, upstream_oauth2::cache::MetadataCache,
};
pub fn healthcheck_router<S, B>() -> Router<S, B>
where
@ -169,6 +172,7 @@ where
BoxRepository: FromRequestParts<S>,
Encrypter: FromRef<S>,
HttpClientFactory: FromRef<S>,
SiteConfig: FromRef<S>,
BoxClock: FromRequestParts<S>,
BoxRng: FromRequestParts<S>,
Policy: FromRequestParts<S>,
@ -225,9 +229,10 @@ where
<B as HttpBody>::Error: std::error::Error + Send + Sync,
S: Clone + Send + Sync + 'static,
UrlBuilder: FromRef<S>,
BoxRepository: FromRequestParts<S>,
SiteConfig: FromRef<S>,
MatrixHomeserver: FromRef<S>,
PasswordManager: FromRef<S>,
BoxRepository: FromRequestParts<S>,
BoxClock: FromRequestParts<S>,
BoxRng: FromRequestParts<S>,
{

View File

@ -47,7 +47,7 @@ use tracing::debug;
use url::Url;
use super::{generate_id_token, generate_token_pair};
use crate::impl_from_error_for_route;
use crate::{impl_from_error_for_route, site_config::SiteConfig};
#[serde_as]
#[skip_serializing_none]
@ -161,6 +161,7 @@ pub(crate) async fn post(
State(key_store): State<Keystore>,
State(url_builder): State<UrlBuilder>,
mut repo: BoxRepository,
State(site_config): State<SiteConfig>,
State(encrypter): State<Encrypter>,
client_authorization: ClientAuthorization<AccessTokenRequest>,
) -> Result<impl IntoResponse, RouteError> {
@ -191,12 +192,13 @@ pub(crate) async fn post(
&client,
&key_store,
&url_builder,
&site_config,
repo,
)
.await?
}
AccessTokenRequest::RefreshToken(grant) => {
refresh_token_grant(&mut rng, &clock, &grant, &client, repo).await?
refresh_token_grant(&mut rng, &clock, &grant, &client, &site_config, repo).await?
}
_ => {
return Err(RouteError::UnsupportedGrantType);
@ -220,6 +222,7 @@ async fn authorization_code_grant(
client: &Client,
key_store: &Keystore,
url_builder: &UrlBuilder,
site_config: &SiteConfig,
mut repo: BoxRepository,
) -> Result<(AccessTokenResponse, BoxRepository), RouteError> {
let authz_grant = repo
@ -312,7 +315,7 @@ async fn authorization_code_grant(
.get_last_authentication(&browser_session)
.await?;
let ttl = Duration::minutes(5);
let ttl = site_config.access_token_ttl;
let (access_token, refresh_token) =
generate_token_pair(&mut rng, clock, &mut repo, &session, ttl).await?;
@ -367,6 +370,7 @@ async fn refresh_token_grant(
clock: &impl Clock,
grant: &RefreshTokenGrant,
client: &Client,
site_config: &SiteConfig,
mut repo: BoxRepository,
) -> Result<(AccessTokenResponse, BoxRepository), RouteError> {
let refresh_token = repo
@ -390,7 +394,7 @@ async fn refresh_token_grant(
return Err(RouteError::InvalidGrant);
}
let ttl = Duration::minutes(5);
let ttl = site_config.access_token_ttl;
let (new_access_token, new_refresh_token) =
generate_token_pair(rng, clock, &mut repo, &session, ttl).await?;

View File

@ -0,0 +1,31 @@
// Copyright 2023 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 chrono::Duration;
/// Random site configuration we don't now where to put yet.
#[derive(Debug, Clone)]
pub struct SiteConfig {
pub access_token_ttl: Duration,
pub compat_token_ttl: Duration,
}
impl Default for SiteConfig {
fn default() -> Self {
Self {
access_token_ttl: Duration::minutes(5),
compat_token_ttl: Duration::minutes(5),
}
}
}

View File

@ -48,6 +48,7 @@ use url::Url;
use crate::{
app_state::ErrorWrapper,
passwords::{Hasher, PasswordManager},
site_config::SiteConfig,
upstream_oauth2::cache::MetadataCache,
MatrixHomeserver,
};
@ -76,6 +77,7 @@ pub(crate) struct TestState {
pub graphql_schema: mas_graphql::Schema,
pub http_client_factory: HttpClientFactory,
pub password_manager: PasswordManager,
pub site_config: SiteConfig,
pub clock: Arc<MockClock>,
pub rng: Arc<Mutex<ChaChaRng>>,
}
@ -133,6 +135,8 @@ impl TestState {
let http_client_factory = HttpClientFactory::new(10);
let site_config = SiteConfig::default();
let clock = Arc::new(MockClock::default());
let rng = Arc::new(Mutex::new(ChaChaRng::seed_from_u64(42)));
@ -160,6 +164,7 @@ impl TestState {
graphql_schema,
http_client_factory,
password_manager,
site_config,
clock,
rng,
})
@ -346,6 +351,12 @@ impl FromRef<TestState> for MetadataCache {
}
}
impl FromRef<TestState> for SiteConfig {
fn from_ref(input: &TestState) -> Self {
input.site_config.clone()
}
}
#[async_trait]
impl FromRequestParts<TestState> for BoxClock {
type Rejection = Infallible;