firebase_rs_sdk/util/
jwt.rs1use 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}