From 7e24cd094809fe600403a1b3f936f2c9f9372b5c Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 1 Feb 2022 09:34:18 +0100 Subject: [PATCH] Move secrets and oauth2 clients config --- Cargo.lock | 3 +- crates/cli/src/server.rs | 9 +- crates/config/Cargo.toml | 1 + crates/config/src/sections/clients.rs | 200 +++++++++++++++ crates/config/src/sections/cookies.rs | 55 ---- crates/config/src/sections/mod.rs | 27 +- .../src/sections/{oauth2.rs => secrets.rs} | 238 +++++------------- crates/handlers/Cargo.toml | 1 - crates/handlers/src/lib.rs | 9 +- crates/handlers/src/oauth2/authorization.rs | 32 ++- crates/handlers/src/oauth2/introspection.rs | 8 +- crates/handlers/src/oauth2/mod.rs | 14 +- crates/handlers/src/oauth2/token.rs | 12 +- crates/handlers/src/oauth2/userinfo.rs | 3 +- crates/handlers/src/views/account/emails.rs | 18 +- crates/handlers/src/views/account/mod.rs | 21 +- crates/handlers/src/views/account/password.rs | 18 +- crates/handlers/src/views/index.rs | 10 +- crates/handlers/src/views/login.rs | 16 +- crates/handlers/src/views/logout.rs | 11 +- crates/handlers/src/views/mod.rs | 25 +- crates/handlers/src/views/reauth.rs | 14 +- crates/handlers/src/views/register.rs | 14 +- crates/handlers/src/views/verify.rs | 10 +- crates/warp-utils/Cargo.toml | 1 - crates/warp-utils/src/filters/client.rs | 64 +++-- crates/warp-utils/src/filters/cookies.rs | 56 ++--- crates/warp-utils/src/filters/csrf.rs | 14 +- crates/warp-utils/src/filters/session.rs | 10 +- crates/warp-utils/src/filters/url_builder.rs | 2 +- 30 files changed, 462 insertions(+), 454 deletions(-) create mode 100644 crates/config/src/sections/clients.rs delete mode 100644 crates/config/src/sections/cookies.rs rename crates/config/src/sections/{oauth2.rs => secrets.rs} (50%) diff --git a/Cargo.lock b/Cargo.lock index b2126e3a..45c70d40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1887,6 +1887,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "chacha20poly1305", "chrono", "elliptic-curve", "figment", @@ -1944,7 +1945,6 @@ version = "0.1.0" dependencies = [ "anyhow", "argon2", - "chacha20poly1305", "chrono", "crc", "data-encoding", @@ -2106,7 +2106,6 @@ version = "0.1.0" dependencies = [ "anyhow", "bincode", - "chacha20poly1305", "chrono", "cookie", "crc", diff --git a/crates/cli/src/server.rs b/crates/cli/src/server.rs index 6386ebb1..b23cde25 100644 --- a/crates/cli/src/server.rs +++ b/crates/cli/src/server.rs @@ -251,13 +251,15 @@ impl ServerCommand { // Initialize the key store let key_store = config - .oauth2 + .secrets .key_store() .await .context("could not import keys from config")?; // Wrap the key store in an Arc let key_store = Arc::new(key_store); + let encrypter = config.secrets.encrypter(); + // Load and compile the templates let templates = Templates::load_from_config(&config.templates) .await @@ -283,7 +285,10 @@ impl ServerCommand { } // Start the server - let root = mas_handlers::root(&pool, &templates, &key_store, &mailer, &config); + let root = mas_handlers::root(&pool, &templates, &key_store, &encrypter, &mailer, &config); + + // Explicitely the config to properly zeroize secret keys + drop(config); let warp_service = warp::service(root); diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index c4282acc..293d7808 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -28,6 +28,7 @@ rand = "0.8.4" rsa = { git = "https://github.com/RustCrypto/RSA.git" } p256 = { version = "0.10.1", features = ["ecdsa", "pem", "pkcs8"] } pkcs8 = { version = "0.8.0", features = ["pem"] } +chacha20poly1305 = { version = "0.9.0", features = ["std"] } elliptic-curve = { version = "0.11.7", features = ["pem", "pkcs8"] } pem-rfc7468 = "0.3.1" diff --git a/crates/config/src/sections/clients.rs b/crates/config/src/sections/clients.rs new file mode 100644 index 00000000..359eeafc --- /dev/null +++ b/crates/config/src/sections/clients.rs @@ -0,0 +1,200 @@ +// Copyright 2021, 2022 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::ops::{Deref, DerefMut}; + +use async_trait::async_trait; +use mas_jose::{JsonWebKeySet, StaticJwksStore}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +use thiserror::Error; +use url::Url; + +use super::ConfigurationSection; + +#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "snake_case")] +pub enum JwksOrJwksUri { + Jwks(JsonWebKeySet), + JwksUri(Url), +} + +impl JwksOrJwksUri { + pub fn key_store(&self) -> StaticJwksStore { + let jwks = match self { + Self::Jwks(jwks) => jwks.clone(), + Self::JwksUri(_) => unimplemented!("jwks_uri are not implemented yet"), + }; + + StaticJwksStore::new(jwks) + } +} + +impl From for JwksOrJwksUri { + fn from(jwks: JsonWebKeySet) -> Self { + Self::Jwks(jwks) + } +} + +#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)] +#[serde(tag = "client_auth_method", rename_all = "snake_case")] +pub enum ClientAuthMethodConfig { + None, + ClientSecretBasic { client_secret: String }, + ClientSecretPost { client_secret: String }, + ClientSecretJwt { client_secret: String }, + PrivateKeyJwt(JwksOrJwksUri), +} + +#[skip_serializing_none] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct ClientConfig { + pub client_id: String, + + #[serde(flatten)] + pub client_auth_method: ClientAuthMethodConfig, + + #[serde(default)] + pub redirect_uris: Vec, +} + +#[derive(Debug, Error)] +#[error("Invalid redirect URI")] +pub struct InvalidRedirectUriError; + +impl ClientConfig { + pub fn resolve_redirect_uri<'a>( + &'a self, + suggested_uri: &'a Option, + ) -> Result<&'a Url, InvalidRedirectUriError> { + suggested_uri.as_ref().map_or_else( + || self.redirect_uris.get(0).ok_or(InvalidRedirectUriError), + |suggested_uri| self.check_redirect_uri(suggested_uri), + ) + } + + pub fn check_redirect_uri<'a>( + &self, + redirect_uri: &'a Url, + ) -> Result<&'a Url, InvalidRedirectUriError> { + if self.redirect_uris.contains(redirect_uri) { + Ok(redirect_uri) + } else { + Err(InvalidRedirectUriError) + } + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] +#[serde(transparent)] +pub struct ClientsConfig(Vec); + +impl Deref for ClientsConfig { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for ClientsConfig { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[async_trait] +impl ConfigurationSection<'_> for ClientsConfig { + fn path() -> &'static str { + "clients" + } + + #[tracing::instrument] + async fn generate() -> anyhow::Result { + Ok(Self::default()) + } + + fn test() -> Self { + 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#" + clients: + - client_id: public + client_auth_method: none + redirect_uris: + - https://exemple.fr/callback + + - client_id: secret-basic + client_auth_method: client_secret_basic + client_secret: hello + + - client_id: secret-post + client_auth_method: client_secret_post + client_secret: hello + + - client_id: secret-jwk + client_auth_method: client_secret_jwt + client_secret: hello + + - client_id: jwks + client_auth_method: private_key_jwt + jwks: + keys: + - kid: "03e84aed4ef4431014e8617567864c4efaaaede9" + kty: "RSA" + alg: "RS256" + use: "sig" + e: "AQAB" + n: "ma2uRyBeSEOatGuDpCiV9oIxlDWix_KypDYuhQfEzqi_BiF4fV266OWfyjcABbam59aJMNvOnKW3u_eZM-PhMCBij5MZ-vcBJ4GfxDJeKSn-GP_dJ09rpDcILh8HaWAnPmMoi4DC0nrfE241wPISvZaaZnGHkOrfN_EnA5DligLgVUbrA5rJhQ1aSEQO_gf1raEOW3DZ_ACU3qhtgO0ZBG3a5h7BPiRs2sXqb2UCmBBgwyvYLDebnpE7AotF6_xBIlR-Cykdap3GHVMXhrIpvU195HF30ZoBU4dMd-AeG6HgRt4Cqy1moGoDgMQfbmQ48Hlunv9_Vi2e2CLvYECcBw" + + - kid: "d01c1abe249269f72ef7ca2613a86c9f05e59567" + kty: "RSA" + alg: "RS256" + use: "sig" + e: "AQAB" + n: "0hukqytPwrj1RbMYhYoepCi3CN5k7DwYkTe_Cmb7cP9_qv4ok78KdvFXt5AnQxCRwBD7-qTNkkfMWO2RxUMBdQD0ED6tsSb1n5dp0XY8dSWiBDCX8f6Hr-KolOpvMLZKRy01HdAWcM6RoL9ikbjYHUEW1C8IJnw3MzVHkpKFDL354aptdNLaAdTCBvKzU9WpXo10g-5ctzSlWWjQuecLMQ4G1mNdsR1LHhUENEnOvgT8cDkX0fJzLbEbyBYkdMgKggyVPEB1bg6evG4fTKawgnf0IDSPxIU-wdS9wdSP9ZCJJPLi5CEp-6t6rE_sb2dGcnzjCGlembC57VwpkUvyMw" + "#, + )?; + + let config = ClientsConfig::load_from_file("config.yaml")?; + + assert_eq!(config.0.len(), 5); + + assert_eq!(config.0[0].client_id, "public"); + assert_eq!( + config.0[0].redirect_uris, + vec!["https://exemple.fr/callback".parse().unwrap()] + ); + + assert_eq!(config.0[1].client_id, "secret-basic"); + assert_eq!(config.0[1].redirect_uris, Vec::new()); + + Ok(()) + }); + } +} diff --git a/crates/config/src/sections/cookies.rs b/crates/config/src/sections/cookies.rs deleted file mode 100644 index daca0c5f..00000000 --- a/crates/config/src/sections/cookies.rs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2021, 2022 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 schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use serde_with::serde_as; - -use super::ConfigurationSection; - -fn example_secret() -> &'static str { - "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff" -} - -/// Cookies-related configuration -#[serde_as] -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -pub struct CookiesConfig { - /// Encryption key for secure cookies - #[schemars( - with = "String", - regex(pattern = r"[0-9a-fA-F]{64}"), - example = "example_secret" - )] - #[serde_as(as = "serde_with::hex::Hex")] - pub secret: [u8; 32], -} - -#[async_trait] -impl ConfigurationSection<'_> for CookiesConfig { - fn path() -> &'static str { - "cookies" - } - - async fn generate() -> anyhow::Result { - Ok(Self { - secret: rand::random(), - }) - } - - fn test() -> Self { - Self { secret: [0xEA; 32] } - } -} diff --git a/crates/config/src/sections/mod.rs b/crates/config/src/sections/mod.rs index 4936b290..16ce9875 100644 --- a/crates/config/src/sections/mod.rs +++ b/crates/config/src/sections/mod.rs @@ -16,22 +16,22 @@ use async_trait::async_trait; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -mod cookies; +mod clients; mod csrf; mod database; mod email; mod http; -mod oauth2; +mod secrets; mod telemetry; mod templates; pub use self::{ - cookies::CookiesConfig, + clients::{ClientAuthMethodConfig, ClientConfig, ClientsConfig}, csrf::CsrfConfig, database::DatabaseConfig, email::{EmailConfig, EmailSmtpMode, EmailTransportConfig}, http::HttpConfig, - oauth2::{OAuth2ClientAuthMethodConfig, OAuth2ClientConfig, OAuth2Config}, + secrets::{Encrypter, SecretsConfig}, telemetry::{ MetricsConfig, MetricsExporterConfig, Propagator, TelemetryConfig, TracingConfig, TracingExporterConfig, @@ -43,8 +43,9 @@ use crate::util::ConfigurationSection; /// Application configuration root #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct RootConfig { - /// Configuration related to OAuth 2.0/OIDC operations - pub oauth2: OAuth2Config, + /// List of OAuth 2.0/OIDC clients config + #[serde(default)] + pub clients: ClientsConfig, /// Configuration of the HTTP server #[serde(default)] @@ -54,9 +55,6 @@ pub struct RootConfig { #[serde(default)] pub database: DatabaseConfig, - /// Configuration related to cookies - pub cookies: CookiesConfig, - /// Configuration related to sending monitoring data #[serde(default)] pub telemetry: TelemetryConfig, @@ -72,6 +70,9 @@ pub struct RootConfig { /// Configuration related to sending emails #[serde(default)] pub email: EmailConfig, + + /// Application secrets + pub secrets: SecretsConfig, } #[async_trait] @@ -82,27 +83,27 @@ impl ConfigurationSection<'_> for RootConfig { async fn generate() -> anyhow::Result { Ok(Self { - oauth2: OAuth2Config::generate().await?, + clients: ClientsConfig::generate().await?, http: HttpConfig::generate().await?, database: DatabaseConfig::generate().await?, - cookies: CookiesConfig::generate().await?, telemetry: TelemetryConfig::generate().await?, templates: TemplatesConfig::generate().await?, csrf: CsrfConfig::generate().await?, email: EmailConfig::generate().await?, + secrets: SecretsConfig::generate().await?, }) } fn test() -> Self { Self { - oauth2: OAuth2Config::test(), + clients: ClientsConfig::test(), http: HttpConfig::test(), database: DatabaseConfig::test(), - cookies: CookiesConfig::test(), telemetry: TelemetryConfig::test(), templates: TemplatesConfig::test(), csrf: CsrfConfig::test(), email: EmailConfig::test(), + secrets: SecretsConfig::test(), } } } diff --git a/crates/config/src/sections/oauth2.rs b/crates/config/src/sections/secrets.rs similarity index 50% rename from crates/config/src/sections/oauth2.rs rename to crates/config/src/sections/secrets.rs index d5fdd45b..b482e5ff 100644 --- a/crates/config/src/sections/oauth2.rs +++ b/crates/config/src/sections/secrets.rs @@ -1,4 +1,4 @@ -// Copyright 2021, 2022 The Matrix.org Foundation C.I.C. +// Copyright 2022 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. @@ -12,11 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::path::PathBuf; +use std::{path::PathBuf, sync::Arc}; use anyhow::Context; use async_trait::async_trait; -use mas_jose::{JsonWebKeySet, StaticJwksStore, StaticKeystore}; +use chacha20poly1305::{ + aead::{generic_array::GenericArray, Aead, NewAead}, + ChaCha20Poly1305, +}; +use mas_jose::StaticKeystore; use pkcs8::DecodePrivateKey; use rsa::{ pkcs1::{DecodeRsaPrivateKey, EncodeRsaPrivateKey}, @@ -24,14 +28,43 @@ use rsa::{ }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; -use thiserror::Error; +use serde_with::serde_as; use tokio::{fs::File, io::AsyncReadExt, task}; use tracing::info; -use url::Url; use super::ConfigurationSection; +#[derive(Clone)] +pub struct Encrypter { + aead: Arc, +} + +impl Encrypter { + #[must_use] + pub fn new(key: &[u8; 32]) -> Self { + let key = GenericArray::from_slice(key); + let aead = ChaCha20Poly1305::new(key); + let aead = Arc::new(aead); + Self { aead } + } + + pub fn encrypt(&self, nonce: &[u8; 12], decrypted: &[u8]) -> anyhow::Result> { + let nonce = GenericArray::from_slice(&nonce[..]); + let encrypted = self.aead.encrypt(nonce, decrypted)?; + Ok(encrypted) + } + + pub fn decrypt(&self, nonce: &[u8; 12], encrypted: &[u8]) -> anyhow::Result> { + let nonce = GenericArray::from_slice(&nonce[..]); + let encrypted = self.aead.decrypt(nonce, encrypted)?; + Ok(encrypted) + } +} + +fn example_secret() -> &'static str { + "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff" +} + #[derive(JsonSchema, Serialize, Deserialize, Clone, Copy, Debug)] #[serde(rename_all = "lowercase")] pub enum KeyType { @@ -53,89 +86,24 @@ pub struct KeyConfig { key: KeyOrPath, } -#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)] -#[serde(rename_all = "snake_case")] -pub enum JwksOrJwksUri { - Jwks(JsonWebKeySet), - JwksUri(Url), -} - -impl JwksOrJwksUri { - pub fn key_store(&self) -> StaticJwksStore { - let jwks = match self { - Self::Jwks(jwks) => jwks.clone(), - Self::JwksUri(_) => unimplemented!("jwks_uri are not implemented yet"), - }; - - StaticJwksStore::new(jwks) - } -} - -impl From for JwksOrJwksUri { - fn from(jwks: JsonWebKeySet) -> Self { - Self::Jwks(jwks) - } -} - -#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)] -#[serde(tag = "client_auth_method", rename_all = "snake_case")] -pub enum OAuth2ClientAuthMethodConfig { - None, - ClientSecretBasic { client_secret: String }, - ClientSecretPost { client_secret: String }, - ClientSecretJwt { client_secret: String }, - PrivateKeyJwt(JwksOrJwksUri), -} - -#[skip_serializing_none] +#[serde_as] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct OAuth2ClientConfig { - pub client_id: String, - - #[serde(flatten)] - pub client_auth_method: OAuth2ClientAuthMethodConfig, +pub struct SecretsConfig { + /// Encryption key for secure cookies + #[schemars( + with = "String", + regex(pattern = r"[0-9a-fA-F]{64}"), + example = "example_secret" + )] + #[serde_as(as = "serde_with::hex::Hex")] + encryption: [u8; 32], + /// List of private keys to use for signing and encrypting payloads #[serde(default)] - pub redirect_uris: Vec, + keys: Vec, } -#[derive(Debug, Error)] -#[error("Invalid redirect URI")] -pub struct InvalidRedirectUriError; - -impl OAuth2ClientConfig { - pub fn resolve_redirect_uri<'a>( - &'a self, - suggested_uri: &'a Option, - ) -> Result<&'a Url, InvalidRedirectUriError> { - suggested_uri.as_ref().map_or_else( - || self.redirect_uris.get(0).ok_or(InvalidRedirectUriError), - |suggested_uri| self.check_redirect_uri(suggested_uri), - ) - } - - pub fn check_redirect_uri<'a>( - &self, - redirect_uri: &'a Url, - ) -> Result<&'a Url, InvalidRedirectUriError> { - if self.redirect_uris.contains(redirect_uri) { - Ok(redirect_uri) - } else { - Err(InvalidRedirectUriError) - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct OAuth2Config { - #[serde(default)] - pub clients: Vec, - - #[serde(default)] - pub keys: Vec, -} - -impl OAuth2Config { +impl SecretsConfig { pub async fn key_store(&self) -> anyhow::Result { let mut store = StaticKeystore::new(); @@ -189,12 +157,17 @@ impl OAuth2Config { Ok(store) } + + #[must_use] + pub fn encrypter(&self) -> Encrypter { + Encrypter::new(&self.encryption) + } } #[async_trait] -impl ConfigurationSection<'_> for OAuth2Config { +impl ConfigurationSection<'_> for SecretsConfig { fn path() -> &'static str { - "oauth2" + "secrets" } #[tracing::instrument] @@ -237,7 +210,7 @@ impl ConfigurationSection<'_> for OAuth2Config { }; Ok(Self { - clients: Vec::new(), + encryption: rand::random(), keys: vec![rsa_key, ecdsa_key], }) } @@ -276,97 +249,8 @@ impl ConfigurationSection<'_> for OAuth2Config { }; Self { - clients: Vec::new(), + encryption: [0xEA; 32], keys: vec![rsa_key, ecdsa_key], } } } - -#[cfg(test)] -mod tests { - use figment::Jail; - - use super::*; - - #[test] - fn load_config() { - Jail::expect_with(|jail| { - jail.create_file( - "config.yaml", - r#" - oauth2: - keys: - - type: rsa - key: | - -----BEGIN PRIVATE KEY----- - MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAymS2RkeIZo7pUeEN - QUGCG4GLJru5jzxomO9jiNr5D/oRcerhpQVc9aCpBfAAg4l4a1SmYdBzWqX0X5pU - scgTtQIDAQABAkEArNIMlrxUK4bSklkCcXtXdtdKE9vuWfGyOw0GyAB69fkEUBxh - 3j65u+u3ZmW+bpMWHgp1FtdobE9nGwb2VBTWAQIhAOyU1jiUEkrwKK004+6b5QRE - vC9UI2vDWy5vioMNx5Y1AiEA2wGAJ6ETF8FF2Vd+kZlkKK7J0em9cl0gbJDsWIEw - N4ECIEyWYkMurD1WQdTQqnk0Po+DMOihdFYOiBYgRdbnPxWBAiEAmtd0xJAd7622 - tPQniMnrBtiN2NxqFXHCev/8Gpc8gAECIBcaPcF59qVeRmYrfqzKBxFm7LmTwlAl - Gh7BNzCeN+D6 - -----END PRIVATE KEY----- - - type: ecdsa - key: | - -----BEGIN PRIVATE KEY----- - MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgqfn5mYO/5Qq/wOOiWgHA - NaiDiepgUJ2GI5eq2V8D8nahRANCAARMK9aKUd/H28qaU+0qvS6bSJItzAge1VHn - OhBAAUVci1RpmUA+KdCL5sw9nadAEiONeiGr+28RYHZmlB9qXnjC - -----END PRIVATE KEY----- - clients: - - client_id: public - client_auth_method: none - redirect_uris: - - https://exemple.fr/callback - - - client_id: secret-basic - client_auth_method: client_secret_basic - client_secret: hello - - - client_id: secret-post - client_auth_method: client_secret_post - client_secret: hello - - - client_id: secret-jwk - client_auth_method: client_secret_jwt - client_secret: hello - - - client_id: jwks - client_auth_method: private_key_jwt - jwks: - keys: - - kid: "03e84aed4ef4431014e8617567864c4efaaaede9" - kty: "RSA" - alg: "RS256" - use: "sig" - e: "AQAB" - n: "ma2uRyBeSEOatGuDpCiV9oIxlDWix_KypDYuhQfEzqi_BiF4fV266OWfyjcABbam59aJMNvOnKW3u_eZM-PhMCBij5MZ-vcBJ4GfxDJeKSn-GP_dJ09rpDcILh8HaWAnPmMoi4DC0nrfE241wPISvZaaZnGHkOrfN_EnA5DligLgVUbrA5rJhQ1aSEQO_gf1raEOW3DZ_ACU3qhtgO0ZBG3a5h7BPiRs2sXqb2UCmBBgwyvYLDebnpE7AotF6_xBIlR-Cykdap3GHVMXhrIpvU195HF30ZoBU4dMd-AeG6HgRt4Cqy1moGoDgMQfbmQ48Hlunv9_Vi2e2CLvYECcBw" - - - kid: "d01c1abe249269f72ef7ca2613a86c9f05e59567" - kty: "RSA" - alg: "RS256" - use: "sig" - e: "AQAB" - n: "0hukqytPwrj1RbMYhYoepCi3CN5k7DwYkTe_Cmb7cP9_qv4ok78KdvFXt5AnQxCRwBD7-qTNkkfMWO2RxUMBdQD0ED6tsSb1n5dp0XY8dSWiBDCX8f6Hr-KolOpvMLZKRy01HdAWcM6RoL9ikbjYHUEW1C8IJnw3MzVHkpKFDL354aptdNLaAdTCBvKzU9WpXo10g-5ctzSlWWjQuecLMQ4G1mNdsR1LHhUENEnOvgT8cDkX0fJzLbEbyBYkdMgKggyVPEB1bg6evG4fTKawgnf0IDSPxIU-wdS9wdSP9ZCJJPLi5CEp-6t6rE_sb2dGcnzjCGlembC57VwpkUvyMw" - "#, - )?; - - let config = OAuth2Config::load_from_file("config.yaml")?; - - assert_eq!(config.clients.len(), 5); - - assert_eq!(config.clients[0].client_id, "public"); - assert_eq!( - config.clients[0].redirect_uris, - vec!["https://exemple.fr/callback".parse().unwrap()] - ); - - assert_eq!(config.clients[1].client_id, "secret-basic"); - assert_eq!(config.clients[1].redirect_uris, Vec::new()); - - Ok(()) - }); - } -} diff --git a/crates/handlers/Cargo.toml b/crates/handlers/Cargo.toml index 527ecf15..5b22d1d2 100644 --- a/crates/handlers/Cargo.toml +++ b/crates/handlers/Cargo.toml @@ -42,7 +42,6 @@ argon2 = { version = "0.3.2", features = ["password-hash"] } rsa = { git = "https://github.com/RustCrypto/RSA.git" } pkcs8 = { version = "0.8.0", features = ["pem"] } elliptic-curve = { version = "0.11.7", features = ["pem"] } -chacha20poly1305 = { version = "0.9.0", features = ["std"] } sha2 = "0.10.1" crc = "2.1.0" diff --git a/crates/handlers/src/lib.rs b/crates/handlers/src/lib.rs index 1aac2ae7..445e38d6 100644 --- a/crates/handlers/src/lib.rs +++ b/crates/handlers/src/lib.rs @@ -24,7 +24,7 @@ use std::sync::Arc; -use mas_config::RootConfig; +use mas_config::{Encrypter, RootConfig}; use mas_email::Mailer; use mas_jose::StaticKeystore; use mas_static_files::filter as static_files; @@ -43,6 +43,7 @@ pub fn root( pool: &PgPool, templates: &Templates, key_store: &Arc, + encrypter: &Encrypter, mailer: &Mailer, config: &RootConfig, ) -> BoxedFilter<(impl Reply,)> { @@ -51,17 +52,17 @@ pub fn root( pool, templates, key_store, - &config.oauth2, + encrypter, + &config.clients, &config.http, - &config.cookies, ); let views = views( pool, templates, mailer, + encrypter, &config.http, &config.csrf, - &config.cookies, ); let static_files = static_files(config.http.web_root.clone()); diff --git a/crates/handlers/src/oauth2/authorization.rs b/crates/handlers/src/oauth2/authorization.rs index f9dab624..8dc4120e 100644 --- a/crates/handlers/src/oauth2/authorization.rs +++ b/crates/handlers/src/oauth2/authorization.rs @@ -20,7 +20,7 @@ use hyper::{ http::uri::{Parts, PathAndQuery, Uri}, StatusCode, }; -use mas_config::{CookiesConfig, OAuth2ClientConfig, OAuth2Config}; +use mas_config::{ClientsConfig, Encrypter}; use mas_data_model::{ Authentication, AuthorizationCode, AuthorizationGrant, AuthorizationGrantStage, BrowserSession, Pkce, StorageBackend, TokenType, @@ -215,33 +215,34 @@ fn resolve_response_mode( pub fn filter( pool: &PgPool, templates: &Templates, - oauth2_config: &OAuth2Config, - cookies_config: &CookiesConfig, + encrypter: &Encrypter, + clients_config: &ClientsConfig, ) -> BoxedFilter<(Box,)> { - let clients = oauth2_config.clients.clone(); + let clients_config = clients_config.clone(); + let clients_config_2 = clients_config.clone(); + let authorize = warp::path!("oauth2" / "authorize") .and(warp::get()) - .map(move || clients.clone()) + .map(move || clients_config.clone()) .and(warp::query()) - .and(optional_session(pool, cookies_config)) + .and(optional_session(pool, encrypter)) .and(transaction(pool)) .and_then(get); let step = warp::path!("oauth2" / "authorize" / "step") .and(warp::get()) .and(warp::query()) - .and(session(pool, cookies_config)) + .and(session(pool, encrypter)) .and(transaction(pool)) .and_then(step); - let clients = oauth2_config.clients.clone(); authorize .or(step) .unify() .recover(recover) .unify() .and(warp::query()) - .and(warp::any().map(move || clients.clone())) + .and(warp::any().map(move || clients_config_2.clone())) .and(with_templates(templates)) .and_then(actually_reply) .boxed() @@ -258,7 +259,7 @@ async fn recover(rejection: Rejection) -> Result async fn actually_reply( rep: ReplyOrBackToClient, q: PartialParams, - clients: Vec, + clients: ClientsConfig, templates: Templates, ) -> Result, Rejection> { let (redirect_uri, response_mode, state, params) = match rep { @@ -278,11 +279,8 @@ async fn actually_reply( } = q; // First, disover the client - let client = client_id.and_then(|client_id| { - clients - .into_iter() - .find(|client| client.client_id == client_id) - }); + let client = client_id + .and_then(|client_id| clients.iter().find(|client| client.client_id == client_id)); let client = match client { Some(client) => client, @@ -314,7 +312,7 @@ async fn actually_reply( } async fn get( - clients: Vec, + clients: ClientsConfig, params: Params, maybe_session: Option>, mut txn: Transaction<'_, Postgres>, @@ -337,7 +335,7 @@ async fn get( // First, find out what client it is let client = clients - .into_iter() + .iter() .find(|client| client.client_id == params.auth.client_id) .ok_or_else(|| anyhow::anyhow!("could not find client")) .wrap_error()?; diff --git a/crates/handlers/src/oauth2/introspection.rs b/crates/handlers/src/oauth2/introspection.rs index b894c9f3..cc81e7a2 100644 --- a/crates/handlers/src/oauth2/introspection.rs +++ b/crates/handlers/src/oauth2/introspection.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use mas_config::{HttpConfig, OAuth2ClientConfig, OAuth2Config}; +use mas_config::{ClientConfig, ClientsConfig, HttpConfig}; use mas_data_model::TokenType; use mas_iana::oauth::{OAuthClientAuthenticationMethod, OAuthTokenTypeHint}; use mas_storage::oauth2::{ @@ -29,7 +29,7 @@ use warp::{filters::BoxedFilter, Filter, Rejection, Reply}; pub fn filter( pool: &PgPool, - oauth2_config: &OAuth2Config, + clients_config: &ClientsConfig, http_config: &HttpConfig, ) -> BoxedFilter<(Box,)> { let audience = UrlBuilder::from(http_config) @@ -40,7 +40,7 @@ pub fn filter( .and( warp::post() .and(connection(pool)) - .and(client_authentication(oauth2_config, audience)) + .and(client_authentication(clients_config, audience)) .and_then(introspect) .recover(recover) .unify(), @@ -66,7 +66,7 @@ const INACTIVE: IntrospectionResponse = IntrospectionResponse { async fn introspect( mut conn: PoolConnection, auth: OAuthClientAuthenticationMethod, - client: OAuth2ClientConfig, + client: ClientConfig, params: IntrospectionRequest, ) -> Result, Rejection> { // Token introspection is only allowed by confidential clients diff --git a/crates/handlers/src/oauth2/mod.rs b/crates/handlers/src/oauth2/mod.rs index 2e6c2a9e..abab2a6f 100644 --- a/crates/handlers/src/oauth2/mod.rs +++ b/crates/handlers/src/oauth2/mod.rs @@ -15,7 +15,7 @@ use std::sync::Arc; use hyper::Method; -use mas_config::{CookiesConfig, HttpConfig, OAuth2Config}; +use mas_config::{ClientsConfig, Encrypter, HttpConfig}; use mas_jose::StaticKeystore; use mas_templates::Templates; use mas_warp_utils::filters::cors::cors; @@ -40,16 +40,16 @@ pub fn filter( pool: &PgPool, templates: &Templates, key_store: &Arc, - oauth2_config: &OAuth2Config, + encrypter: &Encrypter, + clients_config: &ClientsConfig, http_config: &HttpConfig, - cookies_config: &CookiesConfig, ) -> BoxedFilter<(impl Reply,)> { let discovery = discovery(key_store.as_ref(), http_config); let keys = keys(key_store); - let authorization = authorization(pool, templates, oauth2_config, cookies_config); - let userinfo = userinfo(pool, oauth2_config); - let introspection = introspection(pool, oauth2_config, http_config); - let token = token(pool, key_store, oauth2_config, http_config); + let authorization = authorization(pool, templates, encrypter, clients_config); + let userinfo = userinfo(pool); + let introspection = introspection(pool, clients_config, http_config); + let token = token(pool, key_store, clients_config, http_config); let filter = discovery .or(keys) diff --git a/crates/handlers/src/oauth2/token.rs b/crates/handlers/src/oauth2/token.rs index 55693e91..00cf6340 100644 --- a/crates/handlers/src/oauth2/token.rs +++ b/crates/handlers/src/oauth2/token.rs @@ -19,7 +19,7 @@ use chrono::{DateTime, Duration, Utc}; use data_encoding::BASE64URL_NOPAD; use headers::{CacheControl, Pragma}; use hyper::StatusCode; -use mas_config::{HttpConfig, OAuth2ClientConfig, OAuth2Config}; +use mas_config::{ClientConfig, ClientsConfig, HttpConfig}; use mas_data_model::{AuthorizationGrantStage, TokenType}; use mas_iana::{jose::JsonWebSignatureAlg, oauth::OAuthClientAuthenticationMethod}; use mas_jose::{ @@ -98,7 +98,7 @@ where pub fn filter( pool: &PgPool, key_store: &Arc, - oauth2_config: &OAuth2Config, + clients_config: &ClientsConfig, http_config: &HttpConfig, ) -> BoxedFilter<(Box,)> { let key_store = key_store.clone(); @@ -110,7 +110,7 @@ pub fn filter( warp::path!("oauth2" / "token") .and( warp::post() - .and(client_authentication(oauth2_config, audience)) + .and(client_authentication(clients_config, audience)) .and(warp::any().map(move || key_store.clone())) .and(warp::any().map(move || issuer.clone())) .and(connection(pool)) @@ -131,7 +131,7 @@ async fn recover(rejection: Rejection) -> Result, Rejection> { async fn token( _auth: OAuthClientAuthenticationMethod, - client: OAuth2ClientConfig, + client: ClientConfig, req: AccessTokenRequest, key_store: Arc, issuer: Url, @@ -171,7 +171,7 @@ fn hash(mut hasher: H, token: &str) -> anyhow::Result { #[allow(clippy::too_many_lines)] async fn authorization_code_grant( grant: &AuthorizationCodeGrant, - client: &OAuth2ClientConfig, + client: &ClientConfig, key_store: &StaticKeystore, issuer: Url, conn: &mut PoolConnection, @@ -328,7 +328,7 @@ async fn authorization_code_grant( async fn refresh_token_grant( grant: &RefreshTokenGrant, - client: &OAuth2ClientConfig, + client: &ClientConfig, conn: &mut PoolConnection, ) -> Result { let mut txn = conn.begin().await.wrap_error()?; diff --git a/crates/handlers/src/oauth2/userinfo.rs b/crates/handlers/src/oauth2/userinfo.rs index 63e6eb19..83ef383a 100644 --- a/crates/handlers/src/oauth2/userinfo.rs +++ b/crates/handlers/src/oauth2/userinfo.rs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use mas_config::OAuth2Config; use mas_data_model::{AccessToken, Session}; use mas_storage::PostgresqlBackend; use mas_warp_utils::filters::authenticate::{authentication, recover_unauthorized}; @@ -26,7 +25,7 @@ struct UserInfo { username: String, } -pub(super) fn filter(pool: &PgPool, _config: &OAuth2Config) -> BoxedFilter<(Box,)> { +pub(super) fn filter(pool: &PgPool) -> BoxedFilter<(Box,)> { warp::path!("oauth2" / "userinfo") .and( warp::get() diff --git a/crates/handlers/src/views/account/emails.rs b/crates/handlers/src/views/account/emails.rs index a8962e0e..f47f5623 100644 --- a/crates/handlers/src/views/account/emails.rs +++ b/crates/handlers/src/views/account/emails.rs @@ -13,7 +13,7 @@ // limitations under the License. use lettre::{message::Mailbox, Address}; -use mas_config::{CookiesConfig, CsrfConfig, HttpConfig}; +use mas_config::{CsrfConfig, Encrypter, HttpConfig}; use mas_data_model::{BrowserSession, User, UserEmail}; use mas_email::Mailer; use mas_storage::{ @@ -45,27 +45,27 @@ pub(super) fn filter( pool: &PgPool, templates: &Templates, mailer: &Mailer, + encrypter: &Encrypter, http_config: &HttpConfig, csrf_config: &CsrfConfig, - cookies_config: &CookiesConfig, ) -> BoxedFilter<(Box,)> { let mailer = mailer.clone(); let get = with_templates(templates) - .and(encrypted_cookie_saver(cookies_config)) - .and(updated_csrf_token(cookies_config, csrf_config)) - .and(session(pool, cookies_config)) + .and(encrypted_cookie_saver(encrypter)) + .and(updated_csrf_token(encrypter, csrf_config)) + .and(session(pool, encrypter)) .and(connection(pool)) .and_then(get); let post = with_templates(templates) .and(warp::any().map(move || mailer.clone())) .and(url_builder(http_config)) - .and(encrypted_cookie_saver(cookies_config)) - .and(updated_csrf_token(cookies_config, csrf_config)) - .and(session(pool, cookies_config)) + .and(encrypted_cookie_saver(encrypter)) + .and(updated_csrf_token(encrypter, csrf_config)) + .and(session(pool, encrypter)) .and(transaction(pool)) - .and(protected_form(cookies_config)) + .and(protected_form(encrypter)) .and_then(post); let get = warp::get().and(get); diff --git a/crates/handlers/src/views/account/mod.rs b/crates/handlers/src/views/account/mod.rs index 7ff981c1..17d75e67 100644 --- a/crates/handlers/src/views/account/mod.rs +++ b/crates/handlers/src/views/account/mod.rs @@ -15,7 +15,7 @@ mod emails; mod password; -use mas_config::{CookiesConfig, CsrfConfig, HttpConfig}; +use mas_config::{CsrfConfig, Encrypter, HttpConfig}; use mas_data_model::BrowserSession; use mas_email::Mailer; use mas_storage::{ @@ -42,28 +42,21 @@ pub(super) fn filter( pool: &PgPool, templates: &Templates, mailer: &Mailer, + encrypter: &Encrypter, http_config: &HttpConfig, csrf_config: &CsrfConfig, - cookies_config: &CookiesConfig, ) -> BoxedFilter<(Box,)> { let get = warp::get() .and(with_templates(templates)) - .and(encrypted_cookie_saver(cookies_config)) - .and(updated_csrf_token(cookies_config, csrf_config)) - .and(session(pool, cookies_config)) + .and(encrypted_cookie_saver(encrypter)) + .and(updated_csrf_token(encrypter, csrf_config)) + .and(session(pool, encrypter)) .and(connection(pool)) .and_then(get); let index = warp::path::end().and(get); - let password = password(pool, templates, csrf_config, cookies_config); - let emails = emails( - pool, - templates, - mailer, - http_config, - csrf_config, - cookies_config, - ); + let password = password(pool, templates, encrypter, csrf_config); + let emails = emails(pool, templates, mailer, encrypter, http_config, csrf_config); let filter = index.or(password).unify().or(emails).unify(); diff --git a/crates/handlers/src/views/account/password.rs b/crates/handlers/src/views/account/password.rs index dd21e52e..5e7b9498 100644 --- a/crates/handlers/src/views/account/password.rs +++ b/crates/handlers/src/views/account/password.rs @@ -13,7 +13,7 @@ // limitations under the License. use argon2::Argon2; -use mas_config::{CookiesConfig, CsrfConfig}; +use mas_config::{CsrfConfig, Encrypter}; use mas_data_model::BrowserSession; use mas_storage::{ user::{authenticate_session, set_password}, @@ -37,21 +37,21 @@ use warp::{filters::BoxedFilter, reply::html, Filter, Rejection, Reply}; pub(super) fn filter( pool: &PgPool, templates: &Templates, + encrypter: &Encrypter, csrf_config: &CsrfConfig, - cookies_config: &CookiesConfig, ) -> BoxedFilter<(Box,)> { let get = with_templates(templates) - .and(encrypted_cookie_saver(cookies_config)) - .and(updated_csrf_token(cookies_config, csrf_config)) - .and(session(pool, cookies_config)) + .and(encrypted_cookie_saver(encrypter)) + .and(updated_csrf_token(encrypter, csrf_config)) + .and(session(pool, encrypter)) .and_then(get); let post = with_templates(templates) - .and(encrypted_cookie_saver(cookies_config)) - .and(updated_csrf_token(cookies_config, csrf_config)) - .and(session(pool, cookies_config)) + .and(encrypted_cookie_saver(encrypter)) + .and(updated_csrf_token(encrypter, csrf_config)) + .and(session(pool, encrypter)) .and(transaction(pool)) - .and(protected_form(cookies_config)) + .and(protected_form(encrypter)) .and_then(post); let get = warp::get().and(get); diff --git a/crates/handlers/src/views/index.rs b/crates/handlers/src/views/index.rs index 4061a5e2..b8447ff4 100644 --- a/crates/handlers/src/views/index.rs +++ b/crates/handlers/src/views/index.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use mas_config::{CookiesConfig, CsrfConfig, HttpConfig}; +use mas_config::{CsrfConfig, Encrypter, HttpConfig}; use mas_data_model::BrowserSession; use mas_storage::PostgresqlBackend; use mas_templates::{IndexContext, TemplateContext, Templates}; @@ -29,17 +29,17 @@ use warp::{filters::BoxedFilter, reply::html, Filter, Rejection, Reply}; pub(super) fn filter( pool: &PgPool, templates: &Templates, + encrypter: &Encrypter, http_config: &HttpConfig, csrf_config: &CsrfConfig, - cookies_config: &CookiesConfig, ) -> BoxedFilter<(Box,)> { warp::path::end() .and(warp::get()) .and(url_builder(http_config)) .and(with_templates(templates)) - .and(encrypted_cookie_saver(cookies_config)) - .and(updated_csrf_token(cookies_config, csrf_config)) - .and(optional_session(pool, cookies_config)) + .and(encrypted_cookie_saver(encrypter)) + .and(updated_csrf_token(encrypter, csrf_config)) + .and(optional_session(pool, encrypter)) .and_then(get) .boxed() } diff --git a/crates/handlers/src/views/login.rs b/crates/handlers/src/views/login.rs index c2bc46f9..cea1e3e7 100644 --- a/crates/handlers/src/views/login.rs +++ b/crates/handlers/src/views/login.rs @@ -15,7 +15,7 @@ #![allow(clippy::trait_duplication_in_bounds)] use hyper::http::uri::{Parts, PathAndQuery, Uri}; -use mas_config::{CookiesConfig, CsrfConfig}; +use mas_config::{CsrfConfig, Encrypter}; use mas_data_model::{errors::WrapFormError, BrowserSession}; use mas_storage::{user::login, PostgresqlBackend}; use mas_templates::{LoginContext, LoginFormField, TemplateContext, Templates}; @@ -86,24 +86,24 @@ struct LoginForm { pub(super) fn filter( pool: &PgPool, templates: &Templates, + encrypter: &Encrypter, csrf_config: &CsrfConfig, - cookies_config: &CookiesConfig, ) -> BoxedFilter<(Box,)> { let get = warp::get() .and(with_templates(templates)) .and(connection(pool)) - .and(encrypted_cookie_saver(cookies_config)) - .and(updated_csrf_token(cookies_config, csrf_config)) + .and(encrypted_cookie_saver(encrypter)) + .and(updated_csrf_token(encrypter, csrf_config)) .and(warp::query()) - .and(optional_session(pool, cookies_config)) + .and(optional_session(pool, encrypter)) .and_then(get); let post = warp::post() .and(with_templates(templates)) .and(connection(pool)) - .and(encrypted_cookie_saver(cookies_config)) - .and(updated_csrf_token(cookies_config, csrf_config)) - .and(protected_form(cookies_config)) + .and(encrypted_cookie_saver(encrypter)) + .and(updated_csrf_token(encrypter, csrf_config)) + .and(protected_form(encrypter)) .and(warp::query()) .and_then(post); diff --git a/crates/handlers/src/views/logout.rs b/crates/handlers/src/views/logout.rs index 5526bc71..82dbb072 100644 --- a/crates/handlers/src/views/logout.rs +++ b/crates/handlers/src/views/logout.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use mas_config::CookiesConfig; +use mas_config::Encrypter; use mas_data_model::BrowserSession; use mas_storage::{user::end_session, PostgresqlBackend}; use mas_warp_utils::{ @@ -22,15 +22,12 @@ use mas_warp_utils::{ use sqlx::{PgPool, Postgres, Transaction}; use warp::{filters::BoxedFilter, hyper::Uri, Filter, Rejection, Reply}; -pub(super) fn filter( - pool: &PgPool, - cookies_config: &CookiesConfig, -) -> BoxedFilter<(Box,)> { +pub(super) fn filter(pool: &PgPool, encrypter: &Encrypter) -> BoxedFilter<(Box,)> { warp::path!("logout") .and(warp::post()) - .and(session(pool, cookies_config)) + .and(session(pool, encrypter)) .and(transaction(pool)) - .and(protected_form(cookies_config)) + .and(protected_form(encrypter)) .and_then(post) .boxed() } diff --git a/crates/handlers/src/views/mod.rs b/crates/handlers/src/views/mod.rs index f1f9262f..ebd5643f 100644 --- a/crates/handlers/src/views/mod.rs +++ b/crates/handlers/src/views/mod.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use mas_config::{CookiesConfig, CsrfConfig, HttpConfig}; +use mas_config::{CsrfConfig, Encrypter, HttpConfig}; use mas_email::Mailer; use mas_templates::Templates; use sqlx::PgPool; @@ -40,24 +40,17 @@ pub(super) fn filter( pool: &PgPool, templates: &Templates, mailer: &Mailer, + encrypter: &Encrypter, http_config: &HttpConfig, csrf_config: &CsrfConfig, - cookies_config: &CookiesConfig, ) -> BoxedFilter<(Box,)> { - let index = index(pool, templates, http_config, csrf_config, cookies_config); - let account = account( - pool, - templates, - mailer, - http_config, - csrf_config, - cookies_config, - ); - let login = login(pool, templates, csrf_config, cookies_config); - let register = register(pool, templates, csrf_config, cookies_config); - let logout = logout(pool, cookies_config); - let reauth = reauth(pool, templates, csrf_config, cookies_config); - let verify = verify(pool, templates, csrf_config, cookies_config); + let index = index(pool, templates, encrypter, http_config, csrf_config); + let account = account(pool, templates, mailer, encrypter, http_config, csrf_config); + let login = login(pool, templates, encrypter, csrf_config); + let register = register(pool, templates, encrypter, csrf_config); + let logout = logout(pool, encrypter); + let reauth = reauth(pool, templates, encrypter, csrf_config); + let verify = verify(pool, templates, encrypter, csrf_config); index .or(account) diff --git a/crates/handlers/src/views/reauth.rs b/crates/handlers/src/views/reauth.rs index ee56c03a..f045b486 100644 --- a/crates/handlers/src/views/reauth.rs +++ b/crates/handlers/src/views/reauth.rs @@ -13,7 +13,7 @@ // limitations under the License. use hyper::http::uri::{Parts, PathAndQuery}; -use mas_config::{CookiesConfig, CsrfConfig}; +use mas_config::{CsrfConfig, Encrypter}; use mas_data_model::BrowserSession; use mas_storage::{user::authenticate_session, PostgresqlBackend}; use mas_templates::{ReauthContext, TemplateContext, Templates}; @@ -83,22 +83,22 @@ struct ReauthForm { pub(super) fn filter( pool: &PgPool, templates: &Templates, + encrypter: &Encrypter, csrf_config: &CsrfConfig, - cookies_config: &CookiesConfig, ) -> BoxedFilter<(Box,)> { let get = warp::get() .and(with_templates(templates)) .and(connection(pool)) - .and(encrypted_cookie_saver(cookies_config)) - .and(updated_csrf_token(cookies_config, csrf_config)) - .and(session(pool, cookies_config)) + .and(encrypted_cookie_saver(encrypter)) + .and(updated_csrf_token(encrypter, csrf_config)) + .and(session(pool, encrypter)) .and(warp::query()) .and_then(get); let post = warp::post() - .and(session(pool, cookies_config)) + .and(session(pool, encrypter)) .and(transaction(pool)) - .and(protected_form(cookies_config)) + .and(protected_form(encrypter)) .and(warp::query()) .and_then(post); diff --git a/crates/handlers/src/views/register.rs b/crates/handlers/src/views/register.rs index 2ce37c5f..bdae4d2a 100644 --- a/crates/handlers/src/views/register.rs +++ b/crates/handlers/src/views/register.rs @@ -16,7 +16,7 @@ use argon2::Argon2; use hyper::http::uri::{Parts, PathAndQuery, Uri}; -use mas_config::{CookiesConfig, CsrfConfig}; +use mas_config::{CsrfConfig, Encrypter}; use mas_data_model::BrowserSession; use mas_storage::{ user::{register_user, start_session}, @@ -92,22 +92,22 @@ struct RegisterForm { pub(super) fn filter( pool: &PgPool, templates: &Templates, + encrypter: &Encrypter, csrf_config: &CsrfConfig, - cookies_config: &CookiesConfig, ) -> BoxedFilter<(Box,)> { let get = warp::get() .and(with_templates(templates)) .and(connection(pool)) - .and(encrypted_cookie_saver(cookies_config)) - .and(updated_csrf_token(cookies_config, csrf_config)) + .and(encrypted_cookie_saver(encrypter)) + .and(updated_csrf_token(encrypter, csrf_config)) .and(warp::query()) - .and(optional_session(pool, cookies_config)) + .and(optional_session(pool, encrypter)) .and_then(get); let post = warp::post() .and(transaction(pool)) - .and(encrypted_cookie_saver(cookies_config)) - .and(protected_form(cookies_config)) + .and(encrypted_cookie_saver(encrypter)) + .and(protected_form(encrypter)) .and(warp::query()) .and_then(post); diff --git a/crates/handlers/src/views/verify.rs b/crates/handlers/src/views/verify.rs index 91a095f4..0c275ae0 100644 --- a/crates/handlers/src/views/verify.rs +++ b/crates/handlers/src/views/verify.rs @@ -13,7 +13,7 @@ // limitations under the License. use chrono::Duration; -use mas_config::{CookiesConfig, CsrfConfig}; +use mas_config::{CsrfConfig, Encrypter}; use mas_data_model::BrowserSession; use mas_storage::{ user::{ @@ -39,15 +39,15 @@ use warp::{filters::BoxedFilter, reply::html, Filter, Rejection, Reply}; pub(super) fn filter( pool: &PgPool, templates: &Templates, + encrypter: &Encrypter, csrf_config: &CsrfConfig, - cookies_config: &CookiesConfig, ) -> BoxedFilter<(Box,)> { warp::path!("verify" / String) .and(warp::get()) .and(with_templates(templates)) - .and(encrypted_cookie_saver(cookies_config)) - .and(updated_csrf_token(cookies_config, csrf_config)) - .and(optional_session(pool, cookies_config)) + .and(encrypted_cookie_saver(encrypter)) + .and(updated_csrf_token(encrypter, csrf_config)) + .and(optional_session(pool, encrypter)) .and(transaction(pool)) .and_then(get) .boxed() diff --git a/crates/warp-utils/Cargo.toml b/crates/warp-utils/Cargo.toml index 23f075b7..018dfa84 100644 --- a/crates/warp-utils/Cargo.toml +++ b/crates/warp-utils/Cargo.toml @@ -20,7 +20,6 @@ serde_with = { version = "1.11.0", features = ["hex", "chrono"] } serde_json = "1.0.78" serde_urlencoded = "0.7.1" data-encoding = "2.3.2" -chacha20poly1305 = { version = "0.9.0", features = ["std"] } once_cell = "1.9.0" tracing = "0.1.29" opentelemetry = "0.16.0" diff --git a/crates/warp-utils/src/filters/client.rs b/crates/warp-utils/src/filters/client.rs index a73e4eba..474a75d6 100644 --- a/crates/warp-utils/src/filters/client.rs +++ b/crates/warp-utils/src/filters/client.rs @@ -17,7 +17,7 @@ use std::collections::HashMap; use headers::{authorization::Basic, Authorization}; -use mas_config::{OAuth2ClientAuthMethodConfig, OAuth2ClientConfig, OAuth2Config}; +use mas_config::{ClientAuthMethodConfig, ClientConfig, ClientsConfig}; use mas_iana::oauth::OAuthClientAuthenticationMethod; use mas_jose::{ claims::{TimeOptions, AUD, EXP, IAT, ISS, JTI, NBF, SUB}, @@ -33,9 +33,9 @@ use crate::errors::WrapError; /// Protect an enpoint with client authentication #[must_use] pub fn client_authentication( - oauth2_config: &OAuth2Config, + clients_config: &ClientsConfig, audience: String, -) -> impl Filter +) -> impl Filter + Clone + Send + Sync @@ -65,9 +65,9 @@ pub fn client_authentication( .unify() .untuple_one(); - let clients = oauth2_config.clients.clone(); + let clients_config = clients_config.clone(); warp::any() - .map(move || clients.clone()) + .map(move || clients_config.clone()) .and(warp::any().map(move || audience.clone())) .and(credentials) .and_then(authenticate_client) @@ -95,18 +95,18 @@ enum ClientAuthenticationError { impl Reject for ClientAuthenticationError {} async fn authenticate_client( - clients: Vec, + clients_config: ClientsConfig, audience: String, credentials: ClientCredentials, body: T, -) -> Result<(OAuthClientAuthenticationMethod, OAuth2ClientConfig, T), Rejection> { +) -> Result<(OAuthClientAuthenticationMethod, ClientConfig, T), Rejection> { let (auth_method, client) = match credentials { ClientCredentials::Pair { client_id, client_secret, via, } => { - let client = clients + let client = clients_config .iter() .find(|client| client.client_id == client_id) .ok_or_else(|| ClientAuthenticationError::ClientNotFound { @@ -114,12 +114,10 @@ async fn authenticate_client( })?; let auth_method = match (&client.client_auth_method, client_secret, via) { - (OAuth2ClientAuthMethodConfig::None, None, _) => { - OAuthClientAuthenticationMethod::None - } + (ClientAuthMethodConfig::None, None, _) => OAuthClientAuthenticationMethod::None, ( - OAuth2ClientAuthMethodConfig::ClientSecretBasic { + ClientAuthMethodConfig::ClientSecretBasic { client_secret: ref expected_client_secret, }, Some(ref given_client_secret), @@ -135,7 +133,7 @@ async fn authenticate_client( } ( - OAuth2ClientAuthMethodConfig::ClientSecretPost { + ClientAuthMethodConfig::ClientSecretPost { client_secret: ref expected_client_secret, }, Some(ref given_client_secret), @@ -195,7 +193,7 @@ async fn authenticate_client( // from the token, as per rfc7521 sec. 4.2 let client_id = client_id.as_ref().unwrap_or(&sub); - let client = clients + let client = clients_config .iter() .find(|client| &client.client_id == client_id) .ok_or_else(|| ClientAuthenticationError::ClientNotFound { @@ -203,13 +201,13 @@ async fn authenticate_client( })?; let auth_method = match &client.client_auth_method { - OAuth2ClientAuthMethodConfig::PrivateKeyJwt(jwks) => { + ClientAuthMethodConfig::PrivateKeyJwt(jwks) => { let store = jwks.key_store(); token.verify(&decoded, &store).await.wrap_error()?; OAuthClientAuthenticationMethod::PrivateKeyJwt } - OAuth2ClientAuthMethodConfig::ClientSecretJwt { client_secret } => { + ClientAuthMethodConfig::ClientSecretJwt { client_secret } => { let store = SharedSecret::new(client_secret); token.verify(&decoded, &store).await.wrap_error()?; OAuthClientAuthenticationMethod::ClientSecretJwt @@ -291,7 +289,7 @@ struct ClientAuthForm { #[cfg(test)] mod tests { use headers::authorization::Credentials; - use mas_config::{ConfigurationSection, OAuth2ClientAuthMethodConfig}; + use mas_config::{ClientAuthMethodConfig, ConfigurationSection}; use mas_jose::{ExportJwks, SigningKeystore, StaticKeystore}; use serde_json::json; @@ -307,37 +305,37 @@ mod tests { store } - async fn oauth2_config() -> OAuth2Config { - let mut config = OAuth2Config::test(); - config.clients.push(OAuth2ClientConfig { + async fn oauth2_config() -> ClientsConfig { + let mut config = ClientsConfig::test(); + config.push(ClientConfig { client_id: "public".to_string(), - client_auth_method: OAuth2ClientAuthMethodConfig::None, + client_auth_method: ClientAuthMethodConfig::None, redirect_uris: Vec::new(), }); - config.clients.push(OAuth2ClientConfig { + config.push(ClientConfig { client_id: "secret-basic".to_string(), - client_auth_method: OAuth2ClientAuthMethodConfig::ClientSecretBasic { + client_auth_method: ClientAuthMethodConfig::ClientSecretBasic { client_secret: CLIENT_SECRET.to_string(), }, redirect_uris: Vec::new(), }); - config.clients.push(OAuth2ClientConfig { + config.push(ClientConfig { client_id: "secret-post".to_string(), - client_auth_method: OAuth2ClientAuthMethodConfig::ClientSecretPost { + client_auth_method: ClientAuthMethodConfig::ClientSecretPost { client_secret: CLIENT_SECRET.to_string(), }, redirect_uris: Vec::new(), }); - config.clients.push(OAuth2ClientConfig { + config.push(ClientConfig { client_id: "secret-jwt".to_string(), - client_auth_method: OAuth2ClientAuthMethodConfig::ClientSecretJwt { + client_auth_method: ClientAuthMethodConfig::ClientSecretJwt { client_secret: CLIENT_SECRET.to_string(), }, redirect_uris: Vec::new(), }); - config.clients.push(OAuth2ClientConfig { + config.push(ClientConfig { client_id: "secret-jwt-2".to_string(), - client_auth_method: OAuth2ClientAuthMethodConfig::ClientSecretJwt { + client_auth_method: ClientAuthMethodConfig::ClientSecretJwt { client_secret: CLIENT_SECRET.to_string(), }, redirect_uris: Vec::new(), @@ -345,14 +343,14 @@ mod tests { let store = client_private_keystore(); let jwks = store.export_jwks().await.unwrap(); - config.clients.push(OAuth2ClientConfig { + config.push(ClientConfig { client_id: "private-key-jwt".to_string(), - client_auth_method: OAuth2ClientAuthMethodConfig::PrivateKeyJwt(jwks.clone().into()), + client_auth_method: ClientAuthMethodConfig::PrivateKeyJwt(jwks.clone().into()), redirect_uris: Vec::new(), }); - config.clients.push(OAuth2ClientConfig { + config.push(ClientConfig { client_id: "private-key-jwt-2".to_string(), - client_auth_method: OAuth2ClientAuthMethodConfig::PrivateKeyJwt(jwks.into()), + client_auth_method: ClientAuthMethodConfig::PrivateKeyJwt(jwks.into()), redirect_uris: Vec::new(), }); config diff --git a/crates/warp-utils/src/filters/cookies.rs b/crates/warp-utils/src/filters/cookies.rs index c547816b..b5848607 100644 --- a/crates/warp-utils/src/filters/cookies.rs +++ b/crates/warp-utils/src/filters/cookies.rs @@ -16,14 +16,10 @@ use std::{convert::Infallible, marker::PhantomData}; -use chacha20poly1305::{ - aead::{generic_array::GenericArray, Aead, NewAead}, - ChaCha20Poly1305, -}; use cookie::{Cookie, SameSite}; use data_encoding::BASE64URL_NOPAD; use headers::{Header, HeaderValue, SetCookie}; -use mas_config::CookiesConfig; +use mas_config::Encrypter; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use thiserror::Error; use warp::{ @@ -76,23 +72,16 @@ struct EncryptedCookie { impl EncryptedCookie { /// Encrypt from a given key - fn encrypt(payload: T, key: &[u8; 32]) -> anyhow::Result { - let key = GenericArray::from_slice(key); - let aead = ChaCha20Poly1305::new(key); + fn encrypt(payload: T, encrypter: &Encrypter) -> anyhow::Result { let message = bincode::serialize(&payload)?; let nonce: [u8; 12] = rand::random(); - let ciphertext = aead.encrypt(GenericArray::from_slice(&nonce[..]), &message[..])?; + let ciphertext = encrypter.encrypt(&nonce, &message)?; Ok(Self { nonce, ciphertext }) } /// Decrypt the content of the cookie from a given key - fn decrypt(&self, key: &[u8; 32]) -> anyhow::Result { - let key = GenericArray::from_slice(key); - let aead = ChaCha20Poly1305::new(key); - let message = aead.decrypt( - GenericArray::from_slice(&self.nonce[..]), - &self.ciphertext[..], - )?; + fn decrypt(&self, encrypter: &Encrypter) -> anyhow::Result { + let message = encrypter.decrypt(&self.nonce, &self.ciphertext)?; let token = bincode::deserialize(&message)?; Ok(token) } @@ -113,12 +102,12 @@ impl EncryptedCookie { /// Extract an optional encrypted cookie #[must_use] pub fn maybe_encrypted( - options: &CookiesConfig, + encrypter: &Encrypter, ) -> impl Filter,), Error = Rejection> + Clone + Send + Sync + 'static where T: DeserializeOwned + EncryptableCookieValue + 'static, { - encrypted(options) + encrypted(encrypter) .map(Some) .recover(none_on_error::) .unify() @@ -136,28 +125,35 @@ where /// [`CookieDecryptionError`] #[must_use] pub fn encrypted( - options: &CookiesConfig, + encrypter: &Encrypter, ) -> impl Filter + Clone + Send + Sync + 'static where T: DeserializeOwned + EncryptableCookieValue + 'static, { - let secret = options.secret; - warp::cookie::cookie(T::cookie_key()).and_then(move |value: String| async move { - let encrypted = - EncryptedCookie::from_cookie_value(&value).map_err(decryption_error::)?; - let decrypted = encrypted.decrypt(&secret).map_err(decryption_error::)?; - Ok::<_, Rejection>(decrypted) + let encrypter = encrypter.clone(); + warp::cookie::cookie(T::cookie_key()).and_then(move |value: String| { + let encrypter = encrypter.clone(); + async move { + let encrypted = + EncryptedCookie::from_cookie_value(&value).map_err(decryption_error::)?; + let decrypted = encrypted + .decrypt(&encrypter) + .map_err(decryption_error::)?; + Ok::<_, Rejection>(decrypted) + } }) } /// Get an [`EncryptedCookieSaver`] to help saving an [`EncryptableCookieValue`] #[must_use] pub fn encrypted_cookie_saver( - options: &CookiesConfig, + encrypter: &Encrypter, ) -> impl Filter + Clone + Send + Sync + 'static { - let secret = options.secret; - warp::any().map(move || EncryptedCookieSaver { secret }) + let encrypter = encrypter.clone(); + warp::any().map(move || EncryptedCookieSaver { + encrypter: encrypter.clone(), + }) } /// A cookie that can be encrypted with a well-known cookie key @@ -168,7 +164,7 @@ pub trait EncryptableCookieValue: Serialize + Send + Sync + std::fmt::Debug { /// An opaque structure which helps encrypting a cookie and attach it to a reply pub struct EncryptedCookieSaver { - secret: [u8; 32], + encrypter: Encrypter, } impl EncryptedCookieSaver { @@ -178,7 +174,7 @@ impl EncryptedCookieSaver { cookie: &T, reply: R, ) -> Result, Rejection> { - let encrypted = EncryptedCookie::encrypt(cookie, &self.secret) + let encrypted = EncryptedCookie::encrypt(cookie, &self.encrypter) .wrap_error()? .to_cookie_value() .wrap_error()?; diff --git a/crates/warp-utils/src/filters/csrf.rs b/crates/warp-utils/src/filters/csrf.rs index 289a905d..1a6a66ec 100644 --- a/crates/warp-utils/src/filters/csrf.rs +++ b/crates/warp-utils/src/filters/csrf.rs @@ -17,7 +17,7 @@ use chrono::{DateTime, Duration, Utc}; use data_encoding::{DecodeError, BASE64URL_NOPAD}; -use mas_config::{CookiesConfig, CsrfConfig}; +use mas_config::{CsrfConfig, Encrypter}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_with::{serde_as, TimestampSeconds}; use thiserror::Error; @@ -119,9 +119,9 @@ impl CsrfForm { } fn csrf_token( - cookies_config: &CookiesConfig, + encrypter: &Encrypter, ) -> impl Filter + Clone + Send + Sync + 'static { - super::cookies::encrypted(cookies_config).and_then(move |token: CsrfToken| async move { + super::cookies::encrypted(encrypter).and_then(move |token: CsrfToken| async move { let verified = token.verify_expiration()?; Ok::<_, Rejection>(verified) }) @@ -134,11 +134,11 @@ fn csrf_token( /// with [`encrypted_cookie_saver`][`super::cookies::encrypted_cookie_saver`] #[must_use] pub fn updated_csrf_token( - cookies_config: &CookiesConfig, + encrypter: &Encrypter, csrf_config: &CsrfConfig, ) -> impl Filter + Clone + Send + Sync + 'static { let ttl = csrf_config.ttl; - super::cookies::maybe_encrypted(cookies_config).and_then( + super::cookies::maybe_encrypted(encrypter).and_then( move |maybe_token: Option| async move { // Explicitely specify the "Error" type here to have the `?` operation working Ok::<_, Rejection>( @@ -171,12 +171,12 @@ pub fn updated_csrf_token( /// TODO: we might want to unify the last three rejections in one #[must_use] pub fn protected_form( - cookies_config: &CookiesConfig, + encrypter: &Encrypter, ) -> impl Filter + Clone + Send + Sync + 'static where T: DeserializeOwned + Send + 'static, { - csrf_token(cookies_config).and(warp::body::form()).and_then( + csrf_token(encrypter).and(warp::body::form()).and_then( |csrf_token: CsrfToken, protected_form: CsrfForm| async move { let form = protected_form.verify_csrf(&csrf_token)?; Ok::<_, Rejection>(form) diff --git a/crates/warp-utils/src/filters/session.rs b/crates/warp-utils/src/filters/session.rs index 2ab73b96..02b1f7ec 100644 --- a/crates/warp-utils/src/filters/session.rs +++ b/crates/warp-utils/src/filters/session.rs @@ -14,7 +14,7 @@ //! Load user sessions from the database -use mas_config::CookiesConfig; +use mas_config::Encrypter; use mas_data_model::BrowserSession; use mas_storage::{ user::{lookup_active_session, ActiveSessionLookupError}, @@ -88,13 +88,13 @@ impl EncryptableCookieValue for SessionCookie { #[must_use] pub fn optional_session( pool: &PgPool, - cookies_config: &CookiesConfig, + encrypter: &Encrypter, ) -> impl Filter>,), Error = Rejection> + Clone + Send + Sync + 'static { - session(pool, cookies_config) + session(pool, encrypter) .map(Some) .recover(none_on_error::<_, SessionLoadError>) .unify() @@ -110,13 +110,13 @@ pub fn optional_session( #[must_use] pub fn session( pool: &PgPool, - cookies_config: &CookiesConfig, + encrypter: &Encrypter, ) -> impl Filter,), Error = Rejection> + Clone + Send + Sync + 'static { - encrypted(cookies_config) + encrypted(encrypter) .and(connection(pool)) .and_then(load_session) .recover(recover) diff --git a/crates/warp-utils/src/filters/url_builder.rs b/crates/warp-utils/src/filters/url_builder.rs index 2df2b5c0..72939ece 100644 --- a/crates/warp-utils/src/filters/url_builder.rs +++ b/crates/warp-utils/src/filters/url_builder.rs @@ -70,7 +70,7 @@ impl UrlBuilder { /// JWKS URI pub fn jwks_uri(&self) -> Url { - self.base.join("oauth2/jwks.json").expect("build URL") + self.base.join("oauth2/keys.json").expect("build URL") } /// Email verification URL