#![deny(missing_docs)]
use async_trait::async_trait;
use churust_core::{
AppBuilder, Call, Error, FromCallParts, Middleware, Next, Phase, Plugin, Response, Result,
};
use http::header::WWW_AUTHENTICATE;
use http::{HeaderValue, StatusCode};
use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use std::sync::Arc;
type BoxFut<T> = Pin<Box<dyn Future<Output = T> + Send>>;
#[derive(Debug, Clone)]
pub struct Principal<P>(
pub P,
);
#[async_trait]
impl<P> FromCallParts for Principal<P>
where
P: Clone + Send + Sync + 'static,
{
async fn from_call_parts(call: &mut Call) -> Result<Self> {
match call.get::<P>() {
Some(p) => Ok(Principal(p)),
None => Err(
Error::new(StatusCode::UNAUTHORIZED, "authentication required")
.with_response_header(WWW_AUTHENTICATE, HeaderValue::from_static("Bearer")),
),
}
}
}
pub struct Bearer<P, F> {
verify: Arc<F>,
_p: PhantomData<fn() -> P>,
}
pub struct Auth;
impl Auth {
pub fn bearer<P, F, Fut>(verify: F) -> Bearer<P, F>
where
F: Fn(String) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Option<P>> + Send + 'static,
P: Clone + Send + Sync + 'static,
{
Bearer {
verify: Arc::new(verify),
_p: PhantomData,
}
}
pub fn basic<P, F, Fut>(verify: F) -> Basic<P, F>
where
F: Fn(String, String) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Option<P>> + Send + 'static,
P: Clone + Send + Sync + 'static,
{
Basic {
verify: Arc::new(verify),
_p: PhantomData,
}
}
}
impl<P, F, Fut> Plugin for Bearer<P, F>
where
F: Fn(String) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Option<P>> + Send + 'static,
P: Clone + Send + Sync + 'static,
{
fn install(self: Box<Self>, app: &mut AppBuilder) {
let verify = self.verify.clone();
app.add_middleware_in(
Phase::Plugins,
Arc::new(BearerMiddleware::<P> {
verify: Arc::new(move |token: String| {
let verify = verify.clone();
Box::pin(async move { verify(token).await }) as BoxFut<Option<P>>
}),
}),
);
}
}
struct BearerMiddleware<P> {
#[allow(clippy::type_complexity)]
verify: Arc<dyn Fn(String) -> BoxFut<Option<P>> + Send + Sync>,
}
#[async_trait]
impl<P> Middleware for BearerMiddleware<P>
where
P: Clone + Send + Sync + 'static,
{
async fn handle(&self, mut call: Call, next: Next) -> Response {
if let Some(raw) = call.header("authorization") {
if let Some(token) = raw
.strip_prefix("Bearer ")
.or_else(|| raw.strip_prefix("bearer "))
{
if let Some(principal) = (self.verify)(token.trim().to_string()).await {
call.insert(principal);
}
}
}
next.run(call).await
}
}
pub struct Basic<P, F> {
verify: Arc<F>,
_p: PhantomData<fn() -> P>,
}
impl<P, F, Fut> Plugin for Basic<P, F>
where
F: Fn(String, String) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Option<P>> + Send + 'static,
P: Clone + Send + Sync + 'static,
{
fn install(self: Box<Self>, app: &mut AppBuilder) {
let verify = self.verify.clone();
app.add_middleware_in(
Phase::Plugins,
Arc::new(BasicMiddleware::<P> {
verify: Arc::new(move |u: String, p: String| {
let verify = verify.clone();
Box::pin(async move { verify(u, p).await }) as BoxFut<Option<P>>
}),
}),
);
}
}
struct BasicMiddleware<P> {
#[allow(clippy::type_complexity)]
verify: Arc<dyn Fn(String, String) -> BoxFut<Option<P>> + Send + Sync>,
}
#[async_trait]
impl<P> Middleware for BasicMiddleware<P>
where
P: Clone + Send + Sync + 'static,
{
async fn handle(&self, mut call: Call, next: Next) -> Response {
if let Some((user, pass)) = call.header("authorization").and_then(decode_basic) {
if let Some(principal) = (self.verify)(user, pass).await {
call.insert(principal);
}
}
next.run(call).await
}
}
fn decode_basic(header: &str) -> Option<(String, String)> {
use base64::Engine;
let b64 = header
.strip_prefix("Basic ")
.or_else(|| header.strip_prefix("basic "))?;
let decoded = base64::engine::general_purpose::STANDARD
.decode(b64.trim())
.ok()?;
let text = String::from_utf8(decoded).ok()?;
let (user, pass) = text.split_once(':')?;
Some((user.to_string(), pass.to_string()))
}
pub struct Jwt<C> {
key: jsonwebtoken::DecodingKey,
validation: jsonwebtoken::Validation,
_c: PhantomData<fn() -> C>,
}
impl Auth {
pub fn jwt_hs256<C>(secret: &[u8]) -> Jwt<C>
where
C: serde::de::DeserializeOwned + Clone + Send + Sync + 'static,
{
Jwt {
key: jsonwebtoken::DecodingKey::from_secret(secret),
validation: jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256),
_c: PhantomData,
}
}
}
impl<C> Plugin for Jwt<C>
where
C: serde::de::DeserializeOwned + Clone + Send + Sync + 'static,
{
fn install(self: Box<Self>, app: &mut AppBuilder) {
app.add_middleware_in(
Phase::Plugins,
Arc::new(JwtMiddleware::<C> {
key: Arc::new(self.key),
validation: Arc::new(self.validation),
_c: PhantomData,
}),
);
}
}
struct JwtMiddleware<C> {
key: Arc<jsonwebtoken::DecodingKey>,
validation: Arc<jsonwebtoken::Validation>,
_c: PhantomData<fn() -> C>,
}
#[async_trait]
impl<C> Middleware for JwtMiddleware<C>
where
C: serde::de::DeserializeOwned + Clone + Send + Sync + 'static,
{
async fn handle(&self, mut call: Call, next: Next) -> Response {
if let Some(raw) = call.header("authorization") {
if let Some(token) = raw
.strip_prefix("Bearer ")
.or_else(|| raw.strip_prefix("bearer "))
{
if let Ok(data) =
jsonwebtoken::decode::<C>(token.trim(), &self.key, &self.validation)
{
call.insert(data.claims);
}
}
}
next.run(call).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use churust_core::{App, Churust, TestClient};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq)]
struct User {
name: String,
}
fn bearer_app() -> App {
Churust::server()
.install(Auth::bearer(|token: String| async move {
if token == "secret" {
Some(User { name: "ana".into() })
} else {
None
}
}))
.routing(|r| {
r.get("/me", |Principal(u): Principal<User>| async move {
format!("hello {}", u.name)
});
})
.build()
}
#[tokio::test]
async fn valid_bearer_reaches_protected_route() {
let client = TestClient::new(bearer_app());
let res = client
.get("/me")
.header("authorization", "Bearer secret")
.send()
.await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text(), "hello ana");
}
#[tokio::test]
async fn missing_token_is_401_with_challenge() {
let client = TestClient::new(bearer_app());
let res = client.get("/me").send().await;
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
assert_eq!(res.header("www-authenticate"), Some("Bearer"));
}
#[tokio::test]
async fn wrong_token_is_401() {
let client = TestClient::new(bearer_app());
let res = client
.get("/me")
.header("authorization", "Bearer nope")
.send()
.await;
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn basic_auth_works() {
let app = Churust::server()
.install(Auth::basic(|u: String, p: String| async move {
if u == "admin" && p == "pw" {
Some(User { name: u })
} else {
None
}
}))
.routing(|r| {
r.get("/me", |Principal(u): Principal<User>| async move { u.name });
})
.build();
let client = TestClient::new(app);
let res = client
.get("/me")
.header("authorization", "Basic YWRtaW46cHc=")
.send()
.await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text(), "admin");
}
#[derive(Serialize, Deserialize, Clone, Debug)]
struct Claims {
sub: String,
exp: usize,
}
#[tokio::test]
async fn jwt_decodes_claims() {
use jsonwebtoken::{encode, EncodingKey, Header};
let secret = b"topsecret";
let claims = Claims {
sub: "u42".into(),
exp: 9_999_999_999,
};
let token = encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(secret),
)
.unwrap();
let app = Churust::server()
.install(Auth::jwt_hs256::<Claims>(secret))
.routing(|r| {
r.get(
"/who",
|Principal(c): Principal<Claims>| async move { c.sub },
);
})
.build();
let client = TestClient::new(app);
let res = client
.get("/who")
.header("authorization", &format!("Bearer {token}"))
.send()
.await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text(), "u42");
}
}