torii_core/
session.rs

1//! Session management
2//!
3//! This module contains the core session struct and related functionality.
4//!
5//! Sessions are used to track user sessions and are used to authenticate users. The core session struct is defined as follows:
6//!
7//! | Field        | Type             | Description                                            |
8//! | ------------ | ---------------- | ------------------------------------------------------ |
9//! | `id`         | `String`         | The unique identifier for the session.                 |
10//! | `user_id`    | `String`         | The unique identifier for the user.                    |
11//! | `user_agent` | `Option<String>` | The user agent of the client that created the session. |
12//! | `ip_address` | `Option<String>` | The IP address of the client that created the session. |
13//! | `created_at` | `DateTime`       | The timestamp when the session was created.            |
14//! | `updated_at` | `DateTime`       | The timestamp when the session was last updated.       |
15//! | `expires_at` | `DateTime`       | The timestamp when the session will expire.            |
16use std::path::Path;
17use std::sync::Arc;
18
19use async_trait::async_trait;
20use base64::{Engine, prelude::BASE64_URL_SAFE_NO_PAD};
21use chrono::{DateTime, Duration, Utc};
22use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode};
23use rand::{TryRngCore, rngs::OsRng};
24use serde::{Deserialize, Serialize};
25
26use crate::{
27    Error, SessionStorage,
28    error::{SessionError, StorageError, ValidationError},
29    user::UserId,
30};
31
32/// Generate a random string of the specified length
33// TODO: Make this a try_generate_random_string?
34// TODO: Add a time-based token generation method for happy B-Trees?
35fn generate_random_string(length: usize) -> String {
36    if length < 32 {
37        panic!("Length must be at least 32");
38    }
39    let mut bytes = vec![0u8; length];
40    OsRng.try_fill_bytes(&mut bytes).unwrap();
41    BASE64_URL_SAFE_NO_PAD.encode(bytes)
42}
43
44/// Session token type enum - either a simple opaque token or a JWT
45#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
46#[serde(untagged)]
47pub enum SessionToken {
48    /// Opaque token - a token with at least 128 bits of entropy
49    /// used for performing lookups in the session storage
50    Opaque(String),
51    /// JWT token - contains the session data within the token without
52    /// any additional lookup in the session storage
53    Jwt(String),
54}
55
56impl SessionToken {
57    /// Create a new session token from an existing string
58    pub fn new(token: &str) -> Self {
59        // If the token looks like a JWT (contains two '.' separators), use JWT variant
60        if token.chars().filter(|&c| c == '.').count() == 2 {
61            SessionToken::Jwt(token.to_string())
62        } else {
63            SessionToken::Opaque(token.to_string())
64        }
65    }
66
67    /// Create a new random opaque session token
68    pub fn new_random() -> Self {
69        SessionToken::Opaque(generate_random_string(32))
70    }
71
72    /// Create a new JWT session token with the specified algorithm
73    pub fn new_jwt(claims: &JwtClaims, config: &JwtConfig) -> Result<Self, Error> {
74        let header = Header::new(config.jwt_algorithm());
75
76        let encoding_key = config.get_encoding_key()?;
77
78        let token = encode(&header, claims, &encoding_key)
79            .map_err(|e| SessionError::InvalidToken(format!("Failed to encode JWT: {e}")))?;
80
81        Ok(SessionToken::Jwt(token))
82    }
83
84    /// Verify a JWT session token and return the claims
85    pub fn verify_jwt(&self, config: &JwtConfig) -> Result<JwtClaims, Error> {
86        match self {
87            SessionToken::Jwt(token) => {
88                let decoding_key = config.get_decoding_key()?;
89                let validation = config.get_validation();
90
91                let token_data =
92                    decode::<JwtClaims>(token, &decoding_key, &validation).map_err(|e| {
93                        SessionError::InvalidToken(format!("JWT validation failed: {e}"))
94                    })?;
95
96                Ok(token_data.claims)
97            }
98            SessionToken::Opaque(_) => Err(Error::Session(SessionError::InvalidToken(
99                "Not a JWT token".to_string(),
100            ))),
101        }
102    }
103
104    /// Create a new JWT session token using RS256 algorithm
105    pub fn new_jwt_rs256(claims: &JwtClaims, private_key: &[u8]) -> Result<Self, Error> {
106        let config = JwtConfig::new_rs256(private_key.to_vec(), vec![]);
107        Self::new_jwt(claims, &config)
108    }
109
110    /// Verify a JWT session token using RS256 algorithm and return the claims
111    pub fn verify_jwt_rs256(&self, public_key: &[u8]) -> Result<JwtClaims, Error> {
112        let config = JwtConfig::new_rs256(vec![], public_key.to_vec());
113        self.verify_jwt(&config)
114    }
115
116    /// Create a new JWT session token using HS256 algorithm
117    pub fn new_jwt_hs256(claims: &JwtClaims, secret_key: &[u8]) -> Result<Self, Error> {
118        let config = JwtConfig::new_hs256(secret_key.to_vec());
119        Self::new_jwt(claims, &config)
120    }
121
122    /// Verify a JWT session token using HS256 algorithm and return the claims
123    pub fn verify_jwt_hs256(&self, secret_key: &[u8]) -> Result<JwtClaims, Error> {
124        let config = JwtConfig::new_hs256(secret_key.to_vec());
125        self.verify_jwt(&config)
126    }
127
128    /// Get the inner token string
129    pub fn into_inner(self) -> String {
130        match self {
131            SessionToken::Opaque(token) => token,
132            SessionToken::Jwt(token) => token,
133        }
134    }
135
136    /// Get a reference to the token string
137    pub fn as_str(&self) -> &str {
138        match self {
139            SessionToken::Opaque(token) => token,
140            SessionToken::Jwt(token) => token,
141        }
142    }
143}
144
145impl Default for SessionToken {
146    fn default() -> Self {
147        Self::new_random()
148    }
149}
150
151impl From<String> for SessionToken {
152    fn from(s: String) -> Self {
153        Self::new(&s)
154    }
155}
156
157impl From<&str> for SessionToken {
158    fn from(s: &str) -> Self {
159        Self::new(s)
160    }
161}
162
163// TODO: wrap in secrecy string to prevent accidental leaks?
164impl std::fmt::Display for SessionToken {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        match self {
167            SessionToken::Opaque(token) => write!(f, "{token}"),
168            SessionToken::Jwt(token) => write!(f, "{token}"),
169        }
170    }
171}
172
173/// JWT claims for session tokens
174#[derive(Debug, Serialize, Deserialize)]
175pub struct JwtClaims {
176    /// Subject - user ID
177    pub sub: String,
178    /// Issued at in seconds (as UTC timestamp)
179    pub iat: i64,
180    /// Expiration time in seconds (as UTC timestamp)
181    pub exp: i64,
182    /// Issuer
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub iss: Option<String>,
185    /// Additional data (IP, user agent, etc.)
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub metadata: Option<JwtMetadata>,
188}
189
190/// JWT metadata for additional session data
191#[derive(Debug, Serialize, Deserialize)]
192pub struct JwtMetadata {
193    /// User agent string
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub user_agent: Option<String>,
196    /// IP address
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub ip_address: Option<String>,
199}
200
201/// JWT algorithm type
202#[derive(Debug, Clone)]
203pub enum JwtAlgorithm {
204    /// RS256 - RSA with SHA-256
205    RS256 {
206        /// Private key for signing JWTs (PEM format)
207        private_key: Vec<u8>,
208        /// Public key for verifying JWTs (PEM format)
209        public_key: Vec<u8>,
210    },
211    /// HS256 - HMAC with SHA-256
212    HS256 {
213        /// Secret key for both signing and verifying
214        secret_key: Vec<u8>,
215    },
216}
217
218/// Configuration for JWT sessions
219#[derive(Debug, Clone)]
220pub struct JwtConfig {
221    /// Algorithm and keys for JWT
222    pub algorithm: JwtAlgorithm,
223    /// Issuer claim
224    pub issuer: Option<String>,
225    /// Whether to include metadata (IP, user agent) in the JWT
226    pub include_metadata: bool,
227}
228
229impl JwtConfig {
230    /// Create a new JWT configuration with RS256 algorithm
231    pub fn new_rs256(private_key: Vec<u8>, public_key: Vec<u8>) -> Self {
232        Self {
233            algorithm: JwtAlgorithm::RS256 {
234                private_key,
235                public_key,
236            },
237            issuer: None,
238            include_metadata: false,
239        }
240    }
241
242    /// Create a new JWT configuration with HS256 algorithm
243    pub fn new_hs256(secret_key: Vec<u8>) -> Self {
244        Self {
245            algorithm: JwtAlgorithm::HS256 { secret_key },
246            issuer: None,
247            include_metadata: false,
248        }
249    }
250
251    /// Create a new JWT configuration from RSA key files (PEM format)
252    pub fn from_rs256_pem_files(
253        private_key_path: impl AsRef<Path>,
254        public_key_path: impl AsRef<Path>,
255    ) -> Result<Self, Error> {
256        use std::fs::read;
257
258        let private_key = read(private_key_path).map_err(|e| {
259            ValidationError::InvalidField(format!("Failed to read private key file: {e}"))
260        })?;
261
262        let public_key = read(public_key_path).map_err(|e| {
263            ValidationError::InvalidField(format!("Failed to read public key file: {e}"))
264        })?;
265
266        Ok(Self::new_rs256(private_key, public_key))
267    }
268
269    /// Create a JWT configuration with a random HS256 secret key (for testing)
270    #[cfg(test)]
271    pub fn new_random_hs256() -> Self {
272        use rand::TryRngCore;
273
274        let mut secret_key = vec![0u8; 32];
275        rand::rng().try_fill_bytes(&mut secret_key).unwrap();
276        Self::new_hs256(secret_key)
277    }
278
279    /// Set the issuer claim
280    pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
281        self.issuer = Some(issuer.into());
282        self
283    }
284
285    /// Set whether to include metadata in the JWT
286    pub fn with_metadata(mut self, include_metadata: bool) -> Self {
287        self.include_metadata = include_metadata;
288        self
289    }
290
291    /// Get the algorithm to use with jsonwebtoken
292    pub fn jwt_algorithm(&self) -> Algorithm {
293        match &self.algorithm {
294            JwtAlgorithm::RS256 { .. } => Algorithm::RS256,
295            JwtAlgorithm::HS256 { .. } => Algorithm::HS256,
296        }
297    }
298
299    /// Get the encoding key for signing
300    // TODO: Store the key in the config struct instead of creating a new key each time for performance
301    pub fn get_encoding_key(&self) -> Result<EncodingKey, Error> {
302        match &self.algorithm {
303            JwtAlgorithm::RS256 { private_key, .. } => EncodingKey::from_rsa_pem(private_key)
304                .map_err(|e| {
305                    ValidationError::InvalidField(format!("Invalid RSA private key: {e}")).into()
306                }),
307            JwtAlgorithm::HS256 { secret_key } => Ok(EncodingKey::from_secret(secret_key)),
308        }
309    }
310
311    /// Get the decoding key for verification
312    pub fn get_decoding_key(&self) -> Result<DecodingKey, Error> {
313        match &self.algorithm {
314            JwtAlgorithm::RS256 { public_key, .. } => DecodingKey::from_rsa_pem(public_key)
315                .map_err(|e| {
316                    ValidationError::InvalidField(format!("Invalid RSA public key: {e}")).into()
317                }),
318            JwtAlgorithm::HS256 { secret_key } => Ok(DecodingKey::from_secret(secret_key)),
319        }
320    }
321
322    /// Get the validation configuration for JWT verification
323    pub fn get_validation(&self) -> Validation {
324        Validation::new(self.jwt_algorithm())
325    }
326}
327
328#[derive(Debug, Clone, Serialize, Deserialize)]
329pub struct Session {
330    /// The unique identifier for the session.
331    pub token: SessionToken,
332
333    /// The unique identifier for the user.
334    pub user_id: UserId,
335
336    /// The user agent of the client that created the session.
337    pub user_agent: Option<String>,
338
339    /// The IP address of the client that created the session.
340    pub ip_address: Option<String>,
341
342    /// The timestamp when the session was created.
343    pub created_at: DateTime<Utc>,
344
345    /// The timestamp when the session was last updated.
346    pub updated_at: DateTime<Utc>,
347
348    /// The timestamp when the session will expire.
349    pub expires_at: DateTime<Utc>,
350}
351
352impl Session {
353    pub fn builder() -> SessionBuilder {
354        SessionBuilder::default()
355    }
356
357    pub fn is_expired(&self) -> bool {
358        Utc::now() > self.expires_at
359    }
360
361    /// Convert session to JWT claims
362    pub fn to_jwt_claims(&self, issuer: Option<String>, include_metadata: bool) -> JwtClaims {
363        let metadata = if include_metadata {
364            Some(JwtMetadata {
365                user_agent: self.user_agent.clone(),
366                ip_address: self.ip_address.clone(),
367            })
368        } else {
369            None
370        };
371
372        JwtClaims {
373            sub: self.user_id.to_string(),
374            iat: self.created_at.timestamp(),
375            exp: self.expires_at.timestamp(),
376            iss: issuer,
377            metadata,
378        }
379    }
380
381    /// Create a session from JWT claims
382    pub fn from_jwt_claims(token: SessionToken, claims: &JwtClaims) -> Self {
383        let now = Utc::now();
384        let created_at = DateTime::from_timestamp(claims.iat, 0).unwrap_or(now);
385        let expires_at = DateTime::from_timestamp(claims.exp, 0).unwrap_or(now);
386
387        let (user_agent, ip_address) = if let Some(metadata) = &claims.metadata {
388            (metadata.user_agent.clone(), metadata.ip_address.clone())
389        } else {
390            (None, None)
391        };
392
393        Self {
394            token,
395            user_id: UserId::new(&claims.sub),
396            user_agent,
397            ip_address,
398            created_at,
399            updated_at: now,
400            expires_at,
401        }
402    }
403}
404
405#[derive(Default)]
406pub struct SessionBuilder {
407    token: Option<SessionToken>,
408    user_id: Option<UserId>,
409    user_agent: Option<String>,
410    ip_address: Option<String>,
411    created_at: Option<DateTime<Utc>>,
412    updated_at: Option<DateTime<Utc>>,
413    expires_at: Option<DateTime<Utc>>,
414}
415
416impl SessionBuilder {
417    pub fn token(mut self, token: SessionToken) -> Self {
418        self.token = Some(token);
419        self
420    }
421
422    pub fn user_id(mut self, user_id: UserId) -> Self {
423        self.user_id = Some(user_id);
424        self
425    }
426
427    pub fn user_agent(mut self, user_agent: Option<String>) -> Self {
428        self.user_agent = user_agent;
429        self
430    }
431
432    pub fn ip_address(mut self, ip_address: Option<String>) -> Self {
433        self.ip_address = ip_address;
434        self
435    }
436
437    pub fn created_at(mut self, created_at: DateTime<Utc>) -> Self {
438        self.created_at = Some(created_at);
439        self
440    }
441
442    pub fn updated_at(mut self, updated_at: DateTime<Utc>) -> Self {
443        self.updated_at = Some(updated_at);
444        self
445    }
446
447    pub fn expires_at(mut self, expires_at: DateTime<Utc>) -> Self {
448        self.expires_at = Some(expires_at);
449        self
450    }
451
452    pub fn build(self) -> Result<Session, Error> {
453        let now = Utc::now();
454        Ok(Session {
455            token: self.token.unwrap_or_default(),
456            user_id: self.user_id.ok_or(ValidationError::MissingField(
457                "User ID is required".to_string(),
458            ))?,
459            user_agent: self.user_agent,
460            ip_address: self.ip_address,
461            created_at: self.created_at.unwrap_or(now),
462            updated_at: self.updated_at.unwrap_or(now),
463            expires_at: self.expires_at.unwrap_or(now + Duration::days(30)),
464        })
465    }
466}
467
468#[async_trait]
469pub trait SessionManager {
470    async fn create_session(
471        &self,
472        user_id: &UserId,
473        user_agent: Option<String>,
474        ip_address: Option<String>,
475        duration: Duration,
476    ) -> Result<Session, Error>;
477    async fn get_session(&self, id: &SessionToken) -> Result<Session, Error>;
478    async fn delete_session(&self, id: &SessionToken) -> Result<(), Error>;
479    async fn cleanup_expired_sessions(&self) -> Result<(), Error>;
480    async fn delete_sessions_for_user(&self, user_id: &UserId) -> Result<(), Error>;
481}
482
483/// Default session manager that uses a storage backend
484pub struct DefaultSessionManager<S: SessionStorage> {
485    storage: Arc<S>,
486}
487
488impl<S: SessionStorage> DefaultSessionManager<S> {
489    pub fn new(storage: Arc<S>) -> Self {
490        Self { storage }
491    }
492}
493
494#[async_trait]
495impl<S: SessionStorage> SessionManager for DefaultSessionManager<S> {
496    async fn create_session(
497        &self,
498        user_id: &UserId,
499        user_agent: Option<String>,
500        ip_address: Option<String>,
501        duration: Duration,
502    ) -> Result<Session, Error> {
503        let session = Session::builder()
504            .user_id(user_id.clone())
505            .user_agent(user_agent)
506            .ip_address(ip_address)
507            .expires_at(Utc::now() + duration)
508            .build()?;
509
510        let session = self
511            .storage
512            .create_session(&session)
513            .await
514            .map_err(|e| StorageError::Database(e.to_string()))?;
515
516        Ok(session)
517    }
518
519    async fn get_session(&self, id: &SessionToken) -> Result<Session, Error> {
520        let session = self
521            .storage
522            .get_session(id)
523            .await
524            .map_err(|e| StorageError::Database(e.to_string()))?;
525
526        if let Some(session) = session {
527            if session.is_expired() {
528                self.delete_session(id).await?;
529                return Err(Error::Session(SessionError::Expired));
530            }
531            Ok(session)
532        } else {
533            Err(Error::Session(SessionError::NotFound))
534        }
535    }
536
537    async fn delete_session(&self, id: &SessionToken) -> Result<(), Error> {
538        self.storage
539            .delete_session(id)
540            .await
541            .map_err(|e| StorageError::Database(e.to_string()))?;
542
543        Ok(())
544    }
545
546    async fn cleanup_expired_sessions(&self) -> Result<(), Error> {
547        self.storage
548            .cleanup_expired_sessions()
549            .await
550            .map_err(|e| StorageError::Database(e.to_string()))?;
551
552        Ok(())
553    }
554
555    async fn delete_sessions_for_user(&self, user_id: &UserId) -> Result<(), Error> {
556        self.storage
557            .delete_sessions_for_user(user_id)
558            .await
559            .map_err(|e| StorageError::Database(e.to_string()))?;
560
561        Ok(())
562    }
563}
564
565/// JWT session manager - generates and validates JWTs with RS256 algorithm without requiring storage
566/// JWTs are self-contained and don't require storage lookup to validate.
567pub struct JwtSessionManager {
568    config: JwtConfig,
569}
570
571impl JwtSessionManager {
572    pub fn new(config: JwtConfig) -> Self {
573        Self { config }
574    }
575}
576
577#[async_trait]
578impl SessionManager for JwtSessionManager {
579    async fn create_session(
580        &self,
581        user_id: &UserId,
582        user_agent: Option<String>,
583        ip_address: Option<String>,
584        duration: Duration,
585    ) -> Result<Session, Error> {
586        let now = Utc::now();
587        let expires_at = now + duration;
588
589        // Create the session first
590        let session = Session::builder()
591            .user_id(user_id.clone())
592            .user_agent(user_agent)
593            .ip_address(ip_address)
594            .created_at(now)
595            .updated_at(now)
596            .expires_at(expires_at)
597            .build()?;
598
599        // Generate JWT claims from the session
600        let claims =
601            session.to_jwt_claims(self.config.issuer.clone(), self.config.include_metadata);
602
603        // Create the JWT token with the configured algorithm
604        let jwt_token = SessionToken::new_jwt(&claims, &self.config)?;
605
606        // Return a new session with the JWT token
607        Ok(Session {
608            token: jwt_token,
609            ..session
610        })
611    }
612
613    async fn get_session(&self, id: &SessionToken) -> Result<Session, Error> {
614        // Verify the JWT using the configured algorithm and extract claims
615        let claims = match id.verify_jwt(&self.config) {
616            Ok(claims) => claims,
617            Err(Error::Session(SessionError::InvalidToken(msg))) => {
618                // Check if it's an expired token error from JWT validation
619                if msg.contains("ExpiredSignature") {
620                    return Err(Error::Session(SessionError::Expired));
621                }
622                return Err(Error::Session(SessionError::InvalidToken(msg)));
623            }
624            Err(e) => return Err(e),
625        };
626
627        // Check if token is expired
628        let now = Utc::now();
629        let exp = DateTime::from_timestamp(claims.exp, 0).unwrap_or(now);
630        if now > exp {
631            return Err(Error::Session(SessionError::Expired));
632        }
633
634        // Create session from JWT claims
635        let session = Session::from_jwt_claims(id.clone(), &claims);
636
637        Ok(session)
638    }
639
640    async fn delete_session(&self, _id: &SessionToken) -> Result<(), Error> {
641        // JWTs are stateless, so we don't need to delete anything
642        // Client should discard the token
643        Ok(())
644    }
645
646    async fn cleanup_expired_sessions(&self) -> Result<(), Error> {
647        // JWTs are self-expiring and stateless, nothing to clean up
648        Ok(())
649    }
650
651    async fn delete_sessions_for_user(&self, _user_id: &UserId) -> Result<(), Error> {
652        // Without a revocation list, we can't invalidate existing JWTs
653        // This would require implementing a token blacklist
654        tracing::warn!(
655            "JwtSessionManager doesn't support revoking all sessions for a user; tokens will remain valid until they expire"
656        );
657        Ok(())
658    }
659}
660
661#[cfg(test)]
662mod tests {
663    use chrono::Duration;
664
665    use super::*;
666
667    // Test secret for HS256
668    const TEST_HS256_SECRET: &[u8] = b"this_is_a_test_secret_key_for_hs256_jwt_tokens_not_for_prod";
669
670    // Test private key for RS256
671    // DO NOT EVER USE THIS KEY FOR ANYTHING REAL
672    const TEST_RS256_PRIVATE_KEY: &[u8] = b"-----BEGIN PRIVATE KEY-----
673MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDBsFIR164UGIOZ
674R2nT57RQ8AloqAmJXh5KdoKZjHi5uSRALSASp1Dk0tDjiiwqvfWiUItcVqZRqsx4
675VuzjpkdoeWvwBoJ91K+DjFEAG7RjbNoaITgY8Ec5QjulpLTh9WDUeqUu4ZxPp9rF
676H+S3uJK2sD1K2KOGRVcT0a+rIyXDOXr14J7XGbB5W7j2EvkKXZinzKcdMpsL4NBu
6778ArJ8qV6lLBeKB+IbKrV0yUQGFAjTA8eoaSNaHJAZD0kubEdXEprB1SZpvaL3lZM
678AcqS6ZATo8IfiXj7H7RSHLf3ORYxQTX4T01gSfmSfgEOdTySdCSuFmDrsjcR2nWe
679Ly0QWM4jAgMBAAECggEAG9wzueWhtbn0TVB54aVjCP9grcFPTzHkE9w/GzzFmBq6
680+FDlW6QzMm7mkCGYX8o03RT5Lsjh9z5PrKxS5R35CIc/+5Bxew25n1JIIRwFvbAd
681y9i6ZnqYFsg2/IkYDFE3jT4E/keCgeyy6bGVkchcBijh8B8ASo3fzCCDGbqeXG8V
6829WEhN+xrEwJ/5s3IYY0JSVrL4BzoQT/R9/+IsvUQw9aOECDXpFsRLjoze3JVXzYa
683LklDJWe1z3i+4mR/Gwx1GLRL64bJFz0u8zUVSkY5T3SZLr7HGjlrtc/7DIctyx5w
684h80nRDohVih69z1AViXSIzYRvJ3tIq8Gp5EvYjieZQKBgQDi1Y5hvn8+KO9+9mPK
685lx/P92M1pUfSuALILctFWyFbY7XKYApJud0Nme81ASaNofINpka7tWOEBk8H0lyy
686W9uELDYHtVxKU0Ch1Q0joeKb3vcF0wMBMdOiOef+AH4R9ZqF8Mbhc/lwb86vl1BL
6871zFQZVpjg0Un57PMKefwl/yS5wKBgQDal8DTj1UaOGjsx667nUE1x6ILdRlHMIe1
688lf1VqCkP8ykFMe3iDJE1/rW/ct8uO+ZEf/8nbjeCHcnrtdF14HEPdspCSGvXW87W
68965Lsx0O7gdMKZEnN7BarTikpWJU3COcgQHGFsqjZ+07ujQWj8dPrNTd9dsYYFky8
690OKtmXJQ/ZQKBgA5G/NBAKkgiUXi/T2an/nObkZ4FyjCELoClCT9TThUvgHi9dMhR
691L420m67NZLzTbaXYSml0MFBWCVFntzfuujFmivwPOUDgXpgRDeOpQ9clwIyYTH8d
692wMFcPbLqGwVMXS6DCjGUmCWwk+TPdFlhsRPrXTYYRBkP52w5UwT8vAQPAoGAZEMu
6934trfggNVvSVp9AwRGQXUQcUYLxsHZDbD2EIlc3do3UUlg4WYJVgLLSEXVTGMUOcU
694tZVMSJY5Q7BFvvePZDRsWTK2pDUsDlBHN+u+GYdWsXGGmLktPK3BG4HSD0g6GwT0
695DQsBf9pRPgHZEHWfakciiJ2uBuZTlBG6LF1ScjECgYEA4DPQopjh/kS9j5NyUMDA
6965Pvz2mppg0NR7RQjDGET3Lh4/lDgfFyJOlsRLF+kUgAOb4s3tPg+5hujTq2FpotK
697JFQKh2GE6V1BMi+qJ9ipj0ESBv7rqPYC8ShUSr/SbkRU8jg2tOcvw+7KNtaMk6rv
698wl6BPaq7Rv4JOPgimQGP3d4=
699-----END PRIVATE KEY-----";
700
701    const TEST_RS256_PUBLIC_KEY: &[u8] = b"-----BEGIN PUBLIC KEY-----
702MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwbBSEdeuFBiDmUdp0+e0
703UPAJaKgJiV4eSnaCmYx4ubkkQC0gEqdQ5NLQ44osKr31olCLXFamUarMeFbs46ZH
704aHlr8AaCfdSvg4xRABu0Y2zaGiE4GPBHOUI7paS04fVg1HqlLuGcT6faxR/kt7iS
705trA9StijhkVXE9GvqyMlwzl69eCe1xmweVu49hL5Cl2Yp8ynHTKbC+DQbvAKyfKl
706epSwXigfiGyq1dMlEBhQI0wPHqGkjWhyQGQ9JLmxHVxKawdUmab2i95WTAHKkumQ
707E6PCH4l4+x+0Uhy39zkWMUE1+E9NYEn5kn4BDnU8knQkrhZg67I3Edp1ni8tEFjO
708IwIDAQAB
709-----END PUBLIC KEY-----";
710
711    #[test]
712    fn test_random_rs256_key_generation() {
713        // Generate a random keypair
714        let config = JwtConfig::new_rs256(
715            TEST_RS256_PRIVATE_KEY.to_vec(),
716            TEST_RS256_PUBLIC_KEY.to_vec(),
717        );
718
719        // Verify the keys are valid by creating and verifying a token
720        let user_id = UserId::new_random();
721        let session = Session::builder()
722            .user_id(user_id.clone())
723            .expires_at(Utc::now() + Duration::days(1))
724            .build()
725            .unwrap();
726
727        let claims = session.to_jwt_claims(None, false);
728
729        // Create JWT token
730        let token = SessionToken::new_jwt(&claims, &config).unwrap();
731
732        // Verify JWT token
733        let verified_claims = token.verify_jwt(&config).unwrap();
734
735        // Basic verification that keys work
736        assert_eq!(verified_claims.sub, user_id.to_string());
737    }
738
739    #[test]
740    fn test_session_token_simple() {
741        let id = SessionToken::new_random();
742        match &id {
743            SessionToken::Opaque(token) => {
744                assert_eq!(id.to_string(), token.to_string());
745            }
746            _ => panic!("Expected simple token"),
747        }
748    }
749
750    #[test]
751    fn test_session_builder() {
752        let session = Session::builder()
753            .user_id(UserId::new_random())
754            .user_agent(Some("test".to_string()))
755            .ip_address(Some("127.0.0.1".to_string()))
756            .expires_at(Utc::now() + Duration::days(30))
757            .build()
758            .unwrap();
759
760        assert!(!session.is_expired());
761    }
762
763    #[test]
764    fn test_jwt_config_hs256() {
765        let config = JwtConfig::new_hs256(TEST_HS256_SECRET.to_vec());
766
767        match &config.algorithm {
768            JwtAlgorithm::HS256 { secret_key } => {
769                assert_eq!(secret_key, &TEST_HS256_SECRET.to_vec());
770            }
771            _ => panic!("Expected HS256 algorithm"),
772        }
773
774        assert_eq!(config.jwt_algorithm(), Algorithm::HS256);
775    }
776
777    #[test]
778    fn test_jwt_config_random_hs256() {
779        let config = JwtConfig::new_random_hs256();
780
781        match &config.algorithm {
782            JwtAlgorithm::HS256 { secret_key } => {
783                assert_eq!(secret_key.len(), 32);
784            }
785            _ => panic!("Expected HS256 algorithm"),
786        }
787    }
788
789    #[test]
790    fn test_jwt_token_creation_and_verification_hs256() {
791        let config = JwtConfig::new_hs256(TEST_HS256_SECRET.to_vec())
792            .with_issuer("test-issuer-hs256")
793            .with_metadata(true);
794
795        let user_id = UserId::new_random();
796        let session = Session::builder()
797            .user_id(user_id.clone())
798            .user_agent(Some("test-agent-hs256".to_string()))
799            .ip_address(Some("127.0.0.2".to_string()))
800            .expires_at(Utc::now() + Duration::days(1))
801            .build()
802            .unwrap();
803
804        // Create JWT claims from session
805        let claims = session.to_jwt_claims(config.issuer.clone(), config.include_metadata);
806
807        // Create JWT token with HS256
808        let token = SessionToken::new_jwt(&claims, &config).unwrap();
809
810        // Verify JWT token with HS256
811        let verified_claims = token.verify_jwt(&config).unwrap();
812
813        assert_eq!(verified_claims.sub, user_id.to_string());
814        assert_eq!(verified_claims.iss, Some("test-issuer-hs256".to_string()));
815        assert!(verified_claims.metadata.is_some());
816        let metadata = verified_claims.metadata.unwrap();
817        assert_eq!(metadata.user_agent, Some("test-agent-hs256".to_string()));
818        assert_eq!(metadata.ip_address, Some("127.0.0.2".to_string()));
819
820        // Helper methods should also work
821        let token2 = SessionToken::new_jwt_hs256(&claims, TEST_HS256_SECRET).unwrap();
822        let verified_claims2 = token2.verify_jwt_hs256(TEST_HS256_SECRET).unwrap();
823        assert_eq!(verified_claims2.sub, user_id.to_string());
824    }
825
826    #[tokio::test]
827    async fn test_jwt_session_manager() {
828        let config = JwtConfig::new_hs256(TEST_HS256_SECRET.to_vec())
829            .with_issuer("test-issuer-hs256")
830            .with_metadata(true);
831
832        let jwt_manager = JwtSessionManager::new(config.clone());
833
834        let user_id = UserId::new_random();
835        let session = jwt_manager
836            .create_session(
837                &user_id,
838                Some("test-agent-hs256".to_string()),
839                Some("127.0.0.2".to_string()),
840                Duration::days(1),
841            )
842            .await
843            .unwrap();
844
845        // Verify the session can be retrieved
846        let retrieved_session = jwt_manager.get_session(&session.token).await.unwrap();
847
848        assert_eq!(retrieved_session.user_id, user_id);
849        assert_eq!(
850            retrieved_session.user_agent,
851            Some("test-agent-hs256".to_string())
852        );
853        assert_eq!(retrieved_session.ip_address, Some("127.0.0.2".to_string()));
854    }
855
856    #[tokio::test]
857    async fn test_expired_jwt_session() {
858        let config = JwtConfig::new_hs256(TEST_HS256_SECRET.to_vec());
859
860        let jwt_manager = JwtSessionManager::new(config.clone());
861
862        let user_id = UserId::new_random();
863        let now = Utc::now();
864
865        // Create a session that's already expired
866        let session = Session::builder()
867            .user_id(user_id.clone())
868            .expires_at(now - Duration::minutes(5))
869            .build()
870            .unwrap();
871
872        let claims = session.to_jwt_claims(None, false);
873        let token = SessionToken::new_jwt(&claims, &config).unwrap();
874
875        // Try to validate the expired token
876        let result = jwt_manager.get_session(&token).await;
877        assert!(result.is_err());
878
879        if let Err(Error::Session(SessionError::Expired)) = result {
880            // This is the expected error
881        } else {
882            panic!("Expected SessionError::Expired, got {result:?}");
883        }
884    }
885}