use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenClaims {
pub sub: String,
#[serde(default)]
pub iss: String,
#[serde(default)]
pub aud: Audience,
#[serde(default)]
pub exp: u64,
#[serde(default)]
pub iat: u64,
#[serde(default)]
pub nbf: Option<u64>,
#[serde(default)]
pub jti: Option<String>,
#[serde(default)]
pub email: Option<String>,
#[serde(default)]
pub email_verified: Option<bool>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub given_name: Option<String>,
#[serde(default)]
pub family_name: Option<String>,
#[serde(default)]
pub preferred_username: Option<String>,
#[serde(default)]
pub picture: Option<String>,
#[serde(default)]
pub groups: Vec<String>,
#[serde(default)]
pub roles: Vec<String>,
#[serde(default)]
pub tid: Option<String>,
#[serde(default)]
pub hd: Option<String>,
#[serde(flatten)]
pub custom: HashMap<String, serde_json::Value>,
}
impl TokenClaims {
pub fn user_id(&self) -> &str {
self.verified_email().unwrap_or(&self.sub)
}
pub fn email_is_verified(&self) -> bool {
self.email_verified.unwrap_or(false) && self.email.as_deref().is_some()
}
pub fn verified_email(&self) -> Option<&str> {
self.email_is_verified().then_some(self.email.as_deref()).flatten()
}
pub fn all_groups(&self) -> Vec<&str> {
self.groups.iter().chain(self.roles.iter()).map(|s| s.as_str()).collect()
}
pub fn is_expired(&self) -> bool {
let now =
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
self.exp < now
}
pub fn get_custom<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
self.custom.get(key).and_then(|v| serde_json::from_value(v.clone()).ok())
}
pub fn scopes(&self) -> Vec<String> {
let mut scopes = Vec::new();
self.extend_scopes(&mut scopes, "scope");
self.extend_scopes(&mut scopes, "scp");
scopes
}
fn extend_scopes(&self, scopes: &mut Vec<String>, claim: &str) {
let Some(value) = self.custom.get(claim) else {
return;
};
match value {
serde_json::Value::String(scope_string) => {
for scope in scope_string.split_whitespace() {
if !scopes.iter().any(|existing| existing == scope) {
scopes.push(scope.to_string());
}
}
}
serde_json::Value::Array(values) => {
for scope in values.iter().filter_map(serde_json::Value::as_str) {
if !scopes.iter().any(|existing| existing == scope) {
scopes.push(scope.to_string());
}
}
}
_ => {}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(untagged)]
pub enum Audience {
#[default]
None,
Single(String),
Multiple(Vec<String>),
}
impl Audience {
pub fn contains(&self, value: &str) -> bool {
match self {
Audience::None => false,
Audience::Single(s) => s == value,
Audience::Multiple(v) => v.iter().any(|s| s == value),
}
}
pub fn as_vec(&self) -> Vec<&str> {
match self {
Audience::None => vec![],
Audience::Single(s) => vec![s.as_str()],
Audience::Multiple(v) => v.iter().map(|s| s.as_str()).collect(),
}
}
}
impl Default for TokenClaims {
fn default() -> Self {
Self {
sub: String::new(),
iss: String::new(),
aud: Audience::None,
exp: 0,
iat: 0,
nbf: None,
jti: None,
email: None,
email_verified: None,
name: None,
given_name: None,
family_name: None,
preferred_username: None,
picture: None,
groups: Vec::new(),
roles: Vec::new(),
tid: None,
hd: None,
custom: HashMap::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_token_claims_user_id() {
let claims = TokenClaims {
sub: "user-123".into(),
email: Some("alice@example.com".into()),
email_verified: Some(true),
..Default::default()
};
assert_eq!(claims.user_id(), "alice@example.com");
let claims_no_email =
TokenClaims { sub: "user-123".into(), email: None, ..Default::default() };
assert_eq!(claims_no_email.user_id(), "user-123");
}
#[test]
fn test_token_claims_unverified_email_falls_back_to_sub() {
let claims = TokenClaims {
sub: "user-123".into(),
email: Some("alice@example.com".into()),
email_verified: Some(false),
..Default::default()
};
assert_eq!(claims.user_id(), "user-123");
assert_eq!(claims.verified_email(), None);
}
#[test]
fn test_token_claims_scopes_from_scope_and_scp() {
let mut claims = TokenClaims::default();
claims.custom.insert("scope".into(), serde_json::json!("read write"));
claims.custom.insert("scp".into(), serde_json::json!(["write", "admin"]));
assert_eq!(claims.scopes(), vec!["read", "write", "admin"]);
}
#[test]
fn test_audience_contains() {
let single = Audience::Single("client-1".into());
assert!(single.contains("client-1"));
assert!(!single.contains("client-2"));
let multiple = Audience::Multiple(vec!["client-1".into(), "client-2".into()]);
assert!(multiple.contains("client-1"));
assert!(multiple.contains("client-2"));
assert!(!multiple.contains("client-3"));
}
}