From 3bf86c4b2149a313aea84cea919a43a01d035ff1 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 23 Sep 2021 20:53:51 +0200 Subject: [PATCH] Fully document the mas_core::filters module --- crates/core/src/filters/authenticate.rs | 7 ++++++ crates/core/src/filters/client.rs | 22 +++++++++++++++---- crates/core/src/filters/cookies.rs | 11 +++++++++- crates/core/src/filters/csrf.rs | 9 ++++++-- crates/core/src/filters/headers.rs | 4 +++- crates/core/src/filters/mod.rs | 3 +++ crates/core/src/filters/session.rs | 9 ++++++++ .../core/src/handlers/oauth2/introspection.rs | 4 ++-- crates/core/src/handlers/oauth2/token.rs | 4 ++-- crates/core/src/lib.rs | 1 + 10 files changed, 62 insertions(+), 12 deletions(-) diff --git a/crates/core/src/filters/authenticate.rs b/crates/core/src/filters/authenticate.rs index 680b8790..773c076e 100644 --- a/crates/core/src/filters/authenticate.rs +++ b/crates/core/src/filters/authenticate.rs @@ -42,24 +42,31 @@ use crate::{ /// This is recoverable with [`recover_unauthorized`] #[derive(Debug, Error)] pub enum AuthenticationError { + /// The bearer token has an invalid format #[error("invalid token format")] TokenFormat(#[from] TokenFormatError), + /// The bearer token is not an access token #[error("invalid token type {0:?}, expected an access token")] WrongTokenType(TokenType), + /// The access token was not found in the database #[error("unknown token")] TokenNotFound(#[source] AccessTokenLookupError), + /// The access token is no longer active #[error("token is not active")] TokenInactive, + /// The access token expired #[error("token expired")] TokenExpired, + /// The `Authorization` header is missing #[error("missing authorization header")] MissingAuthorizationHeader, + /// The `Authorization` header is invalid #[error("invalid authorization header")] InvalidAuthorizationHeader, } diff --git a/crates/core/src/filters/client.rs b/crates/core/src/filters/client.rs index 32b57c91..acb3619c 100644 --- a/crates/core/src/filters/client.rs +++ b/crates/core/src/filters/client.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Handle client authentication + use headers::{authorization::Basic, Authorization}; use serde::{de::DeserializeOwned, Deserialize}; use thiserror::Error; @@ -20,22 +22,34 @@ use warp::{reject::Reject, Filter, Rejection}; use super::headers::typed_header; use crate::config::{OAuth2ClientConfig, OAuth2Config}; +/// Type of client authentication that succeeded #[derive(Debug, PartialEq, Eq)] pub enum ClientAuthentication { + /// `client_secret_basic` authentication, where the `client_id` and + /// `client_secret` are sent through the `Authorization` header with + /// `Basic` authentication ClientSecretBasic, + + /// `client_secret_post` authentication, where the `client_id` and + /// `client_secret` are sent in the request body ClientSecretPost, + + /// `none` authentication for public clients, where only the `client_id` is + /// sent in the request body None, } impl ClientAuthentication { #[must_use] + /// Check if the authenticated client is public or not pub fn public(&self) -> bool { matches!(self, &Self::None) } } +/// Protect an enpoint with client authentication #[must_use] -pub fn with_client_auth( +pub fn client_authentication( oauth2_config: &OAuth2Config, ) -> impl Filter + Clone @@ -161,7 +175,7 @@ mod tests { #[tokio::test] async fn client_secret_post() { - let filter = with_client_auth::
(&oauth2_config()); + let filter = client_authentication::(&oauth2_config()); let (auth, client, body) = warp::test::request() .method("POST") @@ -178,7 +192,7 @@ mod tests { #[tokio::test] async fn client_secret_basic() { - let filter = with_client_auth::(&oauth2_config()); + let filter = client_authentication::(&oauth2_config()); let (auth, client, body) = warp::test::request() .method("POST") @@ -196,7 +210,7 @@ mod tests { #[tokio::test] async fn none() { - let filter = with_client_auth::(&oauth2_config()); + let filter = client_authentication::(&oauth2_config()); let (auth, client, body) = warp::test::request() .method("POST") diff --git a/crates/core/src/filters/cookies.rs b/crates/core/src/filters/cookies.rs index dafbce3c..95078bd3 100644 --- a/crates/core/src/filters/cookies.rs +++ b/crates/core/src/filters/cookies.rs @@ -12,13 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Deal with encrypted cookies + use std::{convert::Infallible, marker::PhantomData}; use chacha20poly1305::{ aead::{generic_array::GenericArray, Aead, NewAead}, ChaCha20Poly1305, }; -use cookie::Cookie; +use cookie::{Cookie, SameSite}; use data_encoding::BASE64URL_NOPAD; use headers::{Header, HeaderValue, SetCookie}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; @@ -158,6 +160,7 @@ pub fn encrypted_cookie_saver( /// A cookie that can be encrypted with a well-known cookie key pub trait EncryptableCookieValue: Serialize + Send + Sync + std::fmt::Debug { + /// What key should be used for this cookie fn cookie_key() -> &'static str; } @@ -167,6 +170,7 @@ pub struct EncryptedCookieSaver { } impl EncryptedCookieSaver { + /// Save an [`EncryptableCookieValue`] pub fn save_encrypted( &self, cookie: &T, @@ -176,9 +180,14 @@ impl EncryptedCookieSaver { .wrap_error()? .to_cookie_value() .wrap_error()?; + + // TODO: make those options customizable let value = Cookie::build(T::cookie_key(), encrypted) + .http_only(true) + .same_site(SameSite::Strict) .finish() .to_string(); + let header = SetCookie::decode(&mut [HeaderValue::from_str(&value).wrap_error()?].iter()) .wrap_error()?; Ok(with_typed_header(header, reply)) diff --git a/crates/core/src/filters/csrf.rs b/crates/core/src/filters/csrf.rs index 546369d0..2cf84af5 100644 --- a/crates/core/src/filters/csrf.rs +++ b/crates/core/src/filters/csrf.rs @@ -25,20 +25,25 @@ use warp::{reject::Reject, Filter, Rejection}; use super::cookies::EncryptableCookieValue; use crate::config::{CookiesConfig, CsrfConfig}; +/// Failed to validate CSRF token #[derive(Debug, Error)] pub enum CsrfError { + /// The token in the form did not match the token in the cookie #[error("CSRF token mismatch")] Mismatch, + /// The token expired #[error("CSRF token expired")] Expired, + /// Failed to decode the token #[error("could not decode CSRF token")] Decode(#[from] DecodeError), } impl Reject for CsrfError {} +/// A CSRF token #[serde_as] #[derive(Serialize, Deserialize, Debug)] pub struct CsrfToken { @@ -125,8 +130,8 @@ fn csrf_token( /// Extract an up-to-date CSRF token to include in forms /// /// Routes using this should not forget to reply the updated CSRF cookie using -/// an [`super::cookies::EncryptedCookieSaver`] obtained with -/// [`super::cookies::encrypted_cookie_saver`] +/// an [`EncryptedCookieSaver`][`super::cookies::EncryptedCookieSaver`] obtained +/// with [`encrypted_cookie_saver`][`super::cookies::encrypted_cookie_saver`] #[must_use] pub fn updated_csrf_token( cookies_config: &CookiesConfig, diff --git a/crates/core/src/filters/headers.rs b/crates/core/src/filters/headers.rs index 1fc43cce..b5180294 100644 --- a/crates/core/src/filters/headers.rs +++ b/crates/core/src/filters/headers.rs @@ -11,7 +11,9 @@ // 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. -// + +//! Deal with typed headers from the [`headers`] crate + use headers::{Header, HeaderValue}; use thiserror::Error; use warp::{reject::Reject, Filter, Rejection}; diff --git a/crates/core/src/filters/mod.rs b/crates/core/src/filters/mod.rs index 847a68f4..97340ce3 100644 --- a/crates/core/src/filters/mod.rs +++ b/crates/core/src/filters/mod.rs @@ -15,6 +15,7 @@ //! Set of [`warp`] filters #![allow(clippy::unused_async)] // Some warp filters need that +#![deny(missing_docs)] pub mod csrf; // mod errors; @@ -35,6 +36,7 @@ use crate::{ templates::Templates, }; +/// Get the [`Templates`] #[must_use] pub fn with_templates( templates: &Templates, @@ -43,6 +45,7 @@ pub fn with_templates( warp::any().map(move || templates.clone()) } +/// Extract the [`KeySet`] from the [`OAuth2Config`] #[must_use] pub fn with_keys( oauth2_config: &OAuth2Config, diff --git a/crates/core/src/filters/session.rs b/crates/core/src/filters/session.rs index 44b005c7..ccd882bc 100644 --- a/crates/core/src/filters/session.rs +++ b/crates/core/src/filters/session.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Load user sessions from the database + use serde::{Deserialize, Serialize}; use sqlx::{pool::PoolConnection, Executor, PgPool, Postgres}; use thiserror::Error; @@ -31,26 +33,32 @@ use crate::{ storage::{lookup_active_session, user::ActiveSessionLookupError, SessionInfo}, }; +/// The session is missing or failed to load #[derive(Error, Debug)] pub enum SessionLoadError { + /// No session cookie was found #[error("missing session cookie")] MissingCookie, + /// The session cookie is invalid #[error("unable to parse or decrypt session cookie")] InvalidCookie, + /// The session is unknown or inactive #[error("unknown or inactive session")] UnknownSession, } impl Reject for SessionLoadError {} +/// An encrypted cookie to save the session ID #[derive(Serialize, Deserialize, Debug)] pub struct SessionCookie { current: i64, } impl SessionCookie { + /// Forge the cookie from a [`SessionInfo`] #[must_use] pub fn from_session_info(info: &SessionInfo) -> Self { Self { @@ -58,6 +66,7 @@ impl SessionCookie { } } + /// Load the [`SessionInfo`] from database pub async fn load_session_info( &self, executor: impl Executor<'_, Database = Postgres>, diff --git a/crates/core/src/handlers/oauth2/introspection.rs b/crates/core/src/handlers/oauth2/introspection.rs index ea2f56e6..9806774e 100644 --- a/crates/core/src/handlers/oauth2/introspection.rs +++ b/crates/core/src/handlers/oauth2/introspection.rs @@ -22,7 +22,7 @@ use crate::{ config::{OAuth2ClientConfig, OAuth2Config}, errors::WrapError, filters::{ - client::{with_client_auth, ClientAuthentication}, + client::{client_authentication, ClientAuthentication}, database::connection, }, storage::oauth2::{access_token::lookup_access_token, refresh_token::lookup_refresh_token}, @@ -36,7 +36,7 @@ pub fn filter( warp::path!("oauth2" / "introspect") .and(warp::post()) .and(connection(pool)) - .and(with_client_auth(oauth2_config)) + .and(client_authentication(oauth2_config)) .and_then(introspect) .recover(recover) } diff --git a/crates/core/src/handlers/oauth2/token.rs b/crates/core/src/handlers/oauth2/token.rs index 873171c2..7ad14b69 100644 --- a/crates/core/src/handlers/oauth2/token.rs +++ b/crates/core/src/handlers/oauth2/token.rs @@ -40,7 +40,7 @@ use crate::{ config::{KeySet, OAuth2ClientConfig, OAuth2Config}, errors::WrapError, filters::{ - client::{with_client_auth, ClientAuthentication}, + client::{client_authentication, ClientAuthentication}, database::connection, with_keys, }, @@ -91,7 +91,7 @@ pub fn filter( let issuer = oauth2_config.issuer.clone(); warp::path!("oauth2" / "token") .and(warp::post()) - .and(with_client_auth(oauth2_config)) + .and(client_authentication(oauth2_config)) .and(with_keys(oauth2_config)) .and(warp::any().map(move || issuer.clone())) .and(connection(pool)) diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index bbb67ccc..21717359 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -14,6 +14,7 @@ #![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)]