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 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 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 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 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 pub resolved_key: Option<Arc<ResolvedJwtSigningKey>>,
291}