Auth filters

This commit is contained in:
Raphaël Thériault 2020-08-12 01:12:22 -04:00
parent 8a6824b7c0
commit d3032176e4
7 changed files with 165 additions and 6 deletions

9
Cargo.lock generated
View File

@ -122,7 +122,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "base64"
version = "0.12.2"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@ -349,6 +349,7 @@ version = "0.3.0"
dependencies = [
"anyhow 1.0.32 (registry+https://github.com/rust-lang/crates.io-index)",
"askama 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)",
"base64 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl 0.10.30 (registry+https://github.com/rust-lang/crates.io-index)",
@ -542,7 +543,7 @@ name = "headers"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"base64 0.12.2 (registry+https://github.com/rust-lang/crates.io-index)",
"base64 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
"bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"bytes 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)",
"headers-core 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1143,7 +1144,7 @@ name = "rust-argon2"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"base64 0.12.2 (registry+https://github.com/rust-lang/crates.io-index)",
"base64 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
"blake2b_simd 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)",
"constant_time_eq 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1929,7 +1930,7 @@ dependencies = [
"checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2"
"checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
"checksum base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7"
"checksum base64 0.12.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e223af0dc48c96d4f8342ec01a4974f139df863896b316681efd36742f22cc67"
"checksum base64 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
"checksum blake2b_simd 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a"
"checksum block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b"

View File

@ -17,6 +17,7 @@ license = "MIT"
[dependencies]
anyhow = "1.0.32"
askama = "0.10.3"
base64 = "0.12.3"
chrono = { version = "0.4.13", features = ["serde"] }
futures = "0.3.5"
rand = "0.7.3"

View File

@ -1,9 +1,69 @@
use crate::{
db,
db::models::User,
reject::{self, TryExt},
};
use anyhow::Result;
use argon2::Config;
use rand::Rng;
use sqlx::SqlitePool;
use tokio::task;
use warp::{Filter, Rejection};
pub fn auth_optional(
pool: &'static SqlitePool,
) -> impl Filter<Extract = (Option<User>,), Error = Rejection> + Copy + Send + Sync + 'static {
warp::header::optional("Authorization").and_then(move |header| async move {
match header {
Some(h) => match user(h, pool).await {
Ok(u) => Ok(Some(u)),
Err(e) => Err(e),
},
None => Ok(None),
}
})
}
pub fn auth_required(
pool: &'static SqlitePool,
) -> impl Filter<Extract = (User,), Error = Rejection> + Copy + Send + Sync + 'static {
warp::header::header("Authorization").and_then(move |header| user(header, pool))
}
#[tracing::instrument(level = "debug")]
async fn user(header: String, pool: &SqlitePool) -> Result<User, Rejection> {
if &header[..5] != "Basic" {
return Err(reject::unauthorized());
}
let decoded = base64::decode(&header[6..]).or_401()?;
let (user, password) = {
let mut split = None;
for (i, b) in decoded.iter().copied().enumerate() {
if b == b':' {
split = Some(i);
}
}
let split = split.or_401()?;
let (u, p) = (&decoded[..split], &decoded[(split + 1)..]);
(std::str::from_utf8(u).or_401()?, p)
};
let user = db::user(user, pool).await.or_500()?.or_401()?;
if !verify(user.password.clone(), password.to_owned())
.await
.or_500()?
{
return Err(reject::unauthorized());
}
Ok(user)
}
// TODO: Allow custom configuration
#[tracing::instrument(level = "debug", skip(password))]
async fn hash(password: Vec<u8>) -> Result<String> {
let config = Config::default();
Ok(task::spawn_blocking(move || {
@ -13,6 +73,7 @@ async fn hash(password: Vec<u8>) -> Result<String> {
.await??)
}
#[tracing::instrument(level = "debug", skip(encoded, password))]
async fn verify(encoded: String, password: Vec<u8>) -> Result<bool> {
Ok(task::spawn_blocking(move || argon2::verify_encoded(&encoded, &password)).await??)
}

View File

@ -1,4 +1,4 @@
use crate::utils::DefaultExt;
use crate::util::DefaultExt;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::{

View File

@ -1,9 +1,10 @@
mod auth;
mod config;
mod db;
mod reject;
mod routes;
mod runtime;
mod utils;
mod util;
use anyhow::Error;
use config::Config;

95
src/reject.rs Normal file
View File

@ -0,0 +1,95 @@
use std::fmt::Display;
use warp::{
http::StatusCode,
reject::{Reject, Rejection},
reply::{Reply, Response},
};
#[derive(Debug, Copy, Clone)]
enum FiliteRejection {
NotFound,
Unauthorized,
InternalServerError,
}
impl Reject for FiliteRejection {}
impl Reply for FiliteRejection {
fn into_response(self) -> Response {
match self {
FiliteRejection::NotFound => {
warp::reply::with_status("Not Found", StatusCode::NOT_FOUND).into_response()
}
FiliteRejection::Unauthorized => warp::reply::with_status(
warp::reply::with_header(
"Unauthorized",
"WWW-Authenticate",
r#"Basic realm="filite""#,
),
StatusCode::UNAUTHORIZED,
)
.into_response(),
FiliteRejection::InternalServerError => {
warp::reply::with_status("Internal Server Error", StatusCode::INTERNAL_SERVER_ERROR)
.into_response()
}
}
}
}
#[inline]
pub fn unauthorized() -> Rejection {
warp::reject::custom(FiliteRejection::Unauthorized)
}
pub trait TryExt<T> {
fn or_404(self) -> Result<T, Rejection>;
fn or_401(self) -> Result<T, Rejection>;
fn or_500(self) -> Result<T, Rejection>;
}
impl<T, E: Display> TryExt<T> for Result<T, E> {
fn or_404(self) -> Result<T, Rejection> {
self.map_err(|e| {
tracing::info!("{}", e);
warp::reject::custom(FiliteRejection::NotFound)
})
}
fn or_401(self) -> Result<T, Rejection> {
self.map_err(|e| {
tracing::info!("{}", e);
warp::reject::custom(FiliteRejection::Unauthorized)
})
}
fn or_500(self) -> Result<T, Rejection> {
self.map_err(|e| {
tracing::error!("{}", e);
warp::reject::custom(FiliteRejection::InternalServerError)
})
}
}
impl<T> TryExt<T> for Option<T> {
fn or_404(self) -> Result<T, Rejection> {
self.ok_or_else(|| warp::reject::custom(FiliteRejection::NotFound))
}
fn or_401(self) -> Result<T, Rejection> {
self.ok_or_else(|| warp::reject::custom(FiliteRejection::Unauthorized))
}
fn or_500(self) -> Result<T, Rejection> {
self.ok_or_else(|| warp::reject::custom(FiliteRejection::InternalServerError))
}
}
#[tracing::instrument(level = "debug")]
pub async fn handle_rejections(err: Rejection) -> Result<impl Reply, Rejection> {
if err.is_not_found() {
Ok(FiliteRejection::NotFound)
} else if let Some(err) = err.find::<FiliteRejection>() {
Ok(*err)
} else {
Err(err)
}
}