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

Move secrets and oauth2 clients config

This commit is contained in:
Quentin Gliech
2022-02-01 09:34:18 +01:00
parent c0e5b66ea4
commit 7e24cd0948
30 changed files with 462 additions and 454 deletions

3
Cargo.lock generated
View File

@ -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",

View File

@ -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);

View File

@ -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"

View File

@ -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<JsonWebKeySet> 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<Url>,
}
#[derive(Debug, Error)]
#[error("Invalid redirect URI")]
pub struct InvalidRedirectUriError;
impl ClientConfig {
pub fn resolve_redirect_uri<'a>(
&'a self,
suggested_uri: &'a Option<Url>,
) -> 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<ClientConfig>);
impl Deref for ClientsConfig {
type Target = Vec<ClientConfig>;
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<Self> {
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(())
});
}
}

View File

@ -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<Self> {
Ok(Self {
secret: rand::random(),
})
}
fn test() -> Self {
Self { secret: [0xEA; 32] }
}
}

View File

@ -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<Self> {
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(),
}
}
}

View File

@ -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<ChaCha20Poly1305>,
}
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<Vec<u8>> {
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<Vec<u8>> {
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<JsonWebKeySet> 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<Url>,
keys: Vec<KeyConfig>,
}
#[derive(Debug, Error)]
#[error("Invalid redirect URI")]
pub struct InvalidRedirectUriError;
impl OAuth2ClientConfig {
pub fn resolve_redirect_uri<'a>(
&'a self,
suggested_uri: &'a Option<Url>,
) -> 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<OAuth2ClientConfig>,
#[serde(default)]
pub keys: Vec<KeyConfig>,
}
impl OAuth2Config {
impl SecretsConfig {
pub async fn key_store(&self) -> anyhow::Result<StaticKeystore> {
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(())
});
}
}

View File

@ -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"

View File

@ -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<StaticKeystore>,
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());

View File

@ -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<dyn Reply>,)> {
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<ReplyOrBackToClient, Rejection>
async fn actually_reply(
rep: ReplyOrBackToClient,
q: PartialParams,
clients: Vec<OAuth2ClientConfig>,
clients: ClientsConfig,
templates: Templates,
) -> Result<Box<dyn Reply>, 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<OAuth2ClientConfig>,
clients: ClientsConfig,
params: Params,
maybe_session: Option<BrowserSession<PostgresqlBackend>>,
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()?;

View File

@ -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<dyn Reply>,)> {
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<Postgres>,
auth: OAuthClientAuthenticationMethod,
client: OAuth2ClientConfig,
client: ClientConfig,
params: IntrospectionRequest,
) -> Result<Box<dyn Reply>, Rejection> {
// Token introspection is only allowed by confidential clients

View File

@ -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<StaticKeystore>,
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)

View File

