rustauth_core/crypto/
jwt.rs1use 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
17pub 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
46pub 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}