fraiseql_core/security/oidc/
audience.rs1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Deserialize)]
11pub struct JwtClaims {
12 pub sub: Option<String>,
14
15 pub iss: Option<String>,
17
18 #[serde(default)]
20 pub aud: Audience,
21
22 pub exp: Option<i64>,
24
25 pub iat: Option<i64>,
27
28 pub nbf: Option<i64>,
30
31 pub jti: Option<String>,
35
36 pub scope: Option<String>,
38
39 pub scp: Option<Vec<String>>,
41
42 pub permissions: Option<Vec<String>>,
44
45 pub email: Option<String>,
47
48 pub email_verified: Option<bool>,
50
51 pub name: Option<String>,
53}
54
55#[derive(Debug, Clone, Default, Deserialize, Serialize)]
57#[serde(untagged)]
58#[non_exhaustive]
59pub enum Audience {
60 #[default]
62 None,
63 Single(String),
65 Multiple(Vec<String>),
67}
68
69impl Audience {
70 pub fn contains(&self, value: &str) -> bool {
72 match self {
73 Self::None => false,
74 Self::Single(s) => s == value,
75 Self::Multiple(v) => v.iter().any(|s| s == value),
76 }
77 }
78
79 pub fn to_vec(&self) -> Vec<String> {
81 match self {
82 Self::None => Vec::new(),
83 Self::Single(s) => vec![s.clone()],
84 Self::Multiple(v) => v.clone(),
85 }
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 #![allow(clippy::unwrap_used)] use super::*;
94
95 #[test]
96 fn test_audience_none() {
97 let aud = Audience::None;
98 assert!(!aud.contains("test"));
99 assert!(aud.to_vec().is_empty());
100 }
101
102 #[test]
103 fn test_audience_single() {
104 let aud = Audience::Single("my-api".to_string());
105 assert!(aud.contains("my-api"));
106 assert!(!aud.contains("other"));
107 assert_eq!(aud.to_vec(), vec!["my-api"]);
108 }
109
110 #[test]
111 fn test_audience_multiple() {
112 let aud = Audience::Multiple(vec!["api1".to_string(), "api2".to_string()]);
113 assert!(aud.contains("api1"));
114 assert!(aud.contains("api2"));
115 assert!(!aud.contains("api3"));
116 assert_eq!(aud.to_vec(), vec!["api1", "api2"]);
117 }
118
119 #[test]
120 fn test_jwt_claims_deserialization() {
121 let claims_json = r#"{
122 "sub": "user123",
123 "iss": "https://issuer.example.com",
124 "aud": "my-api",
125 "exp": 1735689600,
126 "iat": 1735686000,
127 "scope": "read write",
128 "email": "user@example.com"
129 }"#;
130
131 let claims: JwtClaims = serde_json::from_str(claims_json).unwrap();
132 assert_eq!(claims.sub, Some("user123".to_string()));
133 assert_eq!(claims.iss, Some("https://issuer.example.com".to_string()));
134 assert!(claims.aud.contains("my-api"));
135 assert_eq!(claims.exp, Some(1_735_689_600));
136 assert_eq!(claims.scope, Some("read write".to_string()));
137 }
138
139 #[test]
140 fn test_jwt_claims_array_audience() {
141 let claims_json = r#"{
142 "sub": "user123",
143 "aud": ["api1", "api2"],
144 "exp": 1735689600
145 }"#;
146
147 let claims: JwtClaims = serde_json::from_str(claims_json).unwrap();
148 assert!(claims.aud.contains("api1"));
149 assert!(claims.aud.contains("api2"));
150 }
151}