Skip to main content

arete_auth/
claims.rs

1use serde::{Deserialize, Serialize};
2
3/// Key classification for metering and policy
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "snake_case")]
6pub enum KeyClass {
7    /// Secret API key - long-lived, high trust
8    Secret,
9    /// Publishable key - safe for browsers, constrained
10    Publishable,
11}
12
13/// Resource limits for a session
14#[derive(Debug, Clone, Default, Serialize, Deserialize)]
15pub struct Limits {
16    /// Maximum concurrent connections for this subject
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub max_connections: Option<u32>,
19    /// Maximum subscriptions per connection
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub max_subscriptions: Option<u32>,
22    /// Maximum snapshot rows per request
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub max_snapshot_rows: Option<u32>,
25    /// Maximum messages per minute
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub max_messages_per_minute: Option<u32>,
28    /// Maximum egress bytes per minute
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub max_bytes_per_minute: Option<u64>,
31    /// Maximum HTTP read requests per minute
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub max_http_requests_per_minute: Option<u32>,
34    /// Maximum account addresses accepted in one HTTP batch read
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub max_http_batch_addresses: Option<u32>,
37}
38
39/// Session token claims
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct SessionClaims {
42    /// Issuer - who issued this token
43    pub iss: String,
44    /// Subject - who this token is for
45    pub sub: String,
46    /// Audience - intended recipient (e.g., deployment ID)
47    pub aud: String,
48    /// Issued at (Unix timestamp)
49    pub iat: u64,
50    /// Not valid before (Unix timestamp)
51    pub nbf: u64,
52    /// Expiration time (Unix timestamp)
53    pub exp: u64,
54    /// JWT ID - unique identifier for this token
55    pub jti: String,
56    /// Scope - permissions granted
57    pub scope: String,
58    /// Metering key - for usage attribution
59    pub metering_key: String,
60    /// Deployment ID (optional)
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub deployment_id: Option<String>,
63    /// Origin binding (optional, defense-in-depth)
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub origin: Option<String>,
66    /// Client IP binding (optional, for high-security scenarios)
67    #[serde(skip_serializing_if = "Option::is_none", rename = "client_ip")]
68    pub client_ip: Option<String>,
69    /// Resource limits
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub limits: Option<Limits>,
72    /// Plan identifier (optional)
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub plan: Option<String>,
75    /// Key class (secret vs publishable)
76    #[serde(rename = "key_class")]
77    pub key_class: KeyClass,
78}
79
80impl SessionClaims {
81    /// Create a new session claims builder
82    pub fn builder(
83        iss: impl Into<String>,
84        sub: impl Into<String>,
85        aud: impl Into<String>,
86    ) -> SessionClaimsBuilder {
87        SessionClaimsBuilder::new(iss, sub, aud)
88    }
89
90    /// Check if the token is expired
91    pub fn is_expired(&self, now: u64) -> bool {
92        self.exp <= now
93    }
94
95    /// Check if the token is valid (not before issued)
96    pub fn is_valid(&self, now: u64) -> bool {
97        self.nbf <= now && self.iat <= now
98    }
99}
100
101/// Builder for SessionClaims
102pub struct SessionClaimsBuilder {
103    iss: String,
104    sub: String,
105    aud: String,
106    iat: u64,
107    nbf: u64,
108    exp: u64,
109    jti: String,
110    scope: String,
111    metering_key: String,
112    deployment_id: Option<String>,
113    origin: Option<String>,
114    client_ip: Option<String>,
115    limits: Option<Limits>,
116    plan: Option<String>,
117    key_class: KeyClass,
118}
119
120impl SessionClaimsBuilder {
121    fn new(iss: impl Into<String>, sub: impl Into<String>, aud: impl Into<String>) -> Self {
122        use std::time::{SystemTime, UNIX_EPOCH};
123        let now = SystemTime::now()
124            .duration_since(UNIX_EPOCH)
125            .expect("time should not be before epoch")
126            .as_secs();
127
128        Self {
129            iss: iss.into(),
130            sub: sub.into(),
131            aud: aud.into(),
132            iat: now,
133            nbf: now,
134            exp: now + crate::DEFAULT_SESSION_TTL_SECONDS,
135            jti: uuid::Uuid::new_v4().to_string(),
136            scope: "read".to_string(),
137            metering_key: String::new(),
138            deployment_id: None,
139            origin: None,
140            client_ip: None,
141            limits: None,
142            plan: None,
143            key_class: KeyClass::Publishable,
144        }
145    }
146
147    pub fn with_ttl(mut self, ttl_seconds: u64) -> Self {
148        self.exp = self.iat + ttl_seconds;
149        self
150    }
151
152    pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
153        self.scope = scope.into();
154        self
155    }
156
157    pub fn with_metering_key(mut self, key: impl Into<String>) -> Self {
158        self.metering_key = key.into();
159        self
160    }
161
162    pub fn with_deployment_id(mut self, id: impl Into<String>) -> Self {
163        self.deployment_id = Some(id.into());
164        self
165    }
166
167    pub fn with_origin(mut self, origin: impl Into<String>) -> Self {
168        self.origin = Some(origin.into());
169        self
170    }
171
172    pub fn with_client_ip(mut self, client_ip: impl Into<String>) -> Self {
173        self.client_ip = Some(client_ip.into());
174        self
175    }
176
177    pub fn with_limits(mut self, limits: Limits) -> Self {
178        self.limits = Some(limits);
179        self
180    }
181
182    pub fn with_plan(mut self, plan: impl Into<String>) -> Self {
183        self.plan = Some(plan.into());
184        self
185    }
186
187    pub fn with_key_class(mut self, key_class: KeyClass) -> Self {
188        self.key_class = key_class;
189        self
190    }
191
192    pub fn with_jti(mut self, jti: impl Into<String>) -> Self {
193        self.jti = jti.into();
194        self
195    }
196
197    pub fn build(self) -> SessionClaims {
198        SessionClaims {
199            iss: self.iss,
200            sub: self.sub,
201            aud: self.aud,
202            iat: self.iat,
203            nbf: self.nbf,
204            exp: self.exp,
205            jti: self.jti,
206            scope: self.scope,
207            metering_key: self.metering_key,
208            deployment_id: self.deployment_id,
209            origin: self.origin,
210            client_ip: self.client_ip,
211            limits: self.limits,
212            plan: self.plan,
213            key_class: self.key_class,
214        }
215    }
216}
217
218/// Auth context extracted from a verified token
219#[derive(Debug, Clone)]
220pub struct AuthContext {
221    /// Subject identifier
222    pub subject: String,
223    /// Issuer
224    pub issuer: String,
225    /// Key class (secret vs publishable)
226    pub key_class: KeyClass,
227    /// Metering key for usage attribution
228    pub metering_key: String,
229    /// Deployment ID binding
230    pub deployment_id: Option<String>,
231    /// Token expiration time
232    pub expires_at: u64,
233    /// Granted scope
234    pub scope: String,
235    /// Resource limits
236    pub limits: Limits,
237    /// Plan or access tier associated with the session
238    pub plan: Option<String>,
239    /// Origin binding
240    pub origin: Option<String>,
241    /// Client IP binding
242    pub client_ip: Option<String>,
243    /// JWT ID
244    pub jti: String,
245}
246
247impl AuthContext {
248    /// Create AuthContext from verified claims
249    pub fn from_claims(claims: SessionClaims) -> Self {
250        Self {
251            subject: claims.sub,
252            issuer: claims.iss,
253            key_class: claims.key_class,
254            metering_key: claims.metering_key,
255            deployment_id: claims.deployment_id,
256            expires_at: claims.exp,
257            scope: claims.scope,
258            limits: claims.limits.unwrap_or_default(),
259            plan: claims.plan,
260            origin: claims.origin,
261            client_ip: claims.client_ip,
262            jti: claims.jti,
263        }
264    }
265}