Skip to main content

alibi_plugins/jwt/
config.rs

1use super::DefineJwtPayload;
2use super::DefineJwtSubject;
3use super::JwtKeyring;
4use super::ResolvedJwtSigningKey;
5use super::SignRemoteJwt;
6use super::claims::js_primitive_string;
7use super::claims::js_raw_primitive_string;
8use alibi_core::AuthError;
9use alibi_core::AuthResult;
10use chrono::DateTime;
11use chrono::Duration;
12use chrono::Utc;
13use serde::Deserialize;
14use serde::Serialize;
15use serde_json::Map;
16use serde_json::Value;
17use serde_json::json;
18use std::str::FromStr;
19use std::sync::Arc;
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
21pub enum JwtAlgorithm {
22    #[serde(rename = "EdDSA")]
23    EdDsa,
24    #[serde(rename = "ES256")]
25    Es256,
26    #[serde(rename = "ES512")]
27    Es512,
28    #[serde(rename = "PS256")]
29    Ps256,
30    #[serde(rename = "RS256")]
31    Rs256,
32}
33
34impl JwtAlgorithm {
35    #[must_use]
36    pub const fn as_str(self) -> &'static str {
37        match self {
38            Self::EdDsa => "EdDSA",
39            Self::Es256 => "ES256",
40            Self::Es512 => "ES512",
41            Self::Ps256 => "PS256",
42            Self::Rs256 => "RS256",
43        }
44    }
45    #[must_use]
46    pub const fn curve(self) -> Option<&'static str> {
47        match self {
48            Self::EdDsa => Some("Ed25519"),
49            Self::Es256 => Some("P-256"),
50            Self::Es512 => Some("P-521"),
51            Self::Ps256 | Self::Rs256 => None,
52        }
53    }
54}
55
56impl FromStr for JwtAlgorithm {
57    type Err = AuthError;
58    fn from_str(value: &str) -> AuthResult<Self> {
59        match value {
60            "EdDSA" => Ok(Self::EdDsa),
61            "ES256" => Ok(Self::Es256),
62            "ES512" => Ok(Self::Es512),
63            "PS256" => Ok(Self::Ps256),
64            "RS256" => Ok(Self::Rs256),
65            _ => Err(AuthError::config(format!(
66                "Unsupported JWT algorithm: {value}"
67            ))),
68        }
69    }
70}
71
72#[derive(Clone, Debug)]
73pub struct JwtKeyPairConfig {
74    pub algorithm: JwtAlgorithm,
75    pub modulus_length: Option<usize>,
76}
77
78impl Default for JwtKeyPairConfig {
79    fn default() -> Self {
80        Self {
81            algorithm: JwtAlgorithm::EdDsa,
82            modulus_length: None,
83        }
84    }
85}
86
87#[derive(Clone, Debug)]
88pub enum JwtExpiration {
89    After(Duration),
90    /// Relative lifetime retaining the Source floating-point seconds.
91    AfterSeconds(f64),
92    At(DateTime<Utc>),
93    Numeric(f64),
94}
95
96impl Default for JwtExpiration {
97    fn default() -> Self {
98        Self::After(Duration::minutes(15))
99    }
100}
101
102impl JwtExpiration {
103    #[expect(
104        clippy::as_conversions,
105        clippy::cast_precision_loss,
106        reason = "Remote callbacks observe JavaScript IEEE754 arithmetic"
107    )]
108    pub(in crate::jwt) fn timestamp_raw(
109        &self,
110        issued_at: Option<&alibi_core::utils::json::JsValue>,
111    ) -> alibi_core::utils::json::JsValue {
112        use alibi_core::utils::json::JsValue;
113        let timestamp = match self {
114            Self::After(duration) => {
115                let seconds = (duration.num_milliseconds() as f64 / 1000.0 + 0.5).floor();
116                let base = match issued_at {
117                    None | Some(JsValue::Null) => Utc::now().timestamp() as f64,
118                    Some(JsValue::Bool(value)) => f64::from(u8::from(*value)),
119                    Some(JsValue::Number(value)) => *value,
120                    Some(value) => {
121                        return JsValue::String(format!(
122                            "{}{seconds}",
123                            js_raw_primitive_string(value)
124                        ));
125                    }
126                };
127                base + seconds
128            }
129            Self::AfterSeconds(seconds) => {
130                let base = match issued_at {
131                    None | Some(JsValue::Null) => Utc::now().timestamp() as f64,
132                    Some(JsValue::Bool(value)) => f64::from(u8::from(*value)),
133                    Some(JsValue::Number(value)) => *value,
134                    Some(value) => {
135                        return JsValue::String(format!(
136                            "{}{}",
137                            js_raw_primitive_string(value),
138                            ryu_js::Buffer::new().format(*seconds)
139                        ));
140                    }
141                };
142                base + seconds
143            }
144            Self::At(date) => date.timestamp() as f64,
145            Self::Numeric(value) => *value,
146        };
147        JsValue::Number(timestamp)
148    }
149
150    #[expect(
151        clippy::as_conversions,
152        clippy::cast_possible_truncation,
153        clippy::cast_precision_loss,
154        reason = "JavaScript-compatible numbers deliberately retain IEEE754 rounding and guarded integer coercion at the wire or adapter boundary"
155    )]
156    pub(crate) fn timestamp(&self, issued_at: Option<&Value>) -> Value {
157        let timestamp = match self {
158            Self::After(duration) => {
159                let seconds = (duration.num_milliseconds() as f64 / 1000.0 + 0.5).floor();
160                let base = match issued_at {
161                    None | Some(Value::Null) => Utc::now().timestamp() as f64,
162                    Some(Value::Bool(value)) => f64::from(u8::from(*value)),
163                    Some(Value::Number(value)) => value.as_f64().unwrap_or_default(),
164                    Some(value) => {
165                        // toExpJWT uses JavaScript addition before JOSE parses a
166                        // relative NumericDate. Preserve string concatenation,
167                        // including arrays' and objects' primitive conversion.
168                        return json!(format!(
169                            "{}{}",
170                            js_primitive_string(value),
171                            ryu_js::Buffer::new().format(seconds)
172                        ));
173                    }
174                };
175                base + seconds
176            }
177            Self::AfterSeconds(seconds) => {
178                let base = match issued_at {
179                    None | Some(Value::Null) => Utc::now().timestamp() as f64,
180                    Some(Value::Bool(value)) => f64::from(u8::from(*value)),
181                    Some(Value::Number(value)) => value.as_f64().unwrap_or_default(),
182                    Some(value) => {
183                        return json!(format!(
184                            "{}{}",
185                            js_primitive_string(value),
186                            ryu_js::Buffer::new().format(*seconds)
187                        ));
188                    }
189                };
190                base + seconds
191            }
192            Self::At(date) => date.timestamp() as f64,
193            Self::Numeric(value) => *value,
194        };
195        if timestamp.fract() == 0.0 && timestamp >= i64::MIN as f64 && timestamp < i64::MAX as f64 {
196            json!(timestamp as i64)
197        } else {
198            json!(timestamp)
199        }
200    }
201}
202
203#[derive(Clone, Debug, Serialize, Deserialize)]
204#[serde(untagged)]
205pub enum JwtAudience {
206    One(String),
207    Many(Vec<String>),
208}
209
210impl JwtAudience {
211    pub(in crate::jwt) fn matches(&self, value: &Value) -> bool {
212        let expected: Vec<&str> = match self {
213            Self::One(value_2) => vec![value_2],
214            Self::Many(values) => values.iter().map(String::as_str).collect(),
215        };
216        match value {
217            Value::String(value) => expected.contains(&value.as_str()),
218            Value::Array(values) => values
219                .iter()
220                .filter_map(Value::as_str)
221                .any(|value_3| expected.contains(&value_3)),
222            Value::Null | Value::Bool(_) | Value::Number(_) | Value::Object(_) => false,
223        }
224    }
225}
226
227#[derive(Clone, Debug, Default)]
228pub struct JwtClaimsConfig {
229    pub issuer: Option<String>,
230    pub audience: Option<JwtAudience>,
231    pub expiration: JwtExpiration,
232}
233
234#[derive(Clone)]
235pub struct JwtPluginConfig {
236    pub jwks_path: String,
237    pub remote_url: Option<String>,
238    pub key_pair: JwtKeyPairConfig,
239    pub additional_key_pairs: Vec<JwtKeyPairConfig>,
240    pub rotation_interval: Option<Duration>,
241    pub grace_period: Duration,
242    pub disable_private_key_encryption: bool,
243    pub disable_setting_jwt_header: bool,
244    /// Protect JWT session cookies using the managed local keyring.
245    pub session_cookie_cache: bool,
246    pub claims: JwtClaimsConfig,
247    pub define_payload: Option<Arc<dyn DefineJwtPayload>>,
248    pub define_subject: Option<Arc<dyn DefineJwtSubject>>,
249    pub keyring: Option<Arc<dyn JwtKeyring>>,
250    pub remote_signer: Option<Arc<dyn SignRemoteJwt>>,
251}
252
253impl std::fmt::Debug for JwtPluginConfig {
254    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255        f.debug_struct("JwtPluginConfig").finish_non_exhaustive()
256    }
257}
258
259impl Default for JwtPluginConfig {
260    fn default() -> Self {
261        Self {
262            jwks_path: "/jwks".to_owned(),
263            remote_url: None,
264            key_pair: JwtKeyPairConfig::default(),
265            additional_key_pairs: vec![],
266            rotation_interval: None,
267            grace_period: Duration::days(30),
268            disable_private_key_encryption: false,
269            disable_setting_jwt_header: false,
270            session_cookie_cache: false,
271            claims: JwtClaimsConfig::default(),
272            define_payload: None,
273            define_subject: None,
274            keyring: None,
275            remote_signer: None,
276        }
277    }
278}
279
280#[derive(Clone, Debug, Default)]
281pub struct JwtSignOptions {
282    /// None omits the header argument; Some(empty) supplies an empty object.
283    pub header: Option<Map<String, Value>>,
284    pub signing_key_id: Option<String>,
285    pub signing_algorithm: Option<JwtAlgorithm>,
286    pub claims: Option<JwtClaimsConfig>,
287    /// Reuse a key selected before constructing the payload, for example when
288    /// its algorithm determines an OIDC token hash. This avoids another
289    /// storage read. Remote signers continue to own key selection.
290    pub resolved_key: Option<Arc<ResolvedJwtSigningKey>>,
291}