rust_cfzt_validator/
lib.rs

1use 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
48/// The interface for a component capable of validating a CFZT JWT.
49pub trait Validator: Sync + Send {
50    /// Takes a JWT, team name, and a mutable set of constraints 
51    /// and validates a JWT accordingly.
52    fn validate_token(
53        &self,
54        token: &str,
55        team_name: &str,
56        constraints: &mut Constraints,
57    ) -> ValidationResult<DecodedToken>;
58
59    // A hook to trigger the validator to perform syncronisation
60    // with the Cloudflare Access API
61    fn sync(&self) -> StdResult<bool>;
62}
63
64/// Represents a Validator implementation capable of 
65/// validating tokens associated with a single CFZT team.
66pub struct TeamValidator {
67    pub(crate) team_name: String,
68    cache: cache::Cache,
69    agent: ureq::Agent,
70}
71
72
73impl TeamValidator {
74    /// Initialises a TeamValidator from a team name, Cache struct, and a ureq Agent.
75    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    /// Initialises a TeamValidator from an existing TeamKeys struct and a ureq Agent.
84    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    /// Atttempts to initialise a TeamValidator using a team name and a ureq Agent.
90    /// Keys are retrieved from the CF API.
91    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    /// Attempts to syncronise the TeamValidator's cached keys with
98    /// a provided TeamKeys struct. Returns a bool signalling
99    /// if an update was necessary.
100    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    /// Attempts to validate a token against the CFZT Team associated with the TeamValidator.
115    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    /// Attempts to syncronise the TeamValidator's cached keys with
140    /// those available via the Cloudflare API. Returns a wrapped bool signalling
141    /// if an update was necessary.
142    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
148/// Represents a Validator implementation capable of 
149/// validating tokens associated with many CFZT teams.
150pub 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    /// Adds a single TeamValidator into the MultiTeamValidator TeamCache.
164    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    /// Attempts to syncronise a team added to the MultiTeamValidator with
177    /// those available via the CF API. Returns a wrapped bool signalling
178    /// if an update was necessary.
179    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    /// Attempts to validate a token against a CFZT Team associated with the MultiTeamValidator.
191    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}