1use 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
32fn 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#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
46#[serde(untagged)]
47pub enum SessionToken {
48 Opaque(String),
51 Jwt(String),
54}
55
56impl SessionToken {
57 pub fn new(token: &str) -> Self {
59 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 pub fn new_random() -> Self {
69 SessionToken::Opaque(generate_random_string(32))
70 }
71
72 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 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 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 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 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 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 pub fn into_inner(self) -> String {
130 match self {
131 SessionToken::Opaque(token) => token,
132 SessionToken::Jwt(token) => token,
133 }
134 }
135
136 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
163impl 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#[derive(Debug, Serialize, Deserialize)]
175pub struct JwtClaims {
176 pub sub: String,
178 pub iat: i64,
180 pub exp: i64,
182 #[serde(skip_serializing_if = "Option::is_none")]
184 pub iss: Option<String>,
185 #[serde(skip_serializing_if = "Option::is_none")]
187 pub metadata: Option<JwtMetadata>,
188}
189
190#[derive(Debug, Serialize, Deserialize)]
192pub struct JwtMetadata {
193 #[serde(skip_serializing_if = "Option::is_none")]
195 pub user_agent: Option<String>,
196 #[serde(skip_serializing_if = "Option::is_none")]
198 pub ip_address: Option<String>,
199}
200
201#[derive(Debug, Clone)]
203pub enum JwtAlgorithm {
204 RS256 {
206 private_key: Vec<u8>,
208 public_key: Vec<u8>,
210 },
211 HS256 {
213 secret_key: Vec<u8>,
215 },
216}
217
218#[derive(Debug, Clone)]
220pub struct JwtConfig {
221 pub algorithm: JwtAlgorithm,
223 pub issuer: Option<String>,
225 pub include_metadata: bool,
227}
228
229impl JwtConfig {
230 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 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 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 #[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 pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
281 self.issuer = Some(issuer.into());
282 self
283 }
284
285 pub fn with_metadata(mut self, include_metadata: bool) -> Self {
287 self.include_metadata = include_metadata;
288 self
289 }
290
291 pub fn jwt_algorithm(&self) -> Algorithm {
293 match &self.algorithm {
294 JwtAlgorithm::RS256 { .. } => Algorithm::RS256,
295 JwtAlgorithm::HS256 { .. } => Algorithm::HS256,
296 }
297 }
298
299 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 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 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 pub token: SessionToken,
332
333 pub user_id: UserId,
335
336 pub user_agent: Option<String>,
338
339 pub ip_address: Option<String>,
341
342 pub created_at: DateTime<Utc>,
344
345 pub updated_at: DateTime<Utc>,
347
348 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 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 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
483pub 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
565pub 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 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 let claims =
601 session.to_jwt_claims(self.config.issuer.clone(), self.config.include_metadata);
602
603 let jwt_token = SessionToken::new_jwt(&claims, &self.config)?;
605
606 Ok(Session {
608 token: jwt_token,
609 ..session
610 })
611 }
612
613 async fn get_session(&self, id: &SessionToken) -> Result<Session, Error> {
614 let claims = match id.verify_jwt(&self.config) {
616 Ok(claims) => claims,
617 Err(Error::Session(SessionError::InvalidToken(msg))) => {
618 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 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 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 Ok(())
644 }
645
646 async fn cleanup_expired_sessions(&self) -> Result<(), Error> {
647 Ok(())
649 }
650
651 async fn delete_sessions_for_user(&self, _user_id: &UserId) -> Result<(), Error> {
652 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 const TEST_HS256_SECRET: &[u8] = b"this_is_a_test_secret_key_for_hs256_jwt_tokens_not_for_prod";
669
670 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 let config = JwtConfig::new_rs256(
715 TEST_RS256_PRIVATE_KEY.to_vec(),
716 TEST_RS256_PUBLIC_KEY.to_vec(),
717 );
718
719 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 let token = SessionToken::new_jwt(&claims, &config).unwrap();
731
732 let verified_claims = token.verify_jwt(&config).unwrap();
734
735 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 let claims = session.to_jwt_claims(config.issuer.clone(), config.include_metadata);
806
807 let token = SessionToken::new_jwt(&claims, &config).unwrap();
809
810 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 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 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 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 let result = jwt_manager.get_session(&token).await;
877 assert!(result.is_err());
878
879 if let Err(Error::Session(SessionError::Expired)) = result {
880 } else {
882 panic!("Expected SessionError::Expired, got {result:?}");
883 }
884 }
885}