Skip to main content

grabcli_core/
jwt.rs

1//! Minimal JWT helpers: decode the (unverified) payload, read `exp`.
2
3use base64::Engine;
4use base64::engine::general_purpose::URL_SAFE_NO_PAD;
5use serde_json::Value;
6
7use crate::error::{Error, Result};
8
9pub fn b64url(data: &[u8]) -> String {
10    URL_SAFE_NO_PAD.encode(data)
11}
12
13fn b64url_decode(s: &str) -> Result<Vec<u8>> {
14    URL_SAFE_NO_PAD
15        .decode(s.trim_end_matches('='))
16        .map_err(|e| Error::parse(format!("bad base64url in JWT: {e}")))
17}
18
19/// Decode a JWT's payload without verifying the signature.
20pub fn payload(token: &str) -> Result<Value> {
21    let part = token
22        .split('.')
23        .nth(1)
24        .ok_or_else(|| Error::parse("not a JWT"))?;
25    Ok(serde_json::from_slice(&b64url_decode(part)?)?)
26}
27
28/// `exp` claim in Unix seconds, if present.
29pub fn exp(token: &str) -> Option<i64> {
30    payload(token).ok()?.get("exp")?.as_i64()
31}
32
33pub fn now() -> i64 {
34    std::time::SystemTime::now()
35        .duration_since(std::time::UNIX_EPOCH)
36        .map_or(0, |d| i64::try_from(d.as_secs()).unwrap_or(i64::MAX))
37}
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42
43    #[test]
44    fn decodes_payload() -> Result<()> {
45        let tok = format!(
46            "{}.{}.sig",
47            b64url(br#"{"alg":"none"}"#),
48            b64url(br#"{"exp":1700000000,"sessionKey":"abc"}"#)
49        );
50        assert_eq!(exp(&tok), Some(1_700_000_000));
51        assert_eq!(payload(&tok)?["sessionKey"], "abc");
52        Ok(())
53    }
54}