You've already forked authentication-service
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:
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -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",
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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"
|
||||
|
||||
|
200
crates/config/src/sections/clients.rs
Normal file
200
crates/config/src/sections/clients.rs
Normal 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(())
|
||||
});
|
||||
}
|
||||
}
|
@ -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] }
|
||||
}
|
||||
}
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(())
|
||||
});
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
||||
|
@ -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());
|
||||
|
||||
|
@ -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()?;
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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()?;
|
||||
|
@ -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()
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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()?;
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user