1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-07-28 11:02:02 +03:00

Split the core crate

This commit is contained in:
Quentin Gliech
2021-12-17 18:04:30 +01:00
parent ceb17d3646
commit 2f97ca685d
45 changed files with 418 additions and 408 deletions

95
Cargo.lock generated
View File

@ -1512,9 +1512,11 @@ dependencies = [
"hyper",
"indoc",
"mas-config",
"mas-core",
"mas-handlers",
"mas-storage",
"mas-tasks",
"mas-templates",
"mas-warp-utils",
"opentelemetry",
"opentelemetry-http",
"opentelemetry-jaeger",
@ -1562,39 +1564,38 @@ dependencies = [
]
[[package]]
name = "mas-core"
name = "mas-data-model"
version = "0.1.0"
dependencies = [
"chrono",
"crc",
"oauth2-types",
"rand",
"serde",
"thiserror",
"url",
]
[[package]]
name = "mas-handlers"
version = "0.1.0"
dependencies = [
"anyhow",
"argon2",
"async-trait",
"bincode",
"chacha20poly1305",
"chrono",
"cookie",
"crc",
"data-encoding",
"elliptic-curve",
"futures-util",
"headers",
"hyper",
"indoc",
"itertools",
"jwt-compact",
"k256",
"mas-config",
"mas-data-model",
"mas-static-files",
"mas-storage",
"mas-templates",
"mas-warp-utils",
"mime",
"oauth2-types",
"once_cell",
"opentelemetry",
"password-hash",
"pkcs8",
"rand",
"rsa",
"serde",
"serde_json",
"serde_urlencoded",
@ -1603,23 +1604,11 @@ dependencies = [
"sqlx",
"thiserror",
"tokio",
"tokio-stream",
"tracing",
"url",
"warp",
]
[[package]]
name = "mas-data-model"
version = "0.1.0"
dependencies = [
"chrono",
"oauth2-types",
"serde",
"thiserror",
"url",
]
[[package]]
name = "mas-static-files"
version = "0.1.0"
@ -1650,6 +1639,19 @@ dependencies = [
"warp",
]
[[package]]
name = "mas-tasks"
version = "0.1.0"
dependencies = [
"async-trait",
"futures-util",
"mas-storage",
"sqlx",
"tokio",
"tokio-stream",
"tracing",
]
[[package]]
name = "mas-templates"
version = "0.1.0"
@ -1669,6 +1671,40 @@ dependencies = [
"warp",
]
[[package]]
name = "mas-warp-utils"
version = "0.1.0"
dependencies = [
"anyhow",
"bincode",
"chacha20poly1305",
"chrono",
"cookie",
"crc",
"data-encoding",
"headers",
"hyper",
"jwt-compact",
"mas-config",
"mas-data-model",
"mas-storage",
"mas-templates",
"mime",
"oauth2-types",
"once_cell",
"opentelemetry",
"rand",
"serde",
"serde_json",
"serde_urlencoded",
"serde_with",
"sqlx",
"thiserror",
"tokio",
"tracing",
"warp",
]
[[package]]
name = "matchers"
version = "0.1.0"
@ -1895,7 +1931,6 @@ dependencies = [
"serde_json",
"serde_with",
"sha2 0.10.0",
"sqlx",
"thiserror",
"url",
]

View File

@ -20,6 +20,7 @@ warp = "0.3.2"
url = "2.2.2"
argon2 = { version = "0.3.2", features = ["password-hash"] }
reqwest = { version = "0.11.7", features = ["rustls-tls"], default-features = false, optional = true }
watchman_client = "0.7.1"
tracing = "0.1.29"
tracing-subscriber = { version = "0.3.3", features = ["env-filter"] }
@ -32,17 +33,18 @@ opentelemetry-otlp = { version = "0.9.0", features = ["trace", "metrics"], optio
opentelemetry-zipkin = { version = "0.14.0", features = ["reqwest-client", "reqwest-rustls"], default-features = false, optional = true }
mas-config = { path = "../config" }
mas-core = { path = "../core" }
mas-handlers = { path = "../handlers" }
mas-templates = { path = "../templates" }
mas-storage = { path = "../storage" }
watchman_client = "0.7.1"
mas-tasks = { path = "../tasks" }
mas-warp-utils = { path = "../warp-utils" }
[dev-dependencies]
indoc = "1.0.3"
[features]
default = ["otlp", "jaeger", "zipkin"]
dev = ["mas-templates/dev", "mas-core/dev"]
dev = ["mas-templates/dev", "mas-handlers/dev"]
# Enable OpenTelemetry OTLP exporter. Requires "protoc"
otlp = ["opentelemetry-otlp"]
# Enable OpenTelemetry Jaeger exporter and propagator.

View File

@ -22,8 +22,8 @@ use clap::Parser;
use futures::{future::TryFutureExt, stream::TryStreamExt};
use hyper::{header, Server, Version};
use mas_config::RootConfig;
use mas_core::tasks::{self, TaskQueue};
use mas_storage::MIGRATOR;
use mas_tasks::TaskQueue;
use mas_templates::Templates;
use opentelemetry_http::HeaderExtractor;
use tower::{make::Shared, ServiceBuilder};
@ -233,7 +233,7 @@ impl ServerCommand {
info!("Starting task scheduler");
let queue = TaskQueue::default();
queue.recuring(Duration::from_secs(15), tasks::cleanup_expired(&pool));
queue.recuring(Duration::from_secs(15), mas_tasks::cleanup_expired(&pool));
queue.start();
// Load and compile the templates
@ -254,7 +254,7 @@ impl ServerCommand {
}
// Start the server
let root = mas_core::handlers::root(&pool, &templates, &config);
let root = mas_handlers::root(&pool, &templates, &config);
let warp_service = warp::service(root);

View File

@ -40,7 +40,7 @@ pub fn setup(config: &TelemetryConfig) -> anyhow::Result<Option<Tracer>> {
// The CORS filter needs to know what headers it should whitelist for
// CORS-protected requests.
mas_core::set_propagator(&propagator);
mas_warp_utils::filters::cors::set_propagator(&propagator);
global::set_text_map_propagator(propagator);
let tracer = tracer(&config.tracing.exporter)?;

View File

@ -1,242 +0,0 @@
// Copyright 2021 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.
//! Access token and refresh token generation and validation
//!
//! # Example
//!
//! ```rust
//! extern crate rand;
//!
//! use rand::thread_rng;
//! use mas_core::tokens::{TokenType, AccessToken, RefreshToken};
//!
//! let mut rng = thread_rng();
//!
//! // Generate an access token
//! let token = AccessToken.generate(&mut rng);
//!
//! // Check it and verify its type is right
//! assert_eq!(TokenType::check(&token).unwrap(), AccessToken);
//!
//! // Same, but with a refresh token
//! let token = RefreshToken.generate(&mut rng);
//! assert_eq!(TokenType::check(&token).unwrap(), RefreshToken);
//! ```
#![deny(missing_docs)]
use crc::{Crc, CRC_32_ISO_HDLC};
use oauth2_types::requests::TokenTypeHint;
use rand::{distributions::Alphanumeric, Rng};
use thiserror::Error;
/// Type of token to generate or validate
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TokenType {
/// An access token, used by Relying Parties to authenticate requests
AccessToken,
/// A refresh token, used by the refresh token grant
RefreshToken,
}
pub use TokenType::{AccessToken, RefreshToken};
impl TokenType {
fn prefix(self) -> &'static str {
match self {
TokenType::AccessToken => "mat",
TokenType::RefreshToken => "mar",
}
}
fn match_prefix(prefix: &str) -> Option<Self> {
match prefix {
"mat" => Some(TokenType::AccessToken),
"mar" => Some(TokenType::RefreshToken),
_ => None,
}
}
/// Generate a token for the given type
///
/// ```rust
/// extern crate rand;
///
/// use rand::thread_rng;
/// use mas_core::tokens::{AccessToken, RefreshToken};
///
/// AccessToken.generate(thread_rng());
/// RefreshToken.generate(thread_rng());
/// ```
pub fn generate(self, rng: impl Rng) -> String {
let random_part: String = rng
.sample_iter(&Alphanumeric)
.take(30)
.map(char::from)
.collect();
let base = format!("{}_{}", self.prefix(), random_part);
let crc = CRC.checksum(base.as_bytes());
let crc = base62_encode(crc);
format!("{}_{}", base, crc)
}
/// Check the format of a token and determine its type
///
/// ```rust
/// use mas_core::tokens::TokenType;
///
/// assert_eq!(
/// TokenType::check("mat_kkLSacJDpek22jKWw4AcXG68b7U3W6_0Lg9yb"),
/// Ok(TokenType::AccessToken)
/// );
///
/// assert_eq!(
/// TokenType::check("mar_PkpplxPkfjsqvtdfUlYR1Afg2TpaHF_GaTQd2"),
/// Ok(TokenType::RefreshToken)
/// );
/// ```
pub fn check(token: &str) -> Result<TokenType, TokenFormatError> {
let split: Vec<&str> = token.split('_').collect();
let [prefix, random_part, crc]: [&str; 3] = split
.try_into()
.map_err(|_| TokenFormatError::InvalidFormat)?;
if prefix.len() != 3 || random_part.len() != 30 || crc.len() != 6 {
return Err(TokenFormatError::InvalidFormat);
}
let token_type =
TokenType::match_prefix(prefix).ok_or_else(|| TokenFormatError::UnknownPrefix {
prefix: prefix.to_string(),
})?;
let base = format!("{}_{}", token_type.prefix(), random_part);
let expected_crc = CRC.checksum(base.as_bytes());
let expected_crc = base62_encode(expected_crc);
if crc != expected_crc {
return Err(TokenFormatError::InvalidCrc {
expected: expected_crc,
got: crc.to_string(),
});
}
Ok(token_type)
}
}
impl PartialEq<TokenTypeHint> for TokenType {
fn eq(&self, other: &TokenTypeHint) -> bool {
matches!(
(self, other),
(TokenType::AccessToken, TokenTypeHint::AccessToken)
| (TokenType::RefreshToken, TokenTypeHint::RefreshToken)
)
}
}
const NUM: [u8; 62] = *b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
fn base62_encode(mut num: u32) -> String {
let mut res = String::with_capacity(6);
while num > 0 {
res.push(NUM[(num % 62) as usize] as char);
num /= 62;
}
format!("{:0>6}", res)
}
const CRC: Crc<u32> = Crc::<u32>::new(&CRC_32_ISO_HDLC);
/// Invalid token
#[derive(Debug, Error, PartialEq)]
pub enum TokenFormatError {
/// Overall token format is invalid
#[error("invalid token format")]
InvalidFormat,
/// Token used an unknown prefix
#[error("unknown token prefix {prefix:?}")]
UnknownPrefix {
/// The prefix found in the token
prefix: String,
},
/// The CRC checksum in the token is invalid
#[error("invalid crc {got:?}, expected {expected:?}")]
InvalidCrc {
/// The CRC hash expected to be found in the token
expected: String,
/// The CRC found in the token
got: String,
},
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use rand::thread_rng;
use super::*;
#[test]
fn test_prefix_match() {
use TokenType::{AccessToken, RefreshToken};
assert_eq!(TokenType::match_prefix("mat"), Some(AccessToken));
assert_eq!(TokenType::match_prefix("mar"), Some(RefreshToken));
assert_eq!(TokenType::match_prefix("matt"), None);
assert_eq!(TokenType::match_prefix("marr"), None);
assert_eq!(TokenType::match_prefix("ma"), None);
assert_eq!(
TokenType::match_prefix(TokenType::AccessToken.prefix()),
Some(TokenType::AccessToken)
);
assert_eq!(
TokenType::match_prefix(TokenType::RefreshToken.prefix()),
Some(TokenType::RefreshToken)
);
}
#[test]
fn test_generate_and_check() {
const COUNT: usize = 500; // Generate 500 of each token type
let mut rng = thread_rng();
// Generate many access tokens
let tokens: HashSet<String> = (0..COUNT)
.map(|_| TokenType::AccessToken.generate(&mut rng))
.collect();
// Check that they are all different
assert_eq!(tokens.len(), COUNT, "All tokens are unique");
// Check that they are all valid and detected as access tokens
for token in tokens {
assert_eq!(TokenType::check(&token).unwrap(), TokenType::AccessToken);
}
// Same, but for refresh tokens
let tokens: HashSet<String> = (0..COUNT)
.map(|_| TokenType::RefreshToken.generate(&mut rng))
.collect();
assert_eq!(tokens.len(), COUNT, "All tokens are unique");
for token in tokens {
assert_eq!(TokenType::check(&token).unwrap(), TokenType::RefreshToken);
}
}
}

View File

@ -10,5 +10,7 @@ chrono = "0.4.19"
thiserror = "1.0.30"
serde = "1.0.131"
url = { version = "2.2.2", features = ["serde"] }
crc = "2.1.0"
rand = "0.8.4"
oauth2-types = { path = "../oauth2-types" }

View File

@ -31,7 +31,7 @@ pub use self::{
oauth2::{
AuthorizationCode, AuthorizationGrant, AuthorizationGrantStage, Client, Pkce, Session,
},
tokens::{AccessToken, RefreshToken},
tokens::{AccessToken, RefreshToken, TokenFormatError, TokenType},
traits::{StorageBackend, StorageBackendMarker},
users::{Authentication, BrowserSession, User},
};

View File

@ -13,6 +13,10 @@
// limitations under the License.
use chrono::{DateTime, Duration, Utc};
use crc::{Crc, CRC_32_ISO_HDLC};
use oauth2_types::requests::TokenTypeHint;
use rand::{distributions::Alphanumeric, Rng};
use thiserror::Error;
use crate::traits::{StorageBackend, StorageBackendMarker};
@ -61,3 +65,200 @@ impl<S: StorageBackendMarker> From<RefreshToken<S>> for RefreshToken<()> {
}
}
}
/// Type of token to generate or validate
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TokenType {
/// An access token, used by Relying Parties to authenticate requests
AccessToken,
/// A refresh token, used by the refresh token grant
RefreshToken,
}
impl TokenType {
fn prefix(self) -> &'static str {
match self {
TokenType::AccessToken => "mat",
TokenType::RefreshToken => "mar",
}
}
fn match_prefix(prefix: &str) -> Option<Self> {
match prefix {
"mat" => Some(TokenType::AccessToken),
"mar" => Some(TokenType::RefreshToken),
_ => None,
}
}
/// Generate a token for the given type
///
/// ```rust
/// extern crate rand;
///
/// use rand::thread_rng;
/// use mas_data_model::TokenType::{AccessToken, RefreshToken};
///
/// AccessToken.generate(thread_rng());
/// RefreshToken.generate(thread_rng());
/// ```
pub fn generate(self, rng: impl Rng) -> String {
let random_part: String = rng
.sample_iter(&Alphanumeric)
.take(30)
.map(char::from)
.collect();
let base = format!("{}_{}", self.prefix(), random_part);
let crc = CRC.checksum(base.as_bytes());
let crc = base62_encode(crc);
format!("{}_{}", base, crc)
}
/// Check the format of a token and determine its type
///
/// ```rust
/// use mas_data_model::TokenType;
///
/// assert_eq!(
/// TokenType::check("mat_kkLSacJDpek22jKWw4AcXG68b7U3W6_0Lg9yb"),
/// Ok(TokenType::AccessToken)
/// );
///
/// assert_eq!(
/// TokenType::check("mar_PkpplxPkfjsqvtdfUlYR1Afg2TpaHF_GaTQd2"),
/// Ok(TokenType::RefreshToken)
/// );
/// ```
pub fn check(token: &str) -> Result<TokenType, TokenFormatError> {
let split: Vec<&str> = token.split('_').collect();
let [prefix, random_part, crc]: [&str; 3] = split
.try_into()
.map_err(|_| TokenFormatError::InvalidFormat)?;
if prefix.len() != 3 || random_part.len() != 30 || crc.len() != 6 {
return Err(TokenFormatError::InvalidFormat);
}
let token_type =
TokenType::match_prefix(prefix).ok_or_else(|| TokenFormatError::UnknownPrefix {
prefix: prefix.to_string(),
})?;
let base = format!("{}_{}", token_type.prefix(), random_part);
let expected_crc = CRC.checksum(base.as_bytes());
let expected_crc = base62_encode(expected_crc);
if crc != expected_crc {
return Err(TokenFormatError::InvalidCrc {
expected: expected_crc,
got: crc.to_string(),
});
}
Ok(token_type)
}
}
impl PartialEq<TokenTypeHint> for TokenType {
fn eq(&self, other: &TokenTypeHint) -> bool {
matches!(
(self, other),
(TokenType::AccessToken, TokenTypeHint::AccessToken)
| (TokenType::RefreshToken, TokenTypeHint::RefreshToken)
)
}
}
const NUM: [u8; 62] = *b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
fn base62_encode(mut num: u32) -> String {
let mut res = String::with_capacity(6);
while num > 0 {
res.push(NUM[(num % 62) as usize] as char);
num /= 62;
}
format!("{:0>6}", res)
}
const CRC: Crc<u32> = Crc::<u32>::new(&CRC_32_ISO_HDLC);
/// Invalid token
#[derive(Debug, Error, PartialEq)]
pub enum TokenFormatError {
/// Overall token format is invalid
#[error("invalid token format")]
InvalidFormat,
/// Token used an unknown prefix
#[error("unknown token prefix {prefix:?}")]
UnknownPrefix {
/// The prefix found in the token
prefix: String,
},
/// The CRC checksum in the token is invalid
#[error("invalid crc {got:?}, expected {expected:?}")]
InvalidCrc {
/// The CRC hash expected to be found in the token
expected: String,
/// The CRC found in the token
got: String,
},
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use rand::thread_rng;
use super::*;
#[test]
fn test_prefix_match() {
use TokenType::{AccessToken, RefreshToken};
assert_eq!(TokenType::match_prefix("mat"), Some(AccessToken));
assert_eq!(TokenType::match_prefix("mar"), Some(RefreshToken));
assert_eq!(TokenType::match_prefix("matt"), None);
assert_eq!(TokenType::match_prefix("marr"), None);
assert_eq!(TokenType::match_prefix("ma"), None);
assert_eq!(
TokenType::match_prefix(TokenType::AccessToken.prefix()),
Some(TokenType::AccessToken)
);
assert_eq!(
TokenType::match_prefix(TokenType::RefreshToken.prefix()),
Some(TokenType::RefreshToken)
);
}
#[test]
fn test_generate_and_check() {
const COUNT: usize = 500; // Generate 500 of each token type
let mut rng = thread_rng();
// Generate many access tokens
let tokens: HashSet<String> = (0..COUNT)
.map(|_| TokenType::AccessToken.generate(&mut rng))
.collect();
// Check that they are all different
assert_eq!(tokens.len(), COUNT, "All tokens are unique");
// Check that they are all valid and detected as access tokens
for token in tokens {
assert_eq!(TokenType::check(&token).unwrap(), TokenType::AccessToken);
}
// Same, but for refresh tokens
let tokens: HashSet<String> = (0..COUNT)
.map(|_| TokenType::RefreshToken.generate(&mut rng))
.collect();
assert_eq!(tokens.len(), COUNT, "All tokens are unique");
for token in tokens {
assert_eq!(TokenType::check(&token).unwrap(), TokenType::RefreshToken);
}
}
}

View File

@ -1,5 +1,5 @@
[package]
name = "mas-core"
name = "mas-handlers"
version = "0.1.0"
authors = ["Quentin Gliech <quenting@element.io>"]
edition = "2021"
@ -10,14 +10,10 @@ dev = ["mas-static-files/dev", "mas-templates/dev"]
[dependencies]
# Async runtime
tokio = { version = "1.14.0", features = ["full"] }
async-trait = "0.1.52"
tokio-stream = "0.1.8"
futures-util = "0.3.18"
tokio = { version = "1.14.0", features = ["macros"] }
# Logging and tracing
tracing = "0.1.29"
opentelemetry = "0.16.0"
# Error management
thiserror = "1.0.30"
@ -28,7 +24,7 @@ warp = "0.3.2"
hyper = { version = "0.14.16", features = ["full"] }
# Database access
sqlx = { version = "0.5.9", features = ["runtime-tokio-rustls", "postgres", "migrate", "chrono", "offline"] }
sqlx = { version = "0.5.9", features = ["runtime-tokio-rustls", "postgres"] }
# Various structure (de)serialization
serde = { version = "1.0.131", features = ["derive"] }
@ -38,36 +34,23 @@ serde_urlencoded = "0.7.0"
# Password hashing
argon2 = { version = "0.3.2", features = ["password-hash"] }
password-hash = { version = "0.3.2", features = ["std"] }
# Crypto, hashing and signing stuff
rsa = "0.5.0"
k256 = "0.9.6"
pkcs8 = { version = "0.7.6", features = ["pem"] }
elliptic-curve = { version = "0.10.6", features = ["pem"] }
chacha20poly1305 = { version = "0.9.0", features = ["std"] }
sha2 = "0.10.0"
crc = "2.1.0"
jwt-compact = { version = "0.5.0-beta.1", features = ["with_rsa", "k256"] }
# Various data types and utilities
data-encoding = "2.3.2"
chrono = { version = "0.4.19", features = ["serde"] }
url = { version = "2.2.2", features = ["serde"] }
itertools = "0.10.3"
mime = "0.3.16"
rand = "0.8.4"
bincode = "1.3.3"
headers = "0.3.5"
cookie = "0.15.1"
once_cell = "1.8.0"
oauth2-types = { path = "../oauth2-types", features = ["sqlx_type"] }
oauth2-types = { path = "../oauth2-types" }
mas-config = { path = "../config" }
mas-data-model = { path = "../data-model" }
mas-templates = { path = "../templates" }
mas-static-files = { path = "../static-files" }
mas-storage = { path = "../storage" }
[dev-dependencies]
indoc = "1.0.3"
mas-warp-utils = { path = "../warp-utils" }

View File

@ -13,13 +13,12 @@
// limitations under the License.
use hyper::header::CONTENT_TYPE;
use mas_warp_utils::{errors::WrapError, filters::database::connection};
use mime::TEXT_PLAIN;
use sqlx::{pool::PoolConnection, PgPool, Postgres};
use tracing::{info_span, Instrument};
use warp::{filters::BoxedFilter, reply::with_header, Filter, Rejection, Reply};
use crate::{errors::WrapError, filters::database::connection};
pub fn filter(pool: &PgPool) -> BoxedFilter<(impl Reply,)> {
warp::path!("health")
.and(warp::get())

View File

@ -12,6 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#![forbid(unsafe_code)]
#![deny(clippy::all)]
#![deny(rustdoc::broken_intra_doc_links)]
#![warn(clippy::pedantic)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::implicit_hasher)]
#![allow(clippy::unused_async)] // Some warp filters need that
use mas_config::RootConfig;
@ -32,8 +40,7 @@ pub fn root(
templates: &Templates,
config: &RootConfig,
) -> BoxedFilter<(impl Reply,)> {
static_files(config.http.web_root.clone())
.or(health(pool))
health(pool)
.or(oauth2(pool, templates, &config.oauth2, &config.cookies))
.or(views(
pool,
@ -42,6 +49,7 @@ pub fn root(
&config.csrf,
&config.cookies,
))
.or(static_files(config.http.web_root.clone()))
.with(warp::log(module_path!()))
.boxed()
}

View File

@ -23,7 +23,7 @@ use hyper::{
use mas_config::{CookiesConfig, OAuth2ClientConfig, OAuth2Config};
use mas_data_model::{
Authentication, AuthorizationCode, AuthorizationGrant, AuthorizationGrantStage, BrowserSession,
Pkce, StorageBackend,
Pkce, StorageBackend, TokenType,
};
use mas_storage::{
oauth2::{
@ -36,6 +36,14 @@ use mas_storage::{
PostgresqlBackend,
};
use mas_templates::{FormPostContext, Templates};
use mas_warp_utils::{
errors::WrapError,
filters::{
database::transaction,
session::{optional_session, session},
with_templates,
},
};
use oauth2_types::{
errors::{
ErrorResponse, InvalidGrant, InvalidRequest, LoginRequired, OAuth2Error,
@ -60,16 +68,7 @@ use warp::{
Filter, Rejection, Reply,
};
use crate::{
errors::WrapError,
filters::{
database::transaction,
session::{optional_session, session},
with_templates,
},
handlers::views::{LoginRequest, PostAuthAction, ReauthRequest},
tokens::{AccessToken, RefreshToken},
};
use crate::views::{LoginRequest, PostAuthAction, ReauthRequest};
#[derive(Deserialize)]
struct PartialParams {
@ -523,8 +522,8 @@ async fn step(
let (access_token_str, refresh_token_str) = {
let mut rng = thread_rng();
(
AccessToken.generate(&mut rng),
RefreshToken.generate(&mut rng),
TokenType::AccessToken.generate(&mut rng),
TokenType::RefreshToken.generate(&mut rng),
)
};

View File

@ -16,6 +16,7 @@ use std::collections::HashSet;
use hyper::Method;
use mas_config::OAuth2Config;
use mas_warp_utils::filters::cors::cors;
use oauth2_types::{
oidc::{Metadata, SigningAlgorithm},
pkce::CodeChallengeMethod,
@ -23,8 +24,6 @@ use oauth2_types::{
};
use warp::{Filter, Rejection, Reply};
use crate::filters::cors::cors;
pub(super) fn filter(
config: &OAuth2Config,
) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone + Send + Sync + 'static {

View File

@ -14,9 +14,14 @@
use hyper::Method;
use mas_config::{OAuth2ClientConfig, OAuth2Config};
use mas_data_model::TokenType;
use mas_storage::oauth2::{
access_token::lookup_active_access_token, refresh_token::lookup_active_refresh_token,
};
use mas_warp_utils::{
errors::WrapError,
filters::{client::client_authentication, cors::cors, database::connection},
};
use oauth2_types::requests::{
ClientAuthenticationMethod, IntrospectionRequest, IntrospectionResponse, TokenTypeHint,
};
@ -24,12 +29,6 @@ use sqlx::{pool::PoolConnection, PgPool, Postgres};
use tracing::{info, warn};
use warp::{Filter, Rejection, Reply};
use crate::{
errors::WrapError,
filters::{client::client_authentication, cors::cors, database::connection},
tokens::{self, TokenType},
};
pub fn filter(
pool: &PgPool,
oauth2_config: &OAuth2Config,
@ -88,7 +87,7 @@ async fn introspect(
}
let reply = match token_type {
tokens::TokenType::AccessToken => {
TokenType::AccessToken => {
let (token, session) = lookup_active_access_token(&mut conn, token)
.await
.wrap_error()?;
@ -109,7 +108,7 @@ async fn introspect(
jti: None,
}
}
tokens::TokenType::RefreshToken => {
TokenType::RefreshToken => {
let (token, session) = lookup_active_refresh_token(&mut conn, token)
.await
.wrap_error()?;

View File

@ -14,10 +14,9 @@
use hyper::Method;
use mas_config::OAuth2Config;
use mas_warp_utils::filters::cors::cors;
use warp::{Filter, Rejection, Reply};
use crate::filters::cors::cors;
pub(super) fn filter(
config: &OAuth2Config,
) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone + Send + Sync + 'static {

View File

@ -19,7 +19,7 @@ use headers::{CacheControl, Pragma};
use hyper::{Method, StatusCode};
use jwt_compact::{Claims, Header, TimeOptions};
use mas_config::{KeySet, OAuth2ClientConfig, OAuth2Config};
use mas_data_model::AuthorizationGrantStage;
use mas_data_model::{AuthorizationGrantStage, TokenType};
use mas_storage::{
oauth2::{
access_token::{add_access_token, revoke_access_token},
@ -28,6 +28,11 @@ use mas_storage::{
},
DatabaseInconsistencyError,
};
use mas_warp_utils::{
errors::WrapError,
filters::{client::client_authentication, cors::cors, database::connection, with_keys},
reply::with_typed_header,
};
use oauth2_types::{
errors::{InvalidGrant, InvalidRequest, OAuth2Error, OAuth2ErrorCode, UnauthorizedClient},
requests::{
@ -49,13 +54,6 @@ use warp::{
Filter, Rejection, Reply,
};
use crate::{
errors::WrapError,
filters::{client::client_authentication, cors::cors, database::connection, with_keys},
reply::with_typed_header,
tokens::{AccessToken, RefreshToken},
};
#[serde_as]
#[skip_serializing_none]
#[derive(Serialize, Debug)]
@ -233,8 +231,8 @@ async fn authorization_code_grant(
let (access_token_str, refresh_token_str) = {
let mut rng = thread_rng();
(
AccessToken.generate(&mut rng),
RefreshToken.generate(&mut rng),
TokenType::AccessToken.generate(&mut rng),
TokenType::RefreshToken.generate(&mut rng),
)
};
@ -308,8 +306,8 @@ async fn refresh_token_grant(
let (access_token_str, refresh_token_str) = {
let mut rng = thread_rng();
(
AccessToken.generate(&mut rng),
RefreshToken.generate(&mut rng),
TokenType::AccessToken.generate(&mut rng),
TokenType::RefreshToken.generate(&mut rng),
)
};

View File

@ -16,14 +16,13 @@ use hyper::Method;
use mas_config::OAuth2Config;
use mas_data_model::{AccessToken, Session};
use mas_storage::PostgresqlBackend;
use serde::Serialize;
use sqlx::PgPool;
use warp::{Filter, Rejection, Reply};
use crate::filters::{
use mas_warp_utils::filters::{
authenticate::{authentication, recover_unauthorized},
cors::cors,
};
use serde::Serialize;
use sqlx::PgPool;
use warp::{Filter, Rejection, Reply};
#[derive(Serialize)]
struct UserInfo {

View File

@ -20,11 +20,7 @@ use mas_storage::{
PostgresqlBackend,
};
use mas_templates::{AccountContext, TemplateContext, Templates};
use serde::Deserialize;
use sqlx::{pool::PoolConnection, PgExecutor, PgPool, Postgres, Transaction};
use warp::{reply::html, Filter, Rejection, Reply};
use crate::{
use mas_warp_utils::{
errors::WrapError,
filters::{
cookies::{encrypted_cookie_saver, EncryptedCookieSaver},
@ -34,6 +30,9 @@ use crate::{
with_templates, CsrfToken,
},
};
use serde::Deserialize;
use sqlx::{pool::PoolConnection, PgExecutor, PgPool, Postgres, Transaction};
use warp::{reply::html, Filter, Rejection, Reply};
pub(super) fn filter(
pool: &PgPool,

View File

@ -16,16 +16,15 @@ use mas_config::{CookiesConfig, CsrfConfig, OAuth2Config};
use mas_data_model::BrowserSession;
use mas_storage::PostgresqlBackend;
use mas_templates::{IndexContext, TemplateContext, Templates};
use sqlx::PgPool;
use url::Url;
use warp::{reply::html, Filter, Rejection, Reply};
use crate::filters::{
use mas_warp_utils::filters::{
cookies::{encrypted_cookie_saver, EncryptedCookieSaver},
csrf::updated_csrf_token,
session::optional_session,
with_templates, CsrfToken,
};
use sqlx::PgPool;
use url::Url;
use warp::{reply::html, Filter, Rejection, Reply};
pub(super) fn filter(
pool: &PgPool,

View File

@ -17,12 +17,7 @@ use mas_config::{CookiesConfig, CsrfConfig};
use mas_data_model::{errors::WrapFormError, BrowserSession, StorageBackend};
use mas_storage::{user::login, PostgresqlBackend};
use mas_templates::{LoginContext, LoginFormField, TemplateContext, Templates};
use serde::Deserialize;
use sqlx::{pool::PoolConnection, PgPool, Postgres};
use warp::{reply::html, Filter, Rejection, Reply};
use super::{shared::PostAuthAction, RegisterRequest};
use crate::{
use mas_warp_utils::{
errors::WrapError,
filters::{
cookies::{encrypted_cookie_saver, EncryptedCookieSaver},
@ -32,6 +27,11 @@ use crate::{
with_templates, CsrfToken,
},
};
use serde::Deserialize;
use sqlx::{pool::PoolConnection, PgPool, Postgres};
use warp::{reply::html, Filter, Rejection, Reply};
use super::{shared::PostAuthAction, RegisterRequest};
#[derive(Deserialize)]
#[serde(bound(deserialize = "S::AuthorizationGrantData: std::str::FromStr,

View File

@ -15,13 +15,12 @@
use mas_config::CookiesConfig;
use mas_data_model::BrowserSession;
use mas_storage::{user::end_session, PostgresqlBackend};
use sqlx::{PgPool, Postgres, Transaction};
use warp::{hyper::Uri, Filter, Rejection, Reply};
use crate::{
use mas_warp_utils::{
errors::WrapError,
filters::{csrf::protected_form, database::transaction, session::session},
};
use sqlx::{PgPool, Postgres, Transaction};
use warp::{hyper::Uri, Filter, Rejection, Reply};
pub(super) fn filter(
pool: &PgPool,

View File

@ -17,12 +17,7 @@ use mas_config::{CookiesConfig, CsrfConfig};
use mas_data_model::{BrowserSession, StorageBackend};
use mas_storage::{user::authenticate_session, PostgresqlBackend};
use mas_templates::{ReauthContext, TemplateContext, Templates};
use serde::Deserialize;
use sqlx::{pool::PoolConnection, PgPool, Postgres, Transaction};
use warp::{hyper::Uri, reply::html, Filter, Rejection, Reply};
use super::PostAuthAction;
use crate::{
use mas_warp_utils::{
errors::WrapError,
filters::{
cookies::{encrypted_cookie_saver, EncryptedCookieSaver},
@ -32,6 +27,11 @@ use crate::{
with_templates, CsrfToken,
},
};
use serde::Deserialize;
use sqlx::{pool::PoolConnection, PgPool, Postgres, Transaction};
use warp::{hyper::Uri, reply::html, Filter, Rejection, Reply};
use super::PostAuthAction;
#[derive(Deserialize)]
#[serde(bound(deserialize = "S::AuthorizationGrantData: std::str::FromStr,

View File

@ -21,12 +21,7 @@ use mas_storage::{
PostgresqlBackend,
};
use mas_templates::{RegisterContext, TemplateContext, Templates};
use serde::Deserialize;
use sqlx::{pool::PoolConnection, PgPool, Postgres, Transaction};
use warp::{reply::html, Filter, Rejection, Reply};
use super::{LoginRequest, PostAuthAction};
use crate::{
use mas_warp_utils::{
errors::WrapError,
filters::{
cookies::{encrypted_cookie_saver, EncryptedCookieSaver},
@ -36,6 +31,11 @@ use crate::{
with_templates, CsrfToken,
},
};
use serde::Deserialize;
use sqlx::{pool::PoolConnection, PgPool, Postgres, Transaction};
use warp::{reply::html, Filter, Rejection, Reply};
use super::{LoginRequest, PostAuthAction};
#[derive(Deserialize)]
#[serde(bound(deserialize = "S::AuthorizationGrantData: std::str::FromStr,

View File

@ -14,12 +14,8 @@ url = { version = "2.2.2", features = ["serde"] }
parse-display = "0.5.3"
indoc = "1.0.3"
serde_with = { version = "1.11.0", features = ["chrono"] }
sqlx = { version = "0.5.9", default-features = false, optional = true }
chrono = "0.4.19"
sha2 = "0.10.0"
data-encoding = "2.3.2"
thiserror = "1.0.30"
itertools = "0.10.3"
[features]
sqlx_type = ["sqlx"]

View File

@ -21,5 +21,5 @@ password-hash = { version = "0.3.2", features = ["std"] }
rand = "0.8.4"
url = { version = "2.2.2", features = ["serde"] }
oauth2-types = { path = "../oauth2-types", features = ["sqlx_type"] }
oauth2-types = { path = "../oauth2-types" }
mas-data-model = { path = "../data-model" }

16
crates/tasks/Cargo.toml Normal file
View File

@ -0,0 +1,16 @@
[package]
name = "mas-tasks"
version = "0.1.0"
authors = ["Quentin Gliech <quenting@element.io>"]
edition = "2021"
license = "Apache-2.0"
[dependencies]
tokio = { version = "1.14.0" }
async-trait = "0.1.52"
tokio-stream = "0.1.8"
futures-util = "0.3.18"
tracing = "0.1.29"
sqlx = { version = "0.5.9", features = ["runtime-tokio-rustls", "postgres"] }
mas-storage = { path = "../storage" }

View File

@ -0,0 +1,37 @@
[package]
name = "mas-warp-utils"
version = "0.1.0"
authors = ["Quentin Gliech <quenting@element.io>"]
edition = "2021"
license = "Apache-2.0"
[dependencies]
tokio = { version = "1.14.0", features = ["macros"] }
headers = "0.3.5"
cookie = "0.15.1"
warp = "0.3.2"
hyper = { version = "0.14.16", features = ["full"] }
thiserror = "1.0.30"
anyhow = "1.0.51"
sqlx = { version = "0.5.9", features = ["runtime-tokio-rustls", "postgres"] }
jwt-compact = { version = "0.5.0-beta.1", features = ["with_rsa", "k256"] }
chrono = { version = "0.4.19", features = ["serde"] }
serde = { version = "1.0.131", features = ["derive"] }
serde_with = { version = "1.11.0", features = ["hex", "chrono"] }
serde_json = "1.0.72"
serde_urlencoded = "0.7.0"
data-encoding = "2.3.2"
chacha20poly1305 = { version = "0.9.0", features = ["std"] }
once_cell = "1.8.0"
tracing = "0.1.29"
opentelemetry = "0.16.0"
rand = "0.8.4"
mime = "0.3.16"
bincode = "1.3.3"
crc = "2.1.0"
oauth2-types = { path = "../oauth2-types" }
mas-config = { path = "../config" }
mas-templates = { path = "../templates" }
mas-data-model = { path = "../data-model" }
mas-storage = { path = "../storage" }

View File

@ -19,11 +19,11 @@ pub(crate) struct WrappedError(anyhow::Error);
impl warp::reject::Reject for WrappedError {}
pub(crate) fn wrapped_error<T: Into<anyhow::Error>>(e: T) -> impl Reject {
pub fn wrapped_error<T: Into<anyhow::Error>>(e: T) -> impl Reject {
WrappedError(e.into())
}
pub(crate) trait WrapError<T> {
pub trait WrapError<T> {
fn wrap_error(self) -> Result<T, Rejection>;
}

View File

@ -16,7 +16,7 @@
use headers::{authorization::Bearer, Authorization};
use hyper::StatusCode;
use mas_data_model::{AccessToken, Session};
use mas_data_model::{AccessToken, Session, TokenFormatError, TokenType};
use mas_storage::{
oauth2::access_token::{lookup_active_access_token, AccessTokenLookupError},
PostgresqlBackend,
@ -33,10 +33,7 @@ use super::{
database::connection,
headers::{typed_header, InvalidTypedHeader},
};
use crate::{
errors::wrapped_error,
tokens::{TokenFormatError, TokenType},
};
use crate::errors::wrapped_error;
/// Bearer token authentication failed
///

View File

@ -61,7 +61,7 @@ pub fn with_keys(
///
/// use warp::{filters::header::header, reject::MissingHeader, Filter};
///
/// use mas_core::filters::none_on_error;
/// use mas_warp_utils::filters::none_on_error;
///
/// header("Content-Length")
/// .map(Some)

View File

@ -12,20 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#![forbid(unsafe_code)]
#![deny(clippy::all)]
#![deny(rustdoc::broken_intra_doc_links)]
#![warn(clippy::pedantic)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::implicit_hasher)]
pub mod errors;
pub mod filters;
pub mod handlers;
pub mod reply;
pub mod tasks;
pub mod tokens;
pub use self::filters::cors::set_propagator;

View File

@ -19,7 +19,7 @@
//! extern crate warp;
//!
//! use warp::Reply;
//! use mas_core::reply::with_typed_header;
//! use mas_warp_utils::reply::with_typed_header;
//!
//! let reply = r#"{"hello": "world"}"#;
//! let reply = with_typed_header(headers::ContentType::json(), reply);;