openagent_auth_protocol/
session.rs1use std::collections::HashMap;
9use std::sync::{Arc, RwLock};
10
11use chrono::{DateTime, Utc};
12use serde::{Deserialize, Serialize};
13
14use crate::error::AuthProtocolError;
15
16pub type SessionId = String;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21pub enum SessionState {
22 AwaitingProof,
24 Established,
26 Closed,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct Session {
33 pub id: SessionId,
35 pub state: SessionState,
37 pub initiator_did: String,
39 pub responder_did: String,
41 pub nonce: String,
43 pub created_at: DateTime<Utc>,
45 pub expires_at: DateTime<Utc>,
47 #[serde(default)]
49 pub capabilities: Vec<String>,
50}
51
52impl Session {
53 pub fn is_expired(&self) -> bool {
55 Utc::now() >= self.expires_at
56 }
57
58 pub fn is_valid(&self) -> bool {
60 self.state == SessionState::Established && !self.is_expired()
61 }
62}
63
64#[async_trait::async_trait]
68pub trait SessionStore: Send + Sync {
69 async fn put(&self, session: Session) -> Result<(), AuthProtocolError>;
71 async fn get(&self, id: &str) -> Result<Option<Session>, AuthProtocolError>;
73 async fn remove(&self, id: &str) -> Result<(), AuthProtocolError>;
75}
76
77#[derive(Debug, Clone, Default)]
79pub struct InMemorySessionStore {
80 sessions: Arc<RwLock<HashMap<SessionId, Session>>>,
81}
82
83impl InMemorySessionStore {
84 pub fn new() -> Self {
86 Self::default()
87 }
88}
89
90#[async_trait::async_trait]
91impl SessionStore for InMemorySessionStore {
92 async fn put(&self, session: Session) -> Result<(), AuthProtocolError> {
93 let mut map = self
94 .sessions
95 .write()
96 .map_err(|e| AuthProtocolError::Internal(format!("session lock poisoned: {e}")))?;
97 map.insert(session.id.clone(), session);
98 Ok(())
99 }
100
101 async fn get(&self, id: &str) -> Result<Option<Session>, AuthProtocolError> {
102 let map = self
103 .sessions
104 .read()
105 .map_err(|e| AuthProtocolError::Internal(format!("session lock poisoned: {e}")))?;
106 Ok(map.get(id).cloned())
107 }
108
109 async fn remove(&self, id: &str) -> Result<(), AuthProtocolError> {
110 let mut map = self
111 .sessions
112 .write()
113 .map_err(|e| AuthProtocolError::Internal(format!("session lock poisoned: {e}")))?;
114 map.remove(id);
115 Ok(())
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[tokio::test]
124 async fn put_and_get_session() {
125 let store = InMemorySessionStore::new();
126 let session = Session {
127 id: "sess-001".into(),
128 state: SessionState::AwaitingProof,
129 initiator_did: "did:oas:test:agent:alice".into(),
130 responder_did: "did:oas:test:agent:bob".into(),
131 nonce: "test-nonce".into(),
132 created_at: Utc::now(),
133 expires_at: Utc::now() + chrono::Duration::hours(1),
134 capabilities: vec![],
135 };
136 store.put(session.clone()).await.unwrap();
137 let retrieved = store.get("sess-001").await.unwrap();
138 assert!(retrieved.is_some());
139 assert_eq!(retrieved.unwrap().initiator_did, "did:oas:test:agent:alice");
140 }
141
142 #[tokio::test]
143 async fn get_missing_session_returns_none() {
144 let store = InMemorySessionStore::new();
145 let result = store.get("nonexistent").await.unwrap();
146 assert!(result.is_none());
147 }
148
149 #[tokio::test]
150 async fn remove_session() {
151 let store = InMemorySessionStore::new();
152 let session = Session {
153 id: "sess-002".into(),
154 state: SessionState::Established,
155 initiator_did: "did:oas:test:agent:alice".into(),
156 responder_did: "did:oas:test:agent:bob".into(),
157 nonce: "nonce".into(),
158 created_at: Utc::now(),
159 expires_at: Utc::now() + chrono::Duration::hours(1),
160 capabilities: vec![],
161 };
162 store.put(session).await.unwrap();
163 store.remove("sess-002").await.unwrap();
164 assert!(store.get("sess-002").await.unwrap().is_none());
165 }
166
167 #[test]
168 fn expired_session_is_detected() {
169 let session = Session {
170 id: "sess-003".into(),
171 state: SessionState::Established,
172 initiator_did: "did:oas:test:agent:alice".into(),
173 responder_did: "did:oas:test:agent:bob".into(),
174 nonce: "nonce".into(),
175 created_at: Utc::now() - chrono::Duration::hours(2),
176 expires_at: Utc::now() - chrono::Duration::hours(1),
177 capabilities: vec![],
178 };
179 assert!(session.is_expired());
180 assert!(!session.is_valid());
181 }
182
183 #[test]
184 fn valid_session_passes_check() {
185 let session = Session {
186 id: "sess-004".into(),
187 state: SessionState::Established,
188 initiator_did: "did:oas:test:agent:alice".into(),
189 responder_did: "did:oas:test:agent:bob".into(),
190 nonce: "nonce".into(),
191 created_at: Utc::now(),
192 expires_at: Utc::now() + chrono::Duration::hours(1),
193 capabilities: vec![],
194 };
195 assert!(!session.is_expired());
196 assert!(session.is_valid());
197 }
198}