1use std::sync::Arc;
23
24use jsonwebtoken::{
25 Algorithm, DecodingKey, EncodingKey, Header, TokenData, Validation, decode, decode_header,
26 encode,
27};
28use parking_lot::RwLock;
29use serde::Serialize;
30use serde::de::DeserializeOwned;
31
32use crate::error::{Error, Result};
33
34pub struct ActiveKey {
39 pub kid: String,
40 pub alg: Algorithm,
41 pub encoding_key: EncodingKey,
42 pub decoding_key: DecodingKey,
43 pub expires_at: Option<f64>,
44}
45
46pub struct HistoryKey {
49 pub kid: String,
50 pub alg: Algorithm,
51 pub decoding_key: DecodingKey,
52}
53
54struct Inner {
55 active: Option<ActiveKey>,
56 history: Vec<HistoryKey>,
57 issuer: String,
58 audience: Vec<String>,
59}
60
61#[derive(Clone)]
64pub struct JwtConfig {
65 inner: Arc<RwLock<Inner>>,
66}
67
68impl JwtConfig {
69 pub fn new(issuer: String, audience: Vec<String>) -> Self {
73 Self {
74 inner: Arc::new(RwLock::new(Inner {
75 active: None,
76 history: Vec::new(),
77 issuer,
78 audience,
79 })),
80 }
81 }
82
83 pub fn set_active(&self, active: ActiveKey, history: Vec<HistoryKey>) {
86 let mut guard = self.inner.write();
87 guard.active = Some(active);
88 guard.history = history;
89 }
90
91 pub fn issue<T: Serialize>(&self, claims: &T) -> Result<String> {
94 let guard = self.inner.read();
95 let active = guard
96 .active
97 .as_ref()
98 .ok_or_else(|| Error::Jwt("no active jwt key configured".to_string()))?;
99 let mut header = Header::new(active.alg);
100 header.kid = Some(active.kid.clone());
101 encode(&header, claims, &active.encoding_key).map_err(map_jwt_err)
102 }
103
104 pub fn verify<T: DeserializeOwned>(&self, token: &str) -> Result<TokenData<T>> {
108 let header = decode_header(token).map_err(map_jwt_err)?;
109 let kid = header
110 .kid
111 .as_deref()
112 .ok_or_else(|| Error::Jwt("token has no kid header".to_string()))?;
113 let guard = self.inner.read();
114 let (alg, decoding_key) = lookup_decoding_key(&guard, kid)
115 .ok_or_else(|| Error::Jwt(format!("unknown kid {kid}")))?;
116 let mut validation = Validation::new(alg);
117 validation.set_issuer(std::slice::from_ref(&guard.issuer));
118 if !guard.audience.is_empty() {
119 validation.set_audience(&guard.audience);
120 }
121 decode::<T>(token, decoding_key, &validation).map_err(map_jwt_err)
122 }
123
124 pub fn active_kid(&self) -> Option<String> {
127 self.inner.read().active.as_ref().map(|k| k.kid.clone())
128 }
129
130 pub fn issuer(&self) -> String {
135 self.inner.read().issuer.clone()
136 }
137
138 pub fn audience(&self) -> Vec<String> {
140 self.inner.read().audience.clone()
141 }
142
143 #[cfg(feature = "backend-postgres")]
148 pub async fn load_from_postgres(&self, pool: &sqlx::PgPool) -> Result<()> {
149 use sqlx::Row;
150 let rows = sqlx::query(
151 "SELECT kid, alg, private_pem_encrypted, rotated_at, expires_at
152 FROM auth.jwks_keys
153 ORDER BY created_at",
154 )
155 .fetch_all(pool)
156 .await
157 .map_err(|e| Error::Backend(anyhow::anyhow!("load auth.jwks_keys (pg): {e}")))?;
158
159 let mut active = None;
160 let mut history = Vec::new();
161 for row in rows {
162 let kid: String = row.get("kid");
163 let alg_str: String = row.get("alg");
164 let pem: Option<Vec<u8>> = row.get("private_pem_encrypted");
165 let rotated_at: Option<f64> = row.get("rotated_at");
166 let expires_at: Option<f64> = row.get("expires_at");
167 let alg = parse_alg(&alg_str)?;
168 let pem = pem.ok_or_else(|| {
169 Error::Jwt(format!("auth.jwks_keys row {kid} has no private key"))
170 })?;
171 let (encoding_key, decoding_key) = build_keys(alg, &pem)?;
172 if rotated_at.is_none() && active.is_none() {
173 active = Some(ActiveKey {
174 kid: kid.clone(),
175 alg,
176 encoding_key,
177 decoding_key,
178 expires_at,
179 });
180 } else {
181 history.push(HistoryKey {
182 kid,
183 alg,
184 decoding_key,
185 });
186 }
187 }
188 let mut guard = self.inner.write();
189 guard.active = active;
190 guard.history = history;
191 Ok(())
192 }
193
194 #[cfg(feature = "backend-sqlite")]
196 pub async fn load_from_sqlite(&self, pool: &sqlx::SqlitePool) -> Result<()> {
197 use sqlx::Row;
198 let rows = sqlx::query(
199 "SELECT kid, alg, private_pem_encrypted, rotated_at, expires_at
200 FROM auth.jwks_keys
201 ORDER BY created_at",
202 )
203 .fetch_all(pool)
204 .await
205 .map_err(|e| Error::Backend(anyhow::anyhow!("load auth.jwks_keys (sqlite): {e}")))?;
206
207 let mut active = None;
208 let mut history = Vec::new();
209 for row in rows {
210 let kid: String = row.get("kid");
211 let alg_str: String = row.get("alg");
212 let pem: Option<Vec<u8>> = row.get("private_pem_encrypted");
213 let rotated_at: Option<f64> = row.get("rotated_at");
214 let expires_at: Option<f64> = row.get("expires_at");
215 let alg = parse_alg(&alg_str)?;
216 let pem = pem.ok_or_else(|| {
217 Error::Jwt(format!("auth.jwks_keys row {kid} has no private key"))
218 })?;
219 let (encoding_key, decoding_key) = build_keys(alg, &pem)?;
220 if rotated_at.is_none() && active.is_none() {
221 active = Some(ActiveKey {
222 kid: kid.clone(),
223 alg,
224 encoding_key,
225 decoding_key,
226 expires_at,
227 });
228 } else {
229 history.push(HistoryKey {
230 kid,
231 alg,
232 decoding_key,
233 });
234 }
235 }
236 let mut guard = self.inner.write();
237 guard.active = active;
238 guard.history = history;
239 Ok(())
240 }
241
242 #[cfg(feature = "backend-postgres")]
246 pub async fn rotate_postgres(&self, pool: &sqlx::PgPool) -> Result<String> {
247 let GeneratedKey {
248 kid,
249 alg,
250 private_pem,
251 public_jwk,
252 } = generate_ed25519_key();
253 let (encoding_key, decoding_key) = build_keys(alg, private_pem.as_bytes())?;
254 let now = now_secs();
255 let mut tx = pool
256 .begin()
257 .await
258 .map_err(|e| Error::Backend(anyhow::anyhow!("begin tx (pg rotate): {e}")))?;
259 sqlx::query("UPDATE auth.jwks_keys SET rotated_at = $1 WHERE rotated_at IS NULL")
260 .bind(now)
261 .execute(&mut *tx)
262 .await
263 .map_err(|e| Error::Backend(anyhow::anyhow!("mark old key rotated (pg): {e}")))?;
264 sqlx::query(
265 "INSERT INTO auth.jwks_keys
266 (kid, alg, public_jwk, private_pem_encrypted, created_at, rotated_at, expires_at)
267 VALUES ($1, $2, $3::jsonb, $4, $5, NULL, NULL)",
268 )
269 .bind(&kid)
270 .bind(alg_str(alg))
271 .bind(public_jwk.to_string())
272 .bind(private_pem.as_bytes())
273 .bind(now)
274 .execute(&mut *tx)
275 .await
276 .map_err(|e| Error::Backend(anyhow::anyhow!("insert new key (pg): {e}")))?;
277 tx.commit()
278 .await
279 .map_err(|e| Error::Backend(anyhow::anyhow!("commit tx (pg rotate): {e}")))?;
280 let mut guard = self.inner.write();
282 if let Some(prev) = guard.active.take() {
283 guard.history.push(HistoryKey {
284 kid: prev.kid,
285 alg: prev.alg,
286 decoding_key: prev.decoding_key,
287 });
288 }
289 guard.active = Some(ActiveKey {
290 kid: kid.clone(),
291 alg,
292 encoding_key,
293 decoding_key,
294 expires_at: None,
295 });
296 Ok(kid)
297 }
298
299 #[cfg(feature = "backend-sqlite")]
301 pub async fn rotate_sqlite(&self, pool: &sqlx::SqlitePool) -> Result<String> {
302 let GeneratedKey {
303 kid,
304 alg,
305 private_pem,
306 public_jwk,
307 } = generate_ed25519_key();
308 let (encoding_key, decoding_key) = build_keys(alg, private_pem.as_bytes())?;
309 let now = now_secs();
310 let mut tx = pool
311 .begin()
312 .await
313 .map_err(|e| Error::Backend(anyhow::anyhow!("begin tx (sqlite rotate): {e}")))?;
314 sqlx::query("UPDATE auth.jwks_keys SET rotated_at = ? WHERE rotated_at IS NULL")
315 .bind(now)
316 .execute(&mut *tx)
317 .await
318 .map_err(|e| Error::Backend(anyhow::anyhow!("mark old key rotated (sqlite): {e}")))?;
319 sqlx::query(
320 "INSERT INTO auth.jwks_keys
321 (kid, alg, public_jwk, private_pem_encrypted, created_at, rotated_at, expires_at)
322 VALUES (?, ?, ?, ?, ?, NULL, NULL)",
323 )
324 .bind(&kid)
325 .bind(alg_str(alg))
326 .bind(public_jwk.to_string())
327 .bind(private_pem.as_bytes())
328 .bind(now)
329 .execute(&mut *tx)
330 .await
331 .map_err(|e| Error::Backend(anyhow::anyhow!("insert new key (sqlite): {e}")))?;
332 tx.commit()
333 .await
334 .map_err(|e| Error::Backend(anyhow::anyhow!("commit tx (sqlite rotate): {e}")))?;
335 let mut guard = self.inner.write();
336 if let Some(prev) = guard.active.take() {
337 guard.history.push(HistoryKey {
338 kid: prev.kid,
339 alg: prev.alg,
340 decoding_key: prev.decoding_key,
341 });
342 }
343 guard.active = Some(ActiveKey {
344 kid: kid.clone(),
345 alg,
346 encoding_key,
347 decoding_key,
348 expires_at: None,
349 });
350 Ok(kid)
351 }
352}
353
354fn lookup_decoding_key<'a>(inner: &'a Inner, kid: &str) -> Option<(Algorithm, &'a DecodingKey)> {
355 if let Some(active) = &inner.active
356 && active.kid == kid
357 {
358 return Some((active.alg, &active.decoding_key));
359 }
360 inner
361 .history
362 .iter()
363 .find(|h| h.kid == kid)
364 .map(|h| (h.alg, &h.decoding_key))
365}
366
367fn build_keys(alg: Algorithm, pem: &[u8]) -> Result<(EncodingKey, DecodingKey)> {
371 match alg {
372 Algorithm::EdDSA => {
373 let enc = EncodingKey::from_ed_pem(pem).map_err(map_jwt_err)?;
374 let public_pem = ed25519_public_pem_from_private(pem)?;
375 let dec = DecodingKey::from_ed_pem(public_pem.as_bytes()).map_err(map_jwt_err)?;
376 Ok((enc, dec))
377 }
378 other => Err(Error::Jwt(format!(
381 "unsupported jwt algorithm {other:?} (only EdDSA in phase 4)"
382 ))),
383 }
384}
385
386fn ed25519_public_pem_from_private(private_pem: &[u8]) -> Result<String> {
390 use ed25519_dalek::SigningKey;
391 use ed25519_dalek::pkcs8::DecodePrivateKey;
392 use ed25519_dalek::pkcs8::spki::EncodePublicKey;
393
394 let pem_str = std::str::from_utf8(private_pem)
395 .map_err(|e| Error::Jwt(format!("ed25519 private PEM utf8: {e}")))?;
396 let signing = SigningKey::from_pkcs8_pem(pem_str)
397 .map_err(|e| Error::Jwt(format!("parse ed25519 private PEM: {e}")))?;
398 let verifying = signing.verifying_key();
399 verifying
400 .to_public_key_pem(ed25519_dalek::pkcs8::spki::der::pem::LineEnding::LF)
401 .map_err(|e| Error::Jwt(format!("encode ed25519 public PEM: {e}")))
402}
403
404fn parse_alg(name: &str) -> Result<Algorithm> {
405 match name {
406 "EdDSA" => Ok(Algorithm::EdDSA),
407 other => Err(Error::Jwt(format!(
408 "unknown jwt algorithm {other:?} (only EdDSA in phase 4)"
409 ))),
410 }
411}
412
413fn alg_str(alg: Algorithm) -> &'static str {
414 match alg {
415 Algorithm::EdDSA => "EdDSA",
416 _ => "EdDSA",
420 }
421}
422
423fn map_jwt_err(e: jsonwebtoken::errors::Error) -> Error {
424 Error::Jwt(e.to_string())
425}
426
427fn now_secs() -> f64 {
428 std::time::SystemTime::now()
429 .duration_since(std::time::UNIX_EPOCH)
430 .unwrap_or_default()
431 .as_secs_f64()
432}
433
434struct GeneratedKey {
438 kid: String,
439 alg: Algorithm,
440 private_pem: String,
441 public_jwk: serde_json::Value,
442}
443
444fn generate_ed25519_key() -> GeneratedKey {
445 use ed25519_dalek::SigningKey;
446 use ed25519_dalek::pkcs8::EncodePrivateKey;
447
448 let signing = SigningKey::generate(&mut rand_core_06::OsRng);
449 let private_pem = signing
450 .to_pkcs8_pem(ed25519_dalek::pkcs8::spki::der::pem::LineEnding::LF)
451 .expect("ed25519 PKCS#8 PEM encoding")
452 .to_string();
453 let verifying = signing.verifying_key();
454 let pub_bytes = verifying.to_bytes();
455 let kid = format!(
456 "kid_{}",
457 data_encoding::BASE64URL_NOPAD.encode(&pub_bytes[..16])
458 );
459 let public_jwk = serde_json::json!({
460 "kty": "OKP",
461 "crv": "Ed25519",
462 "alg": "EdDSA",
463 "kid": kid,
464 "use": "sig",
465 "x": data_encoding::BASE64URL_NOPAD.encode(&pub_bytes),
466 });
467 GeneratedKey {
468 kid,
469 alg: Algorithm::EdDSA,
470 private_pem,
471 public_jwk,
472 }
473}
474
475pub fn generate_ephemeral_ed25519(kid: impl Into<String>) -> Result<ActiveKey> {
479 let GeneratedKey { private_pem, .. } = generate_ed25519_key();
480 let (encoding_key, decoding_key) = build_keys(Algorithm::EdDSA, private_pem.as_bytes())?;
481 Ok(ActiveKey {
482 kid: kid.into(),
483 alg: Algorithm::EdDSA,
484 encoding_key,
485 decoding_key,
486 expires_at: None,
487 })
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493 use serde::{Deserialize, Serialize};
494
495 #[derive(Debug, Serialize, Deserialize, PartialEq)]
496 struct Claims {
497 sub: String,
498 iss: String,
499 aud: String,
500 exp: usize,
501 }
502
503 fn config_with_active(issuer: &str, audience: &[&str]) -> JwtConfig {
504 let cfg = JwtConfig::new(
505 issuer.to_string(),
506 audience.iter().map(|s| s.to_string()).collect(),
507 );
508 let active = generate_ephemeral_ed25519("kid_test").unwrap();
509 cfg.set_active(active, Vec::new());
510 cfg
511 }
512
513 fn future_exp() -> usize {
514 (now_secs() as usize) + 3600
515 }
516
517 fn past_exp() -> usize {
518 (now_secs() as usize).saturating_sub(3600)
519 }
520
521 #[test]
522 fn issue_and_verify_round_trip() {
523 let cfg = config_with_active("assay", &["assay-engine"]);
524 let claims = Claims {
525 sub: "user_alice".to_string(),
526 iss: "assay".to_string(),
527 aud: "assay-engine".to_string(),
528 exp: future_exp(),
529 };
530 let token = cfg.issue(&claims).unwrap();
531 let data = cfg.verify::<Claims>(&token).unwrap();
532 assert_eq!(data.claims, claims);
533 assert_eq!(data.header.kid.as_deref(), Some("kid_test"));
534 }
535
536 #[test]
537 fn wrong_audience_is_rejected() {
538 let cfg = config_with_active("assay", &["assay-engine"]);
539 let token = cfg
540 .issue(&Claims {
541 sub: "u".to_string(),
542 iss: "assay".to_string(),
543 aud: "someone-else".to_string(),
544 exp: future_exp(),
545 })
546 .unwrap();
547 let result = cfg.verify::<Claims>(&token);
548 assert!(matches!(result, Err(Error::Jwt(_))));
549 }
550
551 #[test]
552 fn expired_token_is_rejected() {
553 let cfg = config_with_active("assay", &["assay-engine"]);
554 let token = cfg
555 .issue(&Claims {
556 sub: "u".to_string(),
557 iss: "assay".to_string(),
558 aud: "assay-engine".to_string(),
559 exp: past_exp(),
560 })
561 .unwrap();
562 let result = cfg.verify::<Claims>(&token);
563 assert!(matches!(result, Err(Error::Jwt(_))));
564 }
565
566 #[test]
567 fn unknown_kid_is_rejected() {
568 let cfg_a = config_with_active("assay", &["assay-engine"]);
569 let token = cfg_a
570 .issue(&Claims {
571 sub: "u".to_string(),
572 iss: "assay".to_string(),
573 aud: "assay-engine".to_string(),
574 exp: future_exp(),
575 })
576 .unwrap();
577 let cfg_b = JwtConfig::new("assay".to_string(), vec!["assay-engine".to_string()]);
580 let other = generate_ephemeral_ed25519("kid_b").unwrap();
581 cfg_b.set_active(other, Vec::new());
582 let result = cfg_b.verify::<Claims>(&token);
583 assert!(matches!(result, Err(Error::Jwt(_))));
584 }
585}