Skip to main content

soaprs_auth/
lifecycle.rs

1//! Session and token lifecycle ports implemented by storage and crypto packages.
2
3use std::time::SystemTime;
4
5use soaprs_core::{BoxFuture, SoapResult};
6
7use crate::SessionId;
8
9/// Server-side authenticated session value.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct Session<P> {
12    /// Opaque session identity.
13    id: SessionId,
14    /// Authenticated principal snapshot or application session principal.
15    principal: P,
16    /// Creation time supplied by the application clock.
17    created_at: SystemTime,
18    /// Absolute expiration time.
19    expires_at: SystemTime,
20}
21
22impl<P> Session<P> {
23    /// Creates a session with a strictly later expiration time.
24    pub fn new(
25        id: SessionId,
26        principal: P,
27        created_at: SystemTime,
28        expires_at: SystemTime,
29    ) -> SoapResult<Self> {
30        if expires_at <= created_at {
31            return Err(soaprs_core::SoapError::validation(
32                "session expiration must be later than creation",
33            ));
34        }
35        Ok(Self {
36            id,
37            principal,
38            created_at,
39            expires_at,
40        })
41    }
42
43    /// Reports expiration against an externally supplied time.
44    pub fn is_expired_at(&self, now: SystemTime) -> bool {
45        now >= self.expires_at
46    }
47
48    /// Returns the opaque session identity.
49    pub const fn id(&self) -> &SessionId {
50        &self.id
51    }
52
53    /// Returns the authenticated principal snapshot.
54    pub const fn principal(&self) -> &P {
55        &self.principal
56    }
57
58    /// Returns the externally supplied creation time.
59    pub const fn created_at(&self) -> SystemTime {
60        self.created_at
61    }
62
63    /// Returns the absolute expiration time.
64    pub const fn expires_at(&self) -> SystemTime {
65        self.expires_at
66    }
67
68    /// Consumes the session into its principal.
69    pub fn into_principal(self) -> P {
70        self.principal
71    }
72}
73
74/// Persistence port for server-side sessions.
75pub trait SessionStore<P>: Send + Sync
76where
77    P: Send,
78{
79    /// Loads a session when it exists.
80    fn load<'a>(&'a self, id: &'a SessionId) -> BoxFuture<'a, SoapResult<Option<Session<P>>>>;
81
82    /// Inserts or replaces a session.
83    fn save(&self, session: Session<P>) -> BoxFuture<'_, SoapResult<()>>;
84
85    /// Removes a session. Missing sessions are successful no-ops.
86    fn delete<'a>(&'a self, id: &'a SessionId) -> BoxFuture<'a, SoapResult<()>>;
87}
88
89/// Access and refresh credentials issued together.
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct TokenPair<A, R> {
92    /// Short-lived credential used to access protected resources.
93    pub access_token: A,
94    /// Credential used for rotation without re-entering primary credentials.
95    pub refresh_token: R,
96}
97
98/// Token issue, rotation, and revocation application port.
99pub trait TokenService<P>: Send + Sync
100where
101    P: Send + Sync,
102{
103    /// Concrete access-token representation owned by an implementation package.
104    type AccessToken: Send;
105    /// Concrete refresh-token representation owned by an implementation package.
106    type RefreshToken: Send;
107
108    /// Issues a new access/refresh pair.
109    fn issue(
110        &self,
111        principal: &P,
112    ) -> BoxFuture<'_, SoapResult<TokenPair<Self::AccessToken, Self::RefreshToken>>>;
113
114    /// Validates and rotates a refresh token.
115    fn refresh(
116        &self,
117        refresh_token: Self::RefreshToken,
118    ) -> BoxFuture<'_, SoapResult<TokenPair<Self::AccessToken, Self::RefreshToken>>>;
119
120    /// Revokes a refresh token or its token family.
121    fn revoke(&self, refresh_token: Self::RefreshToken) -> BoxFuture<'_, SoapResult<()>>;
122}
123
124#[cfg(test)]
125mod tests {
126    use std::time::{Duration, UNIX_EPOCH};
127
128    use super::Session;
129    use crate::SessionId;
130
131    #[test]
132    fn sessions_require_forward_expiration_and_use_explicit_time() {
133        let Some(id) = SessionId::new("session-1").ok() else {
134            panic!("valid session ID");
135        };
136        assert!(Session::new(id.clone(), (), UNIX_EPOCH, UNIX_EPOCH).is_err());
137        let session = Session::new(id, (), UNIX_EPOCH, UNIX_EPOCH + Duration::from_secs(60));
138        let Some(session) = session.ok() else {
139            panic!("valid session");
140        };
141        assert!(!session.is_expired_at(UNIX_EPOCH + Duration::from_secs(59)));
142        assert!(session.is_expired_at(UNIX_EPOCH + Duration::from_secs(60)));
143    }
144}