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 = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"chacha20poly1305",
|
||||||
"chrono",
|
"chrono",
|
||||||
"elliptic-curve",
|
"elliptic-curve",
|
||||||
"figment",
|
"figment",
|
||||||
@ -1944,7 +1945,6 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
"chacha20poly1305",
|
|
||||||
"chrono",
|
"chrono",
|
||||||
"crc",
|
"crc",
|
||||||
"data-encoding",
|
"data-encoding",
|
||||||
@ -2106,7 +2106,6 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bincode",
|
"bincode",
|
||||||
"chacha20poly1305",
|
|
||||||
"chrono",
|
"chrono",
|
||||||
"cookie",
|
"cookie",
|
||||||
"crc",
|
"crc",
|
||||||
|
@ -251,13 +251,15 @@ impl ServerCommand {
|
|||||||
|
|
||||||
// Initialize the key store
|
// Initialize the key store
|
||||||
let key_store = config
|
let key_store = config
|
||||||
.oauth2
|
.secrets
|
||||||
.key_store()
|
.key_store()
|
||||||
.await
|
.await
|
||||||
.context("could not import keys from config")?;
|
.context("could not import keys from config")?;
|
||||||
// Wrap the key store in an Arc
|
// Wrap the key store in an Arc
|
||||||
let key_store = Arc::new(key_store);
|
let key_store = Arc::new(key_store);
|
||||||
|
|
||||||
|
let encrypter = config.secrets.encrypter();
|
||||||
|
|
||||||
// Load and compile the templates
|
// Load and compile the templates
|
||||||
let templates = Templates::load_from_config(&config.templates)
|
let templates = Templates::load_from_config(&config.templates)
|
||||||
.await
|
.await
|
||||||
@ -283,7 +285,10 @@ impl ServerCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start the server
|
// 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);
|
let warp_service = warp::service(root);
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ rand = "0.8.4"
|
|||||||
rsa = { git = "https://github.com/RustCrypto/RSA.git" }
|
rsa = { git = "https://github.com/RustCrypto/RSA.git" }
|
||||||
p256 = { version = "0.10.1", features = ["ecdsa", "pem", "pkcs8"] }
|
p256 = { version = "0.10.1", features = ["ecdsa", "pem", "pkcs8"] }
|
||||||
pkcs8 = { version = "0.8.0", features = ["pem"] }
|
pkcs8 = { version = "0.8.0", features = ["pem"] }
|
||||||
|
chacha20poly1305 = { version = "0.9.0", features = ["std"] }
|
||||||
elliptic-curve = { version = "0.11.7", features = ["pem", "pkcs8"] }
|
elliptic-curve = { version = "0.11.7", features = ["pem", "pkcs8"] }
|
||||||
pem-rfc7468 = "0.3.1"
|
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 schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
mod cookies;
|
mod clients;
|
||||||
mod csrf;
|
mod csrf;
|
||||||
mod database;
|
mod database;
|
||||||
mod email;
|
mod email;
|
||||||
mod http;
|
mod http;
|
||||||
mod oauth2;
|
mod secrets;
|
||||||
mod telemetry;
|
mod telemetry;
|
||||||
mod templates;
|
mod templates;
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
cookies::CookiesConfig,
|
clients::{ClientAuthMethodConfig, ClientConfig, ClientsConfig},
|
||||||
csrf::CsrfConfig,
|
csrf::CsrfConfig,
|
||||||
database::DatabaseConfig,
|
database::DatabaseConfig,
|
||||||
email::{EmailConfig, EmailSmtpMode, EmailTransportConfig},
|
email::{EmailConfig, EmailSmtpMode, EmailTransportConfig},
|
||||||
http::HttpConfig,
|
http::HttpConfig,
|
||||||
oauth2::{OAuth2ClientAuthMethodConfig, OAuth2ClientConfig, OAuth2Config},
|
secrets::{Encrypter, SecretsConfig},
|
||||||
telemetry::{
|
telemetry::{
|
||||||
MetricsConfig, MetricsExporterConfig, Propagator, TelemetryConfig, TracingConfig,
|
MetricsConfig, MetricsExporterConfig, Propagator, TelemetryConfig, TracingConfig,
|
||||||
TracingExporterConfig,
|
TracingExporterConfig,
|
||||||
@ -43,8 +43,9 @@ use crate::util::ConfigurationSection;
|
|||||||
/// Application configuration root
|
/// Application configuration root
|
||||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct RootConfig {
|
pub struct RootConfig {
|
||||||
/// Configuration related to OAuth 2.0/OIDC operations
|
/// List of OAuth 2.0/OIDC clients config
|
||||||
pub oauth2: OAuth2Config,
|
#[serde(default)]
|
||||||
|
pub clients: ClientsConfig,
|
||||||
|
|
||||||
/// Configuration of the HTTP server
|
/// Configuration of the HTTP server
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@ -54,9 +55,6 @@ pub struct RootConfig {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub database: DatabaseConfig,
|
pub database: DatabaseConfig,
|
||||||
|
|
||||||
/// Configuration related to cookies
|
|
||||||
pub cookies: CookiesConfig,
|
|
||||||
|
|
||||||
/// Configuration related to sending monitoring data
|
/// Configuration related to sending monitoring data
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub telemetry: TelemetryConfig,
|
pub telemetry: TelemetryConfig,
|
||||||
@ -72,6 +70,9 @@ pub struct RootConfig {
|
|||||||
/// Configuration related to sending emails
|
/// Configuration related to sending emails
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub email: EmailConfig,
|
pub email: EmailConfig,
|
||||||
|
|
||||||
|
/// Application secrets
|
||||||
|
pub secrets: SecretsConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@ -82,27 +83,27 @@ impl ConfigurationSection<'_> for RootConfig {
|
|||||||
|
|
||||||
async fn generate() -> anyhow::Result<Self> {
|
async fn generate() -> anyhow::Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
oauth2: OAuth2Config::generate().await?,
|
clients: ClientsConfig::generate().await?,
|
||||||
http: HttpConfig::generate().await?,
|
http: HttpConfig::generate().await?,
|
||||||
database: DatabaseConfig::generate().await?,
|
database: DatabaseConfig::generate().await?,
|
||||||
cookies: CookiesConfig::generate().await?,
|
|
||||||
telemetry: TelemetryConfig::generate().await?,
|
telemetry: TelemetryConfig::generate().await?,
|
||||||
templates: TemplatesConfig::generate().await?,
|
templates: TemplatesConfig::generate().await?,
|
||||||
csrf: CsrfConfig::generate().await?,
|
csrf: CsrfConfig::generate().await?,
|
||||||
email: EmailConfig::generate().await?,
|
email: EmailConfig::generate().await?,
|
||||||
|
secrets: SecretsConfig::generate().await?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test() -> Self {
|
fn test() -> Self {
|
||||||
Self {
|
Self {
|
||||||
oauth2: OAuth2Config::test(),
|
clients: ClientsConfig::test(),
|
||||||
http: HttpConfig::test(),
|
http: HttpConfig::test(),
|
||||||
database: DatabaseConfig::test(),
|
database: DatabaseConfig::test(),
|
||||||
cookies: CookiesConfig::test(),
|
|
||||||
telemetry: TelemetryConfig::test(),
|
telemetry: TelemetryConfig::test(),
|
||||||
templates: TemplatesConfig::test(),
|
templates: TemplatesConfig::test(),
|
||||||
csrf: CsrfConfig::test(),
|
csrf: CsrfConfig::test(),
|
||||||
email: EmailConfig::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");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with 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
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::{path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use async_trait::async_trait;
|
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 pkcs8::DecodePrivateKey;
|
||||||
use rsa::{
|
use rsa::{
|
||||||
pkcs1::{DecodeRsaPrivateKey, EncodeRsaPrivateKey},
|
pkcs1::{DecodeRsaPrivateKey, EncodeRsaPrivateKey},
|
||||||
@ -24,14 +28,43 @@ use rsa::{
|
|||||||
};
|
};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_with::skip_serializing_none;
|
use serde_with::serde_as;
|
||||||
use thiserror::Error;
|
|
||||||
use tokio::{fs::File, io::AsyncReadExt, task};
|
use tokio::{fs::File, io::AsyncReadExt, task};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use super::ConfigurationSection;
|
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)]
|
#[derive(JsonSchema, Serialize, Deserialize, Clone, Copy, Debug)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum KeyType {
|
pub enum KeyType {
|
||||||
@ -53,89 +86,24 @@ pub struct KeyConfig {
|
|||||||
key: KeyOrPath,
|
key: KeyOrPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
|
#[serde_as]
|
||||||
#[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]
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct OAuth2ClientConfig {
|
pub struct SecretsConfig {
|
||||||
pub client_id: String,
|
/// Encryption key for secure cookies
|
||||||
|
#[schemars(
|
||||||
#[serde(flatten)]
|
with = "String",
|
||||||
pub client_auth_method: OAuth2ClientAuthMethodConfig,
|
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)]
|
#[serde(default)]
|
||||||
pub redirect_uris: Vec<Url>,
|
keys: Vec<KeyConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
impl SecretsConfig {
|
||||||
#[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 {
|
|
||||||
pub async fn key_store(&self) -> anyhow::Result<StaticKeystore> {
|
pub async fn key_store(&self) -> anyhow::Result<StaticKeystore> {
|
||||||
let mut store = StaticKeystore::new();
|
let mut store = StaticKeystore::new();
|
||||||
|
|
||||||
@ -189,12 +157,17 @@ impl OAuth2Config {
|
|||||||
|
|
||||||
Ok(store)
|
Ok(store)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn encrypter(&self) -> Encrypter {
|
||||||
|
Encrypter::new(&self.encryption)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl ConfigurationSection<'_> for OAuth2Config {
|
impl ConfigurationSection<'_> for SecretsConfig {
|
||||||
fn path() -> &'static str {
|
fn path() -> &'static str {
|
||||||
"oauth2"
|
"secrets"
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
@ -237,7 +210,7 @@ impl ConfigurationSection<'_> for OAuth2Config {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
clients: Vec::new(),
|
encryption: rand::random(),
|
||||||
keys: vec![rsa_key, ecdsa_key],
|
keys: vec![rsa_key, ecdsa_key],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -276,97 +249,8 @@ impl ConfigurationSection<'_> for OAuth2Config {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
clients: Vec::new(),
|
encryption: [0xEA; 32],
|
||||||
keys: vec![rsa_key, ecdsa_key],
|
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" }
|
rsa = { git = "https://github.com/RustCrypto/RSA.git" }
|
||||||
pkcs8 = { version = "0.8.0", features = ["pem"] }
|
pkcs8 = { version = "0.8.0", features = ["pem"] }
|
||||||
elliptic-curve = { version = "0.11.7", features = ["pem"] }
|
elliptic-curve = { version = "0.11.7", features = ["pem"] }
|
||||||
chacha20poly1305 = { version = "0.9.0", features = ["std"] }
|
|
||||||
sha2 = "0.10.1"
|
sha2 = "0.10.1"
|
||||||
crc = "2.1.0"
|
crc = "2.1.0"
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use mas_config::RootConfig;
|
use mas_config::{Encrypter, RootConfig};
|
||||||
use mas_email::Mailer;
|
use mas_email::Mailer;
|
||||||
use mas_jose::StaticKeystore;
|
use mas_jose::StaticKeystore;
|
||||||
use mas_static_files::filter as static_files;
|
use mas_static_files::filter as static_files;
|
||||||
@ -43,6 +43,7 @@ pub fn root(
|
|||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
templates: &Templates,
|
templates: &Templates,
|
||||||
key_store: &Arc<StaticKeystore>,
|
key_store: &Arc<StaticKeystore>,
|
||||||
|
encrypter: &Encrypter,
|
||||||
mailer: &Mailer,
|
mailer: &Mailer,
|
||||||
config: &RootConfig,
|
config: &RootConfig,
|
||||||
) -> BoxedFilter<(impl Reply,)> {
|
) -> BoxedFilter<(impl Reply,)> {
|
||||||
@ -51,17 +52,17 @@ pub fn root(
|
|||||||
pool,
|
pool,
|
||||||
templates,
|
templates,
|
||||||
key_store,
|
key_store,
|
||||||
&config.oauth2,
|
encrypter,
|
||||||
|
&config.clients,
|
||||||
&config.http,
|
&config.http,
|
||||||
&config.cookies,
|
|
||||||
);
|
);
|
||||||
let views = views(
|
let views = views(
|
||||||
pool,
|
pool,
|
||||||
templates,
|
templates,
|
||||||
mailer,
|
mailer,
|
||||||
|
encrypter,
|
||||||
&config.http,
|
&config.http,
|
||||||
&config.csrf,
|
&config.csrf,
|
||||||
&config.cookies,
|
|
||||||
);
|
);
|
||||||
let static_files = static_files(config.http.web_root.clone());
|
let static_files = static_files(config.http.web_root.clone());
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ use hyper::{
|
|||||||
http::uri::{Parts, PathAndQuery, Uri},
|
http::uri::{Parts, PathAndQuery, Uri},
|
||||||
StatusCode,
|
StatusCode,
|
||||||
};
|
};
|
||||||
use mas_config::{CookiesConfig, OAuth2ClientConfig, OAuth2Config};
|
use mas_config::{ClientsConfig, Encrypter};
|
||||||
use mas_data_model::{
|
use mas_data_model::{
|
||||||
Authentication, AuthorizationCode, AuthorizationGrant, AuthorizationGrantStage, BrowserSession,
|
Authentication, AuthorizationCode, AuthorizationGrant, AuthorizationGrantStage, BrowserSession,
|
||||||
Pkce, StorageBackend, TokenType,
|
Pkce, StorageBackend, TokenType,
|
||||||
@ -215,33 +215,34 @@ fn resolve_response_mode(
|
|||||||
pub fn filter(
|
pub fn filter(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
templates: &Templates,
|
templates: &Templates,
|
||||||
oauth2_config: &OAuth2Config,
|
encrypter: &Encrypter,
|
||||||
cookies_config: &CookiesConfig,
|
clients_config: &ClientsConfig,
|
||||||
) -> BoxedFilter<(Box<dyn Reply>,)> {
|
) -> 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")
|
let authorize = warp::path!("oauth2" / "authorize")
|
||||||
.and(warp::get())
|
.and(warp::get())
|
||||||
.map(move || clients.clone())
|
.map(move || clients_config.clone())
|
||||||
.and(warp::query())
|
.and(warp::query())
|
||||||
.and(optional_session(pool, cookies_config))
|
.and(optional_session(pool, encrypter))
|
||||||
.and(transaction(pool))
|
.and(transaction(pool))
|
||||||
.and_then(get);
|
.and_then(get);
|
||||||
|
|
||||||
let step = warp::path!("oauth2" / "authorize" / "step")
|
let step = warp::path!("oauth2" / "authorize" / "step")
|
||||||
.and(warp::get())
|
.and(warp::get())
|
||||||
.and(warp::query())
|
.and(warp::query())
|
||||||
.and(session(pool, cookies_config))
|
.and(session(pool, encrypter))
|
||||||
.and(transaction(pool))
|
.and(transaction(pool))
|
||||||
.and_then(step);
|
.and_then(step);
|
||||||
|
|
||||||
let clients = oauth2_config.clients.clone();
|
|
||||||
authorize
|
authorize
|
||||||
.or(step)
|
.or(step)
|
||||||
.unify()
|
.unify()
|
||||||
.recover(recover)
|
.recover(recover)
|
||||||
.unify()
|
.unify()
|
||||||
.and(warp::query())
|
.and(warp::query())
|
||||||
.and(warp::any().map(move || clients.clone()))
|
.and(warp::any().map(move || clients_config_2.clone()))
|
||||||
.and(with_templates(templates))
|
.and(with_templates(templates))
|
||||||
.and_then(actually_reply)
|
.and_then(actually_reply)
|
||||||
.boxed()
|
.boxed()
|
||||||
@ -258,7 +259,7 @@ async fn recover(rejection: Rejection) -> Result<ReplyOrBackToClient, Rejection>
|
|||||||
async fn actually_reply(
|
async fn actually_reply(
|
||||||
rep: ReplyOrBackToClient,
|
rep: ReplyOrBackToClient,
|
||||||
q: PartialParams,
|
q: PartialParams,
|
||||||
clients: Vec<OAuth2ClientConfig>,
|
clients: ClientsConfig,
|
||||||
templates: Templates,
|
templates: Templates,
|
||||||
) -> Result<Box<dyn Reply>, Rejection> {
|
) -> Result<Box<dyn Reply>, Rejection> {
|
||||||
let (redirect_uri, response_mode, state, params) = match rep {
|
let (redirect_uri, response_mode, state, params) = match rep {
|
||||||
@ -278,11 +279,8 @@ async fn actually_reply(
|
|||||||
} = q;
|
} = q;
|
||||||
|
|
||||||
// First, disover the client
|
// First, disover the client
|
||||||
let client = client_id.and_then(|client_id| {
|
let client = client_id
|
||||||
clients
|
.and_then(|client_id| clients.iter().find(|client| client.client_id == client_id));
|
||||||
.into_iter()
|
|
||||||
.find(|client| client.client_id == client_id)
|
|
||||||
});
|
|
||||||
|
|
||||||
let client = match client {
|
let client = match client {
|
||||||
Some(client) => client,
|
Some(client) => client,
|
||||||
@ -314,7 +312,7 @@ async fn actually_reply(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get(
|
async fn get(
|
||||||
clients: Vec<OAuth2ClientConfig>,
|
clients: ClientsConfig,
|
||||||
params: Params,
|
params: Params,
|
||||||
maybe_session: Option<BrowserSession<PostgresqlBackend>>,
|
maybe_session: Option<BrowserSession<PostgresqlBackend>>,
|
||||||
mut txn: Transaction<'_, Postgres>,
|
mut txn: Transaction<'_, Postgres>,
|
||||||
@ -337,7 +335,7 @@ async fn get(
|
|||||||
|
|
||||||
// First, find out what client it is
|
// First, find out what client it is
|
||||||
let client = clients
|
let client = clients
|
||||||
.into_iter()
|
.iter()
|
||||||
.find(|client| client.client_id == params.auth.client_id)
|
.find(|client| client.client_id == params.auth.client_id)
|
||||||
.ok_or_else(|| anyhow::anyhow!("could not find client"))
|
.ok_or_else(|| anyhow::anyhow!("could not find client"))
|
||||||
.wrap_error()?;
|
.wrap_error()?;
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use mas_config::{HttpConfig, OAuth2ClientConfig, OAuth2Config};
|
use mas_config::{ClientConfig, ClientsConfig, HttpConfig};
|
||||||
use mas_data_model::TokenType;
|
use mas_data_model::TokenType;
|
||||||
use mas_iana::oauth::{OAuthClientAuthenticationMethod, OAuthTokenTypeHint};
|
use mas_iana::oauth::{OAuthClientAuthenticationMethod, OAuthTokenTypeHint};
|
||||||
use mas_storage::oauth2::{
|
use mas_storage::oauth2::{
|
||||||
@ -29,7 +29,7 @@ use warp::{filters::BoxedFilter, Filter, Rejection, Reply};
|
|||||||
|
|
||||||
pub fn filter(
|
pub fn filter(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
oauth2_config: &OAuth2Config,
|
clients_config: &ClientsConfig,
|
||||||
http_config: &HttpConfig,
|
http_config: &HttpConfig,
|
||||||
) -> BoxedFilter<(Box<dyn Reply>,)> {
|
) -> BoxedFilter<(Box<dyn Reply>,)> {
|
||||||
let audience = UrlBuilder::from(http_config)
|
let audience = UrlBuilder::from(http_config)
|
||||||
@ -40,7 +40,7 @@ pub fn filter(
|
|||||||
.and(
|
.and(
|
||||||
warp::post()
|
warp::post()
|
||||||
.and(connection(pool))
|
.and(connection(pool))
|
||||||
.and(client_authentication(oauth2_config, audience))
|
.and(client_authentication(clients_config, audience))
|
||||||
.and_then(introspect)
|
.and_then(introspect)
|
||||||
.recover(recover)
|
.recover(recover)
|
||||||
.unify(),
|
.unify(),
|
||||||
@ -66,7 +66,7 @@ const INACTIVE: IntrospectionResponse = IntrospectionResponse {
|
|||||||
async fn introspect(
|
async fn introspect(
|
||||||
mut conn: PoolConnection<Postgres>,
|
mut conn: PoolConnection<Postgres>,
|
||||||
auth: OAuthClientAuthenticationMethod,
|
auth: OAuthClientAuthenticationMethod,
|
||||||
client: OAuth2ClientConfig,
|
client: ClientConfig,
|
||||||
params: IntrospectionRequest,
|
params: IntrospectionRequest,
|
||||||
) -> Result<Box<dyn Reply>, Rejection> {
|
) -> Result<Box<dyn Reply>, Rejection> {
|
||||||
// Token introspection is only allowed by confidential clients
|
// Token introspection is only allowed by confidential clients
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use hyper::Method;
|
use hyper::Method;
|
||||||
use mas_config::{CookiesConfig, HttpConfig, OAuth2Config};
|
use mas_config::{ClientsConfig, Encrypter, HttpConfig};
|
||||||
use mas_jose::StaticKeystore;
|
use mas_jose::StaticKeystore;
|
||||||
use mas_templates::Templates;
|
use mas_templates::Templates;
|
||||||
use mas_warp_utils::filters::cors::cors;
|
use mas_warp_utils::filters::cors::cors;
|
||||||
@ -40,16 +40,16 @@ pub fn filter(
|
|||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
templates: &Templates,
|
templates: &Templates,
|
||||||
key_store: &Arc<StaticKeystore>,
|
key_store: &Arc<StaticKeystore>,
|
||||||
oauth2_config: &OAuth2Config,
|
encrypter: &Encrypter,
|
||||||
|
clients_config: &ClientsConfig,
|
||||||
http_config: &HttpConfig,
|
http_config: &HttpConfig,
|
||||||
cookies_config: &CookiesConfig,
|
|
||||||
) -> BoxedFilter<(impl Reply,)> {
|
) -> BoxedFilter<(impl Reply,)> {
|
||||||
let discovery = discovery(key_store.as_ref(), http_config);
|
let discovery = discovery(key_store.as_ref(), http_config);
|
||||||
let keys = keys(key_store);
|
let keys = keys(key_store);
|
||||||
let authorization = authorization(pool, templates, oauth2_config, cookies_config);
|
let authorization = authorization(pool, templates, encrypter, clients_config);
|
||||||
let userinfo = userinfo(pool, oauth2_config);
|
let userinfo = userinfo(pool);
|
||||||
let introspection = introspection(pool, oauth2_config, http_config);
|
let introspection = introspection(pool, clients_config, http_config);
|
||||||
let token = token(pool, key_store, oauth2_config, http_config);
|
let token = token(pool, key_store, clients_config, http_config);
|
||||||
|
|
||||||
let filter = discovery
|
let filter = discovery
|
||||||
.or(keys)
|
.or(keys)
|
||||||
|
@ -19,7 +19,7 @@ use chrono::{DateTime, Duration, Utc};
|
|||||||
use data_encoding::BASE64URL_NOPAD;
|
use data_encoding::BASE64URL_NOPAD;
|
||||||
use headers::{CacheControl, Pragma};
|
use headers::{CacheControl, Pragma};
|
||||||
use hyper::StatusCode;
|
use hyper::StatusCode;
|
||||||
use mas_config::{HttpConfig, OAuth2ClientConfig, OAuth2Config};
|
use mas_config::{ClientConfig, ClientsConfig, HttpConfig};
|
||||||
use mas_data_model::{AuthorizationGrantStage, TokenType};
|
use mas_data_model::{AuthorizationGrantStage, TokenType};
|
||||||
use mas_iana::{jose::JsonWebSignatureAlg, oauth::OAuthClientAuthenticationMethod};
|
use mas_iana::{jose::JsonWebSignatureAlg, oauth::OAuthClientAuthenticationMethod};
|
||||||
use mas_jose::{
|
use mas_jose::{
|
||||||
@ -98,7 +98,7 @@ where
|
|||||||
pub fn filter(
|
pub fn filter(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
key_store: &Arc<StaticKeystore>,
|
key_store: &Arc<StaticKeystore>,
|
||||||
oauth2_config: &OAuth2Config,
|
clients_config: &ClientsConfig,
|
||||||
http_config: &HttpConfig,
|
http_config: &HttpConfig,
|
||||||
) -> BoxedFilter<(Box<dyn Reply>,)> {
|
) -> BoxedFilter<(Box<dyn Reply>,)> {
|
||||||
let key_store = key_store.clone();
|
let key_store = key_store.clone();
|
||||||
@ -110,7 +110,7 @@ pub fn filter(
|
|||||||
warp::path!("oauth2" / "token")
|
warp::path!("oauth2" / "token")
|
||||||
.and(
|
.and(
|
||||||
warp::post()
|
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 || key_store.clone()))
|
||||||
.and(warp::any().map(move || issuer.clone()))
|
.and(warp::any().map(move || issuer.clone()))
|
||||||
.and(connection(pool))
|
.and(connection(pool))
|
||||||
@ -131,7 +131,7 @@ async fn recover(rejection: Rejection) -> Result<Box<dyn Reply>, Rejection> {
|
|||||||
|
|
||||||
async fn token(
|
async fn token(
|
||||||
_auth: OAuthClientAuthenticationMethod,
|
_auth: OAuthClientAuthenticationMethod,
|
||||||
client: OAuth2ClientConfig,
|
client: ClientConfig,
|
||||||
req: AccessTokenRequest,
|
req: AccessTokenRequest,
|
||||||
key_store: Arc<StaticKeystore>,
|
key_store: Arc<StaticKeystore>,
|
||||||
issuer: Url,
|
issuer: Url,
|
||||||
@ -171,7 +171,7 @@ fn hash<H: Digest>(mut hasher: H, token: &str) -> anyhow::Result<String> {
|
|||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
async fn authorization_code_grant(
|
async fn authorization_code_grant(
|
||||||
grant: &AuthorizationCodeGrant,
|
grant: &AuthorizationCodeGrant,
|
||||||
client: &OAuth2ClientConfig,
|
client: &ClientConfig,
|
||||||
key_store: &StaticKeystore,
|
key_store: &StaticKeystore,
|
||||||
issuer: Url,
|
issuer: Url,
|
||||||
conn: &mut PoolConnection<Postgres>,
|
conn: &mut PoolConnection<Postgres>,
|
||||||
@ -328,7 +328,7 @@ async fn authorization_code_grant(
|
|||||||
|
|
||||||
async fn refresh_token_grant(
|
async fn refresh_token_grant(
|
||||||
grant: &RefreshTokenGrant,
|
grant: &RefreshTokenGrant,
|
||||||
client: &OAuth2ClientConfig,
|
client: &ClientConfig,
|
||||||
conn: &mut PoolConnection<Postgres>,
|
conn: &mut PoolConnection<Postgres>,
|
||||||
) -> Result<AccessTokenResponse, Rejection> {
|
) -> Result<AccessTokenResponse, Rejection> {
|
||||||
let mut txn = conn.begin().await.wrap_error()?;
|
let mut txn = conn.begin().await.wrap_error()?;
|
||||||
|
@ -12,7 +12,6 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use mas_config::OAuth2Config;
|
|
||||||
use mas_data_model::{AccessToken, Session};
|
use mas_data_model::{AccessToken, Session};
|
||||||
use mas_storage::PostgresqlBackend;
|
use mas_storage::PostgresqlBackend;
|
||||||
use mas_warp_utils::filters::authenticate::{authentication, recover_unauthorized};
|
use mas_warp_utils::filters::authenticate::{authentication, recover_unauthorized};
|
||||||
@ -26,7 +25,7 @@ struct UserInfo {
|
|||||||
username: String,
|
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")
|
warp::path!("oauth2" / "userinfo")
|
||||||
.and(
|
.and(
|
||||||
warp::get()
|
warp::get()
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use lettre::{message::Mailbox, Address};
|
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_data_model::{BrowserSession, User, UserEmail};
|
||||||
use mas_email::Mailer;
|
use mas_email::Mailer;
|
||||||
use mas_storage::{
|
use mas_storage::{
|
||||||
@ -45,27 +45,27 @@ pub(super) fn filter(
|
|||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
templates: &Templates,
|
templates: &Templates,
|
||||||
mailer: &Mailer,
|
mailer: &Mailer,
|
||||||
|
encrypter: &Encrypter,
|
||||||
http_config: &HttpConfig,
|
http_config: &HttpConfig,
|
||||||
csrf_config: &CsrfConfig,
|
csrf_config: &CsrfConfig,
|
||||||
cookies_config: &CookiesConfig,
|
|
||||||
) -> BoxedFilter<(Box<dyn Reply>,)> {
|
) -> BoxedFilter<(Box<dyn Reply>,)> {
|
||||||
let mailer = mailer.clone();
|
let mailer = mailer.clone();
|
||||||
|
|
||||||
let get = with_templates(templates)
|
let get = with_templates(templates)
|
||||||
.and(encrypted_cookie_saver(cookies_config))
|
.and(encrypted_cookie_saver(encrypter))
|
||||||
.and(updated_csrf_token(cookies_config, csrf_config))
|
.and(updated_csrf_token(encrypter, csrf_config))
|
||||||
.and(session(pool, cookies_config))
|
.and(session(pool, encrypter))
|
||||||
.and(connection(pool))
|
.and(connection(pool))
|
||||||
.and_then(get);
|
.and_then(get);
|
||||||
|
|
||||||
let post = with_templates(templates)
|
let post = with_templates(templates)
|
||||||
.and(warp::any().map(move || mailer.clone()))
|
.and(warp::any().map(move || mailer.clone()))
|
||||||
.and(url_builder(http_config))
|
.and(url_builder(http_config))
|
||||||
.and(encrypted_cookie_saver(cookies_config))
|
.and(encrypted_cookie_saver(encrypter))
|
||||||
.and(updated_csrf_token(cookies_config, csrf_config))
|
.and(updated_csrf_token(encrypter, csrf_config))
|
||||||
.and(session(pool, cookies_config))
|
.and(session(pool, encrypter))
|
||||||
.and(transaction(pool))
|
.and(transaction(pool))
|
||||||
.and(protected_form(cookies_config))
|
.and(protected_form(encrypter))
|
||||||
.and_then(post);
|
.and_then(post);
|
||||||
|
|
||||||
let get = warp::get().and(get);
|
let get = warp::get().and(get);
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
mod emails;
|
mod emails;
|
||||||
mod password;
|
mod password;
|
||||||
|
|
||||||
use mas_config::{CookiesConfig, CsrfConfig, HttpConfig};
|
use mas_config::{CsrfConfig, Encrypter, HttpConfig};
|
||||||
use mas_data_model::BrowserSession;
|
use mas_data_model::BrowserSession;
|
||||||
use mas_email::Mailer;
|
use mas_email::Mailer;
|
||||||
use mas_storage::{
|
use mas_storage::{
|
||||||
@ -42,28 +42,21 @@ pub(super) fn filter(
|
|||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
templates: &Templates,
|
templates: &Templates,
|
||||||
mailer: &Mailer,
|
mailer: &Mailer,
|
||||||
|
encrypter: &Encrypter,
|
||||||
http_config: &HttpConfig,
|
http_config: &HttpConfig,
|
||||||
csrf_config: &CsrfConfig,
|
csrf_config: &CsrfConfig,
|
||||||
cookies_config: &CookiesConfig,
|
|
||||||
) -> BoxedFilter<(Box<dyn Reply>,)> {
|
) -> BoxedFilter<(Box<dyn Reply>,)> {
|
||||||
let get = warp::get()
|
let get = warp::get()
|
||||||
.and(with_templates(templates))
|
.and(with_templates(templates))
|
||||||
.and(encrypted_cookie_saver(cookies_config))
|
.and(encrypted_cookie_saver(encrypter))
|
||||||
.and(updated_csrf_token(cookies_config, csrf_config))
|
.and(updated_csrf_token(encrypter, csrf_config))
|
||||||
.and(session(pool, cookies_config))
|
.and(session(pool, encrypter))
|
||||||
.and(connection(pool))
|
.and(connection(pool))
|
||||||
.and_then(get);
|
.and_then(get);
|
||||||
|
|
||||||
let index = warp::path::end().and(get);
|
let index = warp::path::end().and(get);
|
||||||
let password = password(pool, templates, csrf_config, cookies_config);
|
let password = password(pool, templates, encrypter, csrf_config);
|
||||||
let emails = emails(
|
let emails = emails(pool, templates, mailer, encrypter, http_config, csrf_config);
|
||||||
pool,
|
|
||||||
templates,
|
|
||||||
mailer,
|
|
||||||
http_config,
|
|
||||||
csrf_config,
|
|
||||||
cookies_config,
|
|
||||||
);
|
|
||||||
|
|
||||||
let filter = index.or(password).unify().or(emails).unify();
|
let filter = index.or(password).unify().or(emails).unify();
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use argon2::Argon2;
|
use argon2::Argon2;
|
||||||
use mas_config::{CookiesConfig, CsrfConfig};
|
use mas_config::{CsrfConfig, Encrypter};
|
||||||
use mas_data_model::BrowserSession;
|
use mas_data_model::BrowserSession;
|
||||||
use mas_storage::{
|
use mas_storage::{
|
||||||
user::{authenticate_session, set_password},
|
user::{authenticate_session, set_password},
|
||||||
@ -37,21 +37,21 @@ use warp::{filters::BoxedFilter, reply::html, Filter, Rejection, Reply};
|
|||||||
pub(super) fn filter(
|
pub(super) fn filter(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
templates: &Templates,
|
templates: &Templates,
|
||||||
|
encrypter: &Encrypter,
|
||||||
csrf_config: &CsrfConfig,
|
csrf_config: &CsrfConfig,
|
||||||
cookies_config: &CookiesConfig,
|
|
||||||
) -> BoxedFilter<(Box<dyn Reply>,)> {
|
) -> BoxedFilter<(Box<dyn Reply>,)> {
|
||||||
let get = with_templates(templates)
|
let get = with_templates(templates)
|
||||||
.and(encrypted_cookie_saver(cookies_config))
|
.and(encrypted_cookie_saver(encrypter))
|
||||||
.and(updated_csrf_token(cookies_config, csrf_config))
|
.and(updated_csrf_token(encrypter, csrf_config))
|
||||||
.and(session(pool, cookies_config))
|
.and(session(pool, encrypter))
|
||||||
.and_then(get);
|
.and_then(get);
|
||||||
|
|
||||||
let post = with_templates(templates)
|
let post = with_templates(templates)
|
||||||
.and(encrypted_cookie_saver(cookies_config))
|
.and(encrypted_cookie_saver(encrypter))
|
||||||
.and(updated_csrf_token(cookies_config, csrf_config))
|
.and(updated_csrf_token(encrypter, csrf_config))
|
||||||
.and(session(pool, cookies_config))
|
.and(session(pool, encrypter))
|
||||||
.and(transaction(pool))
|
.and(transaction(pool))
|
||||||
.and(protected_form(cookies_config))
|
.and(protected_form(encrypter))
|
||||||
.and_then(post);
|
.and_then(post);
|
||||||
|
|
||||||
let get = warp::get().and(get);
|
let get = warp::get().and(get);
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use mas_config::{CookiesConfig, CsrfConfig, HttpConfig};
|
use mas_config::{CsrfConfig, Encrypter, HttpConfig};
|
||||||
use mas_data_model::BrowserSession;
|
use mas_data_model::BrowserSession;
|
||||||
use mas_storage::PostgresqlBackend;
|
use mas_storage::PostgresqlBackend;
|
||||||
use mas_templates::{IndexContext, TemplateContext, Templates};
|
use mas_templates::{IndexContext, TemplateContext, Templates};
|
||||||
@ -29,17 +29,17 @@ use warp::{filters::BoxedFilter, reply::html, Filter, Rejection, Reply};
|
|||||||
pub(super) fn filter(
|
pub(super) fn filter(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
templates: &Templates,
|
templates: &Templates,
|
||||||
|
encrypter: &Encrypter,
|
||||||
http_config: &HttpConfig,
|
http_config: &HttpConfig,
|
||||||
csrf_config: &CsrfConfig,
|
csrf_config: &CsrfConfig,
|
||||||
cookies_config: &CookiesConfig,
|
|
||||||
) -> BoxedFilter<(Box<dyn Reply>,)> {
|
) -> BoxedFilter<(Box<dyn Reply>,)> {
|
||||||
warp::path::end()
|
warp::path::end()
|
||||||
.and(warp::get())
|
.and(warp::get())
|
||||||
.and(url_builder(http_config))
|
.and(url_builder(http_config))
|
||||||
.and(with_templates(templates))
|
.and(with_templates(templates))
|
||||||
.and(encrypted_cookie_saver(cookies_config))
|
.and(encrypted_cookie_saver(encrypter))
|
||||||
.and(updated_csrf_token(cookies_config, csrf_config))
|
.and(updated_csrf_token(encrypter, csrf_config))
|
||||||
.and(optional_session(pool, cookies_config))
|
.and(optional_session(pool, encrypter))
|
||||||
.and_then(get)
|
.and_then(get)
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
#![allow(clippy::trait_duplication_in_bounds)]
|
#![allow(clippy::trait_duplication_in_bounds)]
|
||||||
|
|
||||||
use hyper::http::uri::{Parts, PathAndQuery, Uri};
|
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_data_model::{errors::WrapFormError, BrowserSession};
|
||||||
use mas_storage::{user::login, PostgresqlBackend};
|
use mas_storage::{user::login, PostgresqlBackend};
|
||||||
use mas_templates::{LoginContext, LoginFormField, TemplateContext, Templates};
|
use mas_templates::{LoginContext, LoginFormField, TemplateContext, Templates};
|
||||||
@ -86,24 +86,24 @@ struct LoginForm {
|
|||||||
pub(super) fn filter(
|
pub(super) fn filter(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
templates: &Templates,
|
templates: &Templates,
|
||||||
|
encrypter: &Encrypter,
|
||||||
csrf_config: &CsrfConfig,
|
csrf_config: &CsrfConfig,
|
||||||
cookies_config: &CookiesConfig,
|
|
||||||
) -> BoxedFilter<(Box<dyn Reply>,)> {
|
) -> BoxedFilter<(Box<dyn Reply>,)> {
|
||||||
let get = warp::get()
|
let get = warp::get()
|
||||||
.and(with_templates(templates))
|
.and(with_templates(templates))
|
||||||
.and(connection(pool))
|
.and(connection(pool))
|
||||||
.and(encrypted_cookie_saver(cookies_config))
|
.and(encrypted_cookie_saver(encrypter))
|
||||||
.and(updated_csrf_token(cookies_config, csrf_config))
|
.and(updated_csrf_token(encrypter, csrf_config))
|
||||||
.and(warp::query())
|
.and(warp::query())
|
||||||
.and(optional_session(pool, cookies_config))
|
.and(optional_session(pool, encrypter))
|
||||||
.and_then(get);
|
.and_then(get);
|
||||||
|
|
||||||
let post = warp::post()
|
let post = warp::post()
|
||||||
.and(with_templates(templates))
|
.and(with_templates(templates))
|
||||||
.and(connection(pool))
|
.and(connection(pool))
|
||||||
.and(encrypted_cookie_saver(cookies_config))
|
.and(encrypted_cookie_saver(encrypter))
|
||||||
.and(updated_csrf_token(cookies_config, csrf_config))
|
.and(updated_csrf_token(encrypter, csrf_config))
|
||||||
.and(protected_form(cookies_config))
|
.and(protected_form(encrypter))
|
||||||
.and(warp::query())
|
.and(warp::query())
|
||||||
.and_then(post);
|
.and_then(post);
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use mas_config::CookiesConfig;
|
use mas_config::Encrypter;
|
||||||
use mas_data_model::BrowserSession;
|
use mas_data_model::BrowserSession;
|
||||||
use mas_storage::{user::end_session, PostgresqlBackend};
|
use mas_storage::{user::end_session, PostgresqlBackend};
|
||||||
use mas_warp_utils::{
|
use mas_warp_utils::{
|
||||||
@ -22,15 +22,12 @@ use mas_warp_utils::{
|
|||||||
use sqlx::{PgPool, Postgres, Transaction};
|
use sqlx::{PgPool, Postgres, Transaction};
|
||||||
use warp::{filters::BoxedFilter, hyper::Uri, Filter, Rejection, Reply};
|
use warp::{filters::BoxedFilter, hyper::Uri, Filter, Rejection, Reply};
|
||||||
|
|
||||||
pub(super) fn filter(
|
pub(super) fn filter(pool: &PgPool, encrypter: &Encrypter) -> BoxedFilter<(Box<dyn Reply>,)> {
|
||||||
pool: &PgPool,
|
|
||||||
cookies_config: &CookiesConfig,
|
|
||||||
) -> BoxedFilter<(Box<dyn Reply>,)> {
|
|
||||||
warp::path!("logout")
|
warp::path!("logout")
|
||||||
.and(warp::post())
|
.and(warp::post())
|
||||||
.and(session(pool, cookies_config))
|
.and(session(pool, encrypter))
|
||||||
.and(transaction(pool))
|
.and(transaction(pool))
|
||||||
.and(protected_form(cookies_config))
|
.and(protected_form(encrypter))
|
||||||
.and_then(post)
|
.and_then(post)
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use mas_config::{CookiesConfig, CsrfConfig, HttpConfig};
|
use mas_config::{CsrfConfig, Encrypter, HttpConfig};
|
||||||
use mas_email::Mailer;
|
use mas_email::Mailer;
|
||||||
use mas_templates::Templates;
|
use mas_templates::Templates;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
@ -40,24 +40,17 @@ pub(super) fn filter(
|
|||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
templates: &Templates,
|
templates: &Templates,
|
||||||
mailer: &Mailer,
|
mailer: &Mailer,
|
||||||
|
encrypter: &Encrypter,
|
||||||
http_config: &HttpConfig,
|
http_config: &HttpConfig,
|
||||||
csrf_config: &CsrfConfig,
|
csrf_config: &CsrfConfig,
|
||||||
cookies_config: &CookiesConfig,
|
|
||||||
) -> BoxedFilter<(Box<dyn Reply>,)> {
|
) -> BoxedFilter<(Box<dyn Reply>,)> {
|
||||||
let index = index(pool, templates, http_config, csrf_config, cookies_config);
|
let index = index(pool, templates, encrypter, http_config, csrf_config);
|
||||||
let account = account(
|
let account = account(pool, templates, mailer, encrypter, http_config, csrf_config);
|
||||||
pool,
|
let login = login(pool, templates, encrypter, csrf_config);
|
||||||
templates,
|
let register = register(pool, templates, encrypter, csrf_config);
|
||||||
mailer,
|
let logout = logout(pool, encrypter);
|
||||||
http_config,
|
let reauth = reauth(pool, templates, encrypter, csrf_config);
|
||||||
csrf_config,
|
let verify = verify(pool, templates, encrypter, 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);
|
|
||||||
|
|
||||||
index
|
index
|
||||||
.or(account)
|
.or(account)
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use hyper::http::uri::{Parts, PathAndQuery};
|
use hyper::http::uri::{Parts, PathAndQuery};
|
||||||
use mas_config::{CookiesConfig, CsrfConfig};
|
use mas_config::{CsrfConfig, Encrypter};
|
||||||
use mas_data_model::BrowserSession;
|
use mas_data_model::BrowserSession;
|
||||||
use mas_storage::{user::authenticate_session, PostgresqlBackend};
|
use mas_storage::{user::authenticate_session, PostgresqlBackend};
|
||||||
use mas_templates::{ReauthContext, TemplateContext, Templates};
|
use mas_templates::{ReauthContext, TemplateContext, Templates};
|
||||||
@ -83,22 +83,22 @@ struct ReauthForm {
|
|||||||
pub(super) fn filter(
|
pub(super) fn filter(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
templates: &Templates,
|
templates: &Templates,
|
||||||
|
encrypter: &Encrypter,
|
||||||
csrf_config: &CsrfConfig,
|
csrf_config: &CsrfConfig,
|
||||||
cookies_config: &CookiesConfig,
|
|
||||||
) -> BoxedFilter<(Box<dyn Reply>,)> {
|
) -> BoxedFilter<(Box<dyn Reply>,)> {
|
||||||
let get = warp::get()
|
let get = warp::get()
|
||||||
.and(with_templates(templates))
|
.and(with_templates(templates))
|
||||||
.and(connection(pool))
|
.and(connection(pool))
|
||||||
.and(encrypted_cookie_saver(cookies_config))
|
.and(encrypted_cookie_saver(encrypter))
|
||||||
.and(updated_csrf_token(cookies_config, csrf_config))
|
.and(updated_csrf_token(encrypter, csrf_config))
|
||||||
.and(session(pool, cookies_config))
|
.and(session(pool, encrypter))
|
||||||
.and(warp::query())
|
.and(warp::query())
|
||||||
.and_then(get);
|
.and_then(get);
|
||||||
|
|
||||||
let post = warp::post()
|
let post = warp::post()
|
||||||
.and(session(pool, cookies_config))
|
.and(session(pool, encrypter))
|
||||||
.and(transaction(pool))
|
.and(transaction(pool))
|
||||||
.and(protected_form(cookies_config))
|
.and(protected_form(encrypter))
|
||||||
.and(warp::query())
|
.and(warp::query())
|
||||||
.and_then(post);
|
.and_then(post);
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
use argon2::Argon2;
|
use argon2::Argon2;
|
||||||
use hyper::http::uri::{Parts, PathAndQuery, Uri};
|
use hyper::http::uri::{Parts, PathAndQuery, Uri};
|
||||||
use mas_config::{CookiesConfig, CsrfConfig};
|
use mas_config::{CsrfConfig, Encrypter};
|
||||||
use mas_data_model::BrowserSession;
|
use mas_data_model::BrowserSession;
|
||||||
use mas_storage::{
|
use mas_storage::{
|
||||||
user::{register_user, start_session},
|
user::{register_user, start_session},
|
||||||
@ -92,22 +92,22 @@ struct RegisterForm {
|
|||||||
pub(super) fn filter(
|
pub(super) fn filter(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
templates: &Templates,
|
templates: &Templates,
|
||||||
|
encrypter: &Encrypter,
|
||||||
csrf_config: &CsrfConfig,
|
csrf_config: &CsrfConfig,
|
||||||
cookies_config: &CookiesConfig,
|
|
||||||
) -> BoxedFilter<(Box<dyn Reply>,)> {
|
) -> BoxedFilter<(Box<dyn Reply>,)> {
|
||||||
let get = warp::get()
|
let get = warp::get()
|
||||||
.and(with_templates(templates))
|
.and(with_templates(templates))
|
||||||
.and(connection(pool))
|
.and(connection(pool))
|
||||||
.and(encrypted_cookie_saver(cookies_config))
|
.and(encrypted_cookie_saver(encrypter))
|
||||||
.and(updated_csrf_token(cookies_config, csrf_config))
|
.and(updated_csrf_token(encrypter, csrf_config))
|
||||||
.and(warp::query())
|
.and(warp::query())
|
||||||
.and(optional_session(pool, cookies_config))
|
.and(optional_session(pool, encrypter))
|
||||||
.and_then(get);
|
.and_then(get);
|
||||||
|
|
||||||
let post = warp::post()
|
let post = warp::post()
|
||||||
.and(transaction(pool))
|
.and(transaction(pool))
|
||||||
.and(encrypted_cookie_saver(cookies_config))
|
.and(encrypted_cookie_saver(encrypter))
|
||||||
.and(protected_form(cookies_config))
|
.and(protected_form(encrypter))
|
||||||
.and(warp::query())
|
.and(warp::query())
|
||||||
.and_then(post);
|
.and_then(post);
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
use mas_config::{CookiesConfig, CsrfConfig};
|
use mas_config::{CsrfConfig, Encrypter};
|
||||||
use mas_data_model::BrowserSession;
|
use mas_data_model::BrowserSession;
|
||||||
use mas_storage::{
|
use mas_storage::{
|
||||||
user::{
|
user::{
|
||||||
@ -39,15 +39,15 @@ use warp::{filters::BoxedFilter, reply::html, Filter, Rejection, Reply};
|
|||||||
pub(super) fn filter(
|
pub(super) fn filter(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
templates: &Templates,
|
templates: &Templates,
|
||||||
|
encrypter: &Encrypter,
|
||||||
csrf_config: &CsrfConfig,
|
csrf_config: &CsrfConfig,
|
||||||
cookies_config: &CookiesConfig,
|
|
||||||
) -> BoxedFilter<(Box<dyn Reply>,)> {
|
) -> BoxedFilter<(Box<dyn Reply>,)> {
|
||||||
warp::path!("verify" / String)
|
warp::path!("verify" / String)
|
||||||
.and(warp::get())
|
.and(warp::get())
|
||||||
.and(with_templates(templates))
|
.and(with_templates(templates))
|
||||||
.and(encrypted_cookie_saver(cookies_config))
|
.and(encrypted_cookie_saver(encrypter))
|
||||||
.and(updated_csrf_token(cookies_config, csrf_config))
|
.and(updated_csrf_token(encrypter, csrf_config))
|
||||||
.and(optional_session(pool, cookies_config))
|
.and(optional_session(pool, encrypter))
|
||||||
.and(transaction(pool))
|
.and(transaction(pool))
|
||||||
.and_then(get)
|
.and_then(get)
|
||||||
.boxed()
|
.boxed()
|
||||||
|
@ -20,7 +20,6 @@ serde_with = { version = "1.11.0", features = ["hex", "chrono"] }
|
|||||||
serde_json = "1.0.78"
|
serde_json = "1.0.78"
|
||||||
serde_urlencoded = "0.7.1"
|
serde_urlencoded = "0.7.1"
|
||||||
data-encoding = "2.3.2"
|
data-encoding = "2.3.2"
|
||||||
chacha20poly1305 = { version = "0.9.0", features = ["std"] }
|
|
||||||
once_cell = "1.9.0"
|
once_cell = "1.9.0"
|
||||||
tracing = "0.1.29"
|
tracing = "0.1.29"
|
||||||
opentelemetry = "0.16.0"
|
opentelemetry = "0.16.0"
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use headers::{authorization::Basic, Authorization};
|
use headers::{authorization::Basic, Authorization};
|
||||||
use mas_config::{OAuth2ClientAuthMethodConfig, OAuth2ClientConfig, OAuth2Config};
|
use mas_config::{ClientAuthMethodConfig, ClientConfig, ClientsConfig};
|
||||||
use mas_iana::oauth::OAuthClientAuthenticationMethod;
|
use mas_iana::oauth::OAuthClientAuthenticationMethod;
|
||||||
use mas_jose::{
|
use mas_jose::{
|
||||||
claims::{TimeOptions, AUD, EXP, IAT, ISS, JTI, NBF, SUB},
|
claims::{TimeOptions, AUD, EXP, IAT, ISS, JTI, NBF, SUB},
|
||||||
@ -33,9 +33,9 @@ use crate::errors::WrapError;
|
|||||||
/// Protect an enpoint with client authentication
|
/// Protect an enpoint with client authentication
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn client_authentication<T: DeserializeOwned + Send + 'static>(
|
pub fn client_authentication<T: DeserializeOwned + Send + 'static>(
|
||||||
oauth2_config: &OAuth2Config,
|
clients_config: &ClientsConfig,
|
||||||
audience: String,
|
audience: String,
|
||||||
) -> impl Filter<Extract = (OAuthClientAuthenticationMethod, OAuth2ClientConfig, T), Error = Rejection>
|
) -> impl Filter<Extract = (OAuthClientAuthenticationMethod, ClientConfig, T), Error = Rejection>
|
||||||
+ Clone
|
+ Clone
|
||||||
+ Send
|
+ Send
|
||||||
+ Sync
|
+ Sync
|
||||||
@ -65,9 +65,9 @@ pub fn client_authentication<T: DeserializeOwned + Send + 'static>(
|
|||||||
.unify()
|
.unify()
|
||||||
.untuple_one();
|
.untuple_one();
|
||||||
|
|
||||||
let clients = oauth2_config.clients.clone();
|
let clients_config = clients_config.clone();
|
||||||
warp::any()
|
warp::any()
|
||||||
.map(move || clients.clone())
|
.map(move || clients_config.clone())
|
||||||
.and(warp::any().map(move || audience.clone()))
|
.and(warp::any().map(move || audience.clone()))
|
||||||
.and(credentials)
|
.and(credentials)
|
||||||
.and_then(authenticate_client)
|
.and_then(authenticate_client)
|
||||||
@ -95,18 +95,18 @@ enum ClientAuthenticationError {
|
|||||||
impl Reject for ClientAuthenticationError {}
|
impl Reject for ClientAuthenticationError {}
|
||||||
|
|
||||||
async fn authenticate_client<T>(
|
async fn authenticate_client<T>(
|
||||||
clients: Vec<OAuth2ClientConfig>,
|
clients_config: ClientsConfig,
|
||||||
audience: String,
|
audience: String,
|
||||||
credentials: ClientCredentials,
|
credentials: ClientCredentials,
|
||||||
body: T,
|
body: T,
|
||||||
) -> Result<(OAuthClientAuthenticationMethod, OAuth2ClientConfig, T), Rejection> {
|
) -> Result<(OAuthClientAuthenticationMethod, ClientConfig, T), Rejection> {
|
||||||
let (auth_method, client) = match credentials {
|
let (auth_method, client) = match credentials {
|
||||||
ClientCredentials::Pair {
|
ClientCredentials::Pair {
|
||||||
client_id,
|
client_id,
|
||||||
client_secret,
|
client_secret,
|
||||||
via,
|
via,
|
||||||
} => {
|
} => {
|
||||||
let client = clients
|
let client = clients_config
|
||||||
.iter()
|
.iter()
|
||||||
.find(|client| client.client_id == client_id)
|
.find(|client| client.client_id == client_id)
|
||||||
.ok_or_else(|| ClientAuthenticationError::ClientNotFound {
|
.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) {
|
let auth_method = match (&client.client_auth_method, client_secret, via) {
|
||||||
(OAuth2ClientAuthMethodConfig::None, None, _) => {
|
(ClientAuthMethodConfig::None, None, _) => OAuthClientAuthenticationMethod::None,
|
||||||
OAuthClientAuthenticationMethod::None
|
|
||||||
}
|
|
||||||
|
|
||||||
(
|
(
|
||||||
OAuth2ClientAuthMethodConfig::ClientSecretBasic {
|
ClientAuthMethodConfig::ClientSecretBasic {
|
||||||
client_secret: ref expected_client_secret,
|
client_secret: ref expected_client_secret,
|
||||||
},
|
},
|
||||||
Some(ref given_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,
|
client_secret: ref expected_client_secret,
|
||||||
},
|
},
|
||||||
Some(ref given_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
|
// from the token, as per rfc7521 sec. 4.2
|
||||||
let client_id = client_id.as_ref().unwrap_or(&sub);
|
let client_id = client_id.as_ref().unwrap_or(&sub);
|
||||||
|
|
||||||
let client = clients
|
let client = clients_config
|
||||||
.iter()
|
.iter()
|
||||||
.find(|client| &client.client_id == client_id)
|
.find(|client| &client.client_id == client_id)
|
||||||
.ok_or_else(|| ClientAuthenticationError::ClientNotFound {
|
.ok_or_else(|| ClientAuthenticationError::ClientNotFound {
|
||||||
@ -203,13 +201,13 @@ async fn authenticate_client<T>(
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
let auth_method = match &client.client_auth_method {
|
let auth_method = match &client.client_auth_method {
|
||||||
OAuth2ClientAuthMethodConfig::PrivateKeyJwt(jwks) => {
|
ClientAuthMethodConfig::PrivateKeyJwt(jwks) => {
|
||||||
let store = jwks.key_store();
|
let store = jwks.key_store();
|
||||||
token.verify(&decoded, &store).await.wrap_error()?;
|
token.verify(&decoded, &store).await.wrap_error()?;
|
||||||
OAuthClientAuthenticationMethod::PrivateKeyJwt
|
OAuthClientAuthenticationMethod::PrivateKeyJwt
|
||||||
}
|
}
|
||||||
|
|
||||||
OAuth2ClientAuthMethodConfig::ClientSecretJwt { client_secret } => {
|
ClientAuthMethodConfig::ClientSecretJwt { client_secret } => {
|
||||||
let store = SharedSecret::new(client_secret);
|
let store = SharedSecret::new(client_secret);
|
||||||
token.verify(&decoded, &store).await.wrap_error()?;
|
token.verify(&decoded, &store).await.wrap_error()?;
|
||||||
OAuthClientAuthenticationMethod::ClientSecretJwt
|
OAuthClientAuthenticationMethod::ClientSecretJwt
|
||||||
@ -291,7 +289,7 @@ struct ClientAuthForm<T> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use headers::authorization::Credentials;
|
use headers::authorization::Credentials;
|
||||||
use mas_config::{ConfigurationSection, OAuth2ClientAuthMethodConfig};
|
use mas_config::{ClientAuthMethodConfig, ConfigurationSection};
|
||||||
use mas_jose::{ExportJwks, SigningKeystore, StaticKeystore};
|
use mas_jose::{ExportJwks, SigningKeystore, StaticKeystore};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
@ -307,37 +305,37 @@ mod tests {
|
|||||||
store
|
store
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn oauth2_config() -> OAuth2Config {
|
async fn oauth2_config() -> ClientsConfig {
|
||||||
let mut config = OAuth2Config::test();
|
let mut config = ClientsConfig::test();
|
||||||
config.clients.push(OAuth2ClientConfig {
|
config.push(ClientConfig {
|
||||||
client_id: "public".to_string(),
|
client_id: "public".to_string(),
|
||||||
client_auth_method: OAuth2ClientAuthMethodConfig::None,
|
client_auth_method: ClientAuthMethodConfig::None,
|
||||||
redirect_uris: Vec::new(),
|
redirect_uris: Vec::new(),
|
||||||
});
|
});
|
||||||
config.clients.push(OAuth2ClientConfig {
|
config.push(ClientConfig {
|
||||||
client_id: "secret-basic".to_string(),
|
client_id: "secret-basic".to_string(),
|
||||||
client_auth_method: OAuth2ClientAuthMethodConfig::ClientSecretBasic {
|
client_auth_method: ClientAuthMethodConfig::ClientSecretBasic {
|
||||||
client_secret: CLIENT_SECRET.to_string(),
|
client_secret: CLIENT_SECRET.to_string(),
|
||||||
},
|
},
|
||||||
redirect_uris: Vec::new(),
|
redirect_uris: Vec::new(),
|
||||||
});
|
});
|
||||||
config.clients.push(OAuth2ClientConfig {
|
config.push(ClientConfig {
|
||||||
client_id: "secret-post".to_string(),
|
client_id: "secret-post".to_string(),
|
||||||
client_auth_method: OAuth2ClientAuthMethodConfig::ClientSecretPost {
|
client_auth_method: ClientAuthMethodConfig::ClientSecretPost {
|
||||||
client_secret: CLIENT_SECRET.to_string(),
|
client_secret: CLIENT_SECRET.to_string(),
|
||||||
},
|
},
|
||||||
redirect_uris: Vec::new(),
|
redirect_uris: Vec::new(),
|
||||||
});
|
});
|
||||||
config.clients.push(OAuth2ClientConfig {
|
config.push(ClientConfig {
|
||||||
client_id: "secret-jwt".to_string(),
|
client_id: "secret-jwt".to_string(),
|
||||||
client_auth_method: OAuth2ClientAuthMethodConfig::ClientSecretJwt {
|
client_auth_method: ClientAuthMethodConfig::ClientSecretJwt {
|
||||||
client_secret: CLIENT_SECRET.to_string(),
|
client_secret: CLIENT_SECRET.to_string(),
|
||||||
},
|
},
|
||||||
redirect_uris: Vec::new(),
|
redirect_uris: Vec::new(),
|
||||||
});
|
});
|
||||||
config.clients.push(OAuth2ClientConfig {
|
config.push(ClientConfig {
|
||||||
client_id: "secret-jwt-2".to_string(),
|
client_id: "secret-jwt-2".to_string(),
|
||||||
client_auth_method: OAuth2ClientAuthMethodConfig::ClientSecretJwt {
|
client_auth_method: ClientAuthMethodConfig::ClientSecretJwt {
|
||||||
client_secret: CLIENT_SECRET.to_string(),
|
client_secret: CLIENT_SECRET.to_string(),
|
||||||
},
|
},
|
||||||
redirect_uris: Vec::new(),
|
redirect_uris: Vec::new(),
|
||||||
@ -345,14 +343,14 @@ mod tests {
|
|||||||
|
|
||||||
let store = client_private_keystore();
|
let store = client_private_keystore();
|
||||||
let jwks = store.export_jwks().await.unwrap();
|
let jwks = store.export_jwks().await.unwrap();
|
||||||
config.clients.push(OAuth2ClientConfig {
|
config.push(ClientConfig {
|
||||||
client_id: "private-key-jwt".to_string(),
|
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(),
|
redirect_uris: Vec::new(),
|
||||||
});
|
});
|
||||||
config.clients.push(OAuth2ClientConfig {
|
config.push(ClientConfig {
|
||||||
client_id: "private-key-jwt-2".to_string(),
|
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(),
|
redirect_uris: Vec::new(),
|
||||||
});
|
});
|
||||||
config
|
config
|
||||||
|
@ -16,14 +16,10 @@
|
|||||||
|
|
||||||
use std::{convert::Infallible, marker::PhantomData};
|
use std::{convert::Infallible, marker::PhantomData};
|
||||||
|
|
||||||
use chacha20poly1305::{
|
|
||||||
aead::{generic_array::GenericArray, Aead, NewAead},
|
|
||||||
ChaCha20Poly1305,
|
|
||||||
};
|
|
||||||
use cookie::{Cookie, SameSite};
|
use cookie::{Cookie, SameSite};
|
||||||
use data_encoding::BASE64URL_NOPAD;
|
use data_encoding::BASE64URL_NOPAD;
|
||||||
use headers::{Header, HeaderValue, SetCookie};
|
use headers::{Header, HeaderValue, SetCookie};
|
||||||
use mas_config::CookiesConfig;
|
use mas_config::Encrypter;
|
||||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use warp::{
|
use warp::{
|
||||||
@ -76,23 +72,16 @@ struct EncryptedCookie {
|
|||||||
|
|
||||||
impl EncryptedCookie {
|
impl EncryptedCookie {
|
||||||
/// Encrypt from a given key
|
/// Encrypt from a given key
|
||||||
fn encrypt<T: Serialize>(payload: T, key: &[u8; 32]) -> anyhow::Result<Self> {
|
fn encrypt<T: Serialize>(payload: T, encrypter: &Encrypter) -> anyhow::Result<Self> {
|
||||||
let key = GenericArray::from_slice(key);
|
|
||||||
let aead = ChaCha20Poly1305::new(key);
|
|
||||||
let message = bincode::serialize(&payload)?;
|
let message = bincode::serialize(&payload)?;
|
||||||
let nonce: [u8; 12] = rand::random();
|
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 })
|
Ok(Self { nonce, ciphertext })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decrypt the content of the cookie from a given key
|
/// Decrypt the content of the cookie from a given key
|
||||||
fn decrypt<T: DeserializeOwned>(&self, key: &[u8; 32]) -> anyhow::Result<T> {
|
fn decrypt<T: DeserializeOwned>(&self, encrypter: &Encrypter) -> anyhow::Result<T> {
|
||||||
let key = GenericArray::from_slice(key);
|
let message = encrypter.decrypt(&self.nonce, &self.ciphertext)?;
|
||||||
let aead = ChaCha20Poly1305::new(key);
|
|
||||||
let message = aead.decrypt(
|
|
||||||
GenericArray::from_slice(&self.nonce[..]),
|
|
||||||
&self.ciphertext[..],
|
|
||||||
)?;
|
|
||||||
let token = bincode::deserialize(&message)?;
|
let token = bincode::deserialize(&message)?;
|
||||||
Ok(token)
|
Ok(token)
|
||||||
}
|
}
|
||||||
@ -113,12 +102,12 @@ impl EncryptedCookie {
|
|||||||
/// Extract an optional encrypted cookie
|
/// Extract an optional encrypted cookie
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn maybe_encrypted<T>(
|
pub fn maybe_encrypted<T>(
|
||||||
options: &CookiesConfig,
|
encrypter: &Encrypter,
|
||||||
) -> impl Filter<Extract = (Option<T>,), Error = Rejection> + Clone + Send + Sync + 'static
|
) -> impl Filter<Extract = (Option<T>,), Error = Rejection> + Clone + Send + Sync + 'static
|
||||||
where
|
where
|
||||||
T: DeserializeOwned + EncryptableCookieValue + 'static,
|
T: DeserializeOwned + EncryptableCookieValue + 'static,
|
||||||
{
|
{
|
||||||
encrypted(options)
|
encrypted(encrypter)
|
||||||
.map(Some)
|
.map(Some)
|
||||||
.recover(none_on_error::<T, InvalidHeader>)
|
.recover(none_on_error::<T, InvalidHeader>)
|
||||||
.unify()
|
.unify()
|
||||||
@ -136,28 +125,35 @@ where
|
|||||||
/// [`CookieDecryptionError`]
|
/// [`CookieDecryptionError`]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn encrypted<T>(
|
pub fn encrypted<T>(
|
||||||
options: &CookiesConfig,
|
encrypter: &Encrypter,
|
||||||
) -> impl Filter<Extract = (T,), Error = Rejection> + Clone + Send + Sync + 'static
|
) -> impl Filter<Extract = (T,), Error = Rejection> + Clone + Send + Sync + 'static
|
||||||
where
|
where
|
||||||
T: DeserializeOwned + EncryptableCookieValue + 'static,
|
T: DeserializeOwned + EncryptableCookieValue + 'static,
|
||||||
{
|
{
|
||||||
let secret = options.secret;
|
let encrypter = encrypter.clone();
|
||||||
warp::cookie::cookie(T::cookie_key()).and_then(move |value: String| async move {
|
warp::cookie::cookie(T::cookie_key()).and_then(move |value: String| {
|
||||||
let encrypted =
|
let encrypter = encrypter.clone();
|
||||||
EncryptedCookie::from_cookie_value(&value).map_err(decryption_error::<T>)?;
|
async move {
|
||||||
let decrypted = encrypted.decrypt(&secret).map_err(decryption_error::<T>)?;
|
let encrypted =
|
||||||
Ok::<_, Rejection>(decrypted)
|
EncryptedCookie::from_cookie_value(&value).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`]
|
/// Get an [`EncryptedCookieSaver`] to help saving an [`EncryptableCookieValue`]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn encrypted_cookie_saver(
|
pub fn encrypted_cookie_saver(
|
||||||
options: &CookiesConfig,
|
encrypter: &Encrypter,
|
||||||
) -> impl Filter<Extract = (EncryptedCookieSaver,), Error = Infallible> + Clone + Send + Sync + 'static
|
) -> impl Filter<Extract = (EncryptedCookieSaver,), Error = Infallible> + Clone + Send + Sync + 'static
|
||||||
{
|
{
|
||||||
let secret = options.secret;
|
let encrypter = encrypter.clone();
|
||||||
warp::any().map(move || EncryptedCookieSaver { secret })
|
warp::any().map(move || EncryptedCookieSaver {
|
||||||
|
encrypter: encrypter.clone(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A cookie that can be encrypted with a well-known cookie key
|
/// 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
|
/// An opaque structure which helps encrypting a cookie and attach it to a reply
|
||||||
pub struct EncryptedCookieSaver {
|
pub struct EncryptedCookieSaver {
|
||||||
secret: [u8; 32],
|
encrypter: Encrypter,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EncryptedCookieSaver {
|
impl EncryptedCookieSaver {
|
||||||
@ -178,7 +174,7 @@ impl EncryptedCookieSaver {
|
|||||||
cookie: &T,
|
cookie: &T,
|
||||||
reply: R,
|
reply: R,
|
||||||
) -> Result<WithTypedHeader<R, SetCookie>, Rejection> {
|
) -> Result<WithTypedHeader<R, SetCookie>, Rejection> {
|
||||||
let encrypted = EncryptedCookie::encrypt(cookie, &self.secret)
|
let encrypted = EncryptedCookie::encrypt(cookie, &self.encrypter)
|
||||||
.wrap_error()?
|
.wrap_error()?
|
||||||
.to_cookie_value()
|
.to_cookie_value()
|
||||||
.wrap_error()?;
|
.wrap_error()?;
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
use chrono::{DateTime, Duration, Utc};
|
use chrono::{DateTime, Duration, Utc};
|
||||||
use data_encoding::{DecodeError, BASE64URL_NOPAD};
|
use data_encoding::{DecodeError, BASE64URL_NOPAD};
|
||||||
use mas_config::{CookiesConfig, CsrfConfig};
|
use mas_config::{CsrfConfig, Encrypter};
|
||||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
use serde_with::{serde_as, TimestampSeconds};
|
use serde_with::{serde_as, TimestampSeconds};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
@ -119,9 +119,9 @@ impl<T> CsrfForm<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn csrf_token(
|
fn csrf_token(
|
||||||
cookies_config: &CookiesConfig,
|
encrypter: &Encrypter,
|
||||||
) -> impl Filter<Extract = (CsrfToken,), Error = Rejection> + Clone + Send + Sync + 'static {
|
) -> 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()?;
|
let verified = token.verify_expiration()?;
|
||||||
Ok::<_, Rejection>(verified)
|
Ok::<_, Rejection>(verified)
|
||||||
})
|
})
|
||||||
@ -134,11 +134,11 @@ fn csrf_token(
|
|||||||
/// with [`encrypted_cookie_saver`][`super::cookies::encrypted_cookie_saver`]
|
/// with [`encrypted_cookie_saver`][`super::cookies::encrypted_cookie_saver`]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn updated_csrf_token(
|
pub fn updated_csrf_token(
|
||||||
cookies_config: &CookiesConfig,
|
encrypter: &Encrypter,
|
||||||
csrf_config: &CsrfConfig,
|
csrf_config: &CsrfConfig,
|
||||||
) -> impl Filter<Extract = (CsrfToken,), Error = Rejection> + Clone + Send + Sync + 'static {
|
) -> impl Filter<Extract = (CsrfToken,), Error = Rejection> + Clone + Send + Sync + 'static {
|
||||||
let ttl = csrf_config.ttl;
|
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 {
|
move |maybe_token: Option<CsrfToken>| async move {
|
||||||
// Explicitely specify the "Error" type here to have the `?` operation working
|
// Explicitely specify the "Error" type here to have the `?` operation working
|
||||||
Ok::<_, Rejection>(
|
Ok::<_, Rejection>(
|
||||||
@ -171,12 +171,12 @@ pub fn updated_csrf_token(
|
|||||||
/// TODO: we might want to unify the last three rejections in one
|
/// TODO: we might want to unify the last three rejections in one
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn protected_form<T>(
|
pub fn protected_form<T>(
|
||||||
cookies_config: &CookiesConfig,
|
encrypter: &Encrypter,
|
||||||
) -> impl Filter<Extract = (T,), Error = Rejection> + Clone + Send + Sync + 'static
|
) -> impl Filter<Extract = (T,), Error = Rejection> + Clone + Send + Sync + 'static
|
||||||
where
|
where
|
||||||
T: DeserializeOwned + Send + 'static,
|
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 {
|
|csrf_token: CsrfToken, protected_form: CsrfForm<T>| async move {
|
||||||
let form = protected_form.verify_csrf(&csrf_token)?;
|
let form = protected_form.verify_csrf(&csrf_token)?;
|
||||||
Ok::<_, Rejection>(form)
|
Ok::<_, Rejection>(form)
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
//! Load user sessions from the database
|
//! Load user sessions from the database
|
||||||
|
|
||||||
use mas_config::CookiesConfig;
|
use mas_config::Encrypter;
|
||||||
use mas_data_model::BrowserSession;
|
use mas_data_model::BrowserSession;
|
||||||
use mas_storage::{
|
use mas_storage::{
|
||||||
user::{lookup_active_session, ActiveSessionLookupError},
|
user::{lookup_active_session, ActiveSessionLookupError},
|
||||||
@ -88,13 +88,13 @@ impl EncryptableCookieValue for SessionCookie {
|
|||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn optional_session(
|
pub fn optional_session(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
cookies_config: &CookiesConfig,
|
encrypter: &Encrypter,
|
||||||
) -> impl Filter<Extract = (Option<BrowserSession<PostgresqlBackend>>,), Error = Rejection>
|
) -> impl Filter<Extract = (Option<BrowserSession<PostgresqlBackend>>,), Error = Rejection>
|
||||||
+ Clone
|
+ Clone
|
||||||
+ Send
|
+ Send
|
||||||
+ Sync
|
+ Sync
|
||||||
+ 'static {
|
+ 'static {
|
||||||
session(pool, cookies_config)
|
session(pool, encrypter)
|
||||||
.map(Some)
|
.map(Some)
|
||||||
.recover(none_on_error::<_, SessionLoadError>)
|
.recover(none_on_error::<_, SessionLoadError>)
|
||||||
.unify()
|
.unify()
|
||||||
@ -110,13 +110,13 @@ pub fn optional_session(
|
|||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn session(
|
pub fn session(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
cookies_config: &CookiesConfig,
|
encrypter: &Encrypter,
|
||||||
) -> impl Filter<Extract = (BrowserSession<PostgresqlBackend>,), Error = Rejection>
|
) -> impl Filter<Extract = (BrowserSession<PostgresqlBackend>,), Error = Rejection>
|
||||||
+ Clone
|
+ Clone
|
||||||
+ Send
|
+ Send
|
||||||
+ Sync
|
+ Sync
|
||||||
+ 'static {
|
+ 'static {
|
||||||
encrypted(cookies_config)
|
encrypted(encrypter)
|
||||||
.and(connection(pool))
|
.and(connection(pool))
|
||||||
.and_then(load_session)
|
.and_then(load_session)
|
||||||
.recover(recover)
|
.recover(recover)
|
||||||
|
@ -70,7 +70,7 @@ impl UrlBuilder {
|
|||||||
|
|
||||||
/// JWKS URI
|
/// JWKS URI
|
||||||
pub fn jwks_uri(&self) -> Url {
|
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
|
/// Email verification URL
|
||||||
|
Reference in New Issue
Block a user