openauth_plugins/jwt/
claims.rs1use 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}