openauth-plugins 0.0.4

Official OpenAuth plugin modules.
Documentation
use openauth_core::error::OpenAuthError;
use serde_json::{Number, Value};

pub type JwtClaims = serde_json::Map<String, Value>;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TimeInput {
    Seconds(i64),
    UnixTimestamp(i64),
    Duration(String),
}

pub fn to_exp_jwt(expiration_time: TimeInput, iat: i64) -> Result<i64, OpenAuthError> {
    match expiration_time {
        TimeInput::Seconds(value) | TimeInput::UnixTimestamp(value) => Ok(value),
        TimeInput::Duration(value) => parse_duration(&value).map(|seconds| iat + seconds),
    }
}

pub(crate) fn claims_with_defaults(
    mut claims: JwtClaims,
    base_url: &str,
    options: &super::JwtOptions,
) -> Result<JwtClaims, OpenAuthError> {
    let now = time::OffsetDateTime::now_utc().unix_timestamp();
    let iat = numeric_claim(&claims, "iat").unwrap_or(now);
    claims
        .entry("iat".to_owned())
        .or_insert_with(|| Value::Number(Number::from(iat)));
    if !claims.contains_key("exp") {
        let exp = to_exp_jwt(
            options
                .jwt
                .expiration_time
                .clone()
                .unwrap_or_else(|| TimeInput::Duration("15m".to_owned())),
            iat,
        )?;
        claims.insert("exp".to_owned(), Value::Number(Number::from(exp)));
    }
    claims.entry("iss".to_owned()).or_insert_with(|| {
        Value::String(
            options
                .jwt
                .issuer
                .clone()
                .unwrap_or_else(|| base_url.to_owned()),
        )
    });
    if !claims.contains_key("aud") {
        match &options.jwt.audience {
            Some(audience) if audience.len() == 1 => {
                claims.insert("aud".to_owned(), Value::String(audience[0].clone()));
            }
            Some(audience) => {
                claims.insert(
                    "aud".to_owned(),
                    Value::Array(audience.iter().cloned().map(Value::String).collect()),
                );
            }
            None => {
                claims.insert("aud".to_owned(), Value::String(base_url.to_owned()));
            }
        }
    }
    Ok(claims)
}

pub(crate) fn numeric_claim(claims: &JwtClaims, name: &str) -> Option<i64> {
    claims.get(name).and_then(Value::as_i64)
}

fn parse_duration(value: &str) -> Result<i64, OpenAuthError> {
    let mut input = value.trim().to_ascii_lowercase();
    if input.is_empty() {
        return Err(invalid_duration(value));
    }
    let ago = input.ends_with(" ago");
    if ago {
        input.truncate(input.len() - 4);
    }
    if input.ends_with(" from now") {
        input.truncate(input.len() - 9);
    }
    let negative = input.starts_with('-');
    if negative {
        input.remove(0);
    }
    let input = input.trim();
    let number_len = input
        .char_indices()
        .take_while(|(_, ch)| ch.is_ascii_digit())
        .map(|(index, ch)| index + ch.len_utf8())
        .last()
        .ok_or_else(|| invalid_duration(value))?;
    let amount = input[..number_len]
        .parse::<i64>()
        .map_err(|_| invalid_duration(value))?;
    let unit = input[number_len..].trim();
    let multiplier = match unit {
        "s" | "sec" | "secs" | "second" | "seconds" => 1,
        "m" | "min" | "mins" | "minute" | "minutes" => 60,
        "h" | "hr" | "hrs" | "hour" | "hours" => 60 * 60,
        "d" | "day" | "days" => 60 * 60 * 24,
        "w" | "week" | "weeks" => 60 * 60 * 24 * 7,
        "y" | "yr" | "yrs" | "year" | "years" => 31_557_600,
        _ => return Err(invalid_duration(value)),
    };
    let seconds = amount * multiplier;
    Ok(if ago || negative { -seconds } else { seconds })
}

fn invalid_duration(value: &str) -> OpenAuthError {
    OpenAuthError::InvalidConfig(format!("invalid JWT duration `{value}`"))
}