firebase_rs_sdk/util/
jwt.rs

1use crate::util::base64::base64_decode;
2use crate::util::json::json_eval;
3use serde_json::{Map, Value};
4use std::time::{SystemTime, UNIX_EPOCH};
5
6#[derive(Debug, Clone, Default)]
7pub struct DecodedToken {
8    pub header: Value,
9    pub claims: Value,
10    pub data: Value,
11    pub signature: String,
12}
13
14pub fn decode_jwt(token: &str) -> DecodedToken {
15    let mut parts = token.split('.');
16    let header_part = parts.next().unwrap_or_default();
17    let claims_part = parts.next().unwrap_or_default();
18    let signature = parts.next().unwrap_or_default().to_string();
19
20    let header = decode_part(header_part);
21    let (claims, data) = decode_claims(claims_part);
22
23    DecodedToken {
24        header,
25        claims,
26        data,
27        signature,
28    }
29}
30
31pub fn is_valid_timestamp(token: &str) -> bool {
32    let decoded = decode_jwt(token);
33    let claims = match decoded.claims {
34        Value::Object(ref map) => map,
35        _ => return false,
36    };
37
38    let now = SystemTime::now()
39        .duration_since(UNIX_EPOCH)
40        .map(|dur| dur.as_secs() as i64)
41        .unwrap_or_default();
42
43    let valid_since = claims
44        .get("nbf")
45        .or_else(|| claims.get("iat"))
46        .and_then(value_as_i64)
47        .unwrap_or_default();
48
49    let valid_until = claims
50        .get("exp")
51        .and_then(value_as_i64)
52        .unwrap_or(valid_since + 86_400);
53
54    now >= valid_since && now <= valid_until
55}
56
57pub fn issued_at_time(token: &str) -> Option<i64> {
58    let decoded = decode_jwt(token);
59    match decoded.claims {
60        Value::Object(map) => map.get("iat").and_then(value_as_i64),
61        _ => None,
62    }
63}
64
65pub fn is_valid_format(token: &str) -> bool {
66    let decoded = decode_jwt(token);
67    match decoded.claims {
68        Value::Object(map) => map.contains_key("iat"),
69        _ => false,
70    }
71}
72
73pub fn is_admin_token(token: &str) -> bool {
74    let decoded = decode_jwt(token);
75    match decoded.claims {
76        Value::Object(map) => matches!(map.get("admin"), Some(Value::Bool(true))),
77        _ => false,
78    }
79}
80
81fn decode_part(part: &str) -> Value {
82    if part.is_empty() {
83        return Value::Object(Map::new());
84    }
85
86    match base64_decode(part)
87        .ok()
88        .and_then(|decoded| json_eval::<Value>(&decoded).ok())
89    {
90        Some(value) => value,
91        None => Value::Object(Map::new()),
92    }
93}
94
95fn decode_claims(part: &str) -> (Value, Value) {
96    match decode_part(part) {
97        Value::Object(mut map) => {
98            let data = map.remove("d").unwrap_or_else(|| Value::Object(Map::new()));
99            (Value::Object(map), data)
100        }
101        other => (other, Value::Object(Map::new())),
102    }
103}
104
105fn value_as_i64(value: &Value) -> Option<i64> {
106    value.as_i64().or_else(|| value.as_u64().map(|v| v as i64))
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use crate::util::base64::base64_url_encode_trimmed;
113    use serde_json::json;
114
115    fn build_token(claims: &Value) -> String {
116        let header = base64_url_encode_trimmed(&json!({"alg": "none"}).to_string());
117        let claims_str = base64_url_encode_trimmed(&claims.to_string());
118        format!("{}.{}.sig", header, claims_str)
119    }
120
121    #[test]
122    fn decode_extracts_data() {
123        let claims = json!({"iat": 1, "d": {"foo": "bar"}});
124        let token = build_token(&claims);
125        let decoded = decode_jwt(&token);
126        assert_eq!(decoded.data["foo"], json!("bar"));
127        assert!(!decoded.claims.get("d").is_some());
128    }
129
130    #[test]
131    fn format_validation_requires_iat() {
132        let token = build_token(&json!({"exp": 10}));
133        assert!(!is_valid_format(&token));
134    }
135
136    #[test]
137    fn admin_detection() {
138        let token = build_token(&json!({"iat": 1, "admin": true}));
139        assert!(is_admin_token(&token));
140    }
141}