pdk_token_introspection_lib/
lib.rs1mod error;
49mod introspector;
50mod scopes_validator;
51mod time_frame;
52
53use error::TokenError;
54use pdk_core::log::warn;
55use rmp_serde::Serializer;
56use serde::{Deserialize, Serialize};
57
58pub use error::{ConfigError, IntrospectionError, ValidationError};
59pub use introspector::{
60 IntrospectionResult, TokenValidator, TokenValidatorBuildError, TokenValidatorBuilder,
61 TokenValidatorBuilderInstance, TokenValidatorConfig,
62};
63pub use scopes_validator::ScopesValidator;
64pub use serde_json::Value;
65
66pub(crate) use time_frame::FixedTimeFrame;
67
68pub type Object = serde_json::Map<String, Value>;
70
71const CLIENT_ID: &str = "client_id";
73
74const USERNAME: &str = "username";
76
77const ACTIVE_FIELD: &str = "active";
79
80pub trait Token {
82 fn has_expired(&self, current_time_millis: i64) -> bool;
83 fn is_active(&self) -> bool;
84 fn scopes(&self) -> &[String];
85 fn client_id(&self) -> Option<String>;
86 fn username(&self) -> Option<String>;
87 fn raw_token_context(&self) -> &str;
88 fn properties(&self) -> &Object;
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
93#[serde(tag = "ParsedToken")]
94pub enum ParsedToken {
95 ExpirableToken(ExpirableToken),
97 OneTimeUseToken(OneTimeUseToken),
99}
100
101impl ParsedToken {
102 fn as_token(&self) -> &dyn Token {
103 match self {
104 ParsedToken::ExpirableToken(exp_token) => exp_token,
105 ParsedToken::OneTimeUseToken(one_time_token) => one_time_token,
106 }
107 }
108
109 pub fn has_expired(&self, current_time_millis: i64) -> bool {
111 self.as_token().has_expired(current_time_millis)
112 }
113
114 pub fn scopes(&self) -> &[String] {
116 self.as_token().scopes()
117 }
118
119 #[allow(unused)]
121 pub fn client_id(&self) -> Option<String> {
122 self.as_token().client_id()
123 }
124
125 #[allow(unused)]
127 pub fn username(&self) -> Option<String> {
128 self.as_token().username()
129 }
130
131 pub fn raw_token_context(&self) -> &str {
133 self.as_token().raw_token_context()
134 }
135
136 pub fn properties(&self) -> &Object {
138 self.as_token().properties()
139 }
140
141 pub(crate) fn from_binary(raw_data: Vec<u8>) -> Result<Self, TokenError> {
143 let parsed_token: ParsedToken =
144 rmp_serde::decode::from_slice(&raw_data).map_err(|err| {
145 warn!("Error deserializing token: {err:?}");
146 TokenError::BinaryDeserializeError {
147 msg: err.to_string(),
148 }
149 })?;
150
151 Ok(parsed_token)
152 }
153
154 pub(crate) fn to_binary(&self) -> Result<Vec<u8>, TokenError> {
156 let mut raw_data = Vec::new();
157 self.serialize(&mut Serializer::new(&mut raw_data))
158 .map_err(|err| {
159 warn!("Error serializing Token to binary: {err:?}");
160 TokenError::BinarySerializeError {
161 msg: err.to_string(),
162 }
163 })?;
164 Ok(raw_data)
165 }
166}
167
168#[derive(Clone, Serialize, Deserialize)]
170pub struct ExpirableToken {
171 raw_token_context: String,
173 properties: Object,
175 expiration: FixedTimeFrame,
177 is_active: bool,
179 scopes: Vec<String>,
181}
182
183impl Eq for ExpirableToken {}
184
185impl PartialEq for ExpirableToken {
186 fn eq(&self, other: &Self) -> bool {
187 self.properties == other.properties
188 && self.expiration == other.expiration
189 && self.is_active == other.is_active
190 && self.scopes == other.scopes
191 }
192}
193
194impl std::fmt::Debug for ExpirableToken {
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 f.debug_struct("ExpirableToken")
197 .field("properties", &self.properties)
198 .field("expiration", &self.expiration)
199 .field("is_active", &self.is_active)
200 .field("scopes", &self.scopes)
201 .finish()
202 }
203}
204
205impl ExpirableToken {
206 pub fn new(
208 raw_token_context: String,
209 properties: Object,
210 expiration: FixedTimeFrame,
211 scopes: Vec<String>,
212 ) -> Self {
213 let is_active = properties
214 .get(ACTIVE_FIELD)
215 .and_then(|v| v.as_bool())
216 .unwrap_or(true);
217
218 ExpirableToken {
219 raw_token_context,
220 properties,
221 expiration,
222 is_active,
223 scopes,
224 }
225 }
226}
227
228impl Token for ExpirableToken {
229 fn has_expired(&self, current_time_millis: i64) -> bool {
230 self.expiration.has_finished(current_time_millis) || self.expiration.in_millis() == 0
231 }
232
233 fn is_active(&self) -> bool {
234 self.is_active
235 }
236
237 fn scopes(&self) -> &[String] {
238 self.scopes.as_ref()
239 }
240
241 fn properties(&self) -> &Object {
242 &self.properties
243 }
244
245 fn client_id(&self) -> Option<String> {
246 self.properties().get(CLIENT_ID)?.as_str().map(String::from)
247 }
248
249 fn username(&self) -> Option<String> {
250 self.properties().get(USERNAME)?.as_str().map(String::from)
251 }
252
253 fn raw_token_context(&self) -> &str {
254 &self.raw_token_context
255 }
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
260pub struct OneTimeUseToken {
261 raw_token_context: String,
263 properties: Object,
265 is_active: bool,
267 scopes: Vec<String>,
269}
270
271impl Eq for OneTimeUseToken {}
272
273impl OneTimeUseToken {
274 pub fn new(raw_token_context: String, properties: Object, scopes: Vec<String>) -> Self {
276 let is_active = properties
277 .get(ACTIVE_FIELD)
278 .and_then(|v| v.as_bool())
279 .unwrap_or(true);
280 OneTimeUseToken {
281 raw_token_context,
282 properties,
283 is_active,
284 scopes,
285 }
286 }
287}
288
289impl Token for OneTimeUseToken {
290 fn has_expired(&self, _current_time_millis: i64) -> bool {
291 true
292 }
293
294 fn is_active(&self) -> bool {
295 self.is_active
296 }
297
298 fn scopes(&self) -> &[String] {
299 self.scopes.as_ref()
300 }
301
302 fn client_id(&self) -> Option<String> {
303 self.properties().get(CLIENT_ID)?.as_str().map(String::from)
304 }
305
306 fn username(&self) -> Option<String> {
307 self.properties().get(USERNAME)?.as_str().map(String::from)
308 }
309
310 fn raw_token_context(&self) -> &str {
311 &self.raw_token_context
312 }
313
314 fn properties(&self) -> &Object {
315 &self.properties
316 }
317}
318
319#[cfg(test)]
320mod token_tests {
321 use super::*;
322
323 fn create_properties(active: bool, client_id: Option<&str>) -> Object {
324 let mut props = Object::new();
325 props.insert("active".to_string(), serde_json::json!(active));
326 if let Some(id) = client_id {
327 props.insert("client_id".to_string(), serde_json::json!(id));
328 }
329 props
330 }
331
332 #[test]
333 fn expirable_token_not_expired_when_in_range() {
334 let token = ExpirableToken::new(
335 "{}".to_string(),
336 create_properties(true, None),
337 FixedTimeFrame::new(1000, 5000),
338 vec![],
339 );
340 assert!(!token.has_expired(3000));
342 assert!(token.has_expired(7000));
344 }
345
346 #[test]
347 fn expirable_token_expired_when_past_end() {
348 let token = ExpirableToken::new(
349 "{}".to_string(),
350 create_properties(true, None),
351 FixedTimeFrame::new(1000, 5000),
352 vec![],
353 );
354
355 assert!(token.has_expired(7000));
356 assert!(!token.has_expired(3000));
357 }
358
359 #[test]
360 fn expirable_token_expired_when_zero_duration() {
361 let token = ExpirableToken::new(
362 "{}".to_string(),
363 create_properties(true, None),
364 FixedTimeFrame::new(1000, 0),
365 vec![],
366 );
367
368 assert!(token.has_expired(1000));
369
370 let token_with_duration = ExpirableToken::new(
371 "{}".to_string(),
372 create_properties(true, None),
373 FixedTimeFrame::new(1000, 5000),
374 vec![],
375 );
376 assert!(!token_with_duration.has_expired(3000));
377 }
378
379 #[test]
380 fn expirable_token_reads_active_from_properties() {
381 let active_token = ExpirableToken::new(
382 "{}".to_string(),
383 create_properties(true, None),
384 FixedTimeFrame::new(0, 1000),
385 vec![],
386 );
387 let inactive_token = ExpirableToken::new(
388 "{}".to_string(),
389 create_properties(false, None),
390 FixedTimeFrame::new(0, 1000),
391 vec![],
392 );
393
394 assert!(active_token.is_active());
395 assert!(!inactive_token.is_active());
396 }
397
398 #[test]
399 fn one_time_use_token_always_expired() {
400 let token = OneTimeUseToken::new("{}".to_string(), create_properties(true, None), vec![]);
401 assert!(token.has_expired(0));
403 assert!(token.has_expired(i64::MAX));
404 let expirable = ExpirableToken::new(
406 "{}".to_string(),
407 create_properties(true, None),
408 FixedTimeFrame::new(0, i64::MAX),
409 vec![],
410 );
411 assert!(!expirable.has_expired(1000));
412 }
413
414 #[test]
415 fn token_extracts_client_id() {
416 let token_with_id = ExpirableToken::new(
417 "{}".to_string(),
418 create_properties(true, Some("my-client")),
419 FixedTimeFrame::new(0, 1000),
420 vec![],
421 );
422 let token_without_id = ExpirableToken::new(
423 "{}".to_string(),
424 create_properties(true, None),
425 FixedTimeFrame::new(0, 1000),
426 vec![],
427 );
428
429 assert_eq!(token_with_id.client_id(), Some("my-client".to_string()));
430 assert_eq!(token_without_id.client_id(), None);
431 }
432
433 #[test]
434 fn parsed_token_serialization_roundtrip() {
435 let token = ParsedToken::ExpirableToken(ExpirableToken::new(
436 r#"{"active":true}"#.to_string(),
437 create_properties(true, Some("test")),
438 FixedTimeFrame::new(1000, 5000),
439 vec!["read".to_string()],
440 ));
441
442 let binary = token.to_binary().unwrap();
443 let restored = ParsedToken::from_binary(binary).unwrap();
444
445 assert_eq!(token, restored);
446 assert!(ParsedToken::from_binary(vec![0, 1, 2, 3]).is_err());
448 }
449}