@ -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<StaticKeystore>,
oauth2_config: &OAuth2Config,
clients_config: &ClientsConfig,
http_config: &HttpConfig,
) -> BoxedFilter<(Box<dyn Reply>,)> {
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<Box<dyn Reply>, Rejection> {
async fn token(
_auth: OAuthClientAuthenticationMethod,
client: OAuth2ClientConfig,
client: ClientConfig,
req: AccessTokenRequest,
key_store: Arc<StaticKeystore>,
issuer: Url,
@ -171,7 +171,7 @@ fn hash<H: Digest>(mut hasher: H, token: &str) -> anyhow::Result<String> {
#[allow(clippy::too_many_lines)]
async fn authorization_code_grant(
grant: &AuthorizationCodeGrant,
client: &OAuth2ClientConfig,
client: &ClientConfig,
key_store: &StaticKeystore,
issuer: Url,
conn: &mut PoolConnection<Postgres>,
@ -328,7 +328,7 @@ async fn authorization_code_grant(
async fn refresh_token_grant(
grant: &RefreshTokenGrant,
client: &OAuth2ClientConfig,
client: &ClientConfig,
conn: &mut PoolConnection<Postgres>,
) -> Result<AccessTokenResponse, Rejection> {
let mut txn = conn.begin().await.wrap_error()?;

View File

@ -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<dyn Reply>,)> {
pub(super) fn filter(pool: &PgPool) -> BoxedFilter<(Box<dyn Reply>,)> {
warp::path!("oauth2" / "userinfo")
.and(
warp::get()

View File

@ -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<dyn Reply>,)> {
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);

View File

@ -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<dyn Reply>,)> {
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();

View File

@ -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<dyn Reply>,)> {
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);

View File

@ -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<dyn Reply>,)> {
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()
}

View File

@ -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<dyn Reply>,)> {
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);

View File

@ -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<dyn Reply>,)> {
pub(super) fn filter(pool: &PgPool, encrypter: &Encrypter) -> BoxedFilter<(Box<dyn Reply>,)> {
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()
}

View File

@ -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<dyn Reply>,)> {
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)

View File

@ -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<dyn Reply>,)> {
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);

View File

@ -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<dyn Reply>,)> {
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);

View File

@ -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<dyn Reply>,)> {
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()

View File

@ -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"

View File

@ -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<T: DeserializeOwned + Send + 'static>(
oauth2_config: &OAuth2Config,
clients_config: &ClientsConfig,
audience: String,
) -> impl Filter<Extract = (OAuthClientAuthenticationMethod, OAuth2ClientConfig, T), Error = Rejection>
) -> impl Filter<Extract = (OAuthClientAuthenticationMethod, ClientConfig, T), Error = Rejection>
+ Clone
+ Send
+ Sync
@ -65,9 +65,9 @@ pub fn client_authentication<T: DeserializeOwned + Send + 'static>(
.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<T>(
clients: Vec<OAuth2ClientConfig>,
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<T>(
})?;
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<T>(
}
(
OAuth2ClientAuthMethodConfig::ClientSecretPost {
ClientAuthMethodConfig::ClientSecretPost {
client_secret: ref expected_client_secret,
},
Some(ref given_client_secret),
@ -195,7 +193,7 @@ async fn authenticate_client<T>(
// 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<T>(
})?;
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<T> {
#[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

View File

@ -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<T: Serialize>(payload: T, key: &[u8; 32]) -> anyhow::Result<Self> {
let key = GenericArray::from_slice(key);
let aead = ChaCha20Poly1305::new(key);
fn encrypt<T: Serialize>(payload: T, encrypter: &Encrypter) -> anyhow::Result<Self> {
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<T: DeserializeOwned>(&self, key: &[u8; 32]) -> anyhow::Result<T> {
let key = GenericArray::from_slice(key);
let aead = ChaCha20Poly1305::new(key);
let message = aead.decrypt(
GenericArray::from_slice(&self.nonce[..]),
&self.ciphertext[..],
)?;
fn decrypt<T: DeserializeOwned>(&self, encrypter: &Encrypter) -> anyhow::Result<T> {
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<T>(
options: &CookiesConfig,
encrypter: &Encrypter,
) -> impl Filter<Extract = (Option<T>,), Error = Rejection> + Clone + Send + Sync + 'static
where
T: DeserializeOwned + EncryptableCookieValue + 'static,
{
encrypted(options)
encrypted(encrypter)
.map(Some)
.recover(none_on_error::<T, InvalidHeader>)
.unify()
@ -136,28 +125,35 @@ where
/// [`CookieDecryptionError`]
#[must_use]
pub fn encrypted<T>(
options: &CookiesConfig,
encrypter: &Encrypter,
) -> impl Filter<Extract = (T,), Error = Rejection> + 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 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::<T>)?;
let decrypted = encrypted.decrypt(&secret).map_err(decryption_error::<T>)?;
let decrypted = encrypted
.decrypt(&encrypter)
.map_err(decryption_error::<T>)?;
Ok::<_, Rejection>(decrypted)
}
})
}
/// Get an [`EncryptedCookieSaver`] to help saving an [`EncryptableCookieValue`]
#[must_use]
pub fn encrypted_cookie_saver(
options: &CookiesConfig,
encrypter: &Encrypter,
) -> impl Filter<Extract = (EncryptedCookieSaver,), Error = Infallible> + 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<WithTypedHeader<R, SetCookie>, Rejection> {
let encrypted = EncryptedCookie::encrypt(cookie, &self.secret)
let encrypted = EncryptedCookie::encrypt(cookie, &self.encrypter)
.wrap_error()?
.to_cookie_value()
.wrap_error()?;

View File

@ -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<T> CsrfForm<T> {
}
fn csrf_token(
cookies_config: &CookiesConfig,
encrypter: &Encrypter,
) -> impl Filter<Extract = (CsrfToken,), Error = Rejection> + 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<Extract = (CsrfToken,), Error = Rejection> + 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<CsrfToken>| 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<T>(
cookies_config: &CookiesConfig,
encrypter: &Encrypter,
) -> impl Filter<Extract = (T,), Error = Rejection> + 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<T>| async move {
let form = protected_form.verify_csrf(&csrf_token)?;
Ok::<_, Rejection>(form)

View File

@ -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<Extract = (Option<BrowserSession<PostgresqlBackend>>,), 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<Extract = (BrowserSession<PostgresqlBackend>,), Error = Rejection>
+ Clone
+ Send
+ Sync
+ 'static {
encrypted(cookies_config)
encrypted(encrypter)
.and(connection(pool))
.and_then(load_session)
.recover(recover)

View File

@ -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