Skip to main content

rustauth_core/crypto/
jwt.rs

1//! Minimal HS256 JWT helpers.
2
3use base64::engine::general_purpose::URL_SAFE_NO_PAD;
4use base64::Engine;
5use hmac::{Hmac, Mac};
6use serde::de::DeserializeOwned;
7use serde::Serialize;
8use serde_json::{json, Value};
9use sha2::Sha256;
10use time::OffsetDateTime;
11
12use crate::crypto::buffer::constant_time_equal;
13use crate::error::RustAuthError;
14
15type HmacSha256 = Hmac<Sha256>;
16
17/// Sign a JSON-serializable payload as an HS256 JWT.
18pub fn sign_jwt<T>(payload: &T, secret: &str, expires_in: i64) -> Result<String, RustAuthError>
19where
20    T: Serialize,
21{
22    let header = json!({ "alg": "HS256", "typ": "JWT" });
23    let mut payload =
24        serde_json::to_value(payload).map_err(|error| RustAuthError::Crypto(error.to_string()))?;
25    let now = OffsetDateTime::now_utc().unix_timestamp();
26    if let Value::Object(map) = &mut payload {
27        map.insert("iat".to_owned(), json!(now));
28        map.insert("exp".to_owned(), json!(now + expires_in));
29    }
30
31    let header = URL_SAFE_NO_PAD.encode(
32        serde_json::to_vec(&header).map_err(|error| RustAuthError::Crypto(error.to_string()))?,
33    );
34    let payload = URL_SAFE_NO_PAD.encode(
35        serde_json::to_vec(&payload).map_err(|error| RustAuthError::Crypto(error.to_string()))?,
36    );
37    let signing_input = format!("{header}.{payload}");
38    let signature = sign_bytes(signing_input.as_bytes(), secret)?;
39
40    Ok(format!(
41        "{signing_input}.{}",
42        URL_SAFE_NO_PAD.encode(signature)
43    ))
44}
45
46/// Verify an HS256 JWT and deserialize its payload.
47pub fn verify_jwt<T>(token: &str, secret: &str) -> Result<Option<T>, RustAuthError>
48where
49    T: DeserializeOwned,
50{
51    let Some((signing_input, signature)) = token.rsplit_once('.') else {
52        return Ok(None);
53    };
54    let expected = sign_bytes(signing_input.as_bytes(), secret)?;
55    let signature = match URL_SAFE_NO_PAD.decode(signature) {
56        Ok(signature) => signature,
57        Err(_) => return Ok(None),
58    };
59    if !constant_time_equal(expected, signature) {
60        return Ok(None);
61    }
62
63    let mut parts = signing_input.split('.');
64    let Some(_header) = parts.next() else {
65        return Ok(None);
66    };
67    let Some(payload) = parts.next() else {
68        return Ok(None);
69    };
70    if parts.next().is_some() {
71        return Ok(None);
72    }
73
74    let payload = match URL_SAFE_NO_PAD.decode(payload) {
75        Ok(payload) => payload,
76        Err(_) => return Ok(None),
77    };
78    let value: Value = serde_json::from_slice(&payload)
79        .map_err(|error| RustAuthError::Crypto(error.to_string()))?;
80    if let Some(exp) = value.get("exp").and_then(Value::as_i64) {
81        if exp < OffsetDateTime::now_utc().unix_timestamp() {
82            return Ok(None);
83        }
84    }
85
86    serde_json::from_value(value)
87        .map(Some)
88        .map_err(|error| RustAuthError::Crypto(error.to_string()))
89}
90
91fn sign_bytes(data: &[u8], secret: &str) -> Result<Vec<u8>, RustAuthError> {
92    let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
93        .map_err(|error| RustAuthError::Crypto(error.to_string()))?;
94    mac.update(data);
95    Ok(mac.finalize().into_bytes().to_vec())
96}