secure_session/
middleware.rs

1//! Iron specific middleware and handlers.
2
3use crate::error::SessionConfigError;
4use crate::session::{Session, SessionManager};
5use crate::SESSION_COOKIE_NAME;
6use cookie::Cookie;
7use data_encoding::BASE64;
8use iron::headers::{Cookie as IronCookie, SetCookie};
9use iron::middleware::{AroundMiddleware, Handler};
10use iron::{IronResult, Request, Response};
11use serde::de::DeserializeOwned;
12use serde::ser::Serialize;
13use std::marker::PhantomData;
14use time::{Duration, OffsetDateTime};
15use typemap;
16
17/// Uses a `SessionManager` to serialize and deserialize cookies during the request/response cycle.
18pub struct SessionHandler<V, K, S>
19where
20    V: Serialize + DeserializeOwned + 'static,
21    K: typemap::Key<Value = V>,
22    S: SessionManager<V>,
23{
24    manager: S,
25    config: SessionConfig,
26    handler: Box<dyn Handler>,
27    _key: PhantomData<K>,
28}
29
30impl<
31        V: Serialize + DeserializeOwned + 'static,
32        K: typemap::Key<Value = V>,
33        S: SessionManager<V>,
34    > SessionHandler<V, K, S>
35{
36    fn new(manager: S, config: SessionConfig, handler: Box<dyn Handler>) -> Self {
37        SessionHandler {
38            manager: manager,
39            config: config,
40            handler: handler,
41            _key: PhantomData,
42        }
43    }
44
45    fn extract_session_cookie(&self, request: &Request) -> Option<Vec<u8>> {
46        request.headers.get::<IronCookie>().and_then(|raw_cookie| {
47            raw_cookie
48                .0
49                .iter()
50                .filter_map(|c| {
51                    Cookie::parse_encoded(c.clone())
52                        .ok()
53                        .and_then(|cookie| match cookie.name_value() {
54                            (SESSION_COOKIE_NAME, value) => Some(value.to_string()),
55                            _ => None,
56                        })
57                        .and_then(|c| BASE64.decode(c.as_bytes()).ok())
58                })
59                .collect::<Vec<Vec<u8>>>()
60                .first()
61                .map(|c| c.clone())
62        })
63    }
64}
65
66impl<
67        V: Serialize + DeserializeOwned + 'static,
68        K: typemap::Key<Value = V> + Send + Sync,
69        S: SessionManager<V> + 'static,
70    > Handler for SessionHandler<V, K, S>
71{
72    fn handle(&self, mut request: &mut Request) -> IronResult<Response> {
73        // before
74        match self
75            .extract_session_cookie(&request)
76            // TODO ? error out on deserialization failure and remove cookie since it is invalid
77            .and_then(|c| self.manager.deserialize(&c).ok())
78            .and_then(|s| match s.expires {
79                Some(expires) if expires > OffsetDateTime::now_utc() => s.value,
80                None => s.value,
81                _ => None,
82            })
83            .take()
84        {
85            Some(value) => {
86                let _ = request.extensions.insert::<K>(value);
87            }
88            None => (),
89        }
90
91        // main
92        let mut response = self.handler.handle(&mut request)?;
93
94        // after
95        let expires = self
96            .config
97            .ttl_seconds
98            .map(|ttl| OffsetDateTime::now_utc() + Duration::seconds(ttl));
99        let session = Session {
100            expires: expires,
101            value: request.extensions.remove::<K>(),
102        };
103
104        // TODO unwrap
105        let session_str = BASE64.encode(&self.manager.serialize(&session).unwrap());
106
107        let cookie = Cookie::build(SESSION_COOKIE_NAME, session_str)
108            .path(&self.config.path)
109            .http_only(true)
110            .secure(self.config.secure_cookie);
111        // TODO config flag for SameSite
112
113        let cookie = (match self.config.ttl_seconds {
114            Some(ttl) => cookie.max_age(Duration::seconds(ttl)),
115            None => cookie,
116        })
117        .finish();
118
119        let mut cookies = vec![cookie.encoded().to_string()];
120
121        {
122            if let Some(set_cookie) = response.headers.get::<SetCookie>() {
123                cookies.extend(set_cookie.0.clone())
124            }
125        }
126        response.headers.set(SetCookie(cookies));
127
128        Ok(response)
129    }
130}
131
132/// Middleware for automatic session management.
133pub struct SessionMiddleware<V, K, S>
134where
135    V: Serialize + DeserializeOwned + 'static,
136    K: typemap::Key<Value = V>,
137    S: SessionManager<V>,
138{
139    manager: S,
140    config: SessionConfig,
141    _key: PhantomData<K>,
142    _value: PhantomData<V>,
143}
144
145impl<
146        V: Serialize + DeserializeOwned + 'static,
147        K: typemap::Key<Value = V>,
148        S: SessionManager<V>,
149    > SessionMiddleware<V, K, S>
150{
151    /// Create a new `SessionMiddleware` for the given `SessionManager` and `SessionConfig`.
152    pub fn new(manager: S, config: SessionConfig) -> Self {
153        SessionMiddleware {
154            manager: manager,
155            config: config,
156            _key: PhantomData,
157            _value: PhantomData,
158        }
159    }
160}
161
162impl<
163        V: Serialize + DeserializeOwned + 'static,
164        K: typemap::Key<Value = V>,
165        S: SessionManager<V> + 'static,
166    > AroundMiddleware for SessionMiddleware<V, K, S>
167where
168    SessionHandler<V, K, S>: Handler,
169{
170    fn around(self, handler: Box<dyn Handler>) -> Box<dyn Handler> {
171        Box::new(SessionHandler::<V, K, S>::new(
172            self.manager,
173            self.config,
174            handler,
175        ))
176    }
177}
178
179/// Configuration of how sessions and session cookies are created and validated.
180#[derive(Clone)]
181pub struct SessionConfig {
182    ttl_seconds: Option<i64>,
183    secure_cookie: bool,
184    path: String,
185}
186
187impl SessionConfig {
188    /// Create a new builder that is initialized with the default configuration.
189    pub fn build() -> SessionConfigBuilder {
190        SessionConfigBuilder::new()
191    }
192}
193
194impl Default for SessionConfig {
195    fn default() -> Self {
196        Self {
197            ttl_seconds: None,
198            secure_cookie: true,
199            path: "/".into(),
200        }
201    }
202}
203
204/// A utility to help build a `SessionConfig` in an API backwards compatible way.
205#[derive(Clone)]
206pub struct SessionConfigBuilder {
207    config: SessionConfig,
208}
209
210impl SessionConfigBuilder {
211    /// Create a new builder that is initialized with the default configuration.
212    pub fn new() -> Self {
213        SessionConfigBuilder {
214            config: SessionConfig::default(),
215        }
216    }
217
218    /// Set the session time to live (TTL) in seconds. Default: `None`
219    pub fn ttl_seconds(mut self, ttl_seconds: Option<i64>) -> Self {
220        self.config.ttl_seconds = ttl_seconds;
221        self
222    }
223
224    /// Set the the secure flag in the session cookie (HTTPS only).
225    pub fn secure_cookie(mut self, secure_cookie: bool) -> Self {
226        self.config.secure_cookie = secure_cookie;
227        self
228    }
229
230    /// Set the cookie's path.
231    pub fn path<I: Into<String>>(mut self, path: I) -> Self {
232        self.config.path = path.into();
233        self
234    }
235
236    /// Consume the builder and return a config.
237    pub fn finish(self) -> Result<SessionConfig, SessionConfigError> {
238        match self.config.ttl_seconds {
239            Some(ttl) if ttl < 0 => {
240                return Err(SessionConfigError::Undefined);
241            }
242            _ => (),
243        };
244        Ok(self.config)
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251    use crate::session::{
252        AesGcmSessionManager, ChaCha20Poly1305SessionManager, MultiSessionManager,
253    };
254    use iron::headers::{Cookie as IronCookie, Headers, SetCookie};
255    use iron::status;
256    use iron_test::request as mock_request;
257    use serde::{Deserialize, Serialize};
258    use typemap;
259
260    const KEY_32: [u8; 32] = *b"01234567012345670123456701234567";
261
262    #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)]
263    struct Data {
264        string: String,
265    }
266
267    struct DataKey {}
268
269    impl typemap::Key for DataKey {
270        type Value = Data;
271    }
272
273    fn mock_handler(request: &mut Request) -> IronResult<Response> {
274        let stat = match request.extensions.get::<DataKey>() {
275            Some(_) => status::Ok,
276            None => status::NoContent,
277        };
278
279        request.extensions.insert::<DataKey>(Data {
280            string: "wat".to_string(),
281        });
282
283        Ok(Response::with((stat, "")))
284    }
285
286    macro_rules! test_cases {
287        ($strct: ident, $md: ident) => {
288            mod $md {
289                use cookie::Cookie;
290                use iron::headers::{Cookie as IronCookie, Headers, SetCookie};
291                use iron::status;
292                use iron_test::request as mock_request;
293
294                use super::{mock_handler, Data, DataKey, KEY_32};
295                use $crate::middleware::{SessionConfig, SessionConfigBuilder, SessionHandler};
296                use $crate::session::$strct;
297
298                #[test]
299                fn no_expiry() {
300                    let config = SessionConfig::default();
301                    let manager = $strct::<Data>::from_key(KEY_32);
302                    let middleware = SessionHandler::<Data, DataKey, $strct<Data>>::new(
303                        manager,
304                        config,
305                        Box::new(mock_handler),
306                    );
307
308                    let path = "http://localhost/";
309
310                    let response = mock_request::get(path, Headers::new(), &middleware)
311                        .expect("request failed");
312                    assert_eq!(response.status, Some(status::NoContent));
313
314                    // get the cookies out
315                    let set_cookie = response
316                        .headers
317                        .get::<SetCookie>()
318                        .expect("no SetCookie header");
319                    let cookie = Cookie::parse(set_cookie.0[0].clone()).expect("cookie not parsed");
320                    let mut headers = Headers::new();
321                    headers.set(IronCookie(vec![cookie.to_string()]));
322
323                    // resend the request and get a different code back
324                    let response =
325                        mock_request::get(path, headers, &middleware).expect("request failed");
326                    assert_eq!(response.status, Some(status::Ok));
327                }
328
329                #[test]
330                fn sessions_expire() {
331                    let config = SessionConfigBuilder::new()
332                        .ttl_seconds(Some(0))
333                        .finish()
334                        .unwrap();
335                    let manager = $strct::<Data>::from_key(KEY_32);
336                    let middleware = SessionHandler::<Data, DataKey, $strct<Data>>::new(
337                        manager,
338                        config,
339                        Box::new(mock_handler),
340                    );
341
342                    let path = "http://localhost/";
343
344                    let response = mock_request::get(path, Headers::new(), &middleware)
345                        .expect("request failed");
346                    assert_eq!(response.status, Some(status::NoContent));
347
348                    // get the cookies out
349                    let set_cookie = response
350                        .headers
351                        .get::<SetCookie>()
352                        .expect("no SetCookie header");
353                    let cookie = Cookie::parse(set_cookie.0[0].clone()).expect("cookie not parsed");
354                    let mut headers = Headers::new();
355                    headers.set(IronCookie(vec![cookie.to_string()]));
356
357                    // resend the request and get a different code back
358                    let response =
359                        mock_request::get(path, headers, &middleware).expect("request failed");
360                    assert_eq!(response.status, Some(status::NoContent));
361                }
362            }
363        };
364    }
365
366    test_cases!(AesGcmSessionManager, aesgcm);
367    test_cases!(ChaCha20Poly1305SessionManager, chacha20poly1305);
368
369    #[test]
370    fn multisession_and_rotation() {
371        let config = SessionConfig::default();
372
373        let manager_1 = AesGcmSessionManager::<Data>::from_key(KEY_32);
374        let manager_1_clone = AesGcmSessionManager::<Data>::from_key(KEY_32);
375        let middle_1 = SessionMiddleware::<Data, DataKey, AesGcmSessionManager<Data>>::new(
376            manager_1,
377            config.clone(),
378        );
379        let handler_1 = middle_1.around(Box::new(mock_handler));
380
381        let manager_2 = ChaCha20Poly1305SessionManager::<Data>::from_key(KEY_32);
382        let manager_2_clone = ChaCha20Poly1305SessionManager::<Data>::from_key(KEY_32);
383        let middle_2 =
384            SessionMiddleware::<Data, DataKey, ChaCha20Poly1305SessionManager<Data>>::new(
385                manager_2,
386                config.clone(),
387            );
388        let handler_2 = middle_2.around(Box::new(mock_handler));
389
390        let multi = MultiSessionManager::<Data>::new(
391            Box::new(manager_2_clone),
392            vec![Box::new(manager_1_clone)],
393        );
394        let multi_middle =
395            SessionMiddleware::<Data, DataKey, MultiSessionManager<Data>>::new(multi, config);
396        let multi_handler = multi_middle.around(Box::new(mock_handler));
397
398        // make a request to the first handler and get the initial session
399        let resp = mock_request::get("http://localhost/", Headers::new(), &handler_1).unwrap();
400        let set_cookie = resp
401            .headers
402            .get::<SetCookie>()
403            .expect("no SetCookie header");
404        let cookie = Cookie::parse(set_cookie.0[0].clone()).expect("cookie not parsed");
405
406        // make a request to the multi handler
407        let mut headers = Headers::new();
408        headers.set(IronCookie(vec![cookie.to_string()]));
409        let resp = mock_request::get("http://localhost/", headers, &multi_handler).unwrap();
410        assert_eq!(resp.status, Some(status::Ok));
411
412        // get the cookie back out
413        let set_cookie = resp
414            .headers
415            .get::<SetCookie>()
416            .expect("no SetCookie header");
417        let cookie = Cookie::parse(set_cookie.0[0].clone()).expect("cookie not parsed");
418
419        // make a request to the second handler
420        let mut headers = Headers::new();
421        headers.set(IronCookie(vec![cookie.to_string()]));
422        let resp = mock_request::get("http://localhost/", headers, &handler_2).unwrap();
423        assert_eq!(resp.status, Some(status::Ok));
424    }
425}