alibi_core/session/cookie_cache/
jwt.rs1use super::CompactCache;
3use crate::{AuthError, AuthResult, utils::json::JsValue};
4use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
5use hmac::{Hmac, KeyInit, Mac};
6use serde_json::{Value, json};
7use sha2::{Sha256, Sha384, Sha512};
8
9pub fn payload(
14 user: &crate::UserView,
15 session: &crate::SessionView,
16 version: &str,
17 now_ms: i64,
18) -> AuthResult<Value> {
19 Ok(json!({"session":session,"user":user,"updatedAt":now_ms,"version":version}))
20}
21
22pub fn time_claims(mut payload: Value, max_age: f64) -> AuthResult<Value> {
27 let now = chrono::Utc::now().timestamp();
28 let expiry = serde_json::Number::from(now)
29 .as_f64()
30 .ok_or_else(|| AuthError::internal("Invalid issued-at time"))?
31 + max_age;
32 if !expiry.is_finite() {
33 return Err(AuthError::internal("Invalid cookie-cache expiration time"));
34 }
35 let claims = payload
36 .as_object_mut()
37 .ok_or_else(|| AuthError::internal("Invalid cookie-cache payload"))?;
38 drop(claims.insert("iat".into(), json!(now)));
39 drop(claims.insert("exp".into(), json!(expiry)));
40 Ok(payload)
41}
42
43pub(crate) fn encode(payload: Value, secret: &str, max_age: f64) -> AuthResult<String> {
44 let input = format!(
45 "{}.{}",
46 URL_SAFE_NO_PAD.encode(br#"{"alg":"HS256"}"#),
47 URL_SAFE_NO_PAD.encode(crate::utils::json::to_vec(&time_claims(payload, max_age)?)?)
48 );
49 let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())
50 .map_err(|_error| AuthError::internal("Invalid signing key"))?;
51 mac.update(input.as_bytes());
52 Ok(format!(
53 "{input}.{}",
54 URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes())
55 ))
56}
57
58pub(crate) fn decode(token: &str, secret: &str) -> Option<CompactCache> {
59 let parts: Vec<_> = token.split('.').collect();
60 let [header, payload, signature] = parts.as_slice() else {
61 return None;
62 };
63 let header_bytes = URL_SAFE_NO_PAD.decode(header).ok()?;
64 let header_value: Value = serde_json::from_slice(&header_bytes).ok()?;
65 if header_value.get("crit").is_some()
66 || header_value
67 .get("b64")
68 .is_some_and(|value| value != &Value::Bool(true))
69 {
70 return None;
71 }
72 let signature = URL_SAFE_NO_PAD.decode(signature).ok()?;
73 let input = format!("{header}.{payload}");
74 macro_rules! verify {
75 ($hash:ty) => {{
76 let mut mac = Hmac::<$hash>::new_from_slice(secret.as_bytes()).ok()?;
77 mac.update(input.as_bytes());
78 mac.verify_slice(&signature).ok()?;
79 }};
80 }
81 match header_value.get("alg")?.as_str()? {
82 "HS256" => verify!(Sha256),
83 "HS384" => verify!(Sha384),
84 "HS512" => verify!(Sha512),
85 _ => return None,
86 }
87 let claims: JsValue =
88 crate::utils::json::from_slice(&URL_SAFE_NO_PAD.decode(payload).ok()?).ok()?;
89 decode_payload(&claims, 0.0)
90}
91
92#[must_use]
94pub fn decode_payload(claims: &JsValue, tolerance: f64) -> Option<CompactCache> {
95 let now = serde_json::Number::from(chrono::Utc::now().timestamp()).as_f64()?;
96 for name in ["iat", "exp", "nbf"] {
97 if let Some(value) = claims.get(name) {
98 let value = value.as_f64().filter(|value| value.is_finite())?;
99 if name == "exp" && value <= now - tolerance || name == "nbf" && value > now + tolerance
100 {
101 return None;
102 }
103 }
104 }
105 let expires_at = claims
106 .get("exp")
107 .and_then(JsValue::as_f64)
108 .filter(|value| *value != 0.0)
109 .map_or_else(
110 || serde_json::Number::from(chrono::Utc::now().timestamp_millis()).as_f64(),
111 |value| Some(value * 1000.0),
112 )?;
113 super::parse_payload(claims, expires_at)
114}
115
116#[async_trait::async_trait]
118pub trait CookieCacheSigner<S: crate::AuthSchema>: Send + Sync {
119 async fn sign(
122 &self,
123 payload: Value,
124 max_age: f64,
125 ctx: &crate::AuthContext<S>,
126 transaction: Option<&dyn crate::store::AuthTransaction<S>>,
127 ) -> AuthResult<String>;
128 async fn verify(&self, token: &str, ctx: &crate::AuthContext<S>) -> AuthResult<Option<Value>>;
131}
132
133pub struct CookieCacheSignerHandle<S: crate::AuthSchema>(
135 pub std::sync::Arc<dyn CookieCacheSigner<S>>,
136);