torii_core/session/
jwt.rs

1//! JWT session provider implementation
2//!
3//! This module provides a stateless session provider using JSON Web Tokens (JWT).
4//! JWTs are self-contained and don't require database lookups for validation.
5
6use async_trait::async_trait;
7use chrono::{DateTime, Duration, Utc};
8
9use crate::{Error, JwtConfig, Session, SessionToken, UserId, error::SessionError};
10
11use super::provider::SessionProvider;
12
13/// JWT-based session provider
14///
15/// This provider creates and validates JWT tokens without requiring
16/// any persistent storage. Sessions are entirely self-contained within
17/// the JWT payload.
18pub struct JwtSessionProvider {
19    config: JwtConfig,
20}
21
22impl JwtSessionProvider {
23    /// Create a new JWT session provider with the given configuration
24    pub fn new(config: JwtConfig) -> Self {
25        Self { config }
26    }
27}
28
29#[async_trait]
30impl SessionProvider for JwtSessionProvider {
31    async fn create_session(
32        &self,
33        user_id: &UserId,
34        user_agent: Option<String>,
35        ip_address: Option<String>,
36        duration: Duration,
37    ) -> Result<Session, Error> {
38        let now = Utc::now();
39        let expires_at = now + duration;
40
41        // Create the session first
42        let session = Session::builder()
43            .user_id(user_id.clone())
44            .user_agent(user_agent)
45            .ip_address(ip_address)
46            .created_at(now)
47            .updated_at(now)
48            .expires_at(expires_at)
49            .build()?;
50
51        // Generate JWT claims from the session
52        let claims =
53            session.to_jwt_claims(self.config.issuer.clone(), self.config.include_metadata);
54
55        // Create the JWT token with the configured algorithm
56        let jwt_token = SessionToken::new_jwt(&claims, &self.config)?;
57
58        // Return a new session with the JWT token
59        Ok(Session {
60            token: jwt_token,
61            ..session
62        })
63    }
64
65    async fn get_session(&self, token: &SessionToken) -> Result<Session, Error> {
66        // Verify the JWT using the configured algorithm and extract claims
67        let claims = match token.verify_jwt(&self.config) {
68            Ok(claims) => claims,
69            Err(Error::Session(SessionError::InvalidToken(msg))) => {
70                // Check if it's an expired token error from JWT validation
71                if msg.contains("ExpiredSignature") {
72                    return Err(Error::Session(SessionError::Expired));
73                }
74                return Err(Error::Session(SessionError::InvalidToken(msg)));
75            }
76            Err(e) => return Err(e),
77        };
78
79        // Check if token is expired
80        let now = Utc::now();
81        let exp = DateTime::from_timestamp(claims.exp, 0).unwrap_or(now);
82        if now > exp {
83            return Err(Error::Session(SessionError::Expired));
84        }
85
86        // Create session from JWT claims
87        let session = Session::from_jwt_claims(token.clone(), &claims);
88
89        Ok(session)
90    }
91
92    async fn delete_session(&self, _token: &SessionToken) -> Result<(), Error> {
93        // JWTs are stateless, so we don't need to delete anything
94        // Client should discard the token
95        Ok(())
96    }
97
98    async fn cleanup_expired_sessions(&self) -> Result<(), Error> {
99        // JWTs are self-expiring and stateless, nothing to clean up
100        Ok(())
101    }
102
103    async fn delete_sessions_for_user(&self, _user_id: &UserId) -> Result<(), Error> {
104        // Without a revocation list, we can't invalidate existing JWTs
105        // This would require implementing a token blacklist
106        tracing::warn!(
107            "JwtSessionProvider doesn't support revoking all sessions for a user; tokens will remain valid until they expire"
108        );
109        Ok(())
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    const TEST_HS256_SECRET: &[u8] = b"test_secret_key_for_hs256_jwt_tokens_not_for_production_use";
118
119    #[tokio::test]
120    async fn test_jwt_session_provider_create_and_get() {
121        let config = JwtConfig::new_hs256(TEST_HS256_SECRET.to_vec())
122            .with_issuer("test-issuer")
123            .with_metadata(true);
124
125        let provider = JwtSessionProvider::new(config);
126
127        let user_id = UserId::new_random();
128        let user_agent = Some("test-agent".to_string());
129        let ip_address = Some("127.0.0.1".to_string());
130        let duration = Duration::hours(1);
131
132        // Create a session
133        let session = provider
134            .create_session(&user_id, user_agent.clone(), ip_address.clone(), duration)
135            .await
136            .unwrap();
137
138        assert_eq!(session.user_id, user_id);
139        assert_eq!(session.user_agent, user_agent);
140        assert_eq!(session.ip_address, ip_address);
141
142        // Retrieve the session
143        let retrieved = provider.get_session(&session.token).await.unwrap();
144
145        assert_eq!(retrieved.user_id, user_id);
146        assert_eq!(retrieved.user_agent, user_agent);
147        assert_eq!(retrieved.ip_address, ip_address);
148    }
149
150    #[tokio::test]
151    async fn test_jwt_session_provider_expired_session() {
152        let config = JwtConfig::new_hs256(TEST_HS256_SECRET.to_vec());
153        let provider = JwtSessionProvider::new(config.clone());
154
155        let user_id = UserId::new_random();
156
157        // Create an already expired session
158        let now = Utc::now();
159        let session = Session::builder()
160            .user_id(user_id.clone())
161            .expires_at(now - Duration::minutes(5))
162            .build()
163            .unwrap();
164
165        let claims = session.to_jwt_claims(None, false);
166        let token = SessionToken::new_jwt(&claims, &config).unwrap();
167
168        // Try to get the expired session
169        let result = provider.get_session(&token).await;
170
171        assert!(matches!(result, Err(Error::Session(SessionError::Expired))));
172    }
173
174    #[tokio::test]
175    async fn test_jwt_session_provider_invalid_token() {
176        let config = JwtConfig::new_hs256(TEST_HS256_SECRET.to_vec());
177        let provider = JwtSessionProvider::new(config);
178
179        // Create an invalid token
180        let invalid_token = SessionToken::Jwt("invalid.jwt.token".to_string());
181
182        let result = provider.get_session(&invalid_token).await;
183
184        assert!(matches!(
185            result,
186            Err(Error::Session(SessionError::InvalidToken(_)))
187        ));
188    }
189}