mellon 0.1.0

Library for adding contemporary authentication to rust-based websites.
Documentation
use std::sync::OnceLock;
use std::{env::var, str::FromStr};

use serde::{Deserialize, Serialize};

use poem::Request;
use poem_openapi::{SecurityScheme, auth::Bearer};

use jiff::{Span, Timestamp};
use rand::{Rng, thread_rng};

use jsonwebtoken::{DecodingKey, EncodingKey, Header, TokenData, Validation, decode, encode};
use uuid::Uuid;

struct JwtKeys {
    encode: EncodingKey,
    decode: DecodingKey,
}

static JWT_KEYS: OnceLock<JwtKeys> = OnceLock::new();
static JWT_VALIDATION: OnceLock<Validation> = OnceLock::new();

fn init_jwt_keys() -> JwtKeys {
    if let Ok(secret) = var("JWT_KEY_BASE64") {
        const ERR: &str =
            "Environment variable JWT_KEY_BASE64 must be defined with a valid base64 value";
        JwtKeys {
            encode: EncodingKey::from_base64_secret(&secret).expect(ERR),
            decode: DecodingKey::from_base64_secret(&secret).expect(ERR),
        }
    } else {
        let mut rnd = thread_rng();
        let mut secret = [0u8; 32];
        for byte in &mut secret {
            *byte = rnd.r#gen();
        }
        JwtKeys {
            decode: DecodingKey::from_secret(&secret),
            encode: EncodingKey::from_secret(&secret),
        }
    }
}

fn init_jwt_validation() -> Validation {
    let mut result = Validation::new(jsonwebtoken::Algorithm::HS256);
    result.validate_exp = true;
    result
}

pub fn make_jwt(id: Uuid, duration: Span, permission_to_update_credentials: bool) -> String {
    let header = Header::new(jsonwebtoken::Algorithm::HS256);
    let claims = Claims {
        sub: id,
        exp: Timestamp::now()
            .checked_add(duration)
            .unwrap()
            .as_second()
            .try_into()
            .expect("positive"),
        upd: permission_to_update_credentials,
    };
    let key = &JWT_KEYS.get_or_init(init_jwt_keys).encode;
    encode(&header, &claims, key).unwrap()
}

#[derive(Debug, Serialize, Deserialize)]
struct Claims {
    sub: Uuid,
    exp: usize,
    /// May the owner of the token update credentials?
    upd: bool,
}

pub struct AuthUser {
    pub id: Uuid,
    pub update_creds: bool,
}

async fn bearer_checker(_req: &Request, bearer: Bearer) -> Option<AuthUser> {
    check_jwt(&bearer.token)
}

pub fn check_jwt(token: &str) -> Option<AuthUser> {
    let jwt_key = &JWT_KEYS.get_or_init(init_jwt_keys).decode;
    let validator = JWT_VALIDATION.get_or_init(init_jwt_validation);
    let token: TokenData<Claims> = decode(token, jwt_key, validator).ok()?;
    let id = token.claims.sub.to_string();
    let id = Uuid::from_str(&id).ok()?;
    let update_creds = token.claims.upd;
    Some(AuthUser { id, update_creds })
}

#[derive(SecurityScheme)]
#[oai(ty = "bearer", key_in = "header", checker = "bearer_checker")]
pub struct UserToken(pub AuthUser);