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