rust_cfzt_validator/
lib.rs1use std::error::Error;
2
3pub mod api;
4pub mod app_token;
5pub mod cache;
6pub(crate) mod errors;
7pub mod keys;
8pub(crate) mod unpack;
9
10pub type StdResult<T> = Result<T, Box<dyn Error>>;
11
12use std::collections::{HashMap, HashSet};
13
14use crate::{
15 cache::Cache,
16 errors::{ValidationError, ValidationResult},
17};
18
19use jsonwebtoken::{self, TokenData};
20
21pub type DecodedToken = TokenData<serde_json::Value>;
22
23type TeamCache = HashMap<String, TeamValidator>;
24type Constraints = jsonwebtoken::Validation;
25
26fn decode_token_header(token: &str) -> ValidationResult<jsonwebtoken::Header> {
27 match jsonwebtoken::decode_header(token) {
28 Ok(hdr) => Ok(hdr),
29 Err(_) => Err(ValidationError::header_decode_failure()),
30 }
31}
32
33fn decode_token(
34 token: &str,
35 key: &jsonwebtoken::DecodingKey,
36 constraints: &Constraints,
37) -> ValidationResult<DecodedToken> {
38 match jsonwebtoken::decode::<serde_json::Value>(token, key, constraints) {
39 Ok(token_data) => Ok(token_data),
40 Err(_) => Err(ValidationError::invalid_jwt()),
41 }
42}
43
44fn get_kid(header: jsonwebtoken::Header) -> ValidationResult<String> {
45 Ok(header.kid.ok_or(ValidationError::header_missing_kid())?)
46}
47
48pub trait Validator: Sync + Send {
50 fn validate_token(
53 &self,
54 token: &str,
55 team_name: &str,
56 constraints: &mut Constraints,
57 ) -> ValidationResult<DecodedToken>;
58
59 fn sync(&self) -> StdResult<bool>;
62}
63
64pub struct TeamValidator {
67 pub(crate) team_name: String,
68 cache: cache::Cache,
69 agent: ureq::Agent,
70}
71
72
73impl TeamValidator {
74 pub fn new(team_name: &str, cache: Cache, agent: ureq::Agent) -> Self {
76 TeamValidator {
77 team_name: team_name.to_string(),
78 cache,
79 agent,
80 }
81 }
82
83 pub fn from_team_keys(team_keys: api::TeamKeys, agent: ureq::Agent) -> Self {
85 let cache = cache::Cache::new(&team_keys.latest_key_id, team_keys.keys);
86 Self::new(&team_keys.team_name, cache, agent)
87 }
88
89 pub fn from_team_name(team_name: &str, agent: ureq::Agent) -> StdResult<Self> {
92 let team_keys = api::TeamKeys::from_team_name(&team_name, &agent)?;
93 let cache = cache::Cache::new(&team_keys.latest_key_id, team_keys.keys);
94 Ok(Self::new(team_name, cache, agent))
95 }
96
97 pub fn update_keys(&self, team_keys: api::TeamKeys) -> bool {
101 let key_ids: HashSet<String> = team_keys.keys.keys().cloned().collect();
102 let rotate = self.cache.is_rotation_needed(key_ids);
103
104 if rotate {
105 self.cache
106 .rotate_keys(&team_keys.latest_key_id, team_keys.keys);
107 }
108
109 rotate
110 }
111}
112
113impl Validator for TeamValidator {
114 fn validate_token(
116 &self,
117 token: &str,
118 team_name: &str,
119 constraints: &mut Constraints,
120 ) -> ValidationResult<DecodedToken> {
121 if team_name != self.team_name {
122 return Err(ValidationError::team_name_mismatch(
123 team_name,
124 self.team_name.as_str(),
125 ))?;
126 }
127
128 let header = decode_token_header(token)?;
129 let key_id = get_kid(header)?;
130
131 match self.cache.get_decoding_key(&key_id) {
132 Some(key) => {
133 Ok(decode_token(token, &key, &constraints)?)
134 }
135 None => Err(ValidationError::no_kid_in_cache(&key_id)),
136 }
137 }
138
139 fn sync(&self) -> StdResult<bool> {
143 let team_keys = api::TeamKeys::from_team_name(&self.team_name, &self.agent)?;
144 Ok(self.update_keys(team_keys))
145 }
146}
147
148pub struct MultiTeamValidator {
151 teams: TeamCache,
152}
153
154impl Default for MultiTeamValidator {
155 fn default() -> Self {
156 MultiTeamValidator {
157 teams: HashMap::new(),
158 }
159 }
160}
161
162impl MultiTeamValidator {
163 pub fn add_team(&mut self, team_validator: TeamValidator) -> StdResult<()> {
165 self.teams
166 .insert(team_validator.team_name.clone(), team_validator);
167 Ok(())
168 }
169
170 fn get_team_validator(&self, team_name: &str) -> ValidationResult<&TeamValidator> {
171 self.teams
172 .get(team_name)
173 .ok_or(ValidationError::unknown_team_name(team_name))
174 }
175
176 pub fn sync_team(&self, team_name: &str) -> StdResult<bool> {
180 let team = self.get_team_validator(team_name)?;
181 team.sync()
182 }
183
184 pub fn get_team_names(&self) -> Vec<String> {
185 self.teams.keys().into_iter().map(|x| x.to_string()).collect()
186 }
187}
188
189impl Validator for MultiTeamValidator {
190 fn validate_token(
192 &self,
193 token: &str,
194 team_name: &str,
195 constraints: &mut Constraints,
196 ) -> ValidationResult<DecodedToken> {
197 let team = self.get_team_validator(team_name)?;
198 team.validate_token(token, team_name, constraints)
199 }
200
201 fn sync(&self) -> StdResult<bool> {
202 let mut retval = false;
203
204 for team_name in self.teams.keys().into_iter() {
205 retval = self.sync_team(team_name)? || retval
206 }
207
208 Ok(retval)
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use api::TeamKeys;
215
216 use super::*;
217
218 const TEAM_NAME: &str = "molten";
219 const AUDIENCE: &str = "41f1d879c797d912d9bd80710db3dce92d30602a2dcbdf7bab33913071c44bd4";
220 const STATIC_KEYS: &str = include_str!("../test_data/sample_signing_keys.json");
221 const JWT: &str = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImE1ZWE4YmQxYjk0Y2FkZjJhNWYwZjQ3ZGFkMTg4ZTZhYWZiY2QyOGVlYWIyZTcxYjExZGRkOTZkOWNjMjhjNjkifQ.eyJhdWQiOlsiNDFmMWQ4NzljNzk3ZDkxMmQ5YmQ4MDcxMGRiM2RjZTkyZDMwNjAyYTJkY2JkZjdiYWIzMzkxMzA3MWM0NGJkNCJdLCJlbWFpbCI6Im1lQGphY29idGF5bG9yLmlkLmF1IiwiZXhwIjoxNzE3OTgxNDM5LCJpYXQiOjE3MTc5Nzk2MzksIm5iZiI6MTcxNzk3OTYzOSwiaXNzIjoiaHR0cHM6Ly9tb2x0ZW4uY2xvdWRmbGFyZWFjY2Vzcy5jb20iLCJ0eXBlIjoiYXBwIiwiaWRlbnRpdHlfbm9uY2UiOiJBUFhHRnFsT2k5OVNsVVF3Iiwic3ViIjoiNzIwOGVlYTQtNDA5OC01YTMxLTkwNTMtZjA5YjgxYzI4MWZkIiwiY3VzdG9tIjp7ImVtYWlsIjoiIn0sImNvdW50cnkiOiJBVSJ9.nwTTyb2ioh5Fw39zKyBMZJuj0wzxOuP2KxsbzDLQCmOBNekTvhmquAui3bmuwpzhTTfjxP9yAJG1_N0Hmc-h613E8jOQclqAVgr9_JEYPZ2v58exPRgjeokEIQweRYKgLgoqHAqaYTKQ4v8-pHeRL66L-2Ui3uVUi8V8PkeJogKfPHvFjnkCqZPFFpuxkW735x0Vxq5CzQesoHH37hLAJe7ckc4Jav1AholNsLOvlBIxZtC9ET8-3YqO5rOUCqSX_6oKmf0VyOmqzbSw4gaXvnaTBAPiGruU63gg_LsV0NVGeVvddy84Tl3WvQvbPwdCJ9W9KsbkyOryfgbL0lrZPA";
222
223 fn get_team_validator() -> TeamValidator {
224 let agent = ureq::agent();
225 let team_keys = TeamKeys::from_str(TEAM_NAME, STATIC_KEYS).unwrap();
226 TeamValidator::from_team_keys(team_keys, agent)
227 }
228
229 fn get_multi_team_validator() -> MultiTeamValidator {
230 let mut validator = MultiTeamValidator::default();
231 validator.add_team(get_team_validator()).unwrap();
232 validator
233 }
234
235 fn get_constraints() -> Constraints {
236 let mut constraints = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::RS256);
237 constraints.validate_nbf = false;
238 constraints.validate_exp = false;
239 constraints.set_audience(&[AUDIENCE]);
240 constraints
241 }
242
243 #[test]
244 fn test_team_validator_sync() {
245 let validator = get_team_validator();
246 let result = validator.sync();
247 assert!(result.is_ok());
248 assert!(result.unwrap());
249 }
250
251 #[test]
252 fn test_multi_team_validator_team_sync() {
253 let validator = get_multi_team_validator();
254 let result = validator.sync_team(TEAM_NAME);
255 assert!(result.is_ok());
256 assert!(result.unwrap());
257 }
258
259 #[test]
260 fn test_team_validator_validate_token() {
261 let validator = get_team_validator();
262 let mut constraints = get_constraints();
263 let result = validator.validate_token(JWT, TEAM_NAME, &mut constraints);
264 assert!(result.is_ok());
265 }
266
267 #[test]
268 fn test_multi_team_validator_validate_token() {
269 let validator = get_multi_team_validator();
270 let mut constraints = get_constraints();
271 let result = validator.validate_token(JWT, TEAM_NAME, &mut constraints);
272 assert!(result.is_ok());
273 }
274}