Skip to main content

alibi_api/plugins/jwt/
config.rs

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