Skip to main content

stano_security/
security_context.rs

1use serde::{Deserialize, Serialize};
2
3/// JWT payload, generic over an app-defined extension type `E` for custom claims.
4#[derive(Clone, Debug, Serialize, Deserialize)]
5pub struct Claims<E> {
6    /// Subject (typically the app's typed ID as a string)
7    pub sub: String,
8    /// Session ID (optional, for tracking sessions)
9    pub session_id: String,
10    /// Expiration time (as UTC timestamp)
11    pub exp: usize,
12    /// App-defined extensions (e.g., email, role, custom claims)
13    #[serde(flatten)]
14    pub ext: E,
15}
16
17/// Wraps validated JWT [`Claims`] for use in request handlers/extractors.
18#[derive(Clone, Debug)]
19pub struct SecurityContext<E> {
20    claims: Claims<E>,
21}
22
23impl<E> SecurityContext<E> {
24    /// Wrap already-validated claims (typically produced by `decode_jwt`).
25    pub fn new(claims: Claims<E>) -> Self {
26        Self { claims }
27    }
28
29    /// The subject of the token (typically the app's typed user ID as a string).
30    pub fn sub(&self) -> &str {
31        &self.claims.sub
32    }
33
34    /// The session ID the token was issued for.
35    pub fn session_id(&self) -> &str {
36        &self.claims.session_id
37    }
38
39    /// The app-defined extension claims (e.g. email, role).
40    pub fn ext(&self) -> &E {
41        &self.claims.ext
42    }
43
44    /// The full underlying claims.
45    pub fn claims(&self) -> &Claims<E> {
46        &self.claims
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn test_security_context_new_and_accessors_with_unit_ext() {
56        let claims = Claims {
57            sub: "user-1".to_string(),
58            session_id: "session-1".to_string(),
59            exp: 1000,
60            ext: (),
61        };
62        let context = SecurityContext::new(claims.clone());
63        assert_eq!(context.sub(), "user-1");
64        assert_eq!(context.session_id(), "session-1");
65        assert_eq!(*context.ext(), ());
66        assert_eq!(context.claims().exp, 1000);
67    }
68
69    #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
70    struct CustomExt {
71        role: String,
72    }
73
74    #[test]
75    fn test_security_context_new_and_accessors_with_custom_ext_struct() {
76        let claims = Claims {
77            sub: "user-2".to_string(),
78            session_id: "session-2".to_string(),
79            exp: 2000,
80            ext: CustomExt {
81                role: "admin".to_string(),
82            },
83        };
84        let context = SecurityContext::new(claims.clone());
85        assert_eq!(context.sub(), "user-2");
86        assert_eq!(context.session_id(), "session-2");
87        assert_eq!(context.ext().role, "admin");
88        assert_eq!(context.claims().exp, 2000);
89    }
90}