mod error;
mod introspector;
mod scopes_validator;
mod time_frame;
use error::TokenError;
use pdk_core::log::warn;
use rmp_serde::Serializer;
use serde::{Deserialize, Serialize};
pub use error::{ConfigError, IntrospectionError, ValidationError};
pub use introspector::{
IntrospectionResult, TokenValidator, TokenValidatorBuildError, TokenValidatorBuilder,
TokenValidatorBuilderInstance, TokenValidatorConfig,
};
pub use scopes_validator::ScopesValidator;
pub use serde_json::Value;
pub(crate) use time_frame::FixedTimeFrame;
pub type Object = serde_json::Map<String, Value>;
const CLIENT_ID: &str = "client_id";
const USERNAME: &str = "username";
const ACTIVE_FIELD: &str = "active";
pub trait Token {
fn has_expired(&self, current_time_millis: i64) -> bool;
fn is_active(&self) -> bool;
fn scopes(&self) -> &[String];
fn client_id(&self) -> Option<String>;
fn username(&self) -> Option<String>;
fn raw_token_context(&self) -> &str;
fn properties(&self) -> &Object;
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "ParsedToken")]
pub enum ParsedToken {
ExpirableToken(ExpirableToken),
OneTimeUseToken(OneTimeUseToken),
}
impl ParsedToken {
fn as_token(&self) -> &dyn Token {
match self {
ParsedToken::ExpirableToken(exp_token) => exp_token,
ParsedToken::OneTimeUseToken(one_time_token) => one_time_token,
}
}
pub fn has_expired(&self, current_time_millis: i64) -> bool {
self.as_token().has_expired(current_time_millis)
}
pub fn scopes(&self) -> &[String] {
self.as_token().scopes()
}
#[allow(unused)]
pub fn client_id(&self) -> Option<String> {
self.as_token().client_id()
}
#[allow(unused)]
pub fn username(&self) -> Option<String> {
self.as_token().username()
}
pub fn raw_token_context(&self) -> &str {
self.as_token().raw_token_context()
}
pub fn properties(&self) -> &Object {
self.as_token().properties()
}
pub(crate) fn from_binary(raw_data: Vec<u8>) -> Result<Self, TokenError> {
let parsed_token: ParsedToken =
rmp_serde::decode::from_slice(&raw_data).map_err(|err| {
warn!("Error deserializing token: {err:?}");
TokenError::BinaryDeserializeError {
msg: err.to_string(),
}
})?;
Ok(parsed_token)
}
pub(crate) fn to_binary(&self) -> Result<Vec<u8>, TokenError> {
let mut raw_data = Vec::new();
self.serialize(&mut Serializer::new(&mut raw_data))
.map_err(|err| {
warn!("Error serializing Token to binary: {err:?}");
TokenError::BinarySerializeError {
msg: err.to_string(),
}
})?;
Ok(raw_data)
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct ExpirableToken {
raw_token_context: String,
properties: Object,
expiration: FixedTimeFrame,
is_active: bool,
scopes: Vec<String>,
}
impl Eq for ExpirableToken {}
impl PartialEq for ExpirableToken {
fn eq(&self, other: &Self) -> bool {
self.properties == other.properties
&& self.expiration == other.expiration
&& self.is_active == other.is_active
&& self.scopes == other.scopes
}
}
impl std::fmt::Debug for ExpirableToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ExpirableToken")
.field("properties", &self.properties)
.field("expiration", &self.expiration)
.field("is_active", &self.is_active)
.field("scopes", &self.scopes)
.finish()
}
}
impl ExpirableToken {
pub fn new(
raw_token_context: String,
properties: Object,
expiration: FixedTimeFrame,
scopes: Vec<String>,
) -> Self {
let is_active = properties
.get(ACTIVE_FIELD)
.and_then(|v| v.as_bool())
.unwrap_or(true);
ExpirableToken {
raw_token_context,
properties,
expiration,
is_active,
scopes,
}
}
}
impl Token for ExpirableToken {
fn has_expired(&self, current_time_millis: i64) -> bool {
self.expiration.has_finished(current_time_millis) || self.expiration.in_millis() == 0
}
fn is_active(&self) -> bool {
self.is_active
}
fn scopes(&self) -> &[String] {
self.scopes.as_ref()
}
fn properties(&self) -> &Object {
&self.properties
}
fn client_id(&self) -> Option<String> {
self.properties().get(CLIENT_ID)?.as_str().map(String::from)
}
fn username(&self) -> Option<String> {
self.properties().get(USERNAME)?.as_str().map(String::from)
}
fn raw_token_context(&self) -> &str {
&self.raw_token_context
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OneTimeUseToken {
raw_token_context: String,
properties: Object,
is_active: bool,
scopes: Vec<String>,
}
impl Eq for OneTimeUseToken {}
impl OneTimeUseToken {
pub fn new(raw_token_context: String, properties: Object, scopes: Vec<String>) -> Self {
let is_active = properties
.get(ACTIVE_FIELD)
.and_then(|v| v.as_bool())
.unwrap_or(true);
OneTimeUseToken {
raw_token_context,
properties,
is_active,
scopes,
}
}
}
impl Token for OneTimeUseToken {
fn has_expired(&self, _current_time_millis: i64) -> bool {
true
}
fn is_active(&self) -> bool {
self.is_active
}
fn scopes(&self) -> &[String] {
self.scopes.as_ref()
}
fn client_id(&self) -> Option<String> {
self.properties().get(CLIENT_ID)?.as_str().map(String::from)
}
fn username(&self) -> Option<String> {
self.properties().get(USERNAME)?.as_str().map(String::from)
}
fn raw_token_context(&self) -> &str {
&self.raw_token_context
}
fn properties(&self) -> &Object {
&self.properties
}
}
#[cfg(test)]
mod token_tests {
use super::*;
fn create_properties(active: bool, client_id: Option<&str>) -> Object {
let mut props = Object::new();
props.insert("active".to_string(), serde_json::json!(active));
if let Some(id) = client_id {
props.insert("client_id".to_string(), serde_json::json!(id));
}
props
}
#[test]
fn expirable_token_not_expired_when_in_range() {
let token = ExpirableToken::new(
"{}".to_string(),
create_properties(true, None),
FixedTimeFrame::new(1000, 5000),
vec![],
);
assert!(!token.has_expired(3000));
assert!(token.has_expired(7000));
}
#[test]
fn expirable_token_expired_when_past_end() {
let token = ExpirableToken::new(
"{}".to_string(),
create_properties(true, None),
FixedTimeFrame::new(1000, 5000),
vec![],
);
assert!(token.has_expired(7000));
assert!(!token.has_expired(3000));
}
#[test]
fn expirable_token_expired_when_zero_duration() {
let token = ExpirableToken::new(
"{}".to_string(),
create_properties(true, None),
FixedTimeFrame::new(1000, 0),
vec![],
);
assert!(token.has_expired(1000));
let token_with_duration = ExpirableToken::new(
"{}".to_string(),
create_properties(true, None),
FixedTimeFrame::new(1000, 5000),
vec![],
);
assert!(!token_with_duration.has_expired(3000));
}
#[test]
fn expirable_token_reads_active_from_properties() {
let active_token = ExpirableToken::new(
"{}".to_string(),
create_properties(true, None),
FixedTimeFrame::new(0, 1000),
vec![],
);
let inactive_token = ExpirableToken::new(
"{}".to_string(),
create_properties(false, None),
FixedTimeFrame::new(0, 1000),
vec![],
);
assert!(active_token.is_active());
assert!(!inactive_token.is_active());
}
#[test]
fn one_time_use_token_always_expired() {
let token = OneTimeUseToken::new("{}".to_string(), create_properties(true, None), vec![]);
assert!(token.has_expired(0));
assert!(token.has_expired(i64::MAX));
let expirable = ExpirableToken::new(
"{}".to_string(),
create_properties(true, None),
FixedTimeFrame::new(0, i64::MAX),
vec![],
);
assert!(!expirable.has_expired(1000));
}
#[test]
fn token_extracts_client_id() {
let token_with_id = ExpirableToken::new(
"{}".to_string(),
create_properties(true, Some("my-client")),
FixedTimeFrame::new(0, 1000),
vec![],
);
let token_without_id = ExpirableToken::new(
"{}".to_string(),
create_properties(true, None),
FixedTimeFrame::new(0, 1000),
vec![],
);
assert_eq!(token_with_id.client_id(), Some("my-client".to_string()));
assert_eq!(token_without_id.client_id(), None);
}
#[test]
fn parsed_token_serialization_roundtrip() {
let token = ParsedToken::ExpirableToken(ExpirableToken::new(
r#"{"active":true}"#.to_string(),
create_properties(true, Some("test")),
FixedTimeFrame::new(1000, 5000),
vec!["read".to_string()],
));
let binary = token.to_binary().unwrap();
let restored = ParsedToken::from_binary(binary).unwrap();
assert_eq!(token, restored);
assert!(ParsedToken::from_binary(vec![0, 1, 2, 3]).is_err());
}
}