Skip to main content

openauth_plugins/jwt/
claims.rs

1use openauth_core::error::OpenAuthError;
2use serde_json::{Number, Value};
3
4pub type JwtClaims = serde_json::Map<String, Value>;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum TimeInput {
8    Seconds(i64),
9    UnixTimestamp(i64),
10    Duration(String),
11}
12
13pub fn to_exp_jwt(expiration_time: TimeInput, iat: i64) -> Result<i64, OpenAuthError> {
14    match expiration_time {
15        TimeInput::Seconds(value) | TimeInput::UnixTimestamp(value) => Ok(value),
16        TimeInput::Duration(value) => parse_duration(&value).map(|seconds| iat + seconds),
17    }
18}
19
20pub(crate) fn claims_with_defaults(
21    mut claims: JwtClaims,
22    base_url: &str,
23    options: &super::JwtOptions,
24) -> Result<JwtClaims, OpenAuthError> {
25    let now = time::OffsetDateTime::now_utc().unix_timestamp();
26    let iat = numeric_claim(&claims, "iat").unwrap_or(now);
27    claims
28        .entry("iat".to_owned())
29        .or_insert_with(|| Value::Number(Number::from(iat)));
30    if !claims.contains_key("exp") {
31        let exp = to_exp_jwt(
32            options
33                .jwt
34                .expiration_time
35                .clone()
36                .unwrap_or_else(|| TimeInput::Duration("15m".to_owned())),
37            iat,
38        )?;
39        claims.insert("exp".to_owned(), Value::Number(Number::from(exp)));
40    }
41    claims.entry("iss".to_owned()).or_insert_with(|| {
42        Value::String(
43            options
44                .jwt
45                .issuer
46                .clone()
47                .unwrap_or_else(|| base_url.to_owned()),
48        )
49    });
50    if !claims.contains_key("aud") {
51        match &options.jwt.audience {
52            Some(audience) if audience.len() == 1 => {
53                claims.insert("aud".to_owned(), Value::String(audience[0].clone()));
54            }
55            Some(audience) => {
56                claims.insert(
57                    "aud".to_owned(),
58                    Value::Array(audience.iter().cloned().map(Value::String).collect()),
59                );
60            }
61            None => {
62                claims.insert("aud".to_owned(), Value::String(base_url.to_owned()));
63            }
64        }
65    }
66    Ok(claims)
67}
68
69pub(crate) fn numeric_claim(claims: &JwtClaims, name: &str) -> Option<i64> {
70    claims.get(name).and_then(Value::as_i64)
71}
72
73fn parse_duration(value: &str) -> Result<i64, OpenAuthError> {
74    let mut input = value.trim().to_ascii_lowercase();
75    if input.is_empty() {
76        return Err(invalid_duration(value));
77    }
78    let ago = input.ends_with(" ago");
79    if ago {
80        input.truncate(input.len() - 4);
81    }
82    if input.ends_with(" from now") {
83        input.truncate(input.len() - 9);
84    }
85    let negative = input.starts_with('-');
86    if negative {
87        input.remove(0);
88    }
89    let input = input.trim();
90    let number_len = input
91        .char_indices()
92        .take_while(|(_, ch)| ch.is_ascii_digit())
93        .map(|(index, ch)| index + ch.len_utf8())
94        .last()
95        .ok_or_else(|| invalid_duration(value))?;
96    let amount = input[..number_len]
97        .parse::<i64>()
98        .map_err(|_| invalid_duration(value))?;
99    let unit = input[number_len..].trim();
100    let multiplier = match unit {
101        "s" | "sec" | "secs" | "second" | "seconds" => 1,
102        "m" | "min" | "mins" | "minute" | "minutes" => 60,
103        "h" | "hr" | "hrs" | "hour" | "hours" => 60 * 60,
104        "d" | "day" | "days" => 60 * 60 * 24,
105        "w" | "week" | "weeks" => 60 * 60 * 24 * 7,
106        "y" | "yr" | "yrs" | "year" | "years" => 31_557_600,
107        _ => return Err(invalid_duration(value)),
108    };
109    let seconds = amount * multiplier;
110    Ok(if ago || negative { -seconds } else { seconds })
111}
112
113fn invalid_duration(value: &str) -> OpenAuthError {
114    OpenAuthError::InvalidConfig(format!("invalid JWT duration `{value}`"))
115}