From 3c48e00e7a0dfe1252bc6b25ab9dfe365b55e374 Mon Sep 17 00:00:00 2001
From: Yuki Okushi <jtitor@2k36.org>
Date: Wed, 22 Jun 2022 20:42:46 +0900
Subject: [PATCH 01/31] Bump up MSRV to 1.57 (#256)

---
 .github/workflows/ci.yml      | 4 ++--
 actix-cors/CHANGES.md         | 3 ++-
 actix-cors/README.md          | 2 +-
 actix-identity/CHANGES.md     | 3 ++-
 actix-identity/README.md      | 2 +-
 actix-protobuf/CHANGES.md     | 3 ++-
 actix-protobuf/README.md      | 2 +-
 actix-redis/CHANGES.md        | 3 ++-
 actix-redis/README.md         | 2 +-
 actix-session/CHANGES.md      | 3 ++-
 actix-session/README.md       | 2 +-
 actix-web-httpauth/CHANGES.md | 3 ++-
 actix-web-httpauth/README.md  | 2 +-
 clippy.toml                   | 2 +-
 14 files changed, 21 insertions(+), 15 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index da39918c0..8a244adc1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,7 +14,7 @@ jobs:
         target:
           - { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu }
         version:
-          - 1.54.0 # MSRV
+          - 1.57 # MSRV
           - stable
 
     name: ${{ matrix.target.name }} / ${{ matrix.version }}
@@ -85,7 +85,7 @@ jobs:
           - { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
           - { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc }
         version:
-          - 1.54.0 # MSRV
+          - 1.57 # MSRV
           - stable
 
     name: ${{ matrix.target.name }} / ${{ matrix.version }}
diff --git a/actix-cors/CHANGES.md b/actix-cors/CHANGES.md
index 21a489d30..108c65de1 100644
--- a/actix-cors/CHANGES.md
+++ b/actix-cors/CHANGES.md
@@ -1,6 +1,7 @@
 # Changes
 
-## Unreleased - 2021-xx-xx
+## Unreleased - 2022-xx-xx
+- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
 
 
 ## 0.6.1 - 2022-03-07
diff --git a/actix-cors/README.md b/actix-cors/README.md
index 5d76387ad..dc6f4d112 100644
--- a/actix-cors/README.md
+++ b/actix-cors/README.md
@@ -11,4 +11,4 @@
 
 - [API Documentation](https://docs.rs/actix-cors)
 - [Example Project](https://github.com/actix/examples/tree/master/cors)
-- Minimum Supported Rust Version (MSRV): 1.54
+- Minimum Supported Rust Version (MSRV): 1.57
diff --git a/actix-identity/CHANGES.md b/actix-identity/CHANGES.md
index c8dc4d731..5d57b5bac 100644
--- a/actix-identity/CHANGES.md
+++ b/actix-identity/CHANGES.md
@@ -1,6 +1,7 @@
 # Changes
 
-## Unreleased - 2021-xx-xx
+## Unreleased - 2022-xx-xx
+- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
 
 
 ## 0.4.0 - 2022-03-01
diff --git a/actix-identity/README.md b/actix-identity/README.md
index daa48bc9b..4dbbe5e85 100644
--- a/actix-identity/README.md
+++ b/actix-identity/README.md
@@ -10,4 +10,4 @@
 ## Documentation & community resources
 
 * [API Documentation](https://docs.rs/actix-identity)
-* Minimum Supported Rust Version (MSRV): 1.54
+* Minimum Supported Rust Version (MSRV): 1.57
diff --git a/actix-protobuf/CHANGES.md b/actix-protobuf/CHANGES.md
index 41ba5357b..62df809be 100644
--- a/actix-protobuf/CHANGES.md
+++ b/actix-protobuf/CHANGES.md
@@ -1,6 +1,7 @@
 # Changes
 
-## Unreleased - 2021-xx-xx
+## Unreleased - 2022-xx-xx
+- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
 
 
 ## 0.7.0 - 2022-03-01
diff --git a/actix-protobuf/README.md b/actix-protobuf/README.md
index 6833f0927..950cc1279 100644
--- a/actix-protobuf/README.md
+++ b/actix-protobuf/README.md
@@ -11,7 +11,7 @@
 
 - [API Documentation](https://docs.rs/actix-protobuf)
 - [Example Project](https://github.com/actix/examples/tree/master/protobuf)
-- Minimum Supported Rust Version (MSRV): 1.54
+- Minimum Supported Rust Version (MSRV): 1.57
 
 ## Example
 
diff --git a/actix-redis/CHANGES.md b/actix-redis/CHANGES.md
index d73adc1d2..84c5ed91f 100644
--- a/actix-redis/CHANGES.md
+++ b/actix-redis/CHANGES.md
@@ -1,6 +1,7 @@
 # Changes
 
-## Unreleased - 2021-xx-xx
+## Unreleased - 2022-xx-xx
+- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
 
 
 ## 0.11.0 - 2022-03-15
diff --git a/actix-redis/README.md b/actix-redis/README.md
index 26a90b551..cac27a013 100644
--- a/actix-redis/README.md
+++ b/actix-redis/README.md
@@ -11,4 +11,4 @@
 
 - [API Documentation](https://docs.rs/actix-redis)
 - [Example Project](https://github.com/actix/examples/tree/master/auth/redis-session)
-- Minimum Supported Rust Version (MSRV): 1.54
+- Minimum Supported Rust Version (MSRV): 1.57
diff --git a/actix-session/CHANGES.md b/actix-session/CHANGES.md
index cc0c8b10c..93cbecc73 100644
--- a/actix-session/CHANGES.md
+++ b/actix-session/CHANGES.md
@@ -1,6 +1,7 @@
 # Changes
 
-## Unreleased - 2021-xx-xx
+## Unreleased - 2022-xx-xx
+- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
 
 
 ## 0.6.2 - 2022-03-25
diff --git a/actix-session/README.md b/actix-session/README.md
index 127d45819..dc771e6c2 100644
--- a/actix-session/README.md
+++ b/actix-session/README.md
@@ -12,4 +12,4 @@
 
 - [API Documentation](https://docs.rs/actix-session)
 - [Example Projects](https://github.com/actix/examples/tree/master/auth/cookie-session)
-- Minimum Supported Rust Version (MSRV): 1.54
+- Minimum Supported Rust Version (MSRV): 1.57
diff --git a/actix-web-httpauth/CHANGES.md b/actix-web-httpauth/CHANGES.md
index e4c280f26..86a6b5c25 100644
--- a/actix-web-httpauth/CHANGES.md
+++ b/actix-web-httpauth/CHANGES.md
@@ -1,6 +1,7 @@
 # Changes
 
-## Unreleased - 2021-xx-xx
+## Unreleased - 2022-xx-xx
+- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
 
 
 ## 0.6.0 - 2022-03-01
diff --git a/actix-web-httpauth/README.md b/actix-web-httpauth/README.md
index 63c977e18..6b7f0b21d 100644
--- a/actix-web-httpauth/README.md
+++ b/actix-web-httpauth/README.md
@@ -10,7 +10,7 @@
 ## Documentation & Resources
 
 - [API Documentation](https://docs.rs/actix-web-httpauth/)
-- Minimum Supported Rust Version (MSRV): 1.54
+- Minimum Supported Rust Version (MSRV): 1.57
 
 ## Features
 - Typed [Authorization] and [WWW-Authenticate] headers
diff --git a/clippy.toml b/clippy.toml
index 0f31b88d4..5cccb362c 100644
--- a/clippy.toml
+++ b/clippy.toml
@@ -1 +1 @@
-msrv = "1.54.0"
+msrv = "1.57"

From a42ca24327ab334cd167b75e97c7026e6a198ad8 Mon Sep 17 00:00:00 2001
From: Tobias de Bruijn <t.debruijn@array21.dev>
Date: Sun, 26 Jun 2022 01:28:16 +0200
Subject: [PATCH 02/31] Updated Cargo.toml to support Prost 0.10 (#257)

---
 actix-protobuf/CHANGES.md | 1 +
 actix-protobuf/Cargo.toml | 4 ++--
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/actix-protobuf/CHANGES.md b/actix-protobuf/CHANGES.md
index 62df809be..b3df9364d 100644
--- a/actix-protobuf/CHANGES.md
+++ b/actix-protobuf/CHANGES.md
@@ -2,6 +2,7 @@
 
 ## Unreleased - 2022-xx-xx
 - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
+- Update `prost` dependency to `0.10`
 
 
 ## 0.7.0 - 2022-03-01
diff --git a/actix-protobuf/Cargo.toml b/actix-protobuf/Cargo.toml
index e8f199a67..616fa5367 100644
--- a/actix-protobuf/Cargo.toml
+++ b/actix-protobuf/Cargo.toml
@@ -21,8 +21,8 @@ path = "src/lib.rs"
 actix-web = { version = "4", default_features = false }
 derive_more = "0.99.5"
 futures-util = { version = "0.3.7", default-features = false }
-prost = { version = "0.9", default_features = false }
+prost = { version = "0.10", default_features = false }
 
 [dev-dependencies]
 actix-web = { version = "4", default_features = false, features = ["macros"] }
-prost = { version = "0.9", default_features = false, features = ["prost-derive"] }
+prost = { version = "0.10", default_features = false, features = ["prost-derive"] }

From d09299390a7f96a6aa81c4e3f85931288f44e60e Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Sun, 26 Jun 2022 00:36:27 +0100
Subject: [PATCH 03/31] prepare actix-protobuf release 0.8.0

---
 actix-protobuf/CHANGES.md | 5 ++++-
 actix-protobuf/Cargo.toml | 7 +++----
 actix-protobuf/README.md  | 4 ++--
 3 files changed, 9 insertions(+), 7 deletions(-)

diff --git a/actix-protobuf/CHANGES.md b/actix-protobuf/CHANGES.md
index b3df9364d..8b88dc423 100644
--- a/actix-protobuf/CHANGES.md
+++ b/actix-protobuf/CHANGES.md
@@ -1,8 +1,11 @@
 # Changes
 
 ## Unreleased - 2022-xx-xx
+
+
+## 0.8.0 - 2022-06-25
+- Update `prost` dependency to `0.10`.
 - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
-- Update `prost` dependency to `0.10`
 
 
 ## 0.7.0 - 2022-03-01
diff --git a/actix-protobuf/Cargo.toml b/actix-protobuf/Cargo.toml
index 616fa5367..f0a9d3e88 100644
--- a/actix-protobuf/Cargo.toml
+++ b/actix-protobuf/Cargo.toml
@@ -1,17 +1,16 @@
 [package]
 name = "actix-protobuf"
-version = "0.7.0"
+version = "0.8.0"
 edition = "2018"
 authors = [
     "kingxsp <jin.hb.zh@outlook.com>",
-    "Yuki Okushi <huyuumi.dev@gmail.com>"
+    "Yuki Okushi <huyuumi.dev@gmail.com>",
 ]
 description = "Protobuf support for Actix web"
-keywords = ["actix", "protobuf", "protocol", "rpc"]
+keywords = ["actix", "web", "protobuf", "protocol", "rpc"]
 homepage = "https://actix.rs"
 repository = "https://github.com/actix/actix-extras.git"
 license = "MIT OR Apache-2.0"
-exclude = [".cargo/config", "/examples/**"]
 
 [lib]
 name = "actix_protobuf"
diff --git a/actix-protobuf/README.md b/actix-protobuf/README.md
index 950cc1279..9ca0a444f 100644
--- a/actix-protobuf/README.md
+++ b/actix-protobuf/README.md
@@ -3,9 +3,9 @@
 > Protobuf support for Actix Web.
 
 [![crates.io](https://img.shields.io/crates/v/actix-protobuf?label=latest)](https://crates.io/crates/actix-protobuf)
-[![Documentation](https://docs.rs/actix-protobuf/badge.svg?version=0.7.0)](https://docs.rs/actix-protobuf/0.7.0)
+[![Documentation](https://docs.rs/actix-protobuf/badge.svg?version=0.8.0)](https://docs.rs/actix-protobuf/0.8.0)
 ![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-protobuf)
-[![Dependency Status](https://deps.rs/crate/actix-protobuf/0.7.0/status.svg)](https://deps.rs/crate/actix-protobuf/0.7.0)
+[![Dependency Status](https://deps.rs/crate/actix-protobuf/0.8.0/status.svg)](https://deps.rs/crate/actix-protobuf/0.8.0)
 
 ## Documentation & Resources
 

From c2f068db668e660e2ab3e55793c9d493c078ff5c Mon Sep 17 00:00:00 2001
From: Luca Palmieri <contact@lpalmieri.com>
Date: Sun, 3 Jul 2022 21:18:14 +0100
Subject: [PATCH 04/31] Add a new configuration parameter to refresh the TTL of
 the session even if unchanged (#233)

Co-authored-by: Rob Ede <robjtede@icloud.com>
---
 actix-cors/src/all_or_some.rs            |   2 +-
 actix-limitation/src/lib.rs              |   2 +-
 actix-session/CHANGES.md                 |  11 +
 actix-session/Cargo.toml                 |   1 -
 actix-session/src/config.rs              | 369 +++++++++++++++++++++++
 actix-session/src/lib.rs                 | 150 ++++++---
 actix-session/src/middleware.rs          | 287 +++---------------
 actix-session/src/storage/cookie.rs      |  15 +-
 actix-session/src/storage/interface.rs   |   9 +-
 actix-session/src/storage/redis_actor.rs |  27 +-
 actix-session/src/storage/redis_rs.rs    |  26 +-
 actix-session/src/storage/session_key.rs |  23 +-
 actix-session/tests/opaque_errors.rs     |   4 +
 13 files changed, 607 insertions(+), 319 deletions(-)
 create mode 100644 actix-session/src/config.rs

diff --git a/actix-cors/src/all_or_some.rs b/actix-cors/src/all_or_some.rs
index d8cec53a1..999f7e9b4 100644
--- a/actix-cors/src/all_or_some.rs
+++ b/actix-cors/src/all_or_some.rs
@@ -1,5 +1,5 @@
 /// An enum signifying that some of type `T` is allowed, or `All` (anything is allowed).
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub enum AllOrSome<T> {
     /// Everything is allowed. Usually equivalent to the `*` value.
     All,
diff --git a/actix-limitation/src/lib.rs b/actix-limitation/src/lib.rs
index a3cb9363a..43c02c1d9 100644
--- a/actix-limitation/src/lib.rs
+++ b/actix-limitation/src/lib.rs
@@ -73,7 +73,7 @@ pub const DEFAULT_COOKIE_NAME: &str = "sid";
 pub const DEFAULT_SESSION_KEY: &str = "rate-api-id";
 
 /// Rate limiter.
-#[derive(Clone, Debug)]
+#[derive(Debug, Clone)]
 pub struct Limiter {
     client: Client,
     limit: usize,
diff --git a/actix-session/CHANGES.md b/actix-session/CHANGES.md
index 93cbecc73..d76f86a7c 100644
--- a/actix-session/CHANGES.md
+++ b/actix-session/CHANGES.md
@@ -1,8 +1,19 @@
 # Changes
 
 ## Unreleased - 2022-xx-xx
+- Added `TtlExtensionPolicy` enum to support different strategies for extending the TTL attached to the session state. `TtlExtensionPolicy::OnEveryRequest` now allows for long-lived sessions that do not expire if the user remains active. [#233]
+- `SessionLength` is now called `SessionLifecycle`. [#233]
+- `SessionLength::Predetermined` is now called `SessionLifecycle::PersistentSession`. [#233]
+- The fields for Both `SessionLength` variants have been extracted into separate types (`PersistentSession` and `BrowserSession`). All fields are now private, manipulated via methods, to allow adding more configuration parameters in the future in a non-breaking fashion. [#233]
+- `SessionLength::Predetermined::max_session_length` is now called `PersistentSession::session_ttl`. [#233]
+- `SessionLength::BrowserSession::state_ttl` is now called `BrowserSession::session_state_ttl`. [#233]
+- `SessionMiddlewareBuilder::max_session_length` is now called `SessionMiddlewareBuilder::session_lifecycle`. [#233]
+- The `SessionStore` trait requires the implementation of a new method, `SessionStore::update_ttl`. [#233]
+- All types used to configure `SessionMiddleware` have been moved to the `config` sub-module [#233]
 - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
 
+[#233]: https://github.com/actix/actix-extras/pull/233
+
 
 ## 0.6.2 - 2022-03-25
 - Implement `SessionExt` for `GuardContext`. [#234]
diff --git a/actix-session/Cargo.toml b/actix-session/Cargo.toml
index dd9c3e0d2..d5f1f1da3 100644
--- a/actix-session/Cargo.toml
+++ b/actix-session/Cargo.toml
@@ -38,7 +38,6 @@ derive_more = "0.99.5"
 rand = { version = "0.8", optional = true }
 serde = { version = "1" }
 serde_json = { version = "1" }
-time = "0.3"
 tracing = { version = "0.1.30", default-features = false, features = ["log"] }
 
 # redis-actor-session
diff --git a/actix-session/src/config.rs b/actix-session/src/config.rs
new file mode 100644
index 000000000..6cd96764d
--- /dev/null
+++ b/actix-session/src/config.rs
@@ -0,0 +1,369 @@
+//! Configuration options to tune the behaviour of [`SessionMiddleware`].
+
+use actix_web::cookie::{time::Duration, Key, SameSite};
+
+use crate::{storage::SessionStore, SessionMiddleware};
+
+/// Determines what type of session cookie should be used and how its lifecycle should be managed.
+///
+/// Used by [`SessionMiddlewareBuilder::session_lifecycle`].
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub enum SessionLifecycle {
+    /// The session cookie will expire when the current browser session ends.
+    ///
+    /// When does a browser session end? It depends on the browser! Chrome, for example, will often
+    /// continue running in the background when the browser is closed—session cookies are not
+    /// deleted and they will still be available when the browser is opened again.
+    /// Check the documentation of the browsers you are targeting for up-to-date information.
+    BrowserSession(BrowserSession),
+
+    /// The session cookie will be a [persistent cookie].
+    ///
+    /// Persistent cookies have a pre-determined lifetime, specified via the `Max-Age` or `Expires`
+    /// attribute. They do not disappear when the current browser session ends.
+    ///
+    /// [persistent cookie]: https://www.whitehatsec.com/glossary/content/persistent-session-cookie
+    PersistentSession(PersistentSession),
+}
+
+impl From<BrowserSession> for SessionLifecycle {
+    fn from(session: BrowserSession) -> Self {
+        Self::BrowserSession(session)
+    }
+}
+
+impl From<PersistentSession> for SessionLifecycle {
+    fn from(session: PersistentSession) -> Self {
+        Self::PersistentSession(session)
+    }
+}
+
+/// A [session lifecycle](SessionLifecycle) strategy where the session cookie expires when the
+/// browser's current session ends.
+///
+/// When does a browser session end? It depends on the browser. Chrome, for example, will often
+/// continue running in the background when the browser is closed—session cookies are not deleted
+/// and they will still be available when the browser is opened again. Check the documentation of
+/// the browsers you are targeting for up-to-date information.
+#[derive(Debug, Clone)]
+pub struct BrowserSession {
+    state_ttl: Duration,
+    state_ttl_extension_policy: TtlExtensionPolicy,
+}
+
+impl BrowserSession {
+    /// Sets a time-to-live (TTL) when storing the session state in the storage backend.
+    ///
+    /// We do not want to store session states indefinitely, otherwise we will inevitably run out of
+    /// storage by holding on to the state of countless abandoned or expired sessions!
+    ///
+    /// We are dealing with the lifecycle of two uncorrelated object here: the session cookie
+    /// and the session state. It is not a big issue if the session state outlives the cookie—
+    /// we are wasting some space in the backend storage, but it will be cleaned up eventually.
+    /// What happens, instead, if the cookie outlives the session state? A new session starts—
+    /// e.g. if sessions are being used for authentication, the user is de-facto logged out.
+    ///
+    /// It is not possible to predict with certainty how long a browser session is going to
+    /// last—you need to provide a reasonable upper bound. You do so via `state_ttl`—it dictates
+    /// what TTL should be used for session state when the lifecycle of the session cookie is
+    /// tied to the browser session length. [`SessionMiddleware`] will default to 1 day if
+    /// `state_ttl` is left unspecified.
+    ///
+    /// You can mitigate the risk of the session cookie outliving the session state by
+    /// specifying a more aggressive state TTL extension policy - check out
+    /// [`BrowserSession::state_ttl_extension_policy`] for more details.
+    pub fn state_ttl(mut self, ttl: Duration) -> Self {
+        self.state_ttl = ttl;
+        self
+    }
+
+    /// Determine under what circumstances the TTL of your session state should be extended.
+    ///
+    /// Defaults to [`TtlExtensionPolicy::OnStateChanges`] if left unspecified.
+    ///
+    /// See [`TtlExtensionPolicy`] for more details.
+    pub fn state_ttl_extension_policy(mut self, ttl_extension_policy: TtlExtensionPolicy) -> Self {
+        self.state_ttl_extension_policy = ttl_extension_policy;
+        self
+    }
+}
+
+impl Default for BrowserSession {
+    fn default() -> Self {
+        Self {
+            state_ttl: default_ttl(),
+            state_ttl_extension_policy: default_ttl_extension_policy(),
+        }
+    }
+}
+
+/// A [session lifecycle](SessionLifecycle) strategy where the session cookie will be [persistent].
+///
+/// Persistent cookies have a pre-determined expiration, specified via the `Max-Age` or `Expires`
+/// attribute. They do not disappear when the current browser session ends.
+///
+/// [persistent]: https://www.whitehatsec.com/glossary/content/persistent-session-cookie
+#[derive(Debug, Clone)]
+pub struct PersistentSession {
+    session_ttl: Duration,
+    ttl_extension_policy: TtlExtensionPolicy,
+}
+
+impl PersistentSession {
+    /// Specifies how long the session cookie should live.
+    ///
+    /// Defaults to 1 day if left unspecified.
+    ///
+    /// The session TTL is also used as the TTL for the session state in the storage backend.
+    ///
+    /// A persistent session can live more than the specified TTL if the TTL is extended.
+    /// See [`session_ttl_extension_policy`](Self::session_ttl_extension_policy) for more details.
+    pub fn session_ttl(mut self, session_ttl: Duration) -> Self {
+        self.session_ttl = session_ttl;
+        self
+    }
+
+    /// Determines under what circumstances the TTL of your session should be extended.
+    /// See [`TtlExtensionPolicy`] for more details.
+    ///
+    /// Defaults to [`TtlExtensionPolicy::OnStateChanges`] if left unspecified.
+    pub fn session_ttl_extension_policy(
+        mut self,
+        ttl_extension_policy: TtlExtensionPolicy,
+    ) -> Self {
+        self.ttl_extension_policy = ttl_extension_policy;
+        self
+    }
+}
+
+impl Default for PersistentSession {
+    fn default() -> Self {
+        Self {
+            session_ttl: default_ttl(),
+            ttl_extension_policy: default_ttl_extension_policy(),
+        }
+    }
+}
+
+/// Configuration for which events should trigger an extension of the time-to-live for your session.
+///
+/// If you are using a [`BrowserSession`], `TtlExtensionPolicy` controls how often the TTL of
+/// the session state should be refreshed. The browser is in control of the lifecycle of the
+/// session cookie.
+///
+/// If you are using a [`PersistentSession`], `TtlExtensionPolicy` controls both the expiration
+/// of the session cookie and the TTL of the session state.
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub enum TtlExtensionPolicy {
+    /// The TTL is refreshed every time the server receives a request associated with a session.
+    ///
+    /// # Performance impact
+    /// Refreshing the TTL on every request is not free.
+    /// It implies a refresh of the TTL on the session state. This translates into a request over
+    /// the network if you are using a remote system as storage backend (e.g. Redis).
+    /// This impacts both the total load on your storage backend (i.e. number of
+    /// queries it has to handle) and the latency of the requests served by your server.
+    OnEveryRequest,
+
+    /// The TTL is refreshed every time the session state changes or the session key is renewed.
+    OnStateChanges,
+}
+
+/// Determines how to secure the content of the session cookie.
+///
+/// Used by [`SessionMiddlewareBuilder::cookie_content_security`].
+#[derive(Debug, Clone, Copy)]
+pub enum CookieContentSecurity {
+    /// The cookie content is encrypted when using `CookieContentSecurity::Private`.
+    ///
+    /// Encryption guarantees confidentiality and integrity: the client cannot tamper with the
+    /// cookie content nor decode it, as long as the encryption key remains confidential.
+    Private,
+
+    /// The cookie content is signed when using `CookieContentSecurity::Signed`.
+    ///
+    /// Signing guarantees integrity, but it doesn't ensure confidentiality: the client cannot
+    /// tamper with the cookie content, but they can read it.
+    Signed,
+}
+
+pub(crate) const fn default_ttl() -> Duration {
+    Duration::days(1)
+}
+
+pub(crate) const fn default_ttl_extension_policy() -> TtlExtensionPolicy {
+    TtlExtensionPolicy::OnStateChanges
+}
+
+/// A fluent builder to construct a [`SessionMiddleware`] instance with custom configuration
+/// parameters.
+#[must_use]
+pub struct SessionMiddlewareBuilder<Store: SessionStore> {
+    storage_backend: Store,
+    configuration: Configuration,
+}
+
+impl<Store: SessionStore> SessionMiddlewareBuilder<Store> {
+    pub(crate) fn new(store: Store, configuration: Configuration) -> Self {
+        Self {
+            storage_backend: store,
+            configuration,
+        }
+    }
+
+    /// Set the name of the cookie used to store the session ID.
+    ///
+    /// Defaults to `id`.
+    pub fn cookie_name(mut self, name: String) -> Self {
+        self.configuration.cookie.name = name;
+        self
+    }
+
+    /// Set the `Secure` attribute for the cookie used to store the session ID.
+    ///
+    /// If the cookie is set as secure, it will only be transmitted when the connection is secure
+    /// (using `https`).
+    ///
+    /// Default is `true`.
+    pub fn cookie_secure(mut self, secure: bool) -> Self {
+        self.configuration.cookie.secure = secure;
+        self
+    }
+
+    /// Determines what type of session cookie should be used and how its lifecycle should be managed.
+    /// Check out [`SessionLifecycle`]'s documentation for more details on the available options.
+    ///
+    /// Default is [`SessionLifecycle::BrowserSession`].
+    pub fn session_lifecycle<S: Into<SessionLifecycle>>(mut self, session_lifecycle: S) -> Self {
+        match session_lifecycle.into() {
+            SessionLifecycle::BrowserSession(BrowserSession {
+                state_ttl,
+                state_ttl_extension_policy,
+            }) => {
+                self.configuration.cookie.max_age = None;
+                self.configuration.session.state_ttl = state_ttl;
+                self.configuration.ttl_extension_policy = state_ttl_extension_policy;
+            }
+            SessionLifecycle::PersistentSession(PersistentSession {
+                session_ttl,
+                ttl_extension_policy,
+            }) => {
+                self.configuration.cookie.max_age = Some(session_ttl);
+                self.configuration.session.state_ttl = session_ttl;
+                self.configuration.ttl_extension_policy = ttl_extension_policy;
+            }
+        }
+
+        self
+    }
+
+    /// Set the `SameSite` attribute for the cookie used to store the session ID.
+    ///
+    /// By default, the attribute is set to `Lax`.
+    pub fn cookie_same_site(mut self, same_site: SameSite) -> Self {
+        self.configuration.cookie.same_site = same_site;
+        self
+    }
+
+    /// Set the `Path` attribute for the cookie used to store the session ID.
+    ///
+    /// By default, the attribute is set to `/`.
+    pub fn cookie_path(mut self, path: String) -> Self {
+        self.configuration.cookie.path = path;
+        self
+    }
+
+    /// Set the `Domain` attribute for the cookie used to store the session ID.
+    ///
+    /// Use `None` to leave the attribute unspecified. If unspecified, the attribute defaults
+    /// to the same host that set the cookie, excluding subdomains.
+    ///
+    /// By default, the attribute is left unspecified.
+    pub fn cookie_domain(mut self, domain: Option<String>) -> Self {
+        self.configuration.cookie.domain = domain;
+        self
+    }
+
+    /// Choose how the session cookie content should be secured.
+    ///
+    /// - [`CookieContentSecurity::Private`] selects encrypted cookie content.
+    /// - [`CookieContentSecurity::Signed`] selects signed cookie content.
+    ///
+    /// # Default
+    /// By default, the cookie content is encrypted. Encrypted was chosen instead of signed as
+    /// default because it reduces the chances of sensitive information being exposed in the session
+    /// key by accident, regardless of [`SessionStore`] implementation you chose to use.
+    ///
+    /// For example, if you are using cookie-based storage, you definitely want the cookie content
+    /// to be encrypted—the whole session state is embedded in the cookie! If you are using
+    /// Redis-based storage, signed is more than enough - the cookie content is just a unique
+    /// tamper-proof session key.
+    pub fn cookie_content_security(mut self, content_security: CookieContentSecurity) -> Self {
+        self.configuration.cookie.content_security = content_security;
+        self
+    }
+
+    /// Set the `HttpOnly` attribute for the cookie used to store the session ID.
+    ///
+    /// If the cookie is set as `HttpOnly`, it will not be visible to any JavaScript snippets
+    /// running in the browser.
+    ///
+    /// Default is `true`.
+    pub fn cookie_http_only(mut self, http_only: bool) -> Self {
+        self.configuration.cookie.http_only = http_only;
+        self
+    }
+
+    /// Finalise the builder and return a [`SessionMiddleware`] instance.
+    #[must_use]
+    pub fn build(self) -> SessionMiddleware<Store> {
+        SessionMiddleware::from_parts(self.storage_backend, self.configuration)
+    }
+}
+
+#[derive(Clone)]
+pub(crate) struct Configuration {
+    pub(crate) cookie: CookieConfiguration,
+    pub(crate) session: SessionConfiguration,
+    pub(crate) ttl_extension_policy: TtlExtensionPolicy,
+}
+
+#[derive(Clone)]
+pub(crate) struct SessionConfiguration {
+    pub(crate) state_ttl: Duration,
+}
+
+#[derive(Clone)]
+pub(crate) struct CookieConfiguration {
+    pub(crate) secure: bool,
+    pub(crate) http_only: bool,
+    pub(crate) name: String,
+    pub(crate) same_site: SameSite,
+    pub(crate) path: String,
+    pub(crate) domain: Option<String>,
+    pub(crate) max_age: Option<Duration>,
+    pub(crate) content_security: CookieContentSecurity,
+    pub(crate) key: Key,
+}
+
+pub(crate) fn default_configuration(key: Key) -> Configuration {
+    Configuration {
+        cookie: CookieConfiguration {
+            secure: true,
+            http_only: true,
+            name: "id".into(),
+            same_site: SameSite::Lax,
+            path: "/".into(),
+            domain: None,
+            max_age: None,
+            content_security: CookieContentSecurity::Private,
+            key,
+        },
+        session: SessionConfiguration {
+            state_ttl: default_ttl(),
+        },
+        ttl_extension_policy: default_ttl_extension_policy(),
+    }
+}
diff --git a/actix-session/src/lib.rs b/actix-session/src/lib.rs
index 67d5c92a5..fcc51744b 100644
--- a/actix-session/src/lib.rs
+++ b/actix-session/src/lib.rs
@@ -139,32 +139,25 @@
 #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
 #![cfg_attr(docsrs, feature(doc_cfg))]
 
+pub mod config;
 mod middleware;
 mod session;
 mod session_ext;
 pub mod storage;
 
-pub use self::middleware::{
-    CookieContentSecurity, SessionLength, SessionMiddleware, SessionMiddlewareBuilder,
-};
+pub use self::middleware::SessionMiddleware;
 pub use self::session::{Session, SessionStatus};
 pub use self::session_ext::SessionExt;
 
 #[cfg(test)]
 pub mod test_helpers {
     use actix_web::cookie::Key;
-    use rand::{distributions::Alphanumeric, thread_rng, Rng};
 
-    use crate::{storage::SessionStore, CookieContentSecurity};
+    use crate::{config::CookieContentSecurity, storage::SessionStore};
 
     /// Generate a random cookie signing/encryption key.
     pub fn key() -> Key {
-        let signing_key: String = thread_rng()
-            .sample_iter(&Alphanumeric)
-            .take(64)
-            .map(char::from)
-            .collect();
-        Key::from(signing_key.as_bytes())
+        Key::generate()
     }
 
     /// A ready-to-go acceptance test suite to verify that sessions behave as expected
@@ -187,6 +180,11 @@ pub mod test_helpers {
             acceptance_tests::basic_workflow(store_builder.clone(), *policy).await;
             acceptance_tests::expiration_is_refreshed_on_changes(store_builder.clone(), *policy)
                 .await;
+            acceptance_tests::expiration_is_always_refreshed_if_configured_to_refresh_on_every_request(
+                store_builder.clone(),
+                *policy,
+            )
+            .await;
             acceptance_tests::complex_workflow(
                 store_builder.clone(),
                 is_invalidation_supported,
@@ -199,18 +197,18 @@ pub mod test_helpers {
 
     mod acceptance_tests {
         use actix_web::{
-            dev::Service,
+            cookie::time,
+            dev::{Service, ServiceResponse},
             guard, middleware, test,
             web::{self, get, post, resource, Bytes},
             App, HttpResponse, Result,
         };
         use serde::{Deserialize, Serialize};
         use serde_json::json;
-        use time::Duration;
 
+        use crate::config::{CookieContentSecurity, PersistentSession, TtlExtensionPolicy};
         use crate::{
-            middleware::SessionLength, storage::SessionStore, test_helpers::key,
-            CookieContentSecurity, Session, SessionExt, SessionMiddleware,
+            storage::SessionStore, test_helpers::key, Session, SessionExt, SessionMiddleware,
         };
 
         pub(super) async fn basic_workflow<F, Store>(
@@ -228,9 +226,10 @@ pub mod test_helpers {
                             .cookie_name("actix-test".into())
                             .cookie_domain(Some("localhost".into()))
                             .cookie_content_security(policy)
-                            .session_length(SessionLength::Predetermined {
-                                max_session_length: Some(time::Duration::seconds(100)),
-                            })
+                            .session_lifecycle(
+                                PersistentSession::default()
+                                    .session_ttl(time::Duration::seconds(100)),
+                            )
                             .build(),
                     )
                     .service(web::resource("/").to(|ses: Session| async move {
@@ -246,12 +245,7 @@ pub mod test_helpers {
 
             let request = test::TestRequest::get().to_request();
             let response = app.call(request).await.unwrap();
-            let cookie = response
-                .response()
-                .cookies()
-                .find(|c| c.name() == "actix-test")
-                .unwrap()
-                .clone();
+            let cookie = response.get_cookie("actix-test").unwrap().clone();
             assert_eq!(cookie.path().unwrap(), "/test/");
 
             let request = test::TestRequest::with_uri("/test/")
@@ -261,6 +255,55 @@ pub mod test_helpers {
             assert_eq!(body, Bytes::from_static(b"counter: 100"));
         }
 
+        pub(super) async fn expiration_is_always_refreshed_if_configured_to_refresh_on_every_request<
+            F,
+            Store,
+        >(
+            store_builder: F,
+            policy: CookieContentSecurity,
+        ) where
+            Store: SessionStore + 'static,
+            F: Fn() -> Store + Clone + Send + 'static,
+        {
+            let session_ttl = time::Duration::seconds(60);
+            let app = test::init_service(
+                App::new()
+                    .wrap(
+                        SessionMiddleware::builder(store_builder(), key())
+                            .cookie_content_security(policy)
+                            .session_lifecycle(
+                                PersistentSession::default()
+                                    .session_ttl(session_ttl)
+                                    .session_ttl_extension_policy(
+                                        TtlExtensionPolicy::OnEveryRequest,
+                                    ),
+                            )
+                            .build(),
+                    )
+                    .service(web::resource("/").to(|ses: Session| async move {
+                        let _ = ses.insert("counter", 100);
+                        "test"
+                    }))
+                    .service(web::resource("/test/").to(|| async move { "no-changes-in-session" })),
+            )
+            .await;
+
+            // Create session
+            let request = test::TestRequest::get().to_request();
+            let response = app.call(request).await.unwrap();
+            let cookie_1 = response.get_cookie("id").expect("Cookie is set");
+            assert_eq!(cookie_1.max_age(), Some(session_ttl));
+
+            // Fire a request that doesn't touch the session state, check
+            // that the session cookie is present and its expiry is set to the maximum we configured.
+            let request = test::TestRequest::with_uri("/test/")
+                .cookie(cookie_1)
+                .to_request();
+            let response = app.call(request).await.unwrap();
+            let cookie_2 = response.get_cookie("id").expect("Cookie is set");
+            assert_eq!(cookie_2.max_age(), Some(session_ttl));
+        }
+
         pub(super) async fn expiration_is_refreshed_on_changes<F, Store>(
             store_builder: F,
             policy: CookieContentSecurity,
@@ -268,14 +311,15 @@ pub mod test_helpers {
             Store: SessionStore + 'static,
             F: Fn() -> Store + Clone + Send + 'static,
         {
+            let session_ttl = time::Duration::seconds(60);
             let app = test::init_service(
                 App::new()
                     .wrap(
                         SessionMiddleware::builder(store_builder(), key())
                             .cookie_content_security(policy)
-                            .session_length(SessionLength::Predetermined {
-                                max_session_length: Some(time::Duration::seconds(60)),
-                            })
+                            .session_lifecycle(
+                                PersistentSession::default().session_ttl(session_ttl),
+                            )
                             .build(),
                     )
                     .service(web::resource("/").to(|ses: Session| async move {
@@ -288,25 +332,19 @@ pub mod test_helpers {
 
             let request = test::TestRequest::get().to_request();
             let response = app.call(request).await.unwrap();
-            let cookie_1 = response
-                .response()
-                .cookies()
-                .find(|c| c.name() == "id")
-                .expect("Cookie is set");
-            assert_eq!(cookie_1.max_age(), Some(Duration::seconds(60)));
+            let cookie_1 = response.get_cookie("id").expect("Cookie is set");
+            assert_eq!(cookie_1.max_age(), Some(session_ttl));
 
-            let request = test::TestRequest::with_uri("/test/").to_request();
+            let request = test::TestRequest::with_uri("/test/")
+                .cookie(cookie_1.clone())
+                .to_request();
             let response = app.call(request).await.unwrap();
             assert!(response.response().cookies().next().is_none());
 
-            let request = test::TestRequest::get().to_request();
+            let request = test::TestRequest::get().cookie(cookie_1).to_request();
             let response = app.call(request).await.unwrap();
-            let cookie_2 = response
-                .response()
-                .cookies()
-                .find(|c| c.name() == "id")
-                .expect("Cookie is set");
-            assert_eq!(cookie_2.max_age(), Some(Duration::seconds(60)));
+            let cookie_2 = response.get_cookie("id").expect("Cookie is set");
+            assert_eq!(cookie_2.max_age(), Some(session_ttl));
         }
 
         pub(super) async fn guard<F, Store>(store_builder: F, policy: CookieContentSecurity)
@@ -320,9 +358,9 @@ pub mod test_helpers {
                         SessionMiddleware::builder(store_builder(), key())
                             .cookie_name("test-session".into())
                             .cookie_content_security(policy)
-                            .session_length(SessionLength::Predetermined {
-                                max_session_length: Some(time::Duration::days(7)),
-                            })
+                            .session_lifecycle(
+                                PersistentSession::default().session_ttl(time::Duration::days(7)),
+                            )
                             .build(),
                     )
                     .wrap(middleware::Logger::default())
@@ -402,15 +440,16 @@ pub mod test_helpers {
             Store: SessionStore + 'static,
             F: Fn() -> Store + Clone + Send + 'static,
         {
+            let session_ttl = time::Duration::days(7);
             let srv = actix_test::start(move || {
                 App::new()
                     .wrap(
                         SessionMiddleware::builder(store_builder(), key())
                             .cookie_name("test-session".into())
                             .cookie_content_security(policy)
-                            .session_length(SessionLength::Predetermined {
-                                max_session_length: Some(time::Duration::days(7)),
-                            })
+                            .session_lifecycle(
+                                PersistentSession::default().session_ttl(session_ttl),
+                            )
                             .build(),
                     )
                     .wrap(middleware::Logger::default())
@@ -456,7 +495,7 @@ pub mod test_helpers {
                 .into_iter()
                 .find(|c| c.name() == "test-session")
                 .unwrap();
-            assert_eq!(cookie_1.max_age(), Some(Duration::days(7)));
+            assert_eq!(cookie_1.max_age(), Some(session_ttl));
 
             // Step 3:  GET index, including session cookie #1 in request
             //   - set-cookie will *not* be in response
@@ -494,7 +533,7 @@ pub mod test_helpers {
                 .into_iter()
                 .find(|c| c.name() == "test-session")
                 .unwrap();
-            assert_eq!(cookie_2.max_age(), Some(Duration::days(7)));
+            assert_eq!(cookie_2.max_age(), cookie_1.max_age());
 
             // Step 5: POST to login, including session cookie #2 in request
             //   - set-cookie actix-session will be in response  (session cookie #3)
@@ -675,5 +714,18 @@ pub mod test_helpers {
 
             Ok(HttpResponse::Ok().body(body))
         }
+
+        trait ServiceResponseExt {
+            fn get_cookie(&self, cookie_name: &str) -> Option<actix_web::cookie::Cookie<'_>>;
+        }
+
+        impl ServiceResponseExt for ServiceResponse {
+            fn get_cookie(&self, cookie_name: &str) -> Option<actix_web::cookie::Cookie<'_>> {
+                self.response()
+                    .cookies()
+                    .into_iter()
+                    .find(|c| c.name() == cookie_name)
+            }
+        }
     }
 }
diff --git a/actix-session/src/middleware.rs b/actix-session/src/middleware.rs
index ce27cf4b5..09389ec26 100644
--- a/actix-session/src/middleware.rs
+++ b/actix-session/src/middleware.rs
@@ -3,15 +3,18 @@ use std::{collections::HashMap, convert::TryInto, fmt, future::Future, pin::Pin,
 use actix_utils::future::{ready, Ready};
 use actix_web::{
     body::MessageBody,
-    cookie::{Cookie, CookieJar, Key, SameSite},
+    cookie::{Cookie, CookieJar, Key},
     dev::{forward_ready, ResponseHead, Service, ServiceRequest, ServiceResponse, Transform},
     http::header::{HeaderValue, SET_COOKIE},
     HttpResponse,
 };
 use anyhow::Context;
-use time::Duration;
 
 use crate::{
+    config::{
+        self, Configuration, CookieConfiguration, CookieContentSecurity, SessionMiddlewareBuilder,
+        TtlExtensionPolicy,
+    },
     storage::{LoadError, SessionKey, SessionStore},
     Session, SessionStatus,
 };
@@ -66,8 +69,9 @@ use crate::{
 /// If you want to customise use [`builder`](Self::builder) instead of [`new`](Self::new):
 ///
 /// ```no_run
-/// use actix_web::{cookie::Key, web, App, HttpServer, HttpResponse, Error};
-/// use actix_session::{Session, SessionMiddleware, storage::RedisActorSessionStore, SessionLength};
+/// use actix_web::{App, cookie::{Key, time}, Error, HttpResponse, HttpServer, web};
+/// use actix_session::{Session, SessionMiddleware, storage::RedisActorSessionStore};
+/// use actix_session::config::PersistentSession;
 ///
 /// // The secret key would usually be read from a configuration file/environment variables.
 /// fn get_secret_key() -> Key {
@@ -87,9 +91,10 @@ use crate::{
 ///                     RedisActorSessionStore::new(redis_connection_string),
 ///                     secret_key.clone()
 ///                 )
-///                 .session_length(SessionLength::Predetermined {
-///                     max_session_length: Some(time::Duration::days(5)),
-///                 })
+///                 .session_lifecycle(
+///                     PersistentSession::default()
+///                         .session_ttl(time::Duration::days(5))
+///                 )
 ///                 .build(),
 ///             )
 ///             .default_service(web::to(|| HttpResponse::Ok())))
@@ -114,117 +119,6 @@ pub struct SessionMiddleware<Store: SessionStore> {
     configuration: Rc<Configuration>,
 }
 
-#[derive(Clone)]
-struct Configuration {
-    cookie: CookieConfiguration,
-    session: SessionConfiguration,
-}
-
-#[derive(Clone)]
-struct SessionConfiguration {
-    state_ttl: Duration,
-}
-
-#[derive(Clone)]
-struct CookieConfiguration {
-    secure: bool,
-    http_only: bool,
-    name: String,
-    same_site: SameSite,
-    path: String,
-    domain: Option<String>,
-    max_age: Option<Duration>,
-    content_security: CookieContentSecurity,
-    key: Key,
-}
-
-/// Describes how long a session should last.
-///
-/// Used by [`SessionMiddlewareBuilder::session_length`].
-#[derive(Clone, Debug)]
-pub enum SessionLength {
-    /// The session cookie will expire when the current browser session ends.
-    ///
-    /// When does a browser session end? It depends on the browser! Chrome, for example, will often
-    /// continue running in the background when the browser is closed—session cookies are not
-    /// deleted and they will still be available when the browser is opened again. Check the
-    /// documentation of the browsers you are targeting for up-to-date information.
-    BrowserSession {
-        /// We must provide a time-to-live (TTL) when storing the session state in the storage
-        /// backend—we do not want to store session states indefinitely, otherwise we will
-        /// inevitably run out of storage by holding on to the state of countless abandoned or
-        /// expired sessions!
-        ///
-        /// We are dealing with the lifecycle of two uncorrelated object here: the session cookie
-        /// and the session state. It is not a big issue if the session state outlives the cookie—
-        /// we are wasting some space in the backend storage, but it will be cleaned up eventually.
-        /// What happens, instead, if the cookie outlives the session state? A new session starts—
-        /// e.g. if sessions are being used for authentication, the user is de-facto logged out.
-        ///
-        /// It is not possible to predict with certainty how long a browser session is going to
-        /// last—you need to provide a reasonable upper bound. You do so via `state_ttl`—it dictates
-        /// what TTL should be used for session state when the lifecycle of the session cookie is
-        /// tied to the browser session length. [`SessionMiddleware`] will default to 1 day if
-        /// `state_ttl` is left unspecified.
-        state_ttl: Option<Duration>,
-    },
-
-    /// The session cookie will be a [persistent cookie].
-    ///
-    /// Persistent cookies have a pre-determined lifetime, specified via the `Max-Age` or `Expires`
-    /// attribute. They do not disappear when the current browser session ends.
-    ///
-    /// [persistent cookie]: https://www.whitehatsec.com/glossary/content/persistent-session-cookie
-    Predetermined {
-        /// Set `max_session_length` to specify how long the session cookie should live.
-        /// [`SessionMiddleware`] will default to 1 day if `max_session_length` is set to `None`.
-        ///
-        /// `max_session_length` is also used as the TTL for the session state in the
-        /// storage backend.
-        max_session_length: Option<Duration>,
-    },
-}
-
-/// Used by [`SessionMiddlewareBuilder::cookie_content_security`] to determine how to secure
-/// the content of the session cookie.
-#[derive(Debug, Clone, Copy)]
-pub enum CookieContentSecurity {
-    /// The cookie content is encrypted when using `CookieContentSecurity::Private`.
-    ///
-    /// Encryption guarantees confidentiality and integrity: the client cannot tamper with the
-    /// cookie content nor decode it, as long as the encryption key remains confidential.
-    Private,
-
-    /// The cookie content is signed when using `CookieContentSecurity::Signed`.
-    ///
-    /// Signing guarantees integrity, but it doesn't ensure confidentiality: the client cannot
-    /// tamper with the cookie content, but they can read it.
-    Signed,
-}
-
-fn default_configuration(key: Key) -> Configuration {
-    Configuration {
-        cookie: CookieConfiguration {
-            secure: true,
-            http_only: true,
-            name: "id".into(),
-            same_site: SameSite::Lax,
-            path: "/".into(),
-            domain: None,
-            max_age: None,
-            content_security: CookieContentSecurity::Private,
-            key,
-        },
-        session: SessionConfiguration {
-            state_ttl: default_ttl(),
-        },
-    }
-}
-
-fn default_ttl() -> Duration {
-    Duration::days(1)
-}
-
 impl<Store: SessionStore> SessionMiddleware<Store> {
     /// Use [`SessionMiddleware::new`] to initialize the session framework using the default
     /// parameters.
@@ -234,10 +128,7 @@ impl<Store: SessionStore> SessionMiddleware<Store> {
     ///   [`SessionStore]);
     /// - a secret key, to sign or encrypt the content of client-side session cookie.
     pub fn new(store: Store, key: Key) -> Self {
-        Self {
-            storage_backend: Rc::new(store),
-            configuration: Rc::new(default_configuration(key)),
-        }
+        Self::builder(store, key).build()
     }
 
     /// A fluent API to configure [`SessionMiddleware`].
@@ -247,124 +138,13 @@ impl<Store: SessionStore> SessionMiddleware<Store> {
     ///   [`SessionStore]);
     /// - a secret key, to sign or encrypt the content of client-side session cookie.
     pub fn builder(store: Store, key: Key) -> SessionMiddlewareBuilder<Store> {
-        SessionMiddlewareBuilder {
+        SessionMiddlewareBuilder::new(store, config::default_configuration(key))
+    }
+
+    pub(crate) fn from_parts(store: Store, configuration: Configuration) -> Self {
+        Self {
             storage_backend: Rc::new(store),
-            configuration: default_configuration(key),
-        }
-    }
-}
-
-/// A fluent builder to construct a [`SessionMiddleware`] instance with custom configuration
-/// parameters.
-#[must_use]
-pub struct SessionMiddlewareBuilder<Store: SessionStore> {
-    storage_backend: Rc<Store>,
-    configuration: Configuration,
-}
-
-impl<Store: SessionStore> SessionMiddlewareBuilder<Store> {
-    /// Set the name of the cookie used to store the session ID.
-    ///
-    /// Defaults to `id`.
-    pub fn cookie_name(mut self, name: String) -> Self {
-        self.configuration.cookie.name = name;
-        self
-    }
-
-    /// Set the `Secure` attribute for the cookie used to store the session ID.
-    ///
-    /// If the cookie is set as secure, it will only be transmitted when the connection is secure
-    /// (using `https`).
-    ///
-    /// Default is `true`.
-    pub fn cookie_secure(mut self, secure: bool) -> Self {
-        self.configuration.cookie.secure = secure;
-        self
-    }
-
-    /// Determine how long a session should last - check out [`SessionLength`]'s documentation for
-    /// more details on the available options.
-    ///
-    /// Default is [`SessionLength::BrowserSession`].
-    pub fn session_length(mut self, session_length: SessionLength) -> Self {
-        match session_length {
-            SessionLength::BrowserSession { state_ttl } => {
-                self.configuration.cookie.max_age = None;
-                self.configuration.session.state_ttl = state_ttl.unwrap_or_else(default_ttl);
-            }
-            SessionLength::Predetermined { max_session_length } => {
-                let ttl = max_session_length.unwrap_or_else(default_ttl);
-                self.configuration.cookie.max_age = Some(ttl);
-                self.configuration.session.state_ttl = ttl;
-            }
-        }
-
-        self
-    }
-
-    /// Set the `SameSite` attribute for the cookie used to store the session ID.
-    ///
-    /// By default, the attribute is set to `Lax`.
-    pub fn cookie_same_site(mut self, same_site: SameSite) -> Self {
-        self.configuration.cookie.same_site = same_site;
-        self
-    }
-
-    /// Set the `Path` attribute for the cookie used to store the session ID.
-    ///
-    /// By default, the attribute is set to `/`.
-    pub fn cookie_path(mut self, path: String) -> Self {
-        self.configuration.cookie.path = path;
-        self
-    }
-
-    /// Set the `Domain` attribute for the cookie used to store the session ID.
-    ///
-    /// Use `None` to leave the attribute unspecified. If unspecified, the attribute defaults
-    /// to the same host that set the cookie, excluding subdomains.
-    ///
-    /// By default, the attribute is left unspecified.
-    pub fn cookie_domain(mut self, domain: Option<String>) -> Self {
-        self.configuration.cookie.domain = domain;
-        self
-    }
-
-    /// Choose how the session cookie content should be secured.
-    ///
-    /// - [`CookieContentSecurity::Private`] selects encrypted cookie content.
-    /// - [`CookieContentSecurity::Signed`] selects signed cookie content.
-    ///
-    /// # Default
-    /// By default, the cookie content is encrypted. Encrypted was chosen instead of signed as
-    /// default because it reduces the chances of sensitive information being exposed in the session
-    /// key by accident, regardless of [`SessionStore`] implementation you chose to use.
-    ///
-    /// For example, if you are using cookie-based storage, you definitely want the cookie content
-    /// to be encrypted—the whole session state is embedded in the cookie! If you are using
-    /// Redis-based storage, signed is more than enough - the cookie content is just a unique
-    /// tamper-proof session key.
-    pub fn cookie_content_security(mut self, content_security: CookieContentSecurity) -> Self {
-        self.configuration.cookie.content_security = content_security;
-        self
-    }
-
-    /// Set the `HttpOnly` attribute for the cookie used to store the session ID.
-    ///
-    /// If the cookie is set as `HttpOnly`, it will not be visible to any JavaScript snippets
-    /// running in the browser.
-    ///
-    /// Default is `true`.
-    pub fn cookie_http_only(mut self, http_only: bool) -> Self {
-        self.configuration.cookie.http_only = http_only;
-        self
-    }
-
-    /// Finalise the builder and return a [`SessionMiddleware`] instance.
-    #[must_use]
-    pub fn build(self) -> SessionMiddleware<Store> {
-        SessionMiddleware {
-            storage_backend: self.storage_backend,
-            configuration: Rc::new(self.configuration),
+            configuration: Rc::new(configuration),
         }
     }
 }
@@ -509,16 +289,39 @@ where
                         }
 
                         SessionStatus::Unchanged => {
-                            // Nothing to do; we avoid the unnecessary call to the storage.
+                            if matches!(
+                                configuration.ttl_extension_policy,
+                                TtlExtensionPolicy::OnEveryRequest
+                            ) {
+                                storage_backend
+                                    .update_ttl(&session_key, &configuration.session.state_ttl)
+                                    .await
+                                    .map_err(e500)?;
+
+                                if configuration.cookie.max_age.is_some() {
+                                    set_session_cookie(
+                                        res.response_mut().head_mut(),
+                                        session_key,
+                                        &configuration.cookie,
+                                    )
+                                    .map_err(e500)?;
+                                }
+                            }
                         }
-                    }
+                    };
                 }
-            };
+            }
+
             Ok(res)
         })
     }
 }
 
+/// Examines the session cookie attached to the incoming request, if there is one, and tries
+/// to extract the session key.
+///
+/// It returns `None` if there is no session cookie or if the session cookie is considered invalid
+/// (e.g., when failing a signature check).
 fn extract_session_key(req: &ServiceRequest, config: &CookieConfiguration) -> Option<SessionKey> {
     let cookies = req.cookies().ok()?;
     let session_cookie = cookies
diff --git a/actix-session/src/storage/cookie.rs b/actix-session/src/storage/cookie.rs
index 34bdceae4..10cc05bc6 100644
--- a/actix-session/src/storage/cookie.rs
+++ b/actix-session/src/storage/cookie.rs
@@ -1,6 +1,7 @@
 use std::convert::TryInto;
 
-use time::Duration;
+use actix_web::cookie::time::Duration;
+use anyhow::Error;
 
 use super::SessionKey;
 use crate::storage::{
@@ -34,9 +35,9 @@ use crate::storage::{
 /// ```
 ///
 /// # Limitations
-/// Cookies are subject to size limits - we require session keys to be shorter than 4096 bytes. This
-/// translates into a limit on the maximum size of the session state when using cookies as storage
-/// backend.
+/// Cookies are subject to size limits so we require session keys to be shorter than 4096 bytes.
+/// This translates into a limit on the maximum size of the session state when using cookies as
+/// storage backend.
 ///
 /// The session cookie can always be inspected by end users via the developer tools exposed by their
 /// browsers. We strongly recommend setting the policy to [`CookieContentSecurity::Private`] when
@@ -45,7 +46,7 @@ use crate::storage::{
 /// There is no way to invalidate a session before its natural expiry when using cookies as the
 /// storage backend.
 ///
-/// [`CookieContentSecurity::Private`]: crate::CookieContentSecurity::Private
+/// [`CookieContentSecurity::Private`]: crate::config::CookieContentSecurity::Private
 #[cfg_attr(docsrs, doc(cfg(feature = "cookie-session")))]
 #[derive(Default)]
 #[non_exhaustive]
@@ -89,6 +90,10 @@ impl SessionStore for CookieSessionStore {
             })
     }
 
+    async fn update_ttl(&self, _session_key: &SessionKey, _ttl: &Duration) -> Result<(), Error> {
+        Ok(())
+    }
+
     async fn delete(&self, _session_key: &SessionKey) -> Result<(), anyhow::Error> {
         Ok(())
     }
diff --git a/actix-session/src/storage/interface.rs b/actix-session/src/storage/interface.rs
index 64b10338f..2b52c59ed 100644
--- a/actix-session/src/storage/interface.rs
+++ b/actix-session/src/storage/interface.rs
@@ -1,7 +1,7 @@
 use std::collections::HashMap;
 
+use actix_web::cookie::time::Duration;
 use derive_more::Display;
-use time::Duration;
 
 use super::SessionKey;
 
@@ -36,6 +36,13 @@ pub trait SessionStore {
         ttl: &Duration,
     ) -> Result<SessionKey, UpdateError>;
 
+    /// Updates the TTL of the session state associated to a pre-existing session key.
+    async fn update_ttl(
+        &self,
+        session_key: &SessionKey,
+        ttl: &Duration,
+    ) -> Result<(), anyhow::Error>;
+
     /// Deletes a session from the store.
     async fn delete(&self, session_key: &SessionKey) -> Result<(), anyhow::Error>;
 }
diff --git a/actix-session/src/storage/redis_actor.rs b/actix-session/src/storage/redis_actor.rs
index f226dec34..744f01156 100644
--- a/actix-session/src/storage/redis_actor.rs
+++ b/actix-session/src/storage/redis_actor.rs
@@ -1,6 +1,7 @@
 use actix::Addr;
 use actix_redis::{resp_array, Command, RedisActor, RespValue};
-use time::{self, Duration};
+use actix_web::cookie::time::Duration;
+use anyhow::Error;
 
 use super::SessionKey;
 use crate::storage::{
@@ -238,6 +239,24 @@ impl SessionStore for RedisActorSessionStore {
         }
     }
 
+    async fn update_ttl(&self, session_key: &SessionKey, ttl: &Duration) -> Result<(), Error> {
+        let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
+
+        let cmd = Command(resp_array![
+            "EXPIRE",
+            cache_key,
+            ttl.whole_seconds().to_string()
+        ]);
+
+        match self.addr.send(cmd).await? {
+            Ok(RespValue::Integer(_)) => Ok(()),
+            val => Err(anyhow::anyhow!(
+                "Failed to update the session state TTL: {:?}",
+                val
+            )),
+        }
+    }
+
     async fn delete(&self, session_key: &SessionKey) -> Result<(), anyhow::Error> {
         let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
 
@@ -258,9 +277,11 @@ impl SessionStore for RedisActorSessionStore {
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use std::collections::HashMap;
 
+    use actix_web::cookie::time::Duration;
+
     use super::*;
     use crate::test_helpers::acceptance_test_suite;
 
@@ -286,7 +307,7 @@ mod test {
         let session_key = generate_session_key();
         let initial_session_key = session_key.as_ref().to_owned();
         let updated_session_key = store
-            .update(session_key, HashMap::new(), &time::Duration::seconds(1))
+            .update(session_key, HashMap::new(), &Duration::seconds(1))
             .await
             .unwrap();
         assert_ne!(initial_session_key, updated_session_key.as_ref());
diff --git a/actix-session/src/storage/redis_rs.rs b/actix-session/src/storage/redis_rs.rs
index cba2b356b..04a780279 100644
--- a/actix-session/src/storage/redis_rs.rs
+++ b/actix-session/src/storage/redis_rs.rs
@@ -1,7 +1,8 @@
-use std::sync::Arc;
+use std::{convert::TryInto, sync::Arc};
 
-use redis::{aio::ConnectionManager, Cmd, FromRedisValue, RedisResult, Value};
-use time::{self, Duration};
+use actix_web::cookie::time::Duration;
+use anyhow::{Context, Error};
+use redis::{aio::ConnectionManager, AsyncCommands, Cmd, FromRedisValue, RedisResult, Value};
 
 use super::SessionKey;
 use crate::storage::{
@@ -28,6 +29,7 @@ use crate::storage::{
 ///     let secret_key = get_secret_key();
 ///     let redis_connection_string = "redis://127.0.0.1:6379";
 ///     let store = RedisSessionStore::new(redis_connection_string).await.unwrap();
+///
 ///     HttpServer::new(move ||
 ///             App::new()
 ///             .wrap(SessionMiddleware::new(
@@ -221,6 +223,21 @@ impl SessionStore for RedisSessionStore {
         }
     }
 
+    async fn update_ttl(&self, session_key: &SessionKey, ttl: &Duration) -> Result<(), Error> {
+        let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
+
+        self.client
+            .clone()
+            .expire(
+                &cache_key,
+                ttl.whole_seconds().try_into().context(
+                    "Failed to convert the state TTL into the number of whole seconds remaining",
+                )?,
+            )
+            .await?;
+        Ok(())
+    }
+
     async fn delete(&self, session_key: &SessionKey) -> Result<(), anyhow::Error> {
         let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
         self.execute_command(redis::cmd("DEL").arg(&[&cache_key]))
@@ -272,9 +289,10 @@ impl RedisSessionStore {
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use std::collections::HashMap;
 
+    use actix_web::cookie::time;
     use redis::AsyncCommands;
 
     use super::*;
diff --git a/actix-session/src/storage/session_key.rs b/actix-session/src/storage/session_key.rs
index d82e18284..ad5c47a1d 100644
--- a/actix-session/src/storage/session_key.rs
+++ b/actix-session/src/storage/session_key.rs
@@ -2,16 +2,15 @@ use std::convert::TryFrom;
 
 use derive_more::{Display, From};
 
-/// A session key, the string stored in a client-side cookie to associate a user
-/// with its session state on the backend.
+/// A session key, the string stored in a client-side cookie to associate a user with its session
+/// state on the backend.
 ///
-/// ## Validation
-///
-/// Session keys are stored as cookies, therefore they cannot be arbitrary long.
-/// We require session keys to be smaller than 4064 bytes.
+/// # Validation
+/// Session keys are stored as cookies, therefore they cannot be arbitrary long. Session keys are
+/// required to be smaller than 4064 bytes.
 ///
 /// ```rust
-/// use std::convert::TryInto;
+/// # use std::convert::TryInto;
 /// use actix_session::storage::SessionKey;
 ///
 /// let key: String = std::iter::repeat('a').take(4065).collect();
@@ -24,15 +23,15 @@ pub struct SessionKey(String);
 impl TryFrom<String> for SessionKey {
     type Error = InvalidSessionKeyError;
 
-    fn try_from(v: String) -> Result<Self, Self::Error> {
-        if v.len() > 4064 {
+    fn try_from(val: String) -> Result<Self, Self::Error> {
+        if val.len() > 4064 {
             return Err(anyhow::anyhow!(
                 "The session key is bigger than 4064 bytes, the upper limit on cookie content."
             )
             .into());
         }
 
-        Ok(SessionKey(v))
+        Ok(SessionKey(val))
     }
 }
 
@@ -43,8 +42,8 @@ impl AsRef<str> for SessionKey {
 }
 
 impl From<SessionKey> for String {
-    fn from(k: SessionKey) -> Self {
-        k.0
+    fn from(key: SessionKey) -> Self {
+        key.0
     }
 }
 
diff --git a/actix-session/tests/opaque_errors.rs b/actix-session/tests/opaque_errors.rs
index 90e0f5c95..378a34752 100644
--- a/actix-session/tests/opaque_errors.rs
+++ b/actix-session/tests/opaque_errors.rs
@@ -71,6 +71,10 @@ impl SessionStore for MockStore {
         todo!()
     }
 
+    async fn update_ttl(&self, _session_key: &SessionKey, _ttl: &Duration) -> Result<(), Error> {
+        todo!()
+    }
+
     async fn delete(&self, _session_key: &SessionKey) -> Result<(), Error> {
         todo!()
     }

From 69e4264e0c80a30a2fbfe2ddcf72b9c9dfca23f4 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Mon, 4 Jul 2022 16:43:33 +0100
Subject: [PATCH 05/31] install cargo hack faster

---
 .github/workflows/ci-master.yml | 18 ++++++------------
 .github/workflows/ci.yml        | 18 ++++++------------
 2 files changed, 12 insertions(+), 24 deletions(-)

diff --git a/.github/workflows/ci-master.yml b/.github/workflows/ci-master.yml
index 995a7310b..e3c9da2cd 100644
--- a/.github/workflows/ci-master.yml
+++ b/.github/workflows/ci-master.yml
@@ -34,6 +34,9 @@ jobs:
           profile: minimal
           override: true
 
+      - name: Install cargo-hack
+        uses: taiki-e/install-action@cargo-hack
+
       - name: Generate Cargo.lock
         uses: actions-rs/cargo@v1
         with:
@@ -41,12 +44,6 @@ jobs:
       - name: Cache Dependencies
         uses: Swatinem/rust-cache@v1.2.0
 
-      - name: Install cargo-hack
-        uses: actions-rs/cargo@v1
-        with:
-          command: install
-          args: cargo-hack
-
       - name: check minimal
         uses: actions-rs/cargo@v1
         with: { command: ci-min }
@@ -92,6 +89,9 @@ jobs:
           profile: minimal
           override: true
 
+      - name: Install cargo-hack
+        uses: taiki-e/install-action@cargo-hack
+
       - name: Generate Cargo.lock
         uses: actions-rs/cargo@v1
         with:
@@ -99,12 +99,6 @@ jobs:
       - name: Cache Dependencies
         uses: Swatinem/rust-cache@v1.2.0
 
-      - name: Install cargo-hack
-        uses: actions-rs/cargo@v1
-        with:
-          command: install
-          args: cargo-hack
-
       - name: check minimal
         uses: actions-rs/cargo@v1
         with: { command: ci-min }
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8a244adc1..c9cb5522f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -42,6 +42,9 @@ jobs:
           profile: minimal
           override: true
 
+      - name: Install cargo-hack
+        uses: taiki-e/install-action@cargo-hack
+
       - name: Generate Cargo.lock
         uses: actions-rs/cargo@v1
         with:
@@ -49,12 +52,6 @@ jobs:
       - name: Cache Dependencies
         uses: Swatinem/rust-cache@v1.2.0
 
-      - name: Install cargo-hack
-        uses: actions-rs/cargo@v1
-        with:
-          command: install
-          args: cargo-hack
-
       - name: check minimal
         uses: actions-rs/cargo@v1
         with: { command: ci-min }
@@ -101,6 +98,9 @@ jobs:
           profile: minimal
           override: true
 
+      - name: Install cargo-hack
+        uses: taiki-e/install-action@cargo-hack
+
       - name: Generate Cargo.lock
         uses: actions-rs/cargo@v1
         with:
@@ -108,12 +108,6 @@ jobs:
       - name: Cache Dependencies
         uses: Swatinem/rust-cache@v1.2.0
 
-      - name: Install cargo-hack
-        uses: actions-rs/cargo@v1
-        with:
-          command: install
-          args: cargo-hack
-
       - name: check minimal
         uses: actions-rs/cargo@v1
         with: { command: ci-min }

From b8f4a658a92b8d60e4c573b9871a81f7e4930ae2 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Mon, 4 Jul 2022 16:44:48 +0100
Subject: [PATCH 06/31] rename post-merge ci job

---
 .github/workflows/{ci-master.yml => ci-post-merge.yml} | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
 rename .github/workflows/{ci-master.yml => ci-post-merge.yml} (99%)

diff --git a/.github/workflows/ci-master.yml b/.github/workflows/ci-post-merge.yml
similarity index 99%
rename from .github/workflows/ci-master.yml
rename to .github/workflows/ci-post-merge.yml
index e3c9da2cd..2de0ee349 100644
--- a/.github/workflows/ci-master.yml
+++ b/.github/workflows/ci-post-merge.yml
@@ -1,4 +1,4 @@
-name: CI (master only)
+name: CI (post-merge)
 
 on:
   push:

From b1cea6479538e5c66b577f13ce686511817f5745 Mon Sep 17 00:00:00 2001
From: Luca Palmieri <lpalmieri@truelayer.com>
Date: Sat, 9 Jul 2022 19:00:15 +0100
Subject: [PATCH 07/31] Rebuild `actix-identity` on top of `actix-session`
 (#246)

Co-authored-by: Rob Ede <robjtede@icloud.com>
---
 actix-identity/Cargo.toml                     |  16 +-
 actix-identity/examples/identity.rs           |  84 ++
 actix-identity/src/config.rs                  | 101 +++
 actix-identity/src/cookie.rs                  | 828 ------------------
 actix-identity/src/identity.rs                | 274 ++++--
 actix-identity/src/identity_ext.rs            |  27 +
 actix-identity/src/lib.rs                     | 212 ++---
 actix-identity/src/middleware.rs              | 300 ++++---
 actix-identity/tests/integration/fixtures.rs  |  17 +
 .../tests/integration/integration.rs          | 168 ++++
 actix-identity/tests/integration/main.rs      |   3 +
 actix-identity/tests/integration/test_app.rs  | 186 ++++
 actix-session/CHANGES.md                      |   2 +-
 actix-session/src/lib.rs                      |   2 +-
 actix-session/src/session.rs                  |  88 +-
 15 files changed, 1155 insertions(+), 1153 deletions(-)
 create mode 100644 actix-identity/examples/identity.rs
 create mode 100644 actix-identity/src/config.rs
 delete mode 100644 actix-identity/src/cookie.rs
 create mode 100644 actix-identity/src/identity_ext.rs
 create mode 100644 actix-identity/tests/integration/fixtures.rs
 create mode 100644 actix-identity/tests/integration/integration.rs
 create mode 100644 actix-identity/tests/integration/main.rs
 create mode 100644 actix-identity/tests/integration/test_app.rs

diff --git a/actix-identity/Cargo.toml b/actix-identity/Cargo.toml
index 7011da01f..a6ca7690e 100644
--- a/actix-identity/Cargo.toml
+++ b/actix-identity/Cargo.toml
@@ -1,7 +1,10 @@
 [package]
 name = "actix-identity"
 version = "0.4.0"
-authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
+authors = [
+    "Nikolay Kim <fafhrd91@gmail.com>",
+    "Luca Palmieri <rust@lpalmieri.com>",
+]
 description = "Identity service for Actix Web"
 keywords = ["actix", "auth", "identity", "web", "security"]
 homepage = "https://actix.rs"
@@ -15,14 +18,21 @@ path = "src/lib.rs"
 
 [dependencies]
 actix-service = "2"
+actix-session = "0.6.2" # update to 0.7 with release of -session
 actix-utils = "3"
 actix-web = { version = "4", default-features = false, features = ["cookies", "secure-cookies"] }
 
-futures-util = { version = "0.3.7", default-features = false }
+anyhow = "1"
+env_logger = "0.9"
+futures-core = "0.3.7"
 serde = { version = "1", features = ["derive"] }
 serde_json = "1"
 time = "0.3"
+tracing = { version = "0.1.30", default-features = false, features = ["log"] }
 
 [dev-dependencies]
-actix-http = "3.0.0-rc.1"
+actix-http = "3"
 actix-web = { version = "4", default_features = false, features = ["macros", "cookies", "secure-cookies"] }
+actix-session = { version = "0.6.2", features = ["redis-rs-session", "cookie-session"] } # update to 0.7 with release of -session
+uuid = { version = "1", features = ["v4"] }
+reqwest = { version = "0.11", default_features = false, features = ["cookies", "json"] }
diff --git a/actix-identity/examples/identity.rs b/actix-identity/examples/identity.rs
new file mode 100644
index 000000000..e80b69396
--- /dev/null
+++ b/actix-identity/examples/identity.rs
@@ -0,0 +1,84 @@
+//! A rudimentary example of how to set up and use `actix-identity`.
+//!
+//! ```bash
+//! # using HTTPie (https://httpie.io/cli)
+//!
+//! # outputs "Welcome Anonymous!" message
+//! http -v --session=identity GET localhost:8080/
+//!
+//! # log in using fake details, ensuring that --session is used to persist cookies
+//! http -v --session=identity POST localhost:8080/login user_id=foo
+//!
+//! # outputs "Welcome User1" message
+//! http -v --session=identity GET localhost:8080/
+//! ```
+
+use std::io;
+
+use actix_identity::{Identity, IdentityMiddleware};
+use actix_session::{storage::CookieSessionStore, SessionMiddleware};
+use actix_web::{
+    cookie::Key, get, middleware::Logger, post, App, HttpMessage, HttpRequest, HttpResponse,
+    HttpServer, Responder,
+};
+
+#[actix_web::main]
+async fn main() -> io::Result<()> {
+    env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
+
+    let secret_key = Key::generate();
+
+    HttpServer::new(move || {
+        let session_mw =
+            SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone())
+                // disable secure cookie for local testing
+                .cookie_secure(false)
+                .build();
+
+        App::new()
+            // Install the identity framework first.
+            .wrap(IdentityMiddleware::default())
+            // The identity system is built on top of sessions. You must install the session
+            // middleware to leverage `actix-identity`. The session middleware must be mounted
+            // AFTER the identity middleware: `actix-web` invokes middleware in the OPPOSITE
+            // order of registration when it receives an incoming request.
+            .wrap(session_mw)
+            .wrap(Logger::default())
+            .service(index)
+            .service(login)
+            .service(logout)
+    })
+    .bind(("127.0.0.1", 8080))
+    .unwrap()
+    .workers(2)
+    .run()
+    .await
+}
+
+#[get("/")]
+async fn index(user: Option<Identity>) -> impl Responder {
+    if let Some(user) = user {
+        format!("Welcome! {}", user.id().unwrap())
+    } else {
+        "Welcome Anonymous!".to_owned()
+    }
+}
+
+#[post("/login")]
+async fn login(request: HttpRequest) -> impl Responder {
+    // Some kind of authentication should happen here -
+    // e.g. password-based, biometric, etc.
+    // [...]
+
+    // Attached a verified user identity to the active
+    // session.
+    Identity::login(&request.extensions(), "User1".into()).unwrap();
+
+    HttpResponse::Ok()
+}
+
+#[post("/logout")]
+async fn logout(user: Identity) -> impl Responder {
+    user.logout();
+    HttpResponse::NoContent()
+}
diff --git a/actix-identity/src/config.rs b/actix-identity/src/config.rs
new file mode 100644
index 000000000..a10514e9c
--- /dev/null
+++ b/actix-identity/src/config.rs
@@ -0,0 +1,101 @@
+//! Configuration options to tune the behaviour of [`IdentityMiddleware`].
+
+use std::time::Duration;
+
+use crate::IdentityMiddleware;
+
+#[derive(Debug, Clone)]
+pub(crate) struct Configuration {
+    pub(crate) on_logout: LogoutBehaviour,
+    pub(crate) login_deadline: Option<Duration>,
+    pub(crate) visit_deadline: Option<Duration>,
+}
+
+impl Default for Configuration {
+    fn default() -> Self {
+        Self {
+            on_logout: LogoutBehaviour::PurgeSession,
+            login_deadline: None,
+            visit_deadline: None,
+        }
+    }
+}
+
+/// `LogoutBehaviour` controls what actions are going to be performed when [`Identity::logout`] is
+/// invoked.
+///
+/// [`Identity::logout`]: crate::Identity::logout
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub enum LogoutBehaviour {
+    /// When [`Identity::logout`](crate::Identity::logout) is called, purge the current session.
+    ///
+    /// This behaviour might be desirable when you have stored additional information in the
+    /// session state that are tied to the user's identity and should not be retained after logout.
+    PurgeSession,
+
+    /// When [`Identity::logout`](crate::Identity::logout) is called, remove the identity
+    /// information from the current session state. The session itself is not destroyed.
+    ///
+    /// This behaviour might be desirable when you have stored information in the session state that
+    /// is not tied to the user's identity and should be retained after logout.
+    DeleteIdentityKeys,
+}
+
+/// A fluent builder to construct an [`IdentityMiddleware`] instance with custom configuration
+/// parameters.
+///
+/// Use [`IdentityMiddleware::builder`] to get started!
+#[derive(Debug, Clone)]
+pub struct IdentityMiddlewareBuilder {
+    configuration: Configuration,
+}
+
+impl IdentityMiddlewareBuilder {
+    pub(crate) fn new() -> Self {
+        Self {
+            configuration: Configuration::default(),
+        }
+    }
+
+    /// Determines how [`Identity::logout`](crate::Identity::logout) affects the current session.
+    ///
+    /// By default, the current session is purged ([`LogoutBehaviour::PurgeSession`]).
+    pub fn logout_behaviour(mut self, logout_behaviour: LogoutBehaviour) -> Self {
+        self.configuration.on_logout = logout_behaviour;
+        self
+    }
+
+    /// Automatically logs out users after a certain amount of time has passed since they logged in,
+    /// regardless of their activity pattern.
+    ///
+    /// If set to:
+    /// - `None`: login deadline is disabled.
+    /// - `Some(duration)`: login deadline is enabled and users will be logged out after `duration`
+    ///   has passed since their login.
+    ///
+    /// By default, login deadline is disabled.
+    pub fn login_deadline(mut self, deadline: Option<Duration>) -> Self {
+        self.configuration.login_deadline = deadline;
+        self
+    }
+
+    /// Automatically logs out users after a certain amount of time has passed since their last
+    /// visit.
+    ///
+    /// If set to:
+    /// - `None`: visit deadline is disabled.
+    /// - `Some(duration)`: visit deadline is enabled and users will be logged out after `duration`
+    ///   has passed since their last visit.
+    ///
+    /// By default, visit deadline is disabled.
+    pub fn visit_deadline(mut self, deadline: Option<Duration>) -> Self {
+        self.configuration.visit_deadline = deadline;
+        self
+    }
+
+    /// Finalises the builder and returns an [`IdentityMiddleware`] instance.
+    pub fn build(self) -> IdentityMiddleware {
+        IdentityMiddleware::new(self.configuration)
+    }
+}
diff --git a/actix-identity/src/cookie.rs b/actix-identity/src/cookie.rs
deleted file mode 100644
index 0e9733688..000000000
--- a/actix-identity/src/cookie.rs
+++ /dev/null
@@ -1,828 +0,0 @@
-use std::{rc::Rc, time::SystemTime};
-
-use actix_utils::future::{ready, Ready};
-use actix_web::{
-    cookie::{Cookie, CookieJar, Key, SameSite},
-    dev::{ServiceRequest, ServiceResponse},
-    error::{Error, Result},
-    http::header::{self, HeaderValue},
-    HttpMessage,
-};
-use serde::{Deserialize, Serialize};
-use time::Duration;
-
-use crate::IdentityPolicy;
-
-struct CookieIdentityInner {
-    key: Key,
-    key_v2: Key,
-    name: String,
-    path: String,
-    domain: Option<String>,
-    secure: bool,
-    max_age: Option<Duration>,
-    http_only: Option<bool>,
-    same_site: Option<SameSite>,
-    visit_deadline: Option<Duration>,
-    login_deadline: Option<Duration>,
-}
-
-#[derive(Debug, Deserialize, Serialize)]
-struct CookieValue {
-    identity: String,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    login_timestamp: Option<SystemTime>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    visit_timestamp: Option<SystemTime>,
-}
-
-#[derive(Debug)]
-struct CookieIdentityExtension {
-    login_timestamp: Option<SystemTime>,
-}
-
-impl CookieIdentityInner {
-    fn new(key: &[u8]) -> CookieIdentityInner {
-        let key_v2: Vec<u8> = [key, &[1, 0, 0, 0]].concat();
-
-        CookieIdentityInner {
-            key: Key::derive_from(key),
-            key_v2: Key::derive_from(&key_v2),
-            name: "actix-identity".to_owned(),
-            path: "/".to_owned(),
-            domain: None,
-            secure: true,
-            max_age: None,
-            http_only: None,
-            same_site: None,
-            visit_deadline: None,
-            login_deadline: None,
-        }
-    }
-
-    fn set_cookie<B>(
-        &self,
-        resp: &mut ServiceResponse<B>,
-        value: Option<CookieValue>,
-    ) -> Result<()> {
-        let add_cookie = value.is_some();
-        let val = value
-            .map(|val| {
-                if !self.legacy_supported() {
-                    serde_json::to_string(&val)
-                } else {
-                    Ok(val.identity)
-                }
-            })
-            .transpose()?;
-
-        let mut cookie = Cookie::new(self.name.clone(), val.unwrap_or_default());
-        cookie.set_path(self.path.clone());
-        cookie.set_secure(self.secure);
-        cookie.set_http_only(true);
-
-        if let Some(ref domain) = self.domain {
-            cookie.set_domain(domain.clone());
-        }
-
-        if let Some(max_age) = self.max_age {
-            cookie.set_max_age(max_age);
-        }
-
-        if let Some(http_only) = self.http_only {
-            cookie.set_http_only(http_only);
-        }
-
-        if let Some(same_site) = self.same_site {
-            cookie.set_same_site(same_site);
-        }
-
-        let mut jar = CookieJar::new();
-
-        let key = if self.legacy_supported() {
-            &self.key
-        } else {
-            &self.key_v2
-        };
-
-        if add_cookie {
-            jar.private_mut(key).add(cookie);
-        } else {
-            jar.add_original(cookie.clone());
-            jar.private_mut(key).remove(cookie);
-        }
-
-        for cookie in jar.delta() {
-            let val = HeaderValue::from_str(&cookie.to_string())?;
-            resp.headers_mut().append(header::SET_COOKIE, val);
-        }
-
-        Ok(())
-    }
-
-    fn load(&self, req: &ServiceRequest) -> Option<CookieValue> {
-        let cookie = req.cookie(&self.name)?;
-        let mut jar = CookieJar::new();
-        jar.add_original(cookie.clone());
-
-        let res = if self.legacy_supported() {
-            jar.private_mut(&self.key)
-                .get(&self.name)
-                .map(|n| CookieValue {
-                    identity: n.value().to_string(),
-                    login_timestamp: None,
-                    visit_timestamp: None,
-                })
-        } else {
-            None
-        };
-
-        res.or_else(|| {
-            jar.private_mut(&self.key_v2)
-                .get(&self.name)
-                .and_then(|c| self.parse(c))
-        })
-    }
-
-    fn parse(&self, cookie: Cookie<'_>) -> Option<CookieValue> {
-        let value: CookieValue = serde_json::from_str(cookie.value()).ok()?;
-        let now = SystemTime::now();
-
-        if let Some(visit_deadline) = self.visit_deadline {
-            let inactivity = now.duration_since(value.visit_timestamp?).ok()?;
-
-            if inactivity > visit_deadline {
-                return None;
-            }
-        }
-
-        if let Some(login_deadline) = self.login_deadline {
-            let logged_in_dur = now.duration_since(value.login_timestamp?).ok()?;
-
-            if logged_in_dur > login_deadline {
-                return None;
-            }
-        }
-
-        Some(value)
-    }
-
-    fn legacy_supported(&self) -> bool {
-        self.visit_deadline.is_none() && self.login_deadline.is_none()
-    }
-
-    fn always_update_cookie(&self) -> bool {
-        self.visit_deadline.is_some()
-    }
-
-    fn requires_oob_data(&self) -> bool {
-        self.login_deadline.is_some()
-    }
-}
-
-/// Use cookies for request identity storage.
-///
-/// [See this page on MDN](mdn-cookies) for details on cookie attributes.
-///
-/// # Examples
-/// ```
-/// use actix_web::App;
-/// use actix_identity::{CookieIdentityPolicy, IdentityService};
-///
-/// // create cookie identity backend
-/// let policy = CookieIdentityPolicy::new(&[0; 32])
-///            .domain("www.rust-lang.org")
-///            .name("actix_auth")
-///            .path("/")
-///            .secure(true);
-///
-/// let app = App::new()
-///     // wrap policy into identity middleware
-///     .wrap(IdentityService::new(policy));
-/// ```
-///
-/// [mdn-cookies]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies
-pub struct CookieIdentityPolicy(Rc<CookieIdentityInner>);
-
-impl CookieIdentityPolicy {
-    /// Create new `CookieIdentityPolicy` instance.
-    ///
-    /// Key argument is the private key for issued cookies. If this value is changed, all issued
-    /// cookie identities are invalidated.
-    ///
-    /// # Panics
-    /// Panics if `key` is less than 32 bytes in length..
-    pub fn new(key: &[u8]) -> CookieIdentityPolicy {
-        CookieIdentityPolicy(Rc::new(CookieIdentityInner::new(key)))
-    }
-
-    /// Sets the name of issued cookies.
-    pub fn name(mut self, value: impl Into<String>) -> CookieIdentityPolicy {
-        self.inner_mut().name = value.into();
-        self
-    }
-
-    /// Sets the `Path` attribute of issued cookies.
-    pub fn path(mut self, value: impl Into<String>) -> CookieIdentityPolicy {
-        self.inner_mut().path = value.into();
-        self
-    }
-
-    /// Sets the `Domain` attribute of issued cookies.
-    pub fn domain(mut self, value: impl Into<String>) -> CookieIdentityPolicy {
-        self.inner_mut().domain = Some(value.into());
-        self
-    }
-
-    /// Sets the `Secure` attribute of issued cookies.
-    pub fn secure(mut self, value: bool) -> CookieIdentityPolicy {
-        self.inner_mut().secure = value;
-        self
-    }
-
-    /// Sets the `Max-Age` attribute of issued cookies.
-    pub fn max_age(mut self, value: Duration) -> CookieIdentityPolicy {
-        self.inner_mut().max_age = Some(value);
-        self
-    }
-
-    /// Sets the `Max-Age` attribute of issued cookies with given number of seconds.
-    pub fn max_age_secs(self, seconds: i64) -> CookieIdentityPolicy {
-        self.max_age(Duration::seconds(seconds))
-    }
-
-    /// Sets the `HttpOnly` attribute of issued cookies.
-    ///
-    /// By default, the `HttpOnly` attribute is omitted from issued cookies.
-    pub fn http_only(mut self, http_only: bool) -> Self {
-        self.inner_mut().http_only = Some(http_only);
-        self
-    }
-
-    /// Sets the `SameSite` attribute of issued cookies.
-    ///
-    /// By default, the `SameSite` attribute is omitted from issued cookies.
-    pub fn same_site(mut self, same_site: SameSite) -> Self {
-        self.inner_mut().same_site = Some(same_site);
-        self
-    }
-
-    /// Accepts only users who have visited within given deadline.
-    ///
-    /// In other words, invalidate a login after some amount of inactivity. Using this feature
-    /// causes updated cookies to be issued on each response in order to record the user's last
-    /// visitation timestamp.
-    ///
-    /// By default, visit deadline is disabled.
-    pub fn visit_deadline(mut self, deadline: Duration) -> CookieIdentityPolicy {
-        self.inner_mut().visit_deadline = Some(deadline);
-        self
-    }
-
-    /// Accepts only users who authenticated within the given deadline.
-    ///
-    /// In other words, invalidate a login after some amount of time, regardless of activity.
-    /// While [`Max-Age`](CookieIdentityPolicy::max_age) is useful in constraining the cookie
-    /// lifetime, it could be extended manually; using this feature encodes the deadline directly
-    /// into the issued cookies, making it immutable to users.
-    ///
-    /// By default, login deadline is disabled.
-    pub fn login_deadline(mut self, deadline: Duration) -> CookieIdentityPolicy {
-        self.inner_mut().login_deadline = Some(deadline);
-        self
-    }
-
-    fn inner_mut(&mut self) -> &mut CookieIdentityInner {
-        Rc::get_mut(&mut self.0).unwrap()
-    }
-}
-
-impl IdentityPolicy for CookieIdentityPolicy {
-    type Future = Ready<Result<Option<String>, Error>>;
-    type ResponseFuture = Ready<Result<(), Error>>;
-
-    fn from_request(&self, req: &mut ServiceRequest) -> Self::Future {
-        ready(Ok(self.0.load(req).map(|value| {
-            let CookieValue {
-                identity,
-                login_timestamp,
-                ..
-            } = value;
-
-            if self.0.requires_oob_data() {
-                req.extensions_mut()
-                    .insert(CookieIdentityExtension { login_timestamp });
-            }
-
-            identity
-        })))
-    }
-
-    fn to_response<B>(
-        &self,
-        id: Option<String>,
-        changed: bool,
-        res: &mut ServiceResponse<B>,
-    ) -> Self::ResponseFuture {
-        let _ = if changed {
-            let login_timestamp = SystemTime::now();
-
-            self.0.set_cookie(
-                res,
-                id.map(|identity| CookieValue {
-                    identity,
-                    login_timestamp: self.0.login_deadline.map(|_| login_timestamp),
-                    visit_timestamp: self.0.visit_deadline.map(|_| login_timestamp),
-                }),
-            )
-        } else if self.0.always_update_cookie() && id.is_some() {
-            let visit_timestamp = SystemTime::now();
-
-            let login_timestamp = if self.0.requires_oob_data() {
-                let CookieIdentityExtension { login_timestamp } =
-                    res.request().extensions_mut().remove().unwrap();
-
-                login_timestamp
-            } else {
-                None
-            };
-
-            self.0.set_cookie(
-                res,
-                Some(CookieValue {
-                    identity: id.unwrap(),
-                    login_timestamp,
-                    visit_timestamp: self.0.visit_deadline.map(|_| visit_timestamp),
-                }),
-            )
-        } else {
-            Ok(())
-        };
-
-        ready(Ok(()))
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use std::{borrow::Borrow, time::SystemTime};
-
-    use actix_web::{
-        body::{BoxBody, EitherBody},
-        cookie::{Cookie, CookieJar, Key, SameSite},
-        dev::ServiceResponse,
-        http::{header, StatusCode},
-        test::{self, TestRequest},
-        web, App, HttpResponse,
-    };
-    use time::Duration;
-
-    use super::*;
-    use crate::{tests::*, Identity, IdentityService};
-
-    fn login_cookie(
-        identity: &'static str,
-        login_timestamp: Option<SystemTime>,
-        visit_timestamp: Option<SystemTime>,
-    ) -> Cookie<'static> {
-        let mut jar = CookieJar::new();
-        let key: Vec<u8> = COOKIE_KEY_MASTER
-            .iter()
-            .chain([1, 0, 0, 0].iter())
-            .copied()
-            .collect();
-
-        jar.private_mut(&Key::derive_from(&key)).add(Cookie::new(
-            COOKIE_NAME,
-            serde_json::to_string(&CookieValue {
-                identity: identity.to_string(),
-                login_timestamp,
-                visit_timestamp,
-            })
-            .unwrap(),
-        ));
-
-        jar.get(COOKIE_NAME).unwrap().clone()
-    }
-
-    fn assert_login_cookie(
-        response: &mut ServiceResponse<EitherBody<BoxBody>>,
-        identity: &str,
-        login_timestamp: LoginTimestampCheck,
-        visit_timestamp: VisitTimeStampCheck,
-    ) {
-        let mut cookies = CookieJar::new();
-
-        for cookie in response.headers().get_all(header::SET_COOKIE) {
-            cookies.add(Cookie::parse(cookie.to_str().unwrap().to_string()).unwrap());
-        }
-
-        let key: Vec<u8> = COOKIE_KEY_MASTER
-            .iter()
-            .chain([1, 0, 0, 0].iter())
-            .copied()
-            .collect();
-
-        let cookie = cookies
-            .private(&Key::derive_from(&key))
-            .get(COOKIE_NAME)
-            .unwrap();
-
-        let cv: CookieValue = serde_json::from_str(cookie.value()).unwrap();
-        assert_eq!(cv.identity, identity);
-
-        let now = SystemTime::now();
-        let t30sec_ago = now - Duration::seconds(30);
-
-        match login_timestamp {
-            LoginTimestampCheck::NoTimestamp => assert_eq!(cv.login_timestamp, None),
-            LoginTimestampCheck::NewTimestamp => assert!(
-                t30sec_ago <= cv.login_timestamp.unwrap() && cv.login_timestamp.unwrap() <= now
-            ),
-            LoginTimestampCheck::OldTimestamp(old_timestamp) => {
-                assert_eq!(cv.login_timestamp, Some(old_timestamp))
-            }
-        }
-
-        match visit_timestamp {
-            VisitTimeStampCheck::NoTimestamp => assert_eq!(cv.visit_timestamp, None),
-            VisitTimeStampCheck::NewTimestamp => assert!(
-                t30sec_ago <= cv.visit_timestamp.unwrap() && cv.visit_timestamp.unwrap() <= now
-            ),
-        }
-    }
-
-    #[actix_web::test]
-    async fn test_identity_flow() {
-        let srv = test::init_service(
-            App::new()
-                .wrap(IdentityService::new(
-                    CookieIdentityPolicy::new(&COOKIE_KEY_MASTER)
-                        .domain("www.rust-lang.org")
-                        .name(COOKIE_NAME)
-                        .path("/")
-                        .secure(true),
-                ))
-                .service(web::resource("/index").to(|id: Identity| {
-                    if id.identity().is_some() {
-                        HttpResponse::Created()
-                    } else {
-                        HttpResponse::Ok()
-                    }
-                }))
-                .service(web::resource("/login").to(|id: Identity| {
-                    id.remember(COOKIE_LOGIN.to_string());
-                    HttpResponse::Ok()
-                }))
-                .service(web::resource("/logout").to(|id: Identity| {
-                    if id.identity().is_some() {
-                        id.forget();
-                        HttpResponse::Ok()
-                    } else {
-                        HttpResponse::BadRequest()
-                    }
-                })),
-        )
-        .await;
-        let resp = test::call_service(&srv, TestRequest::with_uri("/index").to_request()).await;
-        assert_eq!(resp.status(), StatusCode::OK);
-
-        let resp = test::call_service(&srv, TestRequest::with_uri("/login").to_request()).await;
-        assert_eq!(resp.status(), StatusCode::OK);
-        let c = resp.response().cookies().next().unwrap().to_owned();
-
-        let resp = test::call_service(
-            &srv,
-            TestRequest::with_uri("/index")
-                .cookie(c.clone())
-                .to_request(),
-        )
-        .await;
-        assert_eq!(resp.status(), StatusCode::CREATED);
-
-        let resp = test::call_service(
-            &srv,
-            TestRequest::with_uri("/logout")
-                .cookie(c.clone())
-                .to_request(),
-        )
-        .await;
-        assert_eq!(resp.status(), StatusCode::OK);
-        assert!(resp.headers().contains_key(header::SET_COOKIE))
-    }
-
-    #[actix_web::test]
-    async fn test_identity_max_age_time() {
-        let duration = Duration::days(1);
-
-        let srv = test::init_service(
-            App::new()
-                .wrap(IdentityService::new(
-                    CookieIdentityPolicy::new(&COOKIE_KEY_MASTER)
-                        .domain("www.rust-lang.org")
-                        .name(COOKIE_NAME)
-                        .path("/")
-                        .max_age(duration)
-                        .secure(true),
-                ))
-                .service(web::resource("/login").to(|id: Identity| {
-                    id.remember("test".to_string());
-                    HttpResponse::Ok()
-                })),
-        )
-        .await;
-
-        let resp = test::call_service(&srv, TestRequest::with_uri("/login").to_request()).await;
-        assert_eq!(resp.status(), StatusCode::OK);
-        assert!(resp.headers().contains_key(header::SET_COOKIE));
-        let c = resp.response().cookies().next().unwrap().to_owned();
-        assert_eq!(duration, c.max_age().unwrap());
-    }
-
-    #[actix_web::test]
-    async fn test_http_only_same_site() {
-        let srv = test::init_service(
-            App::new()
-                .wrap(IdentityService::new(
-                    CookieIdentityPolicy::new(&COOKIE_KEY_MASTER)
-                        .domain("www.rust-lang.org")
-                        .name(COOKIE_NAME)
-                        .path("/")
-                        .http_only(true)
-                        .same_site(SameSite::None),
-                ))
-                .service(web::resource("/login").to(|id: Identity| {
-                    id.remember("test".to_string());
-                    HttpResponse::Ok()
-                })),
-        )
-        .await;
-
-        let resp = test::call_service(&srv, TestRequest::with_uri("/login").to_request()).await;
-
-        assert_eq!(resp.status(), StatusCode::OK);
-        assert!(resp.headers().contains_key(header::SET_COOKIE));
-
-        let c = resp.response().cookies().next().unwrap().to_owned();
-        assert!(c.http_only().unwrap());
-        assert_eq!(SameSite::None, c.same_site().unwrap());
-    }
-
-    fn legacy_login_cookie(identity: &'static str) -> Cookie<'static> {
-        let mut jar = CookieJar::new();
-        jar.private_mut(&Key::derive_from(&COOKIE_KEY_MASTER))
-            .add(Cookie::new(COOKIE_NAME, identity));
-        jar.get(COOKIE_NAME).unwrap().clone()
-    }
-
-    async fn assert_logged_in(
-        response: ServiceResponse<EitherBody<BoxBody>>,
-        identity: Option<&str>,
-    ) {
-        let bytes = test::read_body(response).await;
-        let resp: Option<String> = serde_json::from_slice(&bytes[..]).unwrap();
-        assert_eq!(resp.as_ref().map(|s| s.borrow()), identity);
-    }
-
-    fn assert_legacy_login_cookie(
-        response: &mut ServiceResponse<EitherBody<BoxBody>>,
-        identity: &str,
-    ) {
-        let mut cookies = CookieJar::new();
-        for cookie in response.headers().get_all(header::SET_COOKIE) {
-            cookies.add(Cookie::parse(cookie.to_str().unwrap().to_string()).unwrap());
-        }
-        let cookie = cookies
-            .private_mut(&Key::derive_from(&COOKIE_KEY_MASTER))
-            .get(COOKIE_NAME)
-            .unwrap();
-        assert_eq!(cookie.value(), identity);
-    }
-
-    fn assert_no_login_cookie(response: &mut ServiceResponse<EitherBody<BoxBody>>) {
-        let mut cookies = CookieJar::new();
-        for cookie in response.headers().get_all(header::SET_COOKIE) {
-            cookies.add(Cookie::parse(cookie.to_str().unwrap().to_string()).unwrap());
-        }
-        assert!(cookies.get(COOKIE_NAME).is_none());
-    }
-
-    #[actix_web::test]
-    async fn test_identity_max_age() {
-        let seconds = 60;
-        let srv = test::init_service(
-            App::new()
-                .wrap(IdentityService::new(
-                    CookieIdentityPolicy::new(&COOKIE_KEY_MASTER)
-                        .domain("www.rust-lang.org")
-                        .name(COOKIE_NAME)
-                        .path("/")
-                        .max_age_secs(seconds)
-                        .secure(true),
-                ))
-                .service(web::resource("/login").to(|id: Identity| {
-                    id.remember("test".to_string());
-                    HttpResponse::Ok()
-                })),
-        )
-        .await;
-        let resp = test::call_service(&srv, TestRequest::with_uri("/login").to_request()).await;
-        assert_eq!(resp.status(), StatusCode::OK);
-        assert!(resp.headers().contains_key(header::SET_COOKIE));
-        let c = resp.response().cookies().next().unwrap().to_owned();
-        assert_eq!(Duration::seconds(seconds as i64), c.max_age().unwrap());
-    }
-
-    #[actix_web::test]
-    async fn test_identity_legacy_cookie_is_set() {
-        let srv = create_identity_server(|c| c).await;
-        let mut resp = test::call_service(&srv, TestRequest::with_uri("/").to_request()).await;
-        assert_legacy_login_cookie(&mut resp, COOKIE_LOGIN);
-        assert_logged_in(resp, None).await;
-    }
-
-    #[actix_web::test]
-    async fn test_identity_legacy_cookie_works() {
-        let srv = create_identity_server(|c| c).await;
-        let cookie = legacy_login_cookie(COOKIE_LOGIN);
-        let mut resp = test::call_service(
-            &srv,
-            TestRequest::with_uri("/")
-                .cookie(cookie.clone())
-                .to_request(),
-        )
-        .await;
-        assert_no_login_cookie(&mut resp);
-        assert_logged_in(resp, Some(COOKIE_LOGIN)).await;
-    }
-
-    #[actix_web::test]
-    async fn test_identity_legacy_cookie_rejected_if_visit_timestamp_needed() {
-        let srv = create_identity_server(|c| c.visit_deadline(Duration::days(90))).await;
-        let cookie = legacy_login_cookie(COOKIE_LOGIN);
-        let mut resp = test::call_service(
-            &srv,
-            TestRequest::with_uri("/")
-                .cookie(cookie.clone())
-                .to_request(),
-        )
-        .await;
-        assert_login_cookie(
-            &mut resp,
-            COOKIE_LOGIN,
-            LoginTimestampCheck::NoTimestamp,
-            VisitTimeStampCheck::NewTimestamp,
-        );
-        assert_logged_in(resp, None).await;
-    }
-
-    #[actix_web::test]
-    async fn test_identity_legacy_cookie_rejected_if_login_timestamp_needed() {
-        let srv = create_identity_server(|c| c.login_deadline(Duration::days(90))).await;
-        let cookie = legacy_login_cookie(COOKIE_LOGIN);
-        let mut resp = test::call_service(
-            &srv,
-            TestRequest::with_uri("/")
-                .cookie(cookie.clone())
-                .to_request(),
-        )
-        .await;
-        assert_login_cookie(
-            &mut resp,
-            COOKIE_LOGIN,
-            LoginTimestampCheck::NewTimestamp,
-            VisitTimeStampCheck::NoTimestamp,
-        );
-        assert_logged_in(resp, None).await;
-    }
-
-    #[actix_web::test]
-    async fn test_identity_cookie_rejected_if_login_timestamp_needed() {
-        let srv = create_identity_server(|c| c.login_deadline(Duration::days(90))).await;
-        let cookie = login_cookie(COOKIE_LOGIN, None, Some(SystemTime::now()));
-        let mut resp = test::call_service(
-            &srv,
-            TestRequest::with_uri("/")
-                .cookie(cookie.clone())
-                .to_request(),
-        )
-        .await;
-        assert_login_cookie(
-            &mut resp,
-            COOKIE_LOGIN,
-            LoginTimestampCheck::NewTimestamp,
-            VisitTimeStampCheck::NoTimestamp,
-        );
-        assert_logged_in(resp, None).await;
-    }
-
-    #[actix_web::test]
-    async fn test_identity_cookie_rejected_if_visit_timestamp_needed() {
-        let srv = create_identity_server(|c| c.visit_deadline(Duration::days(90))).await;
-        let cookie = login_cookie(COOKIE_LOGIN, Some(SystemTime::now()), None);
-        let mut resp = test::call_service(
-            &srv,
-            TestRequest::with_uri("/")
-                .cookie(cookie.clone())
-                .to_request(),
-        )
-        .await;
-        assert_login_cookie(
-            &mut resp,
-            COOKIE_LOGIN,
-            LoginTimestampCheck::NoTimestamp,
-            VisitTimeStampCheck::NewTimestamp,
-        );
-        assert_logged_in(resp, None).await;
-    }
-
-    #[actix_web::test]
-    async fn test_identity_cookie_rejected_if_login_timestamp_too_old() {
-        let srv = create_identity_server(|c| c.login_deadline(Duration::days(90))).await;
-        let cookie = login_cookie(
-            COOKIE_LOGIN,
-            Some(SystemTime::now() - Duration::days(180)),
-            None,
-        );
-        let mut resp = test::call_service(
-            &srv,
-            TestRequest::with_uri("/")
-                .cookie(cookie.clone())
-                .to_request(),
-        )
-        .await;
-        assert_login_cookie(
-            &mut resp,
-            COOKIE_LOGIN,
-            LoginTimestampCheck::NewTimestamp,
-            VisitTimeStampCheck::NoTimestamp,
-        );
-        assert_logged_in(resp, None).await;
-    }
-
-    #[actix_web::test]
-    async fn test_identity_cookie_rejected_if_visit_timestamp_too_old() {
-        let srv = create_identity_server(|c| c.visit_deadline(Duration::days(90))).await;
-        let cookie = login_cookie(
-            COOKIE_LOGIN,
-            None,
-            Some(SystemTime::now() - Duration::days(180)),
-        );
-        let mut resp = test::call_service(
-            &srv,
-            TestRequest::with_uri("/")
-                .cookie(cookie.clone())
-                .to_request(),
-        )
-        .await;
-        assert_login_cookie(
-            &mut resp,
-            COOKIE_LOGIN,
-            LoginTimestampCheck::NoTimestamp,
-            VisitTimeStampCheck::NewTimestamp,
-        );
-        assert_logged_in(resp, None).await;
-    }
-
-    #[actix_web::test]
-    async fn test_identity_cookie_not_updated_on_login_deadline() {
-        let srv = create_identity_server(|c| c.login_deadline(Duration::days(90))).await;
-        let cookie = login_cookie(COOKIE_LOGIN, Some(SystemTime::now()), None);
-        let mut resp = test::call_service(
-            &srv,
-            TestRequest::with_uri("/")
-                .cookie(cookie.clone())
-                .to_request(),
-        )
-        .await;
-        assert_no_login_cookie(&mut resp);
-        assert_logged_in(resp, Some(COOKIE_LOGIN)).await;
-    }
-
-    #[actix_web::test]
-    async fn test_identity_cookie_updated_on_visit_deadline() {
-        let srv = create_identity_server(|c| {
-            c.visit_deadline(Duration::days(90))
-                .login_deadline(Duration::days(90))
-        })
-        .await;
-        let timestamp = SystemTime::now() - Duration::days(1);
-        let cookie = login_cookie(COOKIE_LOGIN, Some(timestamp), Some(timestamp));
-        let mut resp = test::call_service(
-            &srv,
-            TestRequest::with_uri("/")
-                .cookie(cookie.clone())
-                .to_request(),
-        )
-        .await;
-        assert_login_cookie(
-            &mut resp,
-            COOKIE_LOGIN,
-            LoginTimestampCheck::OldTimestamp(timestamp),
-            VisitTimeStampCheck::NewTimestamp,
-        );
-        assert_logged_in(resp, Some(COOKIE_LOGIN)).await;
-    }
-}
diff --git a/actix-identity/src/identity.rs b/actix-identity/src/identity.rs
index 366e8303d..7a95a8d0b 100644
--- a/actix-identity/src/identity.rs
+++ b/actix-identity/src/identity.rs
@@ -1,89 +1,238 @@
+use actix_session::Session;
 use actix_utils::future::{ready, Ready};
 use actix_web::{
+    cookie::time::OffsetDateTime,
     dev::{Extensions, Payload},
-    Error, FromRequest, HttpMessage as _, HttpRequest,
+    http::StatusCode,
+    Error, FromRequest, HttpMessage, HttpRequest, HttpResponse,
 };
+use anyhow::{anyhow, Context};
 
-pub(crate) struct IdentityItem {
-    pub(crate) id: Option<String>,
-    pub(crate) changed: bool,
-}
+use crate::config::LogoutBehaviour;
 
-/// The extractor type to obtain your identity from a request.
+/// A verified user identity. It can be used as a request extractor.
 ///
+/// The lifecycle of a user identity is tied to the lifecycle of the underlying session. If the
+/// session is destroyed (e.g. the session expired), the user identity will be forgotten, de-facto
+/// forcing a user log out.
+///
+/// # Examples
 /// ```
-/// use actix_web::*;
+/// use actix_web::{
+///     get, post, Responder, HttpRequest, HttpMessage, HttpResponse
+/// };
 /// use actix_identity::Identity;
 ///
 /// #[get("/")]
-/// async fn index(id: Identity) -> impl Responder {
-///     // access request identity
-///     if let Some(id) = id.identity() {
-///         format!("Welcome! {}", id)
+/// async fn index(user: Option<Identity>) -> impl Responder {
+///     if let Some(user) = user {
+///         format!("Welcome! {}", user.id().unwrap())
 ///     } else {
 ///         "Welcome Anonymous!".to_owned()
 ///     }
 /// }
 ///
 /// #[post("/login")]
-/// async fn login(id: Identity) -> impl Responder {
-///     // remember identity
-///     id.remember("User1".to_owned());
-///
+/// async fn login(request: HttpRequest) -> impl Responder {
+///     Identity::login(&request.extensions(), "User1".into());
 ///     HttpResponse::Ok()
 /// }
 ///
 /// #[post("/logout")]
-/// async fn logout(id: Identity) -> impl Responder {
-///     // remove identity
-///     id.forget();
-///
+/// async fn logout(user: Identity) -> impl Responder {
+///     user.logout();
 ///     HttpResponse::Ok()
 /// }
 /// ```
-#[derive(Clone)]
-pub struct Identity(HttpRequest);
-
-impl Identity {
-    /// Return the claimed identity of the user associated request or `None` if no identity can be
-    /// found associated with the request.
-    pub fn identity(&self) -> Option<String> {
-        Identity::get_identity(&self.0.extensions())
-    }
-
-    /// Remember identity.
-    pub fn remember(&self, identity: String) {
-        if let Some(id) = self.0.extensions_mut().get_mut::<IdentityItem>() {
-            id.id = Some(identity);
-            id.changed = true;
-        }
-    }
-
-    /// This method is used to 'forget' the current identity on subsequent requests.
-    pub fn forget(&self) {
-        if let Some(id) = self.0.extensions_mut().get_mut::<IdentityItem>() {
-            id.id = None;
-            id.changed = true;
-        }
-    }
-
-    pub(crate) fn get_identity(extensions: &Extensions) -> Option<String> {
-        let id = extensions.get::<IdentityItem>()?;
-        id.id.clone()
-    }
-}
-
-/// Extractor implementation for Identity type.
 ///
+/// # Extractor Behaviour
+/// What happens if you try to extract an `Identity` out of a request that does not have a valid
+/// identity attached? The API will return a `401 UNAUTHORIZED` to the caller.
+///
+/// If you want to customise this behaviour, consider extracting `Option<Identity>` or
+/// `Result<Identity, actix_web::Error>` instead of a bare `Identity`: you will then be fully in
+/// control of the error path.
+///
+/// ## Examples
 /// ```
-/// # use actix_web::*;
+/// use actix_web::{http::header::LOCATION, get, HttpResponse, Responder};
 /// use actix_identity::Identity;
 ///
 /// #[get("/")]
-/// async fn index(id: Identity) -> impl Responder {
-///     // access request identity
-///     if let Some(id) = id.identity() {
-///         format!("Welcome! {}", id)
+/// async fn index(user: Option<Identity>) -> impl Responder {
+///     if let Some(user) = user {
+///         HttpResponse::Ok().finish()
+///     } else {
+///         // Redirect to login page if unauthenticated
+///         HttpResponse::TemporaryRedirect()
+///             .insert_header((LOCATION, "/login"))
+///             .finish()
+///     }
+/// }
+/// ```
+pub struct Identity(IdentityInner);
+
+#[derive(Clone)]
+pub(crate) struct IdentityInner {
+    pub(crate) session: Session,
+    pub(crate) logout_behaviour: LogoutBehaviour,
+    pub(crate) is_login_deadline_enabled: bool,
+    pub(crate) is_visit_deadline_enabled: bool,
+}
+
+impl IdentityInner {
+    fn extract(ext: &Extensions) -> Self {
+        ext.get::<Self>()
+            .expect(
+                "No `IdentityInner` instance was found in the extensions attached to the \
+                incoming request. This usually means that `IdentityMiddleware` has not been \
+                registered as an application middleware via `App::wrap`. `Identity` cannot be used \
+                unless the identity machine is properly mounted: register `IdentityMiddleware` as \
+                a middleware for your application to fix this panic. If the problem persists, \
+                please file an issue on GitHub.",
+            )
+            .to_owned()
+    }
+
+    /// Retrieve the user id attached to the current session.
+    fn get_identity(&self) -> Result<String, anyhow::Error> {
+        self.session
+            .get::<String>(ID_KEY)
+            .context("Failed to deserialize the user identifier attached to the current session")?
+            .ok_or_else(|| {
+                anyhow!("There is no identity information attached to the current session")
+            })
+    }
+}
+
+pub(crate) const ID_KEY: &str = "actix_identity.user_id";
+pub(crate) const LAST_VISIT_UNIX_TIMESTAMP_KEY: &str = "actix_identity.last_visited_at";
+pub(crate) const LOGIN_UNIX_TIMESTAMP_KEY: &str = "actix_identity.logged_in_at";
+
+impl Identity {
+    /// Return the user id associated to the current session.
+    ///
+    /// # Examples
+    /// ```
+    /// use actix_web::{get, Responder};
+    /// use actix_identity::Identity;
+    ///
+    /// #[get("/")]
+    /// async fn index(user: Option<Identity>) -> impl Responder {
+    ///     if let Some(user) = user {
+    ///         format!("Welcome! {}", user.id().unwrap())
+    ///     } else {
+    ///         "Welcome Anonymous!".to_owned()
+    ///     }
+    /// }
+    /// ```
+    pub fn id(&self) -> Result<String, anyhow::Error> {
+        self.0.session.get(ID_KEY)?.ok_or_else(|| {
+            anyhow!("Bug: the identity information attached to the current session has disappeared")
+        })
+    }
+
+    /// Attach a valid user identity to the current session.
+    ///
+    /// This method should be called after you have successfully authenticated the user. After
+    /// `login` has been called, the user will be able to access all routes that require a valid
+    /// [`Identity`].
+    ///
+    /// # Examples
+    /// ```
+    /// use actix_web::{post, Responder, HttpRequest, HttpMessage, HttpResponse};
+    /// use actix_identity::Identity;
+    ///
+    /// #[post("/login")]
+    /// async fn login(request: HttpRequest) -> impl Responder {
+    ///     Identity::login(&request.extensions(), "User1".into());
+    ///     HttpResponse::Ok()
+    /// }
+    /// ```
+    pub fn login(ext: &Extensions, id: String) -> Result<Self, anyhow::Error> {
+        let inner = IdentityInner::extract(ext);
+        inner.session.insert(ID_KEY, id)?;
+        inner.session.insert(
+            LOGIN_UNIX_TIMESTAMP_KEY,
+            OffsetDateTime::now_utc().unix_timestamp(),
+        )?;
+        inner.session.renew();
+        Ok(Self(inner))
+    }
+
+    /// Remove the user identity from the current session.
+    ///
+    /// After `logout` has been called, the user will no longer be able to access routes that
+    /// require a valid [`Identity`].
+    ///
+    /// The behaviour on logout is determined by [`IdentityMiddlewareBuilder::logout_behaviour`].
+    ///
+    /// # Examples
+    /// ```
+    /// use actix_web::{post, Responder, HttpResponse};
+    /// use actix_identity::Identity;
+    ///
+    /// #[post("/logout")]
+    /// async fn logout(user: Identity) -> impl Responder {
+    ///     user.logout();
+    ///     HttpResponse::Ok()
+    /// }
+    /// ```
+    ///
+    /// [`IdentityMiddlewareBuilder::logout_behaviour`]: crate::config::IdentityMiddlewareBuilder::logout_behaviour
+    pub fn logout(self) {
+        match self.0.logout_behaviour {
+            LogoutBehaviour::PurgeSession => {
+                self.0.session.purge();
+            }
+            LogoutBehaviour::DeleteIdentityKeys => {
+                self.0.session.remove(ID_KEY);
+                if self.0.is_login_deadline_enabled {
+                    self.0.session.remove(LOGIN_UNIX_TIMESTAMP_KEY);
+                }
+                if self.0.is_visit_deadline_enabled {
+                    self.0.session.remove(LAST_VISIT_UNIX_TIMESTAMP_KEY);
+                }
+            }
+        }
+    }
+
+    pub(crate) fn extract(ext: &Extensions) -> Result<Self, anyhow::Error> {
+        let inner = IdentityInner::extract(ext);
+        inner.get_identity()?;
+        Ok(Self(inner))
+    }
+
+    pub(crate) fn logged_at(&self) -> Result<Option<OffsetDateTime>, anyhow::Error> {
+        self.0
+            .session
+            .get(LOGIN_UNIX_TIMESTAMP_KEY)?
+            .map(OffsetDateTime::from_unix_timestamp)
+            .transpose()
+            .map_err(anyhow::Error::from)
+    }
+
+    pub(crate) fn last_visited_at(&self) -> Result<Option<OffsetDateTime>, anyhow::Error> {
+        self.0
+            .session
+            .get(LAST_VISIT_UNIX_TIMESTAMP_KEY)?
+            .map(OffsetDateTime::from_unix_timestamp)
+            .transpose()
+            .map_err(anyhow::Error::from)
+    }
+}
+
+/// Extractor implementation for [`Identity`].
+///
+/// # Examples
+/// ```
+/// use actix_web::{get, Responder};
+/// use actix_identity::Identity;
+///
+/// #[get("/")]
+/// async fn index(user: Option<Identity>) -> impl Responder {
+///     if let Some(user) = user {
+///         format!("Welcome! {}", user.id().unwrap())
 ///     } else {
 ///         "Welcome Anonymous!".to_owned()
 ///     }
@@ -91,10 +240,17 @@ impl Identity {
 /// ```
 impl FromRequest for Identity {
     type Error = Error;
-    type Future = Ready<Result<Identity, Error>>;
+    type Future = Ready<Result<Self, Self::Error>>;
 
     #[inline]
     fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
-        ready(Ok(Identity(req.clone())))
+        ready(Identity::extract(&req.extensions()).map_err(|err| {
+            let res = actix_web::error::InternalError::from_response(
+                err,
+                HttpResponse::new(StatusCode::UNAUTHORIZED),
+            );
+
+            actix_web::Error::from(res)
+        }))
     }
 }
diff --git a/actix-identity/src/identity_ext.rs b/actix-identity/src/identity_ext.rs
new file mode 100644
index 000000000..431539a86
--- /dev/null
+++ b/actix-identity/src/identity_ext.rs
@@ -0,0 +1,27 @@
+use actix_web::{dev::ServiceRequest, guard::GuardContext, HttpMessage, HttpRequest};
+
+use crate::Identity;
+
+/// Helper trait to retrieve an [`Identity`] instance from various `actix-web`'s types.
+pub trait IdentityExt {
+    /// Retrieve the identity attached to the current session, if available.
+    fn get_identity(&self) -> Result<Identity, anyhow::Error>;
+}
+
+impl IdentityExt for HttpRequest {
+    fn get_identity(&self) -> Result<Identity, anyhow::Error> {
+        Identity::extract(&self.extensions())
+    }
+}
+
+impl IdentityExt for ServiceRequest {
+    fn get_identity(&self) -> Result<Identity, anyhow::Error> {
+        Identity::extract(&self.extensions())
+    }
+}
+
+impl<'a> IdentityExt for GuardContext<'a> {
+    fn get_identity(&self) -> Result<Identity, anyhow::Error> {
+        Identity::extract(&self.req_data())
+    }
+}
diff --git a/actix-identity/src/lib.rs b/actix-identity/src/lib.rs
index 2e51fd365..5c172a64e 100644
--- a/actix-identity/src/lib.rs
+++ b/actix-identity/src/lib.rs
@@ -1,163 +1,99 @@
-//! Opinionated request identity service for Actix Web apps.
+//! Identity management for Actix Web.
 //!
-//! [`IdentityService`] middleware can be used with different policies types to store
-//! identity information.
+//! `actix-identity` can be used to track identity of a user across multiple requests. It is built
+//! on top of HTTP sessions, via [`actix-session`](https://docs.rs/actix-session).
 //!
-//! A cookie based policy is provided. [`CookieIdentityPolicy`] uses cookies as identity storage.
+//! # Getting started
+//! To start using identity management in your Actix Web application you must register
+//! [`IdentityMiddleware`] and `SessionMiddleware` as middleware on your `App`:
 //!
-//! To access current request identity, use the [`Identity`] extractor.
+//! ```no_run
+//! # use actix_web::web;
+//! use actix_web::{cookie::Key, App, HttpServer, HttpResponse};
+//! use actix_identity::IdentityMiddleware;
+//! use actix_session::{storage::RedisSessionStore, SessionMiddleware};
 //!
+//! #[actix_web::main]
+//! async fn main() {
+//!     let secret_key = Key::generate();
+//!     let redis_store = RedisSessionStore::new("redis://127.0.0.1:6379")
+//!         .await
+//!         .unwrap();
+//!
+//!     HttpServer::new(move || {
+//!         App::new()
+//!             // Install the identity framework first.
+//!             .wrap(IdentityMiddleware::default())
+//!             // The identity system is built on top of sessions. You must install the session
+//!             // middleware to leverage `actix-identity`. The session middleware must be mounted
+//!             // AFTER the identity middleware: `actix-web` invokes middleware in the OPPOSITE
+//!             // order of registration when it receives an incoming request.
+//!             .wrap(SessionMiddleware::new(
+//!                  redis_store.clone(),
+//!                  secret_key.clone()
+//!             ))
+//!             // Your request handlers [...]
+//!             # .default_service(web::to(|| HttpResponse::Ok()))
+//!     })
+//! # ;
+//! }
 //! ```
-//! use actix_web::*;
-//! use actix_identity::{Identity, CookieIdentityPolicy, IdentityService};
+//!
+//! User identities can be created, accessed and destroyed using the [`Identity`] extractor in your
+//! request handlers:
+//!
+//! ```no_run
+//! use actix_web::{get, post, HttpResponse, Responder, HttpRequest, HttpMessage};
+//! use actix_identity::Identity;
+//! use actix_session::storage::RedisSessionStore;
 //!
 //! #[get("/")]
-//! async fn index(id: Identity) -> String {
-//!     // access request identity
-//!     if let Some(id) = id.identity() {
-//!         format!("Welcome! {}", id)
+//! async fn index(user: Option<Identity>) -> impl Responder {
+//!     if let Some(user) = user {
+//!         format!("Welcome! {}", user.id().unwrap())
 //!     } else {
 //!         "Welcome Anonymous!".to_owned()
 //!     }
 //! }
 //!
 //! #[post("/login")]
-//! async fn login(id: Identity) -> HttpResponse {
-//!     // remember identity
-//!     id.remember("User1".to_owned());
-//!     HttpResponse::Ok().finish()
+//! async fn login(request: HttpRequest) -> impl Responder {
+//!     // Some kind of authentication should happen here
+//!     // e.g. password-based, biometric, etc.
+//!     // [...]
+//!
+//!     // attach a verified user identity to the active session
+//!     Identity::login(&request.extensions(), "User1".into()).unwrap();
+//!
+//!     HttpResponse::Ok()
 //! }
 //!
 //! #[post("/logout")]
-//! async fn logout(id: Identity) -> HttpResponse {
-//!     // remove identity
-//!     id.forget();
-//!     HttpResponse::Ok().finish()
+//! async fn logout(user: Identity) -> impl Responder {
+//!     user.logout();
+//!     HttpResponse::Ok()
 //! }
-//!
-//! HttpServer::new(move || {
-//!     // create cookie identity backend (inside closure, since policy is not Clone)
-//!     let policy = CookieIdentityPolicy::new(&[0; 32])
-//!         .name("auth-cookie")
-//!         .secure(false);
-//!
-//!     App::new()
-//!         // wrap policy into middleware identity middleware
-//!         .wrap(IdentityService::new(policy))
-//!         .service(services![index, login, logout])
-//! })
-//! # ;
 //! ```
+//!
+//! # Advanced configuration
+//! By default, `actix-identity` does not automatically log out users. You can change this behaviour
+//! by customising the configuration for [`IdentityMiddleware`] via [`IdentityMiddleware::builder`].
+//!
+//! In particular, you can automatically log out users who:
+//! - have been inactive for a while (see [`IdentityMiddlewareBuilder::visit_deadline`];
+//! - logged in too long ago (see [`IdentityMiddlewareBuilder::login_deadline`]).
+//!
+//! [`IdentityMiddlewareBuilder::visit_deadline`]: config::IdentityMiddlewareBuilder::visit_deadline
+//! [`IdentityMiddlewareBuilder::login_deadline`]: config::IdentityMiddlewareBuilder::login_deadline
 
-#![deny(rust_2018_idioms, nonstandard_style)]
+#![deny(rust_2018_idioms, nonstandard_style, missing_docs)]
 #![warn(future_incompatible)]
 
-use std::future::Future;
-
-use actix_web::{
-    dev::{ServiceRequest, ServiceResponse},
-    Error, HttpMessage, Result,
-};
-
-mod cookie;
+pub mod config;
 mod identity;
+mod identity_ext;
 mod middleware;
 
-pub use self::cookie::CookieIdentityPolicy;
 pub use self::identity::Identity;
-pub use self::middleware::IdentityService;
-
-/// Identity policy.
-pub trait IdentityPolicy: Sized + 'static {
-    /// The return type of the middleware
-    type Future: Future<Output = Result<Option<String>, Error>>;
-
-    /// The return type of the middleware
-    type ResponseFuture: Future<Output = Result<(), Error>>;
-
-    /// Parse the session from request and load data from a service identity.
-    #[allow(clippy::wrong_self_convention)]
-    fn from_request(&self, req: &mut ServiceRequest) -> Self::Future;
-
-    /// Write changes to response
-    fn to_response<B>(
-        &self,
-        identity: Option<String>,
-        changed: bool,
-        response: &mut ServiceResponse<B>,
-    ) -> Self::ResponseFuture;
-}
-
-/// Helper trait that allows to get Identity.
-///
-/// It could be used in middleware but identity policy must be set before any other middleware that
-/// needs identity. RequestIdentity is implemented both for `ServiceRequest` and `HttpRequest`.
-pub trait RequestIdentity {
-    fn get_identity(&self) -> Option<String>;
-}
-
-impl<T> RequestIdentity for T
-where
-    T: HttpMessage,
-{
-    fn get_identity(&self) -> Option<String> {
-        Identity::get_identity(&self.extensions())
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use std::time::SystemTime;
-
-    use actix_web::{
-        body::{BoxBody, EitherBody},
-        dev::ServiceResponse,
-        test, web, App, Error,
-    };
-
-    use super::*;
-
-    pub(crate) const COOKIE_KEY_MASTER: [u8; 32] = [0; 32];
-    pub(crate) const COOKIE_NAME: &str = "actix_auth";
-    pub(crate) const COOKIE_LOGIN: &str = "test";
-
-    #[allow(clippy::enum_variant_names)]
-    pub(crate) enum LoginTimestampCheck {
-        NoTimestamp,
-        NewTimestamp,
-        OldTimestamp(SystemTime),
-    }
-
-    #[allow(clippy::enum_variant_names)]
-    pub(crate) enum VisitTimeStampCheck {
-        NoTimestamp,
-        NewTimestamp,
-    }
-
-    pub(crate) async fn create_identity_server<
-        F: Fn(CookieIdentityPolicy) -> CookieIdentityPolicy + Sync + Send + Clone + 'static,
-    >(
-        f: F,
-    ) -> impl actix_service::Service<
-        actix_http::Request,
-        Response = ServiceResponse<EitherBody<BoxBody>>,
-        Error = Error,
-    > {
-        test::init_service(
-            App::new()
-                .wrap(IdentityService::new(f(CookieIdentityPolicy::new(
-                    &COOKIE_KEY_MASTER,
-                )
-                .secure(false)
-                .name(COOKIE_NAME))))
-                .service(web::resource("/").to(|id: Identity| async move {
-                    let identity = id.identity();
-                    if identity.is_none() {
-                        id.remember(COOKIE_LOGIN.to_string())
-                    }
-                    web::Json(identity)
-                })),
-        )
-        .await
-    }
-}
+pub use self::identity_ext::IdentityExt;
+pub use self::middleware::IdentityMiddleware;
diff --git a/actix-identity/src/middleware.rs b/actix-identity/src/middleware.rs
index 9e5deb2b2..f65f48faa 100644
--- a/actix-identity/src/middleware.rs
+++ b/actix-identity/src/middleware.rs
@@ -1,171 +1,251 @@
 use std::rc::Rc;
 
+use actix_session::SessionExt;
 use actix_utils::future::{ready, Ready};
 use actix_web::{
-    body::{EitherBody, MessageBody},
+    body::MessageBody,
+    cookie::time::format_description::well_known::Rfc3339,
     dev::{Service, ServiceRequest, ServiceResponse, Transform},
-    Error, HttpMessage, Result,
+    Error, HttpMessage as _, Result,
 };
-use futures_util::future::{FutureExt as _, LocalBoxFuture};
+use futures_core::future::LocalBoxFuture;
+use time::OffsetDateTime;
 
-use crate::{identity::IdentityItem, IdentityPolicy};
+use crate::{
+    config::{Configuration, IdentityMiddlewareBuilder},
+    identity::IdentityInner,
+    Identity,
+};
 
-/// Request identity middleware
+/// Identity management middleware.
 ///
+/// ```no_run
+/// use actix_web::{cookie::Key, App, HttpServer};
+/// use actix_session::storage::RedisSessionStore;
+/// use actix_identity::{Identity, IdentityMiddleware};
+/// use actix_session::{Session, SessionMiddleware};
+///
+/// #[actix_web::main]
+/// async fn main() {
+///     let secret_key = Key::generate();
+///     let redis_store = RedisSessionStore::new("redis://127.0.0.1:6379").await.unwrap();
+///
+///     HttpServer::new(move || {
+///        App::new()
+///            // Install the identity framework first.
+///            .wrap(IdentityMiddleware::default())
+///            // The identity system is built on top of sessions.
+///            // You must install the session middleware to leverage `actix-identity`.
+///            .wrap(SessionMiddleware::new(redis_store.clone(), secret_key.clone()))
+///     })
+/// # ;
+/// }
 /// ```
-/// use actix_web::App;
-/// use actix_identity::{CookieIdentityPolicy, IdentityService};
-///
-/// // create cookie identity backend
-/// let policy = CookieIdentityPolicy::new(&[0; 32])
-///            .name("auth-cookie")
-///            .secure(false);
-///
-/// let app = App::new()
-///     // wrap policy into identity middleware
-///     .wrap(IdentityService::new(policy));
-/// ```
-pub struct IdentityService<T> {
-    backend: Rc<T>,
+#[derive(Default, Clone)]
+pub struct IdentityMiddleware {
+    configuration: Rc<Configuration>,
 }
 
-impl<T> IdentityService<T> {
-    /// Create new identity service with specified backend.
-    pub fn new(backend: T) -> Self {
-        IdentityService {
-            backend: Rc::new(backend),
+impl IdentityMiddleware {
+    pub(crate) fn new(configuration: Configuration) -> Self {
+        Self {
+            configuration: Rc::new(configuration),
         }
     }
+
+    /// A fluent API to configure [`IdentityMiddleware`].
+    pub fn builder() -> IdentityMiddlewareBuilder {
+        IdentityMiddlewareBuilder::new()
+    }
 }
 
-impl<S, T, B> Transform<S, ServiceRequest> for IdentityService<T>
+impl<S, B> Transform<S, ServiceRequest> for IdentityMiddleware
 where
     S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
     S::Future: 'static,
-    T: IdentityPolicy,
     B: MessageBody + 'static,
 {
-    type Response = ServiceResponse<EitherBody<B>>;
+    type Response = ServiceResponse<B>;
     type Error = Error;
+    type Transform = InnerIdentityMiddleware<S>;
     type InitError = ();
-    type Transform = IdentityServiceMiddleware<S, T>;
     type Future = Ready<Result<Self::Transform, Self::InitError>>;
 
     fn new_transform(&self, service: S) -> Self::Future {
-        ready(Ok(IdentityServiceMiddleware {
-            backend: self.backend.clone(),
+        ready(Ok(InnerIdentityMiddleware {
             service: Rc::new(service),
+            configuration: Rc::clone(&self.configuration),
         }))
     }
 }
 
-pub struct IdentityServiceMiddleware<S, T> {
-    pub(crate) service: Rc<S>,
-    pub(crate) backend: Rc<T>,
+#[doc(hidden)]
+pub struct InnerIdentityMiddleware<S> {
+    service: Rc<S>,
+    configuration: Rc<Configuration>,
 }
 
-impl<S, T> Clone for IdentityServiceMiddleware<S, T> {
+impl<S> Clone for InnerIdentityMiddleware<S> {
     fn clone(&self) -> Self {
         Self {
-            backend: Rc::clone(&self.backend),
             service: Rc::clone(&self.service),
+            configuration: Rc::clone(&self.configuration),
         }
     }
 }
 
-impl<S, T, B> Service<ServiceRequest> for IdentityServiceMiddleware<S, T>
+impl<S, B> Service<ServiceRequest> for InnerIdentityMiddleware<S>
 where
     S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
     S::Future: 'static,
-    T: IdentityPolicy,
     B: MessageBody + 'static,
 {
-    type Response = ServiceResponse<EitherBody<B>>;
+    type Response = ServiceResponse<B>;
     type Error = Error;
     type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
 
     actix_service::forward_ready!(service);
 
-    fn call(&self, mut req: ServiceRequest) -> Self::Future {
+    fn call(&self, req: ServiceRequest) -> Self::Future {
         let srv = Rc::clone(&self.service);
-        let backend = Rc::clone(&self.backend);
-        let fut = self.backend.from_request(&mut req);
-
-        async move {
-            match fut.await {
-                Ok(id) => {
-                    req.extensions_mut()
-                        .insert(IdentityItem { id, changed: false });
-
-                    let mut res = srv.call(req).await?;
-                    let id = res.request().extensions_mut().remove::<IdentityItem>();
-
-                    if let Some(id) = id {
-                        match backend.to_response(id.id, id.changed, &mut res).await {
-                            Ok(_) => Ok(res.map_into_left_body()),
-                            Err(err) => Ok(res.error_response(err).map_into_right_body()),
-                        }
-                    } else {
-                        Ok(res.map_into_left_body())
-                    }
-                }
-                Err(err) => Ok(req.error_response(err).map_into_right_body()),
-            }
-        }
-        .boxed_local()
+        let configuration = Rc::clone(&self.configuration);
+        Box::pin(async move {
+            let identity_inner = IdentityInner {
+                session: req.get_session(),
+                logout_behaviour: configuration.on_logout.clone(),
+                is_login_deadline_enabled: configuration.login_deadline.is_some(),
+                is_visit_deadline_enabled: configuration.visit_deadline.is_some(),
+            };
+            req.extensions_mut().insert(identity_inner);
+            enforce_policies(&req, &configuration);
+            srv.call(req).await
+        })
     }
 }
 
-#[cfg(test)]
-mod tests {
-    use std::{rc::Rc, time::Duration};
+// easier to scan with returns where they are
+// especially if the function body were to evolve in the future
+#[allow(clippy::needless_return)]
+fn enforce_policies(req: &ServiceRequest, configuration: &Configuration) {
+    let must_extract_identity =
+        configuration.login_deadline.is_some() || configuration.visit_deadline.is_some();
 
-    use actix_service::into_service;
-    use actix_web::{dev, error, test, Error, Result};
+    if !must_extract_identity {
+        return;
+    }
 
-    use super::*;
-
-    #[actix_web::test]
-    async fn test_borrowed_mut_error() {
-        use actix_utils::future::{ok, Ready};
-        use futures_util::future::lazy;
-
-        struct Ident;
-        impl IdentityPolicy for Ident {
-            type Future = Ready<Result<Option<String>, Error>>;
-            type ResponseFuture = Ready<Result<(), Error>>;
-
-            fn from_request(&self, _: &mut dev::ServiceRequest) -> Self::Future {
-                ok(Some("test".to_string()))
-            }
-
-            fn to_response<B>(
-                &self,
-                _: Option<String>,
-                _: bool,
-                _: &mut dev::ServiceResponse<B>,
-            ) -> Self::ResponseFuture {
-                ok(())
-            }
+    let identity = match Identity::extract(&req.extensions()) {
+        Ok(identity) => identity,
+        Err(err) => {
+            tracing::debug!(
+                error.display = %err,
+                error.debug = ?err,
+                "Failed to extract an `Identity` from the incoming request."
+            );
+            return;
         }
+    };
 
-        let srv = crate::middleware::IdentityServiceMiddleware {
-            backend: Rc::new(Ident),
-            service: Rc::new(into_service(|_: dev::ServiceRequest| async move {
-                actix_web::rt::time::sleep(Duration::from_secs(100)).await;
-                Err::<dev::ServiceResponse, _>(error::ErrorBadRequest("error"))
-            })),
-        };
+    if let Some(login_deadline) = configuration.login_deadline {
+        if matches!(
+            enforce_login_deadline(&identity, login_deadline),
+            PolicyDecision::LogOut
+        ) {
+            identity.logout();
+            return;
+        }
+    }
 
-        let srv2 = srv.clone();
-        let req = test::TestRequest::default().to_srv_request();
-
-        actix_web::rt::spawn(async move {
-            let _ = srv2.call(req).await;
-        });
-
-        actix_web::rt::time::sleep(Duration::from_millis(50)).await;
-
-        let _ = lazy(|cx| srv.poll_ready(cx)).await;
+    if let Some(visit_deadline) = configuration.visit_deadline {
+        if matches!(
+            enforce_visit_deadline(&identity, visit_deadline),
+            PolicyDecision::LogOut
+        ) {
+            identity.logout();
+            return;
+        }
     }
 }
+
+fn enforce_login_deadline(
+    identity: &Identity,
+    login_deadline: std::time::Duration,
+) -> PolicyDecision {
+    match identity.logged_at() {
+        Ok(None) => {
+            tracing::info!(
+                "Login deadline is enabled, but there is no login timestamp in the session \
+                state attached to the incoming request. Logging the user out."
+            );
+            PolicyDecision::LogOut
+        }
+        Err(err) => {
+            tracing::info!(
+                error.display = %err,
+                error.debug = ?err,
+                "Login deadline is enabled but we failed to extract the login timestamp from the \
+                session state attached to the incoming request. Logging the user out."
+            );
+            PolicyDecision::LogOut
+        }
+        Ok(Some(logged_in_at)) => {
+            let elapsed = OffsetDateTime::now_utc() - logged_in_at;
+            if elapsed > login_deadline {
+                tracing::info!(
+                    user.logged_in_at = %logged_in_at.format(&Rfc3339).unwrap_or_default(),
+                    identity.login_deadline_seconds = login_deadline.as_secs(),
+                    identity.elapsed_since_login_seconds = elapsed.whole_seconds(),
+                    "Login deadline is enabled and too much time has passed since the user logged \
+                    in. Logging the user out."
+                );
+                PolicyDecision::LogOut
+            } else {
+                PolicyDecision::StayLoggedIn
+            }
+        }
+    }
+}
+
+fn enforce_visit_deadline(
+    identity: &Identity,
+    visit_deadline: std::time::Duration,
+) -> PolicyDecision {
+    match identity.last_visited_at() {
+        Ok(None) => {
+            tracing::info!(
+                "Last visit deadline is enabled, but there is no last visit timestamp in the \
+                session state attached to the incoming request. Logging the user out."
+            );
+            PolicyDecision::LogOut
+        }
+        Err(err) => {
+            tracing::info!(
+                error.display = %err,
+                error.debug = ?err,
+                "Last visit deadline is enabled but we failed to extract the last visit timestamp \
+                from the session state attached to the incoming request. Logging the user out."
+            );
+            PolicyDecision::LogOut
+        }
+        Ok(Some(last_visited_at)) => {
+            let elapsed = OffsetDateTime::now_utc() - last_visited_at;
+            if elapsed > visit_deadline {
+                tracing::info!(
+                    user.last_visited_at = %last_visited_at.format(&Rfc3339).unwrap_or_default(),
+                    identity.visit_deadline_seconds = visit_deadline.as_secs(),
+                    identity.elapsed_since_last_visit_seconds = elapsed.whole_seconds(),
+                    "Last visit deadline is enabled and too much time has passed since the last \
+                    time the user visited. Logging the user out."
+                );
+                PolicyDecision::LogOut
+            } else {
+                PolicyDecision::StayLoggedIn
+            }
+        }
+    }
+}
+
+enum PolicyDecision {
+    StayLoggedIn,
+    LogOut,
+}
diff --git a/actix-identity/tests/integration/fixtures.rs b/actix-identity/tests/integration/fixtures.rs
new file mode 100644
index 000000000..f3fce595b
--- /dev/null
+++ b/actix-identity/tests/integration/fixtures.rs
@@ -0,0 +1,17 @@
+use actix_session::{storage::CookieSessionStore, SessionMiddleware};
+use actix_web::cookie::Key;
+use uuid::Uuid;
+
+pub fn store() -> CookieSessionStore {
+    CookieSessionStore::default()
+}
+
+pub fn user_id() -> String {
+    Uuid::new_v4().to_string()
+}
+
+pub fn session_middleware() -> SessionMiddleware<CookieSessionStore> {
+    SessionMiddleware::builder(store(), Key::generate())
+        .cookie_domain(Some("localhost".into()))
+        .build()
+}
diff --git a/actix-identity/tests/integration/integration.rs b/actix-identity/tests/integration/integration.rs
new file mode 100644
index 000000000..71d6dc5ba
--- /dev/null
+++ b/actix-identity/tests/integration/integration.rs
@@ -0,0 +1,168 @@
+use std::time::Duration;
+
+use actix_identity::{config::LogoutBehaviour, IdentityMiddleware};
+use actix_web::http::StatusCode;
+
+use crate::{fixtures::user_id, test_app::TestApp};
+
+#[actix_web::test]
+async fn opaque_401_is_returned_for_unauthenticated_users() {
+    let app = TestApp::spawn();
+
+    let response = app.get_identity_required().await;
+    assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
+    assert!(response.bytes().await.unwrap().is_empty());
+}
+
+#[actix_web::test]
+async fn login_works() {
+    let app = TestApp::spawn();
+    let user_id = user_id();
+
+    // Log-in
+    let body = app.post_login(user_id.clone()).await;
+    assert_eq!(body.user_id, Some(user_id.clone()));
+
+    // Access identity-restricted route successfully
+    let response = app.get_identity_required().await;
+    assert!(response.status().is_success());
+}
+
+#[actix_web::test]
+async fn logging_in_again_replaces_the_current_identity() {
+    let app = TestApp::spawn();
+    let first_user_id = user_id();
+    let second_user_id = user_id();
+
+    // Log-in
+    let body = app.post_login(first_user_id.clone()).await;
+    assert_eq!(body.user_id, Some(first_user_id.clone()));
+
+    // Log-in again
+    let body = app.post_login(second_user_id.clone()).await;
+    assert_eq!(body.user_id, Some(second_user_id.clone()));
+
+    let body = app.get_current().await;
+    assert_eq!(body.user_id, Some(second_user_id.clone()));
+}
+
+#[actix_web::test]
+async fn session_key_is_renewed_on_login() {
+    let app = TestApp::spawn();
+    let user_id = user_id();
+
+    // Create an anonymous session
+    let body = app.post_increment().await;
+    assert_eq!(body.user_id, None);
+    assert_eq!(body.counter, 1);
+    assert_eq!(body.session_status, "changed");
+
+    // Log-in
+    let body = app.post_login(user_id.clone()).await;
+    assert_eq!(body.user_id, Some(user_id.clone()));
+    assert_eq!(body.counter, 1);
+    assert_eq!(body.session_status, "renewed");
+}
+
+#[actix_web::test]
+async fn logout_works() {
+    let app = TestApp::spawn();
+    let user_id = user_id();
+
+    // Log-in
+    let body = app.post_login(user_id.clone()).await;
+    assert_eq!(body.user_id, Some(user_id.clone()));
+
+    // Log-out
+    let response = app.post_logout().await;
+    assert!(response.status().is_success());
+
+    // Try to access identity-restricted route
+    let response = app.get_identity_required().await;
+    assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
+}
+
+#[actix_web::test]
+async fn logout_can_avoid_destroying_the_whole_session() {
+    let app = TestApp::spawn_with_config(
+        IdentityMiddleware::builder().logout_behaviour(LogoutBehaviour::DeleteIdentityKeys),
+    );
+    let user_id = user_id();
+
+    // Log-in
+    let body = app.post_login(user_id.clone()).await;
+    assert_eq!(body.user_id, Some(user_id.clone()));
+    assert_eq!(body.counter, 0);
+
+    // Increment counter
+    let body = app.post_increment().await;
+    assert_eq!(body.user_id, Some(user_id.clone()));
+    assert_eq!(body.counter, 1);
+
+    // Log-out
+    let response = app.post_logout().await;
+    assert!(response.status().is_success());
+
+    // Check the state of the counter attached to the session state
+    let body = app.get_current().await;
+    assert_eq!(body.user_id, None);
+    // It would be 0 if the session state had been entirely lost!
+    assert_eq!(body.counter, 1);
+}
+
+#[actix_web::test]
+async fn user_is_logged_out_when_login_deadline_is_elapsed() {
+    let login_deadline = Duration::from_millis(10);
+    let app = TestApp::spawn_with_config(
+        IdentityMiddleware::builder().login_deadline(Some(login_deadline)),
+    );
+    let user_id = user_id();
+
+    // Log-in
+    let body = app.post_login(user_id.clone()).await;
+    assert_eq!(body.user_id, Some(user_id.clone()));
+
+    // Wait for deadline to pass
+    actix_web::rt::time::sleep(login_deadline * 2).await;
+
+    let body = app.get_current().await;
+    // We have been logged out!
+    assert_eq!(body.user_id, None);
+}
+
+#[actix_web::test]
+async fn login_deadline_does_not_log_users_out_before_their_time() {
+    // 1 hour
+    let login_deadline = Duration::from_secs(60 * 60);
+    let app = TestApp::spawn_with_config(
+        IdentityMiddleware::builder().login_deadline(Some(login_deadline)),
+    );
+    let user_id = user_id();
+
+    // Log-in
+    let body = app.post_login(user_id.clone()).await;
+    assert_eq!(body.user_id, Some(user_id.clone()));
+
+    let body = app.get_current().await;
+    assert_eq!(body.user_id, Some(user_id));
+}
+
+#[actix_web::test]
+async fn user_is_logged_out_when_visit_deadline_is_elapsed() {
+    let visit_deadline = Duration::from_millis(10);
+    let app = TestApp::spawn_with_config(
+        IdentityMiddleware::builder().visit_deadline(Some(visit_deadline)),
+    );
+    let user_id = user_id();
+
+    // Log-in
+    let body = app.post_login(user_id.clone()).await;
+    assert_eq!(body.user_id, Some(user_id.clone()));
+
+    // Wait for deadline to pass
+    actix_web::rt::time::sleep(visit_deadline * 2).await;
+
+    let body = app.get_current().await;
+    // We have been logged out!
+    assert_eq!(body.user_id, None);
+}
diff --git a/actix-identity/tests/integration/main.rs b/actix-identity/tests/integration/main.rs
new file mode 100644
index 000000000..8ebd2e86a
--- /dev/null
+++ b/actix-identity/tests/integration/main.rs
@@ -0,0 +1,3 @@
+pub mod fixtures;
+mod integration;
+pub mod test_app;
diff --git a/actix-identity/tests/integration/test_app.rs b/actix-identity/tests/integration/test_app.rs
new file mode 100644
index 000000000..93f41b85e
--- /dev/null
+++ b/actix-identity/tests/integration/test_app.rs
@@ -0,0 +1,186 @@
+use std::net::TcpListener;
+
+use actix_identity::{config::IdentityMiddlewareBuilder, Identity, IdentityMiddleware};
+use actix_session::{Session, SessionStatus};
+use actix_web::{web, App, HttpMessage, HttpRequest, HttpResponse, HttpServer};
+use serde::{Deserialize, Serialize};
+
+use crate::fixtures::session_middleware;
+
+pub struct TestApp {
+    port: u16,
+    api_client: reqwest::Client,
+}
+
+impl TestApp {
+    /// Spawn a test application using a custom configuration for `IdentityMiddleware`.
+    pub fn spawn_with_config(builder: IdentityMiddlewareBuilder) -> Self {
+        // Random OS port
+        let listener = TcpListener::bind("localhost:0").unwrap();
+        let port = listener.local_addr().unwrap().port();
+        let server = HttpServer::new(move || {
+            App::new()
+                .wrap(builder.clone().build())
+                .wrap(session_middleware())
+                .route("/increment", web::post().to(increment))
+                .route("/current", web::get().to(show))
+                .route("/login", web::post().to(login))
+                .route("/logout", web::post().to(logout))
+                .route("/identity_required", web::get().to(identity_required))
+        })
+        .workers(1)
+        .listen(listener)
+        .unwrap()
+        .run();
+        let _ = actix_web::rt::spawn(server);
+
+        let client = reqwest::Client::builder()
+            .cookie_store(true)
+            .build()
+            .unwrap();
+
+        TestApp {
+            port,
+            api_client: client,
+        }
+    }
+
+    /// Spawn a test application using the default configuration settings for `IdentityMiddleware`.
+    pub fn spawn() -> Self {
+        Self::spawn_with_config(IdentityMiddleware::builder())
+    }
+
+    fn url(&self) -> String {
+        format!("http://localhost:{}", self.port)
+    }
+
+    pub async fn get_identity_required(&self) -> reqwest::Response {
+        self.api_client
+            .get(format!("{}/identity_required", &self.url()))
+            .send()
+            .await
+            .unwrap()
+    }
+
+    pub async fn get_current(&self) -> EndpointResponse {
+        self.api_client
+            .get(format!("{}/current", &self.url()))
+            .send()
+            .await
+            .unwrap()
+            .json()
+            .await
+            .unwrap()
+    }
+
+    pub async fn post_increment(&self) -> EndpointResponse {
+        let response = self
+            .api_client
+            .post(format!("{}/increment", &self.url()))
+            .send()
+            .await
+            .unwrap();
+        response.json().await.unwrap()
+    }
+
+    pub async fn post_login(&self, user_id: String) -> EndpointResponse {
+        let response = self
+            .api_client
+            .post(format!("{}/login", &self.url()))
+            .json(&LoginRequest { user_id })
+            .send()
+            .await
+            .unwrap();
+        response.json().await.unwrap()
+    }
+
+    pub async fn post_logout(&self) -> reqwest::Response {
+        self.api_client
+            .post(format!("{}/logout", &self.url()))
+            .send()
+            .await
+            .unwrap()
+    }
+}
+
+#[derive(Serialize, Deserialize, Debug, PartialEq)]
+pub struct EndpointResponse {
+    pub user_id: Option<String>,
+    pub counter: i32,
+    pub session_status: String,
+}
+
+#[derive(Serialize, Deserialize, Debug, PartialEq)]
+struct LoginRequest {
+    user_id: String,
+}
+
+async fn show(user: Option<Identity>, session: Session) -> HttpResponse {
+    let user_id = user.map(|u| u.id().unwrap());
+    let counter: i32 = session
+        .get::<i32>("counter")
+        .unwrap_or(Some(0))
+        .unwrap_or(0);
+
+    HttpResponse::Ok().json(&EndpointResponse {
+        user_id,
+        counter,
+        session_status: session_status(session),
+    })
+}
+
+async fn increment(session: Session, user: Option<Identity>) -> HttpResponse {
+    let user_id = user.map(|u| u.id().unwrap());
+    let counter: i32 = session
+        .get::<i32>("counter")
+        .unwrap_or(Some(0))
+        .map_or(1, |inner| inner + 1);
+    session.insert("counter", &counter).unwrap();
+
+    HttpResponse::Ok().json(&EndpointResponse {
+        user_id,
+        counter,
+        session_status: session_status(session),
+    })
+}
+
+async fn login(
+    user_id: web::Json<LoginRequest>,
+    request: HttpRequest,
+    session: Session,
+) -> HttpResponse {
+    let id = user_id.into_inner().user_id;
+    let user = Identity::login(&request.extensions(), id).unwrap();
+
+    let counter: i32 = session
+        .get::<i32>("counter")
+        .unwrap_or(Some(0))
+        .unwrap_or(0);
+
+    HttpResponse::Ok().json(&EndpointResponse {
+        user_id: Some(user.id().unwrap()),
+        counter,
+        session_status: session_status(session),
+    })
+}
+
+async fn logout(user: Option<Identity>) -> HttpResponse {
+    if let Some(user) = user {
+        user.logout();
+    }
+    HttpResponse::Ok().finish()
+}
+
+async fn identity_required(_identity: Identity) -> HttpResponse {
+    HttpResponse::Ok().finish()
+}
+
+fn session_status(session: Session) -> String {
+    match session.status() {
+        SessionStatus::Changed => "changed",
+        SessionStatus::Purged => "purged",
+        SessionStatus::Renewed => "renewed",
+        SessionStatus::Unchanged => "unchanged",
+    }
+    .into()
+}
diff --git a/actix-session/CHANGES.md b/actix-session/CHANGES.md
index d76f86a7c..2f2391f01 100644
--- a/actix-session/CHANGES.md
+++ b/actix-session/CHANGES.md
@@ -1,6 +1,6 @@
 # Changes
 
-## Unreleased - 2022-xx-xx
+## Unreleased - 2021-xx-xx
 - Added `TtlExtensionPolicy` enum to support different strategies for extending the TTL attached to the session state. `TtlExtensionPolicy::OnEveryRequest` now allows for long-lived sessions that do not expire if the user remains active. [#233]
 - `SessionLength` is now called `SessionLifecycle`. [#233]
 - `SessionLength::Predetermined` is now called `SessionLifecycle::PersistentSession`. [#233]
diff --git a/actix-session/src/lib.rs b/actix-session/src/lib.rs
index fcc51744b..917121429 100644
--- a/actix-session/src/lib.rs
+++ b/actix-session/src/lib.rs
@@ -146,7 +146,7 @@ mod session_ext;
 pub mod storage;
 
 pub use self::middleware::SessionMiddleware;
-pub use self::session::{Session, SessionStatus};
+pub use self::session::{Session, SessionGetError, SessionInsertError, SessionStatus};
 pub use self::session_ext::SessionExt;
 
 #[cfg(test)]
diff --git a/actix-session/src/session.rs b/actix-session/src/session.rs
index 2aa3bae68..219cdde95 100644
--- a/actix-session/src/session.rs
+++ b/actix-session/src/session.rs
@@ -1,16 +1,20 @@
 use std::{
     cell::{Ref, RefCell},
     collections::HashMap,
+    error::Error as StdError,
     mem,
     rc::Rc,
 };
 
 use actix_utils::future::{ready, Ready};
 use actix_web::{
+    body::BoxBody,
     dev::{Extensions, Payload, ServiceRequest, ServiceResponse},
     error::Error,
-    FromRequest, HttpMessage, HttpRequest,
+    FromRequest, HttpMessage, HttpRequest, HttpResponse, ResponseError,
 };
+use anyhow::Context;
+use derive_more::{Display, From};
 use serde::{de::DeserializeOwned, Serialize};
 
 /// The primary interface to access and modify session state.
@@ -38,6 +42,7 @@ use serde::{de::DeserializeOwned, Serialize};
 /// [`SessionExt`].
 ///
 /// [`SessionExt`]: crate::SessionExt
+#[derive(Clone)]
 pub struct Session(Rc<RefCell<SessionInner>>);
 
 /// Status of a [`Session`].
@@ -78,9 +83,20 @@ impl Session {
     /// Get a `value` from the session.
     ///
     /// It returns an error if it fails to deserialize as `T` the JSON value associated with `key`.
-    pub fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>, serde_json::Error> {
+    pub fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>, SessionGetError> {
         if let Some(val_str) = self.0.borrow().state.get(key) {
-            Ok(Some(serde_json::from_str(val_str)?))
+            Ok(Some(
+                serde_json::from_str(val_str)
+                    .with_context(|| {
+                        format!(
+                            "Failed to deserialize the JSON-encoded session data attached to key \
+                            `{}` as a `{}` type",
+                            key,
+                            std::any::type_name::<T>()
+                        )
+                    })
+                    .map_err(SessionGetError)?,
+            ))
         } else {
             Ok(None)
         }
@@ -104,17 +120,29 @@ impl Session {
     /// only a reference to the value is taken.
     ///
     /// It returns an error if it fails to serialize `value` to JSON.
-    pub fn insert(
+    pub fn insert<T: Serialize>(
         &self,
         key: impl Into<String>,
-        value: impl Serialize,
-    ) -> Result<(), serde_json::Error> {
+        value: T,
+    ) -> Result<(), SessionInsertError> {
         let mut inner = self.0.borrow_mut();
 
         if inner.status != SessionStatus::Purged {
             inner.status = SessionStatus::Changed;
-            let val = serde_json::to_string(&value)?;
-            inner.state.insert(key.into(), val);
+
+            let key = key.into();
+            let val = serde_json::to_string(&value)
+                .with_context(|| {
+                    format!(
+                        "Failed to serialize the provided `{}` type instance as JSON in order to \
+                        attach as session data to the `{}` key",
+                        std::any::type_name::<T>(),
+                        &key
+                    )
+                })
+                .map_err(SessionInsertError)?;
+
+            inner.state.insert(key, val);
         }
 
         Ok(())
@@ -136,7 +164,7 @@ impl Session {
 
     /// Remove value from the session and deserialize.
     ///
-    /// Returns None if key was not present in session. Returns `T` if deserialization succeeds,
+    /// Returns `None` if key was not present in session. Returns `T` if deserialization succeeds,
     /// otherwise returns un-deserialized JSON string.
     pub fn remove_as<T: DeserializeOwned>(&self, key: &str) -> Option<Result<T, String>> {
         self.remove(key)
@@ -144,7 +172,7 @@ impl Session {
                 Ok(val) => Ok(val),
                 Err(_err) => {
                     tracing::debug!(
-                        "removed value (key: {}) could not be deserialized as {}",
+                        "Removed value (key: {}) could not be deserialized as {}",
                         key,
                         std::any::type_name::<T>()
                     );
@@ -195,9 +223,9 @@ impl Session {
 
     /// Returns session status and iterator of key-value pairs of changes.
     ///
-    /// This is a destructive operation - the session state is removed from the request extensions typemap,
-    /// leaving behind a new empty map. It should only be used when the session is being finalised (i.e.
-    /// in `SessionMiddleware`).
+    /// This is a destructive operation - the session state is removed from the request extensions
+    /// typemap, leaving behind a new empty map. It should only be used when the session is being
+    /// finalised (i.e. in `SessionMiddleware`).
     pub(crate) fn get_changes<B>(
         res: &mut ServiceResponse<B>,
     ) -> (SessionStatus, HashMap<String, String>) {
@@ -254,3 +282,37 @@ impl FromRequest for Session {
         ready(Ok(Session::get_session(&mut *req.extensions_mut())))
     }
 }
+
+/// Error returned by [`Session::get`].
+#[derive(Debug, Display, From)]
+#[display(fmt = "{}", _0)]
+pub struct SessionGetError(anyhow::Error);
+
+impl StdError for SessionGetError {
+    fn source(&self) -> Option<&(dyn StdError + 'static)> {
+        Some(self.0.as_ref())
+    }
+}
+
+impl ResponseError for SessionGetError {
+    fn error_response(&self) -> HttpResponse<BoxBody> {
+        HttpResponse::new(self.status_code())
+    }
+}
+
+/// Error returned by [`Session::insert`].
+#[derive(Debug, Display, From)]
+#[display(fmt = "{}", _0)]
+pub struct SessionInsertError(anyhow::Error);
+
+impl StdError for SessionInsertError {
+    fn source(&self) -> Option<&(dyn StdError + 'static)> {
+        Some(self.0.as_ref())
+    }
+}
+
+impl ResponseError for SessionInsertError {
+    fn error_response(&self) -> HttpResponse<BoxBody> {
+        HttpResponse::new(self.status_code())
+    }
+}

From c52ea7a5d2901bc0f154dafa2df3a6ca4135089c Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Sat, 9 Jul 2022 19:03:28 +0100
Subject: [PATCH 08/31] prepare actix-session release 0.7.0

---
 actix-identity/Cargo.toml   | 4 ++--
 actix-limitation/Cargo.toml | 2 +-
 actix-session/CHANGES.md    | 3 +++
 actix-session/Cargo.toml    | 2 +-
 actix-session/README.md     | 5 ++---
 5 files changed, 9 insertions(+), 7 deletions(-)

diff --git a/actix-identity/Cargo.toml b/actix-identity/Cargo.toml
index a6ca7690e..b0081715e 100644
--- a/actix-identity/Cargo.toml
+++ b/actix-identity/Cargo.toml
@@ -18,7 +18,7 @@ path = "src/lib.rs"
 
 [dependencies]
 actix-service = "2"
-actix-session = "0.6.2" # update to 0.7 with release of -session
+actix-session = "0.7"
 actix-utils = "3"
 actix-web = { version = "4", default-features = false, features = ["cookies", "secure-cookies"] }
 
@@ -33,6 +33,6 @@ tracing = { version = "0.1.30", default-features = false, features = ["log"] }
 [dev-dependencies]
 actix-http = "3"
 actix-web = { version = "4", default_features = false, features = ["macros", "cookies", "secure-cookies"] }
-actix-session = { version = "0.6.2", features = ["redis-rs-session", "cookie-session"] } # update to 0.7 with release of -session
+actix-session = { version = "0.7", features = ["redis-rs-session", "cookie-session"] }
 uuid = { version = "1", features = ["v4"] }
 reqwest = { version = "0.11", default_features = false, features = ["cookies", "json"] }
diff --git a/actix-limitation/Cargo.toml b/actix-limitation/Cargo.toml
index 912b7c161..64596fffc 100644
--- a/actix-limitation/Cargo.toml
+++ b/actix-limitation/Cargo.toml
@@ -13,7 +13,7 @@ license = "MIT OR Apache-2.0"
 edition = "2018"
 
 [dependencies]
-actix-session = "0.5"
+actix-session = "0.7"
 actix-utils = "3"
 actix-web = { version = "4", default-features = false }
 
diff --git a/actix-session/CHANGES.md b/actix-session/CHANGES.md
index 2f2391f01..3ebd19cbd 100644
--- a/actix-session/CHANGES.md
+++ b/actix-session/CHANGES.md
@@ -1,6 +1,9 @@
 # Changes
 
 ## Unreleased - 2021-xx-xx
+
+
+## 0.7.0 - 2022-07-09
 - Added `TtlExtensionPolicy` enum to support different strategies for extending the TTL attached to the session state. `TtlExtensionPolicy::OnEveryRequest` now allows for long-lived sessions that do not expire if the user remains active. [#233]
 - `SessionLength` is now called `SessionLifecycle`. [#233]
 - `SessionLength::Predetermined` is now called `SessionLifecycle::PersistentSession`. [#233]
diff --git a/actix-session/Cargo.toml b/actix-session/Cargo.toml
index d5f1f1da3..2d4555f8d 100644
--- a/actix-session/Cargo.toml
+++ b/actix-session/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "actix-session"
-version = "0.6.2"
+version = "0.7.0"
 authors = [
   "Nikolay Kim <fafhrd91@gmail.com>",
   "Luca Palmieri <rust@lpalmieri.com>",
diff --git a/actix-session/README.md b/actix-session/README.md
index dc771e6c2..c9aceceb9 100644
--- a/actix-session/README.md
+++ b/actix-session/README.md
@@ -3,10 +3,9 @@
 > Session management for Actix Web applications.
 
 [![crates.io](https://img.shields.io/crates/v/actix-session?label=latest)](https://crates.io/crates/actix-session)
-[![Documentation](https://docs.rs/actix-session/badge.svg?version=0.6.2)](https://docs.rs/actix-session/0.6.2)
+[![Documentation](https://docs.rs/actix-session/badge.svg?version=0.7.0)](https://docs.rs/actix-session/0.7.0)
 ![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-session)
-[![Dependency Status](https://deps.rs/crate/actix-session/0.6.2/status.svg)](https://deps.rs/crate/actix-session/0.6.2)
-
+[![Dependency Status](https://deps.rs/crate/actix-session/0.7.0/status.svg)](https://deps.rs/crate/actix-session/0.7.0)
 
 ## Documentation & Resources
 

From 1830f66dca3f620778118c2f2ed17bd7d0ab4c80 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Sat, 9 Jul 2022 19:29:01 +0100
Subject: [PATCH 09/31] revert session dep in limitation

---
 actix-limitation/Cargo.toml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/actix-limitation/Cargo.toml b/actix-limitation/Cargo.toml
index 64596fffc..912b7c161 100644
--- a/actix-limitation/Cargo.toml
+++ b/actix-limitation/Cargo.toml
@@ -13,7 +13,7 @@ license = "MIT OR Apache-2.0"
 edition = "2018"
 
 [dependencies]
-actix-session = "0.7"
+actix-session = "0.5"
 actix-utils = "3"
 actix-web = { version = "4", default-features = false }
 

From 3b1c161547d8266e093b871a3ee114999b8e5d69 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Sat, 9 Jul 2022 19:55:38 +0100
Subject: [PATCH 10/31] remove protobuf example

---
 Cargo.toml                                    |  3 -
 actix-protobuf/Cargo.toml                     |  2 +-
 .../examples/prost-example/Cargo.toml         | 15 ----
 .../examples/prost-example/client.py          | 68 -----------------
 .../examples/prost-example/src/main.rs        | 33 --------
 .../examples/prost-example/test.proto         |  6 --
 .../examples/prost-example/test_pb2.py        | 75 -------------------
 7 files changed, 1 insertion(+), 201 deletions(-)
 delete mode 100644 actix-protobuf/examples/prost-example/Cargo.toml
 delete mode 100755 actix-protobuf/examples/prost-example/client.py
 delete mode 100644 actix-protobuf/examples/prost-example/src/main.rs
 delete mode 100644 actix-protobuf/examples/prost-example/test.proto
 delete mode 100644 actix-protobuf/examples/prost-example/test_pb2.py

diff --git a/Cargo.toml b/Cargo.toml
index eaf622615..9a953a3ea 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,9 +10,6 @@ members = [
     "actix-web-httpauth",
 ]
 
-# TODO: move this example to examples repo
-# "actix-protobuf/examples/prost-example",
-
 [patch.crates-io]
 actix-cors = { path = "./actix-cors" }
 actix-identity = { path = "./actix-identity" }
diff --git a/actix-protobuf/Cargo.toml b/actix-protobuf/Cargo.toml
index f0a9d3e88..439e28592 100644
--- a/actix-protobuf/Cargo.toml
+++ b/actix-protobuf/Cargo.toml
@@ -6,7 +6,7 @@ authors = [
     "kingxsp <jin.hb.zh@outlook.com>",
     "Yuki Okushi <huyuumi.dev@gmail.com>",
 ]
-description = "Protobuf support for Actix web"
+description = "Protobuf support for Actix Web"
 keywords = ["actix", "web", "protobuf", "protocol", "rpc"]
 homepage = "https://actix.rs"
 repository = "https://github.com/actix/actix-extras.git"
diff --git a/actix-protobuf/examples/prost-example/Cargo.toml b/actix-protobuf/examples/prost-example/Cargo.toml
deleted file mode 100644
index f5baf4bae..000000000
--- a/actix-protobuf/examples/prost-example/Cargo.toml
+++ /dev/null
@@ -1,15 +0,0 @@
-[package]
-name = "prost-example"
-version = "0.5.1"
-edition = "2018"
-authors = [
-    "kingxsp <jin.hb.zh@outlook.com>",
-    "Yuki Okushi <huyuumi.dev@gmail.com>"
-]
-
-[dependencies]
-actix-web = "4"
-actix-protobuf = { path = "../../" }
-
-env_logger = "0.8"
-prost = { version = "0.8", default_features = false, features = ["prost-derive"] }
diff --git a/actix-protobuf/examples/prost-example/client.py b/actix-protobuf/examples/prost-example/client.py
deleted file mode 100755
index 99a93e7d3..000000000
--- a/actix-protobuf/examples/prost-example/client.py
+++ /dev/null
@@ -1,68 +0,0 @@
-#!/usr/bin/env python3
-# just start server and run client.py
-
-# wget https://github.com/protocolbuffers/protobuf/releases/download/v3.11.2/protobuf-python-3.11.2.zip
-# unzip protobuf-python-3.11.2.zip
-# cd protobuf-3.11.2/python/
-# python3 setup.py install
-
-# pip3 install --upgrade pip
-# pip3 install aiohttp
-
-# python3 client.py
-
-import test_pb2
-import traceback
-import sys
-
-import asyncio
-import aiohttp
-
-def op():
-    try:
-        obj = test_pb2.MyObj()
-        obj.number = 9
-        obj.name = 'USB'
-
-        #Serialize
-        sendDataStr = obj.SerializeToString()
-        #print serialized string value
-        print('serialized string:', sendDataStr)
-        #------------------------#
-        #  message transmission  #
-        #------------------------#
-        receiveDataStr = sendDataStr
-        receiveData = test_pb2.MyObj()
-
-        #Deserialize
-        receiveData.ParseFromString(receiveDataStr)
-        print('pares serialize string, return: devId = ', receiveData.number, ', name = ', receiveData.name)
-    except(Exception, e):
-        print(Exception, ':', e)
-        print(traceback.print_exc())
-        errInfo = sys.exc_info()
-        print(errInfo[0], ':', errInfo[1])
-
-
-async def fetch(session):
-    obj = test_pb2.MyObj()
-    obj.number = 9
-    obj.name = 'USB'
-    async with session.post('http://127.0.0.1:8081/', data=obj.SerializeToString(),
-        headers={"content-type": "application/protobuf"}) as resp:
-        print(resp.status)
-        data = await resp.read()
-        receiveObj = test_pb2.MyObj()
-        receiveObj.ParseFromString(data)
-        print(receiveObj)
-
-async def go(loop):
-    obj = test_pb2.MyObj()
-    obj.number = 9
-    obj.name = 'USB'
-    async with aiohttp.ClientSession(loop=loop) as session:
-        await fetch(session)
-
-loop = asyncio.get_event_loop()
-loop.run_until_complete(go(loop))
-loop.close()
\ No newline at end of file
diff --git a/actix-protobuf/examples/prost-example/src/main.rs b/actix-protobuf/examples/prost-example/src/main.rs
deleted file mode 100644
index 8dab194d7..000000000
--- a/actix-protobuf/examples/prost-example/src/main.rs
+++ /dev/null
@@ -1,33 +0,0 @@
-use actix_protobuf::*;
-use actix_web::*;
-use prost::Message;
-
-#[derive(Clone, PartialEq, Message)]
-pub struct MyObj {
-    #[prost(int32, tag = "1")]
-    pub number: i32,
-
-    #[prost(string, tag = "2")]
-    pub name: String,
-}
-
-async fn index(msg: ProtoBuf<MyObj>) -> Result<HttpResponse> {
-    println!("model: {:?}", msg);
-    HttpResponse::Ok().protobuf(msg.0) // <- send response
-}
-
-#[actix_web::main]
-async fn main() -> std::io::Result<()> {
-    std::env::set_var("RUST_LOG", "actix_web=debug,actix_server=info");
-    env_logger::init();
-
-    HttpServer::new(|| {
-        App::new()
-            .wrap(middleware::Logger::default())
-            .service(web::resource("/").route(web::post().to(index)))
-    })
-    .bind("127.0.0.1:8081")?
-    .shutdown_timeout(1)
-    .run()
-    .await
-}
diff --git a/actix-protobuf/examples/prost-example/test.proto b/actix-protobuf/examples/prost-example/test.proto
deleted file mode 100644
index 8ec278ca4..000000000
--- a/actix-protobuf/examples/prost-example/test.proto
+++ /dev/null
@@ -1,6 +0,0 @@
-syntax = "proto3";
-
-message MyObj {
-    int32 number = 1;
-    string name = 2;
-}
\ No newline at end of file
diff --git a/actix-protobuf/examples/prost-example/test_pb2.py b/actix-protobuf/examples/prost-example/test_pb2.py
deleted file mode 100644
index ea5a1d9d6..000000000
--- a/actix-protobuf/examples/prost-example/test_pb2.py
+++ /dev/null
@@ -1,75 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by the protocol buffer compiler.  DO NOT EDIT!
-# source: test.proto
-
-from google.protobuf import descriptor as _descriptor
-from google.protobuf import message as _message
-from google.protobuf import reflection as _reflection
-from google.protobuf import symbol_database as _symbol_database
-# @@protoc_insertion_point(imports)
-
-_sym_db = _symbol_database.Default()
-
-
-
-
-DESCRIPTOR = _descriptor.FileDescriptor(
-  name='test.proto',
-  package='',
-  syntax='proto3',
-  serialized_options=None,
-  serialized_pb=b'\n\ntest.proto\"%\n\x05MyObj\x12\x0e\n\x06number\x18\x01 \x01(\x05\x12\x0c\n\x04name\x18\x02 \x01(\tb\x06proto3'
-)
-
-
-
-
-_MYOBJ = _descriptor.Descriptor(
-  name='MyObj',
-  full_name='MyObj',
-  filename=None,
-  file=DESCRIPTOR,
-  containing_type=None,
-  fields=[
-    _descriptor.FieldDescriptor(
-      name='number', full_name='MyObj.number', index=0,
-      number=1, type=5, cpp_type=1, label=1,
-      has_default_value=False, default_value=0,
-      message_type=None, enum_type=None, containing_type=None,
-      is_extension=False, extension_scope=None,
-      serialized_options=None, file=DESCRIPTOR),
-    _descriptor.FieldDescriptor(
-      name='name', full_name='MyObj.name', index=1,
-      number=2, type=9, cpp_type=9, label=1,
-      has_default_value=False, default_value=b"".decode('utf-8'),
-      message_type=None, enum_type=None, containing_type=None,
-      is_extension=False, extension_scope=None,
-      serialized_options=None, file=DESCRIPTOR),
-  ],
-  extensions=[
-  ],
-  nested_types=[],
-  enum_types=[
-  ],
-  serialized_options=None,
-  is_extendable=False,
-  syntax='proto3',
-  extension_ranges=[],
-  oneofs=[
-  ],
-  serialized_start=14,
-  serialized_end=51,
-)
-
-DESCRIPTOR.message_types_by_name['MyObj'] = _MYOBJ
-_sym_db.RegisterFileDescriptor(DESCRIPTOR)
-
-MyObj = _reflection.GeneratedProtocolMessageType('MyObj', (_message.Message,), {
-  'DESCRIPTOR' : _MYOBJ,
-  '__module__' : 'test_pb2'
-  # @@protoc_insertion_point(class_scope:MyObj)
-  })
-_sym_db.RegisterMessage(MyObj)
-
-
-# @@protoc_insertion_point(module_scope)

From 97ee54405767234263551ef07b8b1bcca0cb061c Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Sat, 9 Jul 2022 19:55:53 +0100
Subject: [PATCH 11/31] update limitation's session version

---
 actix-limitation/CHANGES.md        | 1 +
 actix-limitation/Cargo.toml        | 2 +-
 actix-limitation/src/middleware.rs | 2 +-
 3 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/actix-limitation/CHANGES.md b/actix-limitation/CHANGES.md
index 4070cb196..d914e8f89 100644
--- a/actix-limitation/CHANGES.md
+++ b/actix-limitation/CHANGES.md
@@ -1,6 +1,7 @@
 # Changes
 
 ## Unreleased - 2022-xx-xx
+- Updated `session-session` dependency to `0.7`.
 
 
 ## 0.2.0 - 2022-03-22
diff --git a/actix-limitation/Cargo.toml b/actix-limitation/Cargo.toml
index 912b7c161..64596fffc 100644
--- a/actix-limitation/Cargo.toml
+++ b/actix-limitation/Cargo.toml
@@ -13,7 +13,7 @@ license = "MIT OR Apache-2.0"
 edition = "2018"
 
 [dependencies]
-actix-session = "0.5"
+actix-session = "0.7"
 actix-utils = "3"
 actix-web = { version = "4", default-features = false }
 
diff --git a/actix-limitation/src/middleware.rs b/actix-limitation/src/middleware.rs
index cf46cddca..2acdb776a 100644
--- a/actix-limitation/src/middleware.rs
+++ b/actix-limitation/src/middleware.rs
@@ -1,6 +1,6 @@
 use std::{future::Future, pin::Pin, rc::Rc};
 
-use actix_session::UserSession;
+use actix_session::SessionExt as _;
 use actix_utils::future::{ok, Ready};
 use actix_web::{
     body::EitherBody,

From e0ffd4e59268b1c4c3cecca29eca3876daff9981 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Sat, 9 Jul 2022 19:58:42 +0100
Subject: [PATCH 12/31] bump uuid dev dep

---
 actix-limitation/Cargo.toml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/actix-limitation/Cargo.toml b/actix-limitation/Cargo.toml
index 64596fffc..11d580c89 100644
--- a/actix-limitation/Cargo.toml
+++ b/actix-limitation/Cargo.toml
@@ -25,5 +25,5 @@ time = "0.3"
 
 [dev-dependencies]
 actix-web = "4"
-uuid = { version = "0.8", features = ["v4"] }
 static_assertions = "1"
+uuid = { version = "1", features = ["v4"] }

From 9bc014b96fa8532baf2d90e55695d511b1bfa343 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Sat, 9 Jul 2022 20:05:47 +0100
Subject: [PATCH 13/31] forbid unsafe in all crates

---
 actix-identity/src/lib.rs | 1 +
 actix-redis/src/lib.rs    | 1 +
 actix-session/src/lib.rs  | 1 +
 3 files changed, 3 insertions(+)

diff --git a/actix-identity/src/lib.rs b/actix-identity/src/lib.rs
index 5c172a64e..9ef232a52 100644
--- a/actix-identity/src/lib.rs
+++ b/actix-identity/src/lib.rs
@@ -86,6 +86,7 @@
 //! [`IdentityMiddlewareBuilder::visit_deadline`]: config::IdentityMiddlewareBuilder::visit_deadline
 //! [`IdentityMiddlewareBuilder::login_deadline`]: config::IdentityMiddlewareBuilder::login_deadline
 
+#![forbid(unsafe_code)]
 #![deny(rust_2018_idioms, nonstandard_style, missing_docs)]
 #![warn(future_incompatible)]
 
diff --git a/actix-redis/src/lib.rs b/actix-redis/src/lib.rs
index 26a94a72f..1c183fa81 100644
--- a/actix-redis/src/lib.rs
+++ b/actix-redis/src/lib.rs
@@ -1,5 +1,6 @@
 //! Redis integration for `actix`.
 
+#![forbid(unsafe_code)]
 #![deny(rust_2018_idioms, nonstandard_style)]
 #![warn(future_incompatible)]
 
diff --git a/actix-session/src/lib.rs b/actix-session/src/lib.rs
index 917121429..db8271281 100644
--- a/actix-session/src/lib.rs
+++ b/actix-session/src/lib.rs
@@ -133,6 +133,7 @@
 //! [`RedisSessionStore`]: storage::RedisSessionStore
 //! [`RedisActorSessionStore`]: storage::RedisActorSessionStore
 
+#![forbid(unsafe_code)]
 #![deny(rust_2018_idioms, nonstandard_style)]
 #![warn(future_incompatible, missing_docs)]
 #![doc(html_logo_url = "https://actix.rs/img/logo.png")]

From ecd775664495353b0f079ac9f6a8ca279bc3c547 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Sat, 9 Jul 2022 20:11:14 +0100
Subject: [PATCH 14/31] update redis deps

---
 actix-redis/CHANGES.md | 3 +++
 actix-redis/Cargo.toml | 7 +++----
 2 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/actix-redis/CHANGES.md b/actix-redis/CHANGES.md
index 84c5ed91f..c3aa544d4 100644
--- a/actix-redis/CHANGES.md
+++ b/actix-redis/CHANGES.md
@@ -1,6 +1,9 @@
 # Changes
 
 ## Unreleased - 2022-xx-xx
+- Update `actix` dependency to `0.13`.
+- Update `redis-async` dependency to `0.13`.
+- Update `tokio-util` dependency to `0.7`.
 - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
 
 
diff --git a/actix-redis/Cargo.toml b/actix-redis/Cargo.toml
index 4720770e5..a2661c610 100644
--- a/actix-redis/Cargo.toml
+++ b/actix-redis/Cargo.toml
@@ -8,7 +8,6 @@ keywords = ["actix", "redis", "async", "session"]
 homepage = "https://actix.rs"
 repository = "https://github.com/actix/actix-extras.git"
 categories = ["network-programming", "asynchronous"]
-exclude = [".cargo/config"]
 edition = "2018"
 
 [lib]
@@ -22,7 +21,7 @@ default = ["web"]
 web = ["actix-web"]
 
 [dependencies]
-actix = { version = "0.12", default-features = false }
+actix = { version = "0.13", default-features = false }
 actix-rt = { version = "2.1", default-features = false }
 actix-service = "2"
 actix-tls = { version = "3", default-features = false, features = ["connect"] }
@@ -31,10 +30,10 @@ log = "0.4.6"
 backoff = "0.4.0"
 derive_more = "0.99.5"
 futures-core = { version = "0.3.7", default-features = false }
-redis-async = { version = "0.12", default-features = false, features = ["tokio10"] }
+redis-async = "0.13"
 time = "0.3"
 tokio = { version = "1.13.1", features = ["sync"] }
-tokio-util = "0.6.1"
+tokio-util = "0.7"
 actix-web = { version = "4", default_features = false, optional = true }
 
 [dev-dependencies]

From ca9879425b7767d05ce4bf468238a8b1f5c94a31 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Sat, 9 Jul 2022 20:11:56 +0100
Subject: [PATCH 15/31] prepare actix-redis release 0.12.0

---
 actix-redis/CHANGES.md | 3 +++
 actix-redis/Cargo.toml | 2 +-
 actix-redis/README.md  | 4 ++--
 3 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/actix-redis/CHANGES.md b/actix-redis/CHANGES.md
index c3aa544d4..abc691c7a 100644
--- a/actix-redis/CHANGES.md
+++ b/actix-redis/CHANGES.md
@@ -1,6 +1,9 @@
 # Changes
 
 ## Unreleased - 2022-xx-xx
+
+
+## 0.12.0 - 2022-07-09
 - Update `actix` dependency to `0.13`.
 - Update `redis-async` dependency to `0.13`.
 - Update `tokio-util` dependency to `0.7`.
diff --git a/actix-redis/Cargo.toml b/actix-redis/Cargo.toml
index a2661c610..44c79e982 100644
--- a/actix-redis/Cargo.toml
+++ b/actix-redis/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "actix-redis"
-version = "0.11.0"
+version = "0.12.0"
 authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
 description = "Redis integration for Actix"
 license = "MIT OR Apache-2.0"
diff --git a/actix-redis/README.md b/actix-redis/README.md
index cac27a013..c3dcd2234 100644
--- a/actix-redis/README.md
+++ b/actix-redis/README.md
@@ -3,9 +3,9 @@
 > Redis integration for Actix.
 
 [![crates.io](https://img.shields.io/crates/v/actix-redis?label=latest)](https://crates.io/crates/actix-redis)
-[![Documentation](https://docs.rs/actix-redis/badge.svg?version=0.11.0)](https://docs.rs/actix-redis/0.11.0)
+[![Documentation](https://docs.rs/actix-redis/badge.svg?version=0.12.0)](https://docs.rs/actix-redis/0.12.0)
 ![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-redis)
-[![Dependency Status](https://deps.rs/crate/actix-redis/0.11.0/status.svg)](https://deps.rs/crate/actix-redis/0.11.0)
+[![Dependency Status](https://deps.rs/crate/actix-redis/0.12.0/status.svg)](https://deps.rs/crate/actix-redis/0.12.0)
 
 ## Documentation & Resources
 

From 3e002a677b6194014ee981560309c422efb2a361 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Sat, 9 Jul 2022 20:14:27 +0100
Subject: [PATCH 16/31] bump session version of redis

---
 actix-redis/Cargo.toml   | 6 +++++-
 actix-session/CHANGES.md | 4 +++-
 actix-session/Cargo.toml | 4 ++--
 3 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/actix-redis/Cargo.toml b/actix-redis/Cargo.toml
index 44c79e982..3fdfd1518 100644
--- a/actix-redis/Cargo.toml
+++ b/actix-redis/Cargo.toml
@@ -4,12 +4,16 @@ version = "0.12.0"
 authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
 description = "Redis integration for Actix"
 license = "MIT OR Apache-2.0"
-keywords = ["actix", "redis", "async", "session"]
+keywords = ["actix", "redis", "async"]
 homepage = "https://actix.rs"
 repository = "https://github.com/actix/actix-extras.git"
 categories = ["network-programming", "asynchronous"]
 edition = "2018"
 
+[package.metadata.docs.rs]
+all-features = true
+rustdoc-args = ["--cfg", "docsrs"]
+
 [lib]
 name = "actix_redis"
 path = "src/lib.rs"
diff --git a/actix-session/CHANGES.md b/actix-session/CHANGES.md
index 3ebd19cbd..b1ea34be1 100644
--- a/actix-session/CHANGES.md
+++ b/actix-session/CHANGES.md
@@ -12,7 +12,9 @@
 - `SessionLength::BrowserSession::state_ttl` is now called `BrowserSession::session_state_ttl`. [#233]
 - `SessionMiddlewareBuilder::max_session_length` is now called `SessionMiddlewareBuilder::session_lifecycle`. [#233]
 - The `SessionStore` trait requires the implementation of a new method, `SessionStore::update_ttl`. [#233]
-- All types used to configure `SessionMiddleware` have been moved to the `config` sub-module [#233]
+- All types used to configure `SessionMiddleware` have been moved to the `config` sub-module. [#233]
+- Update `actix` dependency to `0.13`.
+- Update `actix-redis` dependency to `0.12`.
 - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
 
 [#233]: https://github.com/actix/actix-extras/pull/233
diff --git a/actix-session/Cargo.toml b/actix-session/Cargo.toml
index 2d4555f8d..fdbf9a6d7 100644
--- a/actix-session/Cargo.toml
+++ b/actix-session/Cargo.toml
@@ -41,8 +41,8 @@ serde_json = { version = "1" }
 tracing = { version = "0.1.30", default-features = false, features = ["log"] }
 
 # redis-actor-session
-actix = { version = "0.12.0", default-features = false, optional = true }
-actix-redis = { version = "0.11.0", optional = true }
+actix = { version = "0.13", default-features = false, optional = true }
+actix-redis = { version = "0.12", optional = true }
 futures-core = { version = "0.3.7", default-features = false, optional = true }
 
 # redis-rs-session

From 910f9641001c156550ada6338b852cd51a4ef1d8 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Sat, 9 Jul 2022 20:18:47 +0100
Subject: [PATCH 17/31] update community crates

---
 README.md | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/README.md b/README.md
index 8cfb562d3..1e39c2718 100644
--- a/README.md
+++ b/README.md
@@ -27,19 +27,19 @@ These crates are provided by the community.
 
 | Crate                      |                                                                                                                                                                                                                                                                                        |                                                                                                   |
 | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
-| [actix-web-lab]            | [![crates.io](https://img.shields.io/crates/v/actix-web-lab?label=latest)](https://crates.io/crates/actix-web-lab) [![dependency status](https://deps.rs/crate/actix-web-lab/0.15.0/status.svg)](https://deps.rs/crate/actix-web-lab/0.15.0)                                           | Experimental extractors, middleware, and other extras for possible inclusion in Actix Web.        |
+| [actix-web-lab]            | [![crates.io](https://img.shields.io/crates/v/actix-web-lab?label=latest)](https://crates.io/crates/actix-web-lab) [![dependency status](https://deps.rs/crate/actix-web-lab/0.16.4/status.svg)](https://deps.rs/crate/actix-web-lab/0.16.4)                                           | Experimental extractors, middleware, and other extras for possible inclusion in Actix Web.        |
 | [actix-form-data]          | [![crates.io](https://img.shields.io/crates/v/actix-form-data?label=latest)](https://crates.io/crates/actix-form-data) [![dependency status](https://deps.rs/crate/actix-form-data/0.6.2/status.svg)](https://deps.rs/crate/actix-form-data/0.6.2)                                     | Rate-limiting backed by form-data.                                                                |
 | [actix-governor]           | [![crates.io](https://img.shields.io/crates/v/actix-governor?label=latest)](https://crates.io/crates/actix-governor) [![dependency status](https://deps.rs/crate/actix-governor/0.3.0/status.svg)](https://deps.rs/crate/actix-governor/0.3.0)                                         | Rate-limiting backed by governor.                                                                 |
 | [actix-casbin]             | [![crates.io](https://img.shields.io/crates/v/actix-casbin?label=latest)](https://crates.io/crates/actix-casbin) [![dependency status](https://deps.rs/crate/actix-casbin/0.4.2/status.svg)](https://deps.rs/crate/actix-casbin/0.4.2)                                                 | Authorization library that supports access control models like ACL, RBAC & ABAC.                  |
 | [actix-ip-filter]          | [![crates.io](https://img.shields.io/crates/v/actix-ip-filter?label=latest)](https://crates.io/crates/actix-ip-filter) [![dependency status](https://deps.rs/crate/actix-ip-filter/0.3.1/status.svg)](https://deps.rs/crate/actix-ip-filter/0.3.1)                                     | IP address filter. Supports glob patterns.                                                        |
 | [actix-web-static-files]   | [![crates.io](https://img.shields.io/crates/v/actix-web-static-files?label=latest)](https://crates.io/crates/actix-web-static-files) [![dependency status](https://deps.rs/crate/actix-web-static-files/4.0.0/status.svg)](https://deps.rs/crate/actix-web-static-files/4.0.0)         | Static files as embedded resources.                                                               |
-| [actix-web-grants]         | [![crates.io](https://img.shields.io/crates/v/actix-web-grants?label=latest)](https://crates.io/crates/actix-web-grants) [![dependency status](https://deps.rs/crate/actix-web-grants/3.0.0-beta.6/status.svg)](https://deps.rs/crate/actix-web-grants/3.0.0-beta.6)                   | Extension for validating user authorities.                                                        |
-| [aliri_actix]              | [![crates.io](https://img.shields.io/crates/v/aliri_actix?label=latest)](https://crates.io/crates/aliri_actix) [![dependency status](https://deps.rs/crate/aliri_actix/0.6.0/status.svg)](https://deps.rs/crate/aliri_actix/0.6.0)                                                     | Endpoint authorization and authentication using scoped OAuth2 JWT tokens.                         |
-| [actix-web-flash-messages] | [![crates.io](https://img.shields.io/crates/v/actix-web-flash-messages?label=latest)](https://crates.io/crates/actix-web-flash-messages) [![dependency status](https://deps.rs/crate/actix-web-flash-messages/0.3.2/status.svg)](https://deps.rs/crate/actix-web-flash-messages/0.3.2) | Support for flash messages/one-time notifications in `actix-web`.                                 |
+| [actix-web-grants]         | [![crates.io](https://img.shields.io/crates/v/actix-web-grants?label=latest)](https://crates.io/crates/actix-web-grants) [![dependency status](https://deps.rs/crate/actix-web-grants/3.0.1/status.svg)](https://deps.rs/crate/actix-web-grants/3.0.1)                                 | Extension for validating user authorities.                                                        |
+| [aliri_actix]              | [![crates.io](https://img.shields.io/crates/v/aliri_actix?label=latest)](https://crates.io/crates/aliri_actix) [![dependency status](https://deps.rs/crate/aliri_actix/0.7.0/status.svg)](https://deps.rs/crate/aliri_actix/0.7.0)                                                     | Endpoint authorization and authentication using scoped OAuth2 JWT tokens.                         |
+| [actix-web-flash-messages] | [![crates.io](https://img.shields.io/crates/v/actix-web-flash-messages?label=latest)](https://crates.io/crates/actix-web-flash-messages) [![dependency status](https://deps.rs/crate/actix-web-flash-messages/0.4.1/status.svg)](https://deps.rs/crate/actix-web-flash-messages/0.4.1) | Support for flash messages/one-time notifications in `actix-web`.                                 |
 | [awmp]                     | [![crates.io](https://img.shields.io/crates/v/awmp?label=latest)](https://crates.io/crates/awmp) [![dependency status](https://deps.rs/crate/awmp/0.8.1/status.svg)](https://deps.rs/crate/awmp/0.8.1)                                                                                 | An easy to use wrapper around multipart fields for Actix Web.                                     |
-| [tracing-actix-web]        | [![crates.io](https://img.shields.io/crates/v/tracing-actix-web?label=latest)](https://crates.io/crates/tracing-actix-web) [![dependency status](https://deps.rs/crate/tracing-actix-web/0.5.1/status.svg)](https://deps.rs/crate/tracing-actix-web/0.5.1)                             | A middleware to collect telemetry data from applications built on top of the actix-web framework. |
-| [actix-ws]                 | [![crates.io](https://img.shields.io/crates/v/actix-ws?label=latest)](https://crates.io/crates/actix-ws) [![dependency status](https://deps.rs/crate/actix-ws/0.2.5/status.svg)](https://deps.rs/crate/actix-ws/0.2.5)                                                                 | Actor-less Websockets for the Actix Runtime. |
-| [actix-hash]               | [![crates.io](https://img.shields.io/crates/v/actix-hash?label=latest)](https://crates.io/crates/actix-hash) [![dependency status](https://deps.rs/crate/actix-hash/0.3.0/status.svg)](https://deps.rs/crate/actix-hash/0.3.0)                                                         | Hashing utilities for Actix Web.                                                                  |
+| [tracing-actix-web]        | [![crates.io](https://img.shields.io/crates/v/tracing-actix-web?label=latest)](https://crates.io/crates/tracing-actix-web) [![dependency status](https://deps.rs/crate/tracing-actix-web/0.6.0/status.svg)](https://deps.rs/crate/tracing-actix-web/0.6.0)                             | A middleware to collect telemetry data from applications built on top of the actix-web framework. |
+| [actix-ws]                 | [![crates.io](https://img.shields.io/crates/v/actix-ws?label=latest)](https://crates.io/crates/actix-ws) [![dependency status](https://deps.rs/crate/actix-ws/0.2.5/status.svg)](https://deps.rs/crate/actix-ws/0.2.5)                                                                 | Actor-less WebSockets for the Actix Runtime.                                                      |
+| [actix-hash]               | [![crates.io](https://img.shields.io/crates/v/actix-hash?label=latest)](https://crates.io/crates/actix-hash) [![dependency status](https://deps.rs/crate/actix-hash/0.4.0/status.svg)](https://deps.rs/crate/actix-hash/0.4.0)                                                         | Hashing utilities for Actix Web.                                                                  |
 
 To add a crate to this list, submit a pull request.
 

From 4e1a95fc758b056fcff3f5890150180c428ae02f Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Sat, 9 Jul 2022 20:26:00 +0100
Subject: [PATCH 18/31] link community crates to crates.io

and add actix-multipart-extract
---
 README.md | 58 ++++++++++++++++++++++++++++---------------------------
 1 file changed, 30 insertions(+), 28 deletions(-)

diff --git a/README.md b/README.md
index 1e39c2718..9414904f2 100644
--- a/README.md
+++ b/README.md
@@ -25,21 +25,22 @@
 
 These crates are provided by the community.
 
-| Crate                      |                                                                                                                                                                                                                                                                                        |                                                                                                   |
-| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
-| [actix-web-lab]            | [![crates.io](https://img.shields.io/crates/v/actix-web-lab?label=latest)](https://crates.io/crates/actix-web-lab) [![dependency status](https://deps.rs/crate/actix-web-lab/0.16.4/status.svg)](https://deps.rs/crate/actix-web-lab/0.16.4)                                           | Experimental extractors, middleware, and other extras for possible inclusion in Actix Web.        |
-| [actix-form-data]          | [![crates.io](https://img.shields.io/crates/v/actix-form-data?label=latest)](https://crates.io/crates/actix-form-data) [![dependency status](https://deps.rs/crate/actix-form-data/0.6.2/status.svg)](https://deps.rs/crate/actix-form-data/0.6.2)                                     | Rate-limiting backed by form-data.                                                                |
-| [actix-governor]           | [![crates.io](https://img.shields.io/crates/v/actix-governor?label=latest)](https://crates.io/crates/actix-governor) [![dependency status](https://deps.rs/crate/actix-governor/0.3.0/status.svg)](https://deps.rs/crate/actix-governor/0.3.0)                                         | Rate-limiting backed by governor.                                                                 |
-| [actix-casbin]             | [![crates.io](https://img.shields.io/crates/v/actix-casbin?label=latest)](https://crates.io/crates/actix-casbin) [![dependency status](https://deps.rs/crate/actix-casbin/0.4.2/status.svg)](https://deps.rs/crate/actix-casbin/0.4.2)                                                 | Authorization library that supports access control models like ACL, RBAC & ABAC.                  |
-| [actix-ip-filter]          | [![crates.io](https://img.shields.io/crates/v/actix-ip-filter?label=latest)](https://crates.io/crates/actix-ip-filter) [![dependency status](https://deps.rs/crate/actix-ip-filter/0.3.1/status.svg)](https://deps.rs/crate/actix-ip-filter/0.3.1)                                     | IP address filter. Supports glob patterns.                                                        |
-| [actix-web-static-files]   | [![crates.io](https://img.shields.io/crates/v/actix-web-static-files?label=latest)](https://crates.io/crates/actix-web-static-files) [![dependency status](https://deps.rs/crate/actix-web-static-files/4.0.0/status.svg)](https://deps.rs/crate/actix-web-static-files/4.0.0)         | Static files as embedded resources.                                                               |
-| [actix-web-grants]         | [![crates.io](https://img.shields.io/crates/v/actix-web-grants?label=latest)](https://crates.io/crates/actix-web-grants) [![dependency status](https://deps.rs/crate/actix-web-grants/3.0.1/status.svg)](https://deps.rs/crate/actix-web-grants/3.0.1)                                 | Extension for validating user authorities.                                                        |
-| [aliri_actix]              | [![crates.io](https://img.shields.io/crates/v/aliri_actix?label=latest)](https://crates.io/crates/aliri_actix) [![dependency status](https://deps.rs/crate/aliri_actix/0.7.0/status.svg)](https://deps.rs/crate/aliri_actix/0.7.0)                                                     | Endpoint authorization and authentication using scoped OAuth2 JWT tokens.                         |
-| [actix-web-flash-messages] | [![crates.io](https://img.shields.io/crates/v/actix-web-flash-messages?label=latest)](https://crates.io/crates/actix-web-flash-messages) [![dependency status](https://deps.rs/crate/actix-web-flash-messages/0.4.1/status.svg)](https://deps.rs/crate/actix-web-flash-messages/0.4.1) | Support for flash messages/one-time notifications in `actix-web`.                                 |
-| [awmp]                     | [![crates.io](https://img.shields.io/crates/v/awmp?label=latest)](https://crates.io/crates/awmp) [![dependency status](https://deps.rs/crate/awmp/0.8.1/status.svg)](https://deps.rs/crate/awmp/0.8.1)                                                                                 | An easy to use wrapper around multipart fields for Actix Web.                                     |
-| [tracing-actix-web]        | [![crates.io](https://img.shields.io/crates/v/tracing-actix-web?label=latest)](https://crates.io/crates/tracing-actix-web) [![dependency status](https://deps.rs/crate/tracing-actix-web/0.6.0/status.svg)](https://deps.rs/crate/tracing-actix-web/0.6.0)                             | A middleware to collect telemetry data from applications built on top of the actix-web framework. |
-| [actix-ws]                 | [![crates.io](https://img.shields.io/crates/v/actix-ws?label=latest)](https://crates.io/crates/actix-ws) [![dependency status](https://deps.rs/crate/actix-ws/0.2.5/status.svg)](https://deps.rs/crate/actix-ws/0.2.5)                                                                 | Actor-less WebSockets for the Actix Runtime.                                                      |
-| [actix-hash]               | [![crates.io](https://img.shields.io/crates/v/actix-hash?label=latest)](https://crates.io/crates/actix-hash) [![dependency status](https://deps.rs/crate/actix-hash/0.4.0/status.svg)](https://deps.rs/crate/actix-hash/0.4.0)                                                         | Hashing utilities for Actix Web.                                                                  |
+| Crate                      |                                                                                                                                                                                                                                                               |                                                                                                   |
+| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
+| [actix-web-lab]            | [![crates.io](https://img.shields.io/crates/v/actix-web-lab?label=latest)](actix-web-lab) [![dependency status](https://deps.rs/crate/actix-web-lab/0.16.4/status.svg)](https://deps.rs/crate/actix-web-lab/0.16.4)                                           | Experimental extractors, middleware, and other extras for possible inclusion in Actix Web.        |
+| [actix-multipart-extract]  | [![crates.io](https://img.shields.io/crates/v/actix-multipart-extract?label=latest)](actix-multipart-extract) [![dependency status](https://deps.rs/crate/actix-multipart-extract/0.1.4/status.svg)](https://deps.rs/crate/actix-multipart-extract/0.1.4)     | Better multipart form support for Actix Web.                                                      |
+| [actix-form-data]          | [![crates.io](https://img.shields.io/crates/v/actix-form-data?label=latest)](actix-form-data) [![dependency status](https://deps.rs/crate/actix-form-data/0.6.2/status.svg)](https://deps.rs/crate/actix-form-data/0.6.2)                                     | Rate-limiting backed by form-data.                                                                |
+| [actix-governor]           | [![crates.io](https://img.shields.io/crates/v/actix-governor?label=latest)](actix-governor) [![dependency status](https://deps.rs/crate/actix-governor/0.3.0/status.svg)](https://deps.rs/crate/actix-governor/0.3.0)                                         | Rate-limiting backed by governor.                                                                 |
+| [actix-casbin]             | [![crates.io](https://img.shields.io/crates/v/actix-casbin?label=latest)](actix-casbin) [![dependency status](https://deps.rs/crate/actix-casbin/0.4.2/status.svg)](https://deps.rs/crate/actix-casbin/0.4.2)                                                 | Authorization library that supports access control models like ACL, RBAC & ABAC.                  |
+| [actix-ip-filter]          | [![crates.io](https://img.shields.io/crates/v/actix-ip-filter?label=latest)](actix-ip-filter) [![dependency status](https://deps.rs/crate/actix-ip-filter/0.3.1/status.svg)](https://deps.rs/crate/actix-ip-filter/0.3.1)                                     | IP address filter. Supports glob patterns.                                                        |
+| [actix-web-static-files]   | [![crates.io](https://img.shields.io/crates/v/actix-web-static-files?label=latest)](actix-web-static-files) [![dependency status](https://deps.rs/crate/actix-web-static-files/4.0.0/status.svg)](https://deps.rs/crate/actix-web-static-files/4.0.0)         | Static files as embedded resources.                                                               |
+| [actix-web-grants]         | [![crates.io](https://img.shields.io/crates/v/actix-web-grants?label=latest)](actix-web-grants) [![dependency status](https://deps.rs/crate/actix-web-grants/3.0.1/status.svg)](https://deps.rs/crate/actix-web-grants/3.0.1)                                 | Extension for validating user authorities.                                                        |
+| [aliri_actix]              | [![crates.io](https://img.shields.io/crates/v/aliri_actix?label=latest)](aliri_actix) [![dependency status](https://deps.rs/crate/aliri_actix/0.7.0/status.svg)](https://deps.rs/crate/aliri_actix/0.7.0)                                                     | Endpoint authorization and authentication using scoped OAuth2 JWT tokens.                         |
+| [actix-web-flash-messages] | [![crates.io](https://img.shields.io/crates/v/actix-web-flash-messages?label=latest)](actix-web-flash-messages) [![dependency status](https://deps.rs/crate/actix-web-flash-messages/0.4.1/status.svg)](https://deps.rs/crate/actix-web-flash-messages/0.4.1) | Support for flash messages/one-time notifications in `actix-web`.                                 |
+| [awmp]                     | [![crates.io](https://img.shields.io/crates/v/awmp?label=latest)](awmp) [![dependency status](https://deps.rs/crate/awmp/0.8.1/status.svg)](https://deps.rs/crate/awmp/0.8.1)                                                                                 | An easy to use wrapper around multipart fields for Actix Web.                                     |
+| [tracing-actix-web]        | [![crates.io](https://img.shields.io/crates/v/tracing-actix-web?label=latest)](tracing-actix-web) [![dependency status](https://deps.rs/crate/tracing-actix-web/0.6.0/status.svg)](https://deps.rs/crate/tracing-actix-web/0.6.0)                             | A middleware to collect telemetry data from applications built on top of the actix-web framework. |
+| [actix-ws]                 | [![crates.io](https://img.shields.io/crates/v/actix-ws?label=latest)](actix-ws) [![dependency status](https://deps.rs/crate/actix-ws/0.2.5/status.svg)](https://deps.rs/crate/actix-ws/0.2.5)                                                                 | Actor-less WebSockets for the Actix Runtime.                                                      |
+| [actix-hash]               | [![crates.io](https://img.shields.io/crates/v/actix-hash?label=latest)](actix-hash) [![dependency status](https://deps.rs/crate/actix-hash/0.4.0/status.svg)](https://deps.rs/crate/actix-hash/0.4.0)                                                         | Hashing utilities for Actix Web.                                                                  |
 
 To add a crate to this list, submit a pull request.
 
@@ -55,16 +56,17 @@ To add a crate to this list, submit a pull request.
 [actix-redis]: actix-redis
 [actix-session]: actix-session
 [actix-web-httpauth]: actix-web-httpauth
-[actix-web-lab]: https://github.com/robjtede/actix-web-lab/tree/main/actix-web-lab
-[actix-form-data]: https://git.asonix.dog/asonix/actix-form-data
-[actix-casbin]: https://github.com/casbin-rs/actix-casbin
-[actix-ip-filter]: https://github.com/jhen0409/actix-ip-filter
-[actix-web-static-files]: https://github.com/kilork/actix-web-static-files
-[actix-web-grants]: https://github.com/DDtKey/actix-web-grants
-[actix-web-flash-messages]: https://github.com/LukeMathWalker/actix-web-flash-messages
-[actix-governor]: https://github.com/AaronErhardt/actix-governor
-[aliri_actix]: https://github.com/neoeinstein/aliri
-[awmp]: https://github.com/kardeiz/awmp
-[tracing-actix-web]: https://github.com/LukeMathWalker/tracing-actix-web
-[actix-ws]: https://git.asonix.dog/asonix/actix-actorless-websockets
-[actix-hash]: https://github.com/robjtede/actix-web-lab/tree/main/actix-hash
+[actix-web-lab]: https://crates.io/crates/actix-web-lab
+[actix-multipart-extract]: https://crates.io/crates/actix-multipart-extract
+[actix-form-data]: https://crates.io/crates/actix-form-data
+[actix-casbin]: https://crates.io/crates/actix-casbin
+[actix-ip-filter]: https://crates.io/crates/actix-ip-filter
+[actix-web-static-files]: https://crates.io/crates/actix-web-static-files
+[actix-web-grants]: https://crates.io/crates/actix-web-grants
+[actix-web-flash-messages]: https://crates.io/crates/actix-web-flash-messages
+[actix-governor]: https://crates.io/crates/actix-governor
+[aliri_actix]: https://crates.io/crates/aliri_actix
+[awmp]: https://crates.io/crates/awmp
+[tracing-actix-web]: https://crates.io/crates/tracing-actix-web
+[actix-ws]: https://crates.io/crates/actix-ws
+[actix-hash]: https://crates.io/crates/actix-hash

From d4384932ff7f95d210a5aa7c3581fa84eada3161 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Sat, 9 Jul 2022 20:27:01 +0100
Subject: [PATCH 19/31] relative links on dir references

---
 README.md | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/README.md b/README.md
index 9414904f2..41e250c07 100644
--- a/README.md
+++ b/README.md
@@ -49,13 +49,13 @@ To add a crate to this list, submit a pull request.
 [actix]: https://github.com/actix/actix
 [actix web]: https://github.com/actix/actix-web
 [actix-extras]: https://github.com/actix/actix-extras
-[actix-cors]: actix-cors
-[actix-identity]: actix-identity
-[actix-limitation]: actix-limitation
-[actix-protobuf]: actix-protobuf
-[actix-redis]: actix-redis
-[actix-session]: actix-session
-[actix-web-httpauth]: actix-web-httpauth
+[actix-cors]: ./actix-cors
+[actix-identity]: ./actix-identity
+[actix-limitation]: ./actix-limitation
+[actix-protobuf]: ./actix-protobuf
+[actix-redis]: ./actix-redis
+[actix-session]: ./actix-session
+[actix-web-httpauth]: ./actix-web-httpauth
 [actix-web-lab]: https://crates.io/crates/actix-web-lab
 [actix-multipart-extract]: https://crates.io/crates/actix-multipart-extract
 [actix-form-data]: https://crates.io/crates/actix-form-data

From 169b262c6658b9ae332de362c7c719c3a9de6ca1 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Sat, 9 Jul 2022 20:29:08 +0100
Subject: [PATCH 20/31] fix reference links

---
 README.md | 28 ++++++++++++++--------------
 1 file changed, 14 insertions(+), 14 deletions(-)

diff --git a/README.md b/README.md
index 41e250c07..662ed8fe1 100644
--- a/README.md
+++ b/README.md
@@ -27,20 +27,20 @@ These crates are provided by the community.
 
 | Crate                      |                                                                                                                                                                                                                                                               |                                                                                                   |
 | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
-| [actix-web-lab]            | [![crates.io](https://img.shields.io/crates/v/actix-web-lab?label=latest)](actix-web-lab) [![dependency status](https://deps.rs/crate/actix-web-lab/0.16.4/status.svg)](https://deps.rs/crate/actix-web-lab/0.16.4)                                           | Experimental extractors, middleware, and other extras for possible inclusion in Actix Web.        |
-| [actix-multipart-extract]  | [![crates.io](https://img.shields.io/crates/v/actix-multipart-extract?label=latest)](actix-multipart-extract) [![dependency status](https://deps.rs/crate/actix-multipart-extract/0.1.4/status.svg)](https://deps.rs/crate/actix-multipart-extract/0.1.4)     | Better multipart form support for Actix Web.                                                      |
-| [actix-form-data]          | [![crates.io](https://img.shields.io/crates/v/actix-form-data?label=latest)](actix-form-data) [![dependency status](https://deps.rs/crate/actix-form-data/0.6.2/status.svg)](https://deps.rs/crate/actix-form-data/0.6.2)                                     | Rate-limiting backed by form-data.                                                                |
-| [actix-governor]           | [![crates.io](https://img.shields.io/crates/v/actix-governor?label=latest)](actix-governor) [![dependency status](https://deps.rs/crate/actix-governor/0.3.0/status.svg)](https://deps.rs/crate/actix-governor/0.3.0)                                         | Rate-limiting backed by governor.                                                                 |
-| [actix-casbin]             | [![crates.io](https://img.shields.io/crates/v/actix-casbin?label=latest)](actix-casbin) [![dependency status](https://deps.rs/crate/actix-casbin/0.4.2/status.svg)](https://deps.rs/crate/actix-casbin/0.4.2)                                                 | Authorization library that supports access control models like ACL, RBAC & ABAC.                  |
-| [actix-ip-filter]          | [![crates.io](https://img.shields.io/crates/v/actix-ip-filter?label=latest)](actix-ip-filter) [![dependency status](https://deps.rs/crate/actix-ip-filter/0.3.1/status.svg)](https://deps.rs/crate/actix-ip-filter/0.3.1)                                     | IP address filter. Supports glob patterns.                                                        |
-| [actix-web-static-files]   | [![crates.io](https://img.shields.io/crates/v/actix-web-static-files?label=latest)](actix-web-static-files) [![dependency status](https://deps.rs/crate/actix-web-static-files/4.0.0/status.svg)](https://deps.rs/crate/actix-web-static-files/4.0.0)         | Static files as embedded resources.                                                               |
-| [actix-web-grants]         | [![crates.io](https://img.shields.io/crates/v/actix-web-grants?label=latest)](actix-web-grants) [![dependency status](https://deps.rs/crate/actix-web-grants/3.0.1/status.svg)](https://deps.rs/crate/actix-web-grants/3.0.1)                                 | Extension for validating user authorities.                                                        |
-| [aliri_actix]              | [![crates.io](https://img.shields.io/crates/v/aliri_actix?label=latest)](aliri_actix) [![dependency status](https://deps.rs/crate/aliri_actix/0.7.0/status.svg)](https://deps.rs/crate/aliri_actix/0.7.0)                                                     | Endpoint authorization and authentication using scoped OAuth2 JWT tokens.                         |
-| [actix-web-flash-messages] | [![crates.io](https://img.shields.io/crates/v/actix-web-flash-messages?label=latest)](actix-web-flash-messages) [![dependency status](https://deps.rs/crate/actix-web-flash-messages/0.4.1/status.svg)](https://deps.rs/crate/actix-web-flash-messages/0.4.1) | Support for flash messages/one-time notifications in `actix-web`.                                 |
-| [awmp]                     | [![crates.io](https://img.shields.io/crates/v/awmp?label=latest)](awmp) [![dependency status](https://deps.rs/crate/awmp/0.8.1/status.svg)](https://deps.rs/crate/awmp/0.8.1)                                                                                 | An easy to use wrapper around multipart fields for Actix Web.                                     |
-| [tracing-actix-web]        | [![crates.io](https://img.shields.io/crates/v/tracing-actix-web?label=latest)](tracing-actix-web) [![dependency status](https://deps.rs/crate/tracing-actix-web/0.6.0/status.svg)](https://deps.rs/crate/tracing-actix-web/0.6.0)                             | A middleware to collect telemetry data from applications built on top of the actix-web framework. |
-| [actix-ws]                 | [![crates.io](https://img.shields.io/crates/v/actix-ws?label=latest)](actix-ws) [![dependency status](https://deps.rs/crate/actix-ws/0.2.5/status.svg)](https://deps.rs/crate/actix-ws/0.2.5)                                                                 | Actor-less WebSockets for the Actix Runtime.                                                      |
-| [actix-hash]               | [![crates.io](https://img.shields.io/crates/v/actix-hash?label=latest)](actix-hash) [![dependency status](https://deps.rs/crate/actix-hash/0.4.0/status.svg)](https://deps.rs/crate/actix-hash/0.4.0)                                                         | Hashing utilities for Actix Web.                                                                  |
+| [actix-web-lab]            | [![crates.io](https://img.shields.io/crates/v/actix-web-lab?label=latest)][actix-web-lab] [![dependency status](https://deps.rs/crate/actix-web-lab/0.16.4/status.svg)](https://deps.rs/crate/actix-web-lab/0.16.4)                                           | Experimental extractors, middleware, and other extras for possible inclusion in Actix Web.        |
+| [actix-multipart-extract]  | [![crates.io](https://img.shields.io/crates/v/actix-multipart-extract?label=latest)][actix-multipart-extract] [![dependency status](https://deps.rs/crate/actix-multipart-extract/0.1.4/status.svg)](https://deps.rs/crate/actix-multipart-extract/0.1.4)     | Better multipart form support for Actix Web.                                                      |
+| [actix-form-data]          | [![crates.io](https://img.shields.io/crates/v/actix-form-data?label=latest)][actix-form-data] [![dependency status](https://deps.rs/crate/actix-form-data/0.6.2/status.svg)](https://deps.rs/crate/actix-form-data/0.6.2)                                     | Rate-limiting backed by form-data.                                                                |
+| [actix-governor]           | [![crates.io](https://img.shields.io/crates/v/actix-governor?label=latest)][actix-governor] [![dependency status](https://deps.rs/crate/actix-governor/0.3.0/status.svg)](https://deps.rs/crate/actix-governor/0.3.0)                                         | Rate-limiting backed by governor.                                                                 |
+| [actix-casbin]             | [![crates.io](https://img.shields.io/crates/v/actix-casbin?label=latest)][actix-casbin] [![dependency status](https://deps.rs/crate/actix-casbin/0.4.2/status.svg)](https://deps.rs/crate/actix-casbin/0.4.2)                                                 | Authorization library that supports access control models like ACL, RBAC & ABAC.                  |
+| [actix-ip-filter]          | [![crates.io](https://img.shields.io/crates/v/actix-ip-filter?label=latest)][actix-ip-filter] [![dependency status](https://deps.rs/crate/actix-ip-filter/0.3.1/status.svg)](https://deps.rs/crate/actix-ip-filter/0.3.1)                                     | IP address filter. Supports glob patterns.                                                        |
+| [actix-web-static-files]   | [![crates.io](https://img.shields.io/crates/v/actix-web-static-files?label=latest)][actix-web-static-files] [![dependency status](https://deps.rs/crate/actix-web-static-files/4.0.0/status.svg)](https://deps.rs/crate/actix-web-static-files/4.0.0)         | Static files as embedded resources.                                                               |
+| [actix-web-grants]         | [![crates.io](https://img.shields.io/crates/v/actix-web-grants?label=latest)][actix-web-grants] [![dependency status](https://deps.rs/crate/actix-web-grants/3.0.1/status.svg)](https://deps.rs/crate/actix-web-grants/3.0.1)                                 | Extension for validating user authorities.                                                        |
+| [aliri_actix]              | [![crates.io](https://img.shields.io/crates/v/aliri_actix?label=latest)][aliri_actix] [![dependency status](https://deps.rs/crate/aliri_actix/0.7.0/status.svg)](https://deps.rs/crate/aliri_actix/0.7.0)                                                     | Endpoint authorization and authentication using scoped OAuth2 JWT tokens.                         |
+| [actix-web-flash-messages] | [![crates.io](https://img.shields.io/crates/v/actix-web-flash-messages?label=latest)][actix-web-flash-messages] [![dependency status](https://deps.rs/crate/actix-web-flash-messages/0.4.1/status.svg)](https://deps.rs/crate/actix-web-flash-messages/0.4.1) | Support for flash messages/one-time notifications in `actix-web`.                                 |
+| [awmp]                     | [![crates.io](https://img.shields.io/crates/v/awmp?label=latest)][awmp] [![dependency status](https://deps.rs/crate/awmp/0.8.1/status.svg)](https://deps.rs/crate/awmp/0.8.1)                                                                                 | An easy to use wrapper around multipart fields for Actix Web.                                     |
+| [tracing-actix-web]        | [![crates.io](https://img.shields.io/crates/v/tracing-actix-web?label=latest)][tracing-actix-web] [![dependency status](https://deps.rs/crate/tracing-actix-web/0.6.0/status.svg)](https://deps.rs/crate/tracing-actix-web/0.6.0)                             | A middleware to collect telemetry data from applications built on top of the actix-web framework. |
+| [actix-ws]                 | [![crates.io](https://img.shields.io/crates/v/actix-ws?label=latest)][actix-ws] [![dependency status](https://deps.rs/crate/actix-ws/0.2.5/status.svg)](https://deps.rs/crate/actix-ws/0.2.5)                                                                 | Actor-less WebSockets for the Actix Runtime.                                                      |
+| [actix-hash]               | [![crates.io](https://img.shields.io/crates/v/actix-hash?label=latest)][actix-hash] [![dependency status](https://deps.rs/crate/actix-hash/0.4.0/status.svg)](https://deps.rs/crate/actix-hash/0.4.0)                                                         | Hashing utilities for Actix Web.                                                                  |
 
 To add a crate to this list, submit a pull request.
 

From d5dc087e93cda2e078056a04fb8ded472484ddd6 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Mon, 11 Jul 2022 02:05:22 +0100
Subject: [PATCH 21/31] remove Limiter builder lifetime

---
 actix-limitation/CHANGES.md     |  4 +++-
 actix-limitation/src/builder.rs | 16 ++++++++--------
 actix-limitation/src/lib.rs     |  4 ++--
 3 files changed, 13 insertions(+), 11 deletions(-)

diff --git a/actix-limitation/CHANGES.md b/actix-limitation/CHANGES.md
index d914e8f89..f17fd2176 100644
--- a/actix-limitation/CHANGES.md
+++ b/actix-limitation/CHANGES.md
@@ -1,7 +1,9 @@
 # Changes
 
 ## Unreleased - 2022-xx-xx
-- Updated `session-session` dependency to `0.7`.
+- `Limiter::builder` now takes an `impl Into<String>`.
+- Removed lifetime from `Builder`.
+- Updated `actix-session` dependency to `0.7`.
 
 
 ## 0.2.0 - 2022-03-22
diff --git a/actix-limitation/src/builder.rs b/actix-limitation/src/builder.rs
index 99072fe2d..3bc316037 100644
--- a/actix-limitation/src/builder.rs
+++ b/actix-limitation/src/builder.rs
@@ -4,17 +4,17 @@ use redis::Client;
 
 use crate::{errors::Error, Limiter};
 
-/// Rate limit builder.
+/// Rate limiter builder.
 #[derive(Debug)]
-pub struct Builder<'a> {
-    pub(crate) redis_url: &'a str,
+pub struct Builder {
+    pub(crate) redis_url: String,
     pub(crate) limit: usize,
     pub(crate) period: Duration,
     pub(crate) cookie_name: Cow<'static, str>,
     pub(crate) session_key: Cow<'static, str>,
 }
 
-impl Builder<'_> {
+impl Builder {
     /// Set upper limit.
     pub fn limit(&mut self, limit: usize) -> &mut Self {
         self.limit = limit;
@@ -45,7 +45,7 @@ impl Builder<'_> {
     /// **synchronous** operation.
     pub fn build(&self) -> Result<Limiter, Error> {
         Ok(Limiter {
-            client: Client::open(self.redis_url)?,
+            client: Client::open(self.redis_url.as_str())?,
             limit: self.limit,
             period: self.period,
             cookie_name: self.cookie_name.clone(),
@@ -63,7 +63,7 @@ mod tests {
         let redis_url = "redis://127.0.0.1";
         let period = Duration::from_secs(10);
         let builder = Builder {
-            redis_url,
+            redis_url: redis_url.to_owned(),
             limit: 100,
             period,
             cookie_name: Cow::Owned("session".to_string()),
@@ -82,7 +82,7 @@ mod tests {
         let redis_url = "redis://127.0.0.1";
         let period = Duration::from_secs(20);
         let mut builder = Builder {
-            redis_url,
+            redis_url: redis_url.to_owned(),
             limit: 100,
             period: Duration::from_secs(10),
             session_key: Cow::Borrowed("key"),
@@ -109,7 +109,7 @@ mod tests {
         let redis_url = "127.0.0.1";
         let period = Duration::from_secs(20);
         let mut builder = Builder {
-            redis_url,
+            redis_url: redis_url.to_owned(),
             limit: 100,
             period: Duration::from_secs(10),
             session_key: Cow::Borrowed("key"),
diff --git a/actix-limitation/src/lib.rs b/actix-limitation/src/lib.rs
index 43c02c1d9..35da4e423 100644
--- a/actix-limitation/src/lib.rs
+++ b/actix-limitation/src/lib.rs
@@ -88,9 +88,9 @@ impl Limiter {
     /// See [`redis-rs` docs](https://docs.rs/redis/0.21/redis/#connection-parameters) on connection
     /// parameters for how to set the Redis URL.
     #[must_use]
-    pub fn builder(redis_url: &str) -> Builder<'_> {
+    pub fn builder(redis_url: impl Into<String>) -> Builder {
         Builder {
-            redis_url,
+            redis_url: redis_url.into(),
             limit: DEFAULT_REQUEST_LIMIT,
             period: Duration::from_secs(DEFAULT_PERIOD_SECS),
             cookie_name: Cow::Borrowed(DEFAULT_COOKIE_NAME),

From f39a64f5266c7eef6b9907c06e5ea8de9a519bdf Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Mon, 11 Jul 2022 02:05:40 +0100
Subject: [PATCH 22/31] prepare actix-limitation release 0.3.0

---
 actix-limitation/CHANGES.md | 3 +++
 actix-limitation/Cargo.toml | 2 +-
 actix-limitation/README.md  | 4 ++--
 3 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/actix-limitation/CHANGES.md b/actix-limitation/CHANGES.md
index f17fd2176..83287babd 100644
--- a/actix-limitation/CHANGES.md
+++ b/actix-limitation/CHANGES.md
@@ -1,6 +1,9 @@
 # Changes
 
 ## Unreleased - 2022-xx-xx
+
+
+## 0.3.0 - 2022-07-11
 - `Limiter::builder` now takes an `impl Into<String>`.
 - Removed lifetime from `Builder`.
 - Updated `actix-session` dependency to `0.7`.
diff --git a/actix-limitation/Cargo.toml b/actix-limitation/Cargo.toml
index 11d580c89..ce5da8197 100644
--- a/actix-limitation/Cargo.toml
+++ b/actix-limitation/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "actix-limitation"
-version = "0.2.0"
+version = "0.3.0"
 authors = [
     "0xmad <0xmad@users.noreply.github.com>",
     "Rob Ede <robjtede@icloud.com>",
diff --git a/actix-limitation/README.md b/actix-limitation/README.md
index 309f34b70..41fb93d32 100644
--- a/actix-limitation/README.md
+++ b/actix-limitation/README.md
@@ -4,9 +4,9 @@
 > Originally based on <https://github.com/fnichol/limitation>.
 
 [![crates.io](https://img.shields.io/crates/v/actix-limitation?label=latest)](https://crates.io/crates/actix-limitation)
-[![Documentation](https://docs.rs/actix-limitation/badge.svg?version=0.2.0)](https://docs.rs/actix-limitation/0.2.0)
+[![Documentation](https://docs.rs/actix-limitation/badge.svg?version=0.3.0)](https://docs.rs/actix-limitation/0.3.0)
 ![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-limitation)
-[![Dependency Status](https://deps.rs/crate/actix-limitation/0.2.0/status.svg)](https://deps.rs/crate/actix-limitation/0.2.0)
+[![Dependency Status](https://deps.rs/crate/actix-limitation/0.3.0/status.svg)](https://deps.rs/crate/actix-limitation/0.3.0)
 
 ## Examples
 

From ee71d4cfa78b6e2a3c3381cc2e60d1fc6de578d0 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Mon, 11 Jul 2022 02:15:47 +0100
Subject: [PATCH 23/31] update readme

---
 actix-limitation/README.md  | 2 +-
 actix-limitation/src/lib.rs | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/actix-limitation/README.md b/actix-limitation/README.md
index 41fb93d32..007ef4634 100644
--- a/actix-limitation/README.md
+++ b/actix-limitation/README.md
@@ -13,7 +13,7 @@
 ```toml
 [dependencies]
 actix-web = "4"
-actix-limitation = "0.1.4"
+actix-limitation = "0.3"
 ```
 
 ```rust
diff --git a/actix-limitation/src/lib.rs b/actix-limitation/src/lib.rs
index 35da4e423..7f160a309 100644
--- a/actix-limitation/src/lib.rs
+++ b/actix-limitation/src/lib.rs
@@ -3,7 +3,7 @@
 //! ```toml
 //! [dependencies]
 //! actix-web = "4"
-//! actix-limitation = "0.1.4"
+#![doc = concat!("actix-limitation = \"", env!("CARGO_PKG_VERSION_MAJOR"), ".", env!("CARGO_PKG_VERSION_MINOR"),"\"")]
 //! ```
 //!
 //! ```no_run
@@ -34,7 +34,7 @@
 //!             .app_data(limiter.clone())
 //!             .service(index)
 //!     })
-//!     .bind("127.0.0.1:8080")?
+//!     .bind(("127.0.0.1", 8080))?
 //!     .run()
 //!     .await
 //! }

From d3fb5643809a415662c0c7bb8d57a4ddbc4b6c32 Mon Sep 17 00:00:00 2001
From: Luca Palmieri <lpalmieri@truelayer.com>
Date: Mon, 11 Jul 2022 12:46:49 +0100
Subject: [PATCH 24/31] Add changelog for actix-identity (#258)

---
 actix-identity/CHANGES.md | 34 +++++++++++++++++++++++++++++++++-
 1 file changed, 33 insertions(+), 1 deletion(-)

diff --git a/actix-identity/CHANGES.md b/actix-identity/CHANGES.md
index 5d57b5bac..790b9a50c 100644
--- a/actix-identity/CHANGES.md
+++ b/actix-identity/CHANGES.md
@@ -1,7 +1,39 @@
 # Changes
 
-## Unreleased - 2022-xx-xx
+## 0.5.0 - 2022-xx-xx
+
+`actix-identity:0.5.0` is a complete rewrite. The goal is to streamline user experience and reduce maintenance overhead.  
+
+`actix-identity:0.5.0` is designed as an additional layer on top of `actix-session:0.7.x`, focused on identity management.  
+The identity information is stored in the session state, which is managed by `actix-session` and can be stored using any of the supported `SessionStore` implementations. This reduces the surface area in `actix-identity` (it is no longer concerned with cookies!) and provides a smooth upgrade path for users: if you need to work with sessions, you no longer need to choose between `actix-session` and `actix-identity`; they work together now!
+
+`actix-identity:0.5.0` has feature-parity with `actix-identity:0.4.x` - if you bump into any blocker when upgrading, please open an issue.
+
+Changes:
+
 - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
+- `IdentityService`, `IdentityPolicy` and `CookieIdentityPolicy` have been replaced by `IdentityMiddleware`. [#246]
+- `RequestIdentity` has been replaced by `IdentityExt`. [#246]
+- Trying to extract an `Identity` for an unauthenticated user will return a `401` response to the caller. Extract an `Option<Identity>` or a `Result<Identity, actix_web::Error>` if you need to work with users that may or may not be authenticated [#246]. Example:
+
+```rust
+use actix_web::{http::header::LOCATION, get, HttpResponse, Responder};
+use actix_identity::Identity;
+
+#[get("/")]
+async fn index(user: Option<Identity>) -> impl Responder {
+    if let Some(user) = user {
+        HttpResponse::Ok().finish()
+    } else {
+        // Redirect to login page if unauthenticated
+        HttpResponse::TemporaryRedirect()
+            .insert_header((LOCATION, "/login"))
+            .finish()
+    }
+}
+```
+
+[#246]: https://github.com/actix/actix-extras/pull/246
 
 
 ## 0.4.0 - 2022-03-01

From 603215095a347d571ea266e360fd7370ce174b9f Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Mon, 11 Jul 2022 13:39:14 +0100
Subject: [PATCH 25/31] prepare actix-identity release 0.5.0

---
 actix-identity/CHANGES.md | 49 +++++++++++++++++++++------------------
 actix-identity/Cargo.toml |  2 +-
 actix-identity/README.md  |  4 ++--
 3 files changed, 29 insertions(+), 26 deletions(-)

diff --git a/actix-identity/CHANGES.md b/actix-identity/CHANGES.md
index 790b9a50c..cff585b4b 100644
--- a/actix-identity/CHANGES.md
+++ b/actix-identity/CHANGES.md
@@ -1,37 +1,40 @@
 # Changes
 
-## 0.5.0 - 2022-xx-xx
+## Unreleased - 2022-xx-xx
 
-`actix-identity:0.5.0` is a complete rewrite. The goal is to streamline user experience and reduce maintenance overhead.  
 
-`actix-identity:0.5.0` is designed as an additional layer on top of `actix-session:0.7.x`, focused on identity management.  
-The identity information is stored in the session state, which is managed by `actix-session` and can be stored using any of the supported `SessionStore` implementations. This reduces the surface area in `actix-identity` (it is no longer concerned with cookies!) and provides a smooth upgrade path for users: if you need to work with sessions, you no longer need to choose between `actix-session` and `actix-identity`; they work together now!
+## 0.5.0 - 2022-07-11
+`actix-identity` v0.5 is a complete rewrite. The goal is to streamline user experience and reduce maintenance overhead.
 
-`actix-identity:0.5.0` has feature-parity with `actix-identity:0.4.x` - if you bump into any blocker when upgrading, please open an issue.
+`actix-identity` is now designed as an additional layer on top of `actix-session` v0.7, focused on identity management. The identity information is stored in the session state, which is managed by `actix-session` and can be stored using any of the supported `SessionStore` implementations. This reduces the surface area in `actix-identity` (e.g., it is no longer concerned with cookies!) and provides a smooth upgrade path for users: if you need to work with sessions, you no longer need to choose between `actix-session` and `actix-identity`; they work together now!
+
+`actix-identity` v0.5 has feature-parity with `actix-identity` v0.4; if you bump into any blocker when upgrading, please open an issue.
 
 Changes:
 
 - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
 - `IdentityService`, `IdentityPolicy` and `CookieIdentityPolicy` have been replaced by `IdentityMiddleware`. [#246]
-- `RequestIdentity` has been replaced by `IdentityExt`. [#246]
-- Trying to extract an `Identity` for an unauthenticated user will return a `401` response to the caller. Extract an `Option<Identity>` or a `Result<Identity, actix_web::Error>` if you need to work with users that may or may not be authenticated [#246]. Example:
+- Rename `RequestIdentity` trait to `IdentityExt`. [#246]
+- Trying to extract an `Identity` for an unauthenticated user will return a `401 Unauthorized` response to the client. Extract an `Option<Identity>` or a `Result<Identity, actix_web::Error>` if you need to handle cases where requests may or may not be authenticated. [#246]
+  
+  Example:
+  
+  ```rust
+  use actix_web::{http::header::LOCATION, get, HttpResponse, Responder};
+  use actix_identity::Identity;
 
-```rust
-use actix_web::{http::header::LOCATION, get, HttpResponse, Responder};
-use actix_identity::Identity;
-
-#[get("/")]
-async fn index(user: Option<Identity>) -> impl Responder {
-    if let Some(user) = user {
-        HttpResponse::Ok().finish()
-    } else {
-        // Redirect to login page if unauthenticated
-        HttpResponse::TemporaryRedirect()
-            .insert_header((LOCATION, "/login"))
-            .finish()
-    }
-}
-```
+  #[get("/")]
+  async fn index(user: Option<Identity>) -> impl Responder {
+      if let Some(user) = user {
+          HttpResponse::Ok().finish()
+      } else {
+          // Redirect to login page if unauthenticated
+          HttpResponse::TemporaryRedirect()
+              .insert_header((LOCATION, "/login"))
+              .finish()
+      }
+  }
+  ```
 
 [#246]: https://github.com/actix/actix-extras/pull/246
 
diff --git a/actix-identity/Cargo.toml b/actix-identity/Cargo.toml
index b0081715e..58224d086 100644
--- a/actix-identity/Cargo.toml
+++ b/actix-identity/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "actix-identity"
-version = "0.4.0"
+version = "0.5.0"
 authors = [
     "Nikolay Kim <fafhrd91@gmail.com>",
     "Luca Palmieri <rust@lpalmieri.com>",
diff --git a/actix-identity/README.md b/actix-identity/README.md
index 4dbbe5e85..3e1e85253 100644
--- a/actix-identity/README.md
+++ b/actix-identity/README.md
@@ -3,9 +3,9 @@
 > Identity service for actix-web framework.
 
 [![crates.io](https://img.shields.io/crates/v/actix-identity?label=latest)](https://crates.io/crates/actix-identity)
-[![Documentation](https://docs.rs/actix-identity/badge.svg?version=0.4.0)](https://docs.rs/actix-identity/0.4.0)
+[![Documentation](https://docs.rs/actix-identity/badge.svg?version=0.5.0)](https://docs.rs/actix-identity/0.5.0)
 ![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-identity)
-[![Dependency Status](https://deps.rs/crate/actix-identity/0.4.0/status.svg)](https://deps.rs/crate/actix-identity/0.4.0)
+[![Dependency Status](https://deps.rs/crate/actix-identity/0.5.0/status.svg)](https://deps.rs/crate/actix-identity/0.5.0)
 
 ## Documentation & community resources
 

From d853c115b6710d0bde4d5f7c4e76625b5df93d4d Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Mon, 11 Jul 2022 18:07:26 +0100
Subject: [PATCH 26/31] trim unnecessary identity deps (#259)

* trim unnecessary identity deps

* update changelog
---
 actix-identity/CHANGES.md        | 3 +++
 actix-identity/Cargo.toml        | 7 +++----
 actix-identity/src/middleware.rs | 3 +--
 3 files changed, 7 insertions(+), 6 deletions(-)

diff --git a/actix-identity/CHANGES.md b/actix-identity/CHANGES.md
index cff585b4b..984810b36 100644
--- a/actix-identity/CHANGES.md
+++ b/actix-identity/CHANGES.md
@@ -1,6 +1,9 @@
 # Changes
 
 ## Unreleased - 2022-xx-xx
+- Remove unnecessary dependencies. [#259]
+
+[#259]: https://github.com/actix/actix-extras/pull/259
 
 
 ## 0.5.0 - 2022-07-11
diff --git a/actix-identity/Cargo.toml b/actix-identity/Cargo.toml
index 58224d086..2bba40808 100644
--- a/actix-identity/Cargo.toml
+++ b/actix-identity/Cargo.toml
@@ -23,16 +23,15 @@ actix-utils = "3"
 actix-web = { version = "4", default-features = false, features = ["cookies", "secure-cookies"] }
 
 anyhow = "1"
-env_logger = "0.9"
 futures-core = "0.3.7"
 serde = { version = "1", features = ["derive"] }
-serde_json = "1"
-time = "0.3"
 tracing = { version = "0.1.30", default-features = false, features = ["log"] }
 
 [dev-dependencies]
 actix-http = "3"
 actix-web = { version = "4", default_features = false, features = ["macros", "cookies", "secure-cookies"] }
 actix-session = { version = "0.7", features = ["redis-rs-session", "cookie-session"] }
-uuid = { version = "1", features = ["v4"] }
+
+env_logger = "0.9"
 reqwest = { version = "0.11", default_features = false, features = ["cookies", "json"] }
+uuid = { version = "1", features = ["v4"] }
diff --git a/actix-identity/src/middleware.rs b/actix-identity/src/middleware.rs
index f65f48faa..d281f481c 100644
--- a/actix-identity/src/middleware.rs
+++ b/actix-identity/src/middleware.rs
@@ -4,12 +4,11 @@ use actix_session::SessionExt;
 use actix_utils::future::{ready, Ready};
 use actix_web::{
     body::MessageBody,
-    cookie::time::format_description::well_known::Rfc3339,
+    cookie::time::{format_description::well_known::Rfc3339, OffsetDateTime},
     dev::{Service, ServiceRequest, ServiceResponse, Transform},
     Error, HttpMessage as _, Result,
 };
 use futures_core::future::LocalBoxFuture;
-use time::OffsetDateTime;
 
 use crate::{
     config::{Configuration, IdentityMiddlewareBuilder},

From 1cc37c371ece2f7849ddfa582a689bf7eacbffc2 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Mon, 11 Jul 2022 18:07:50 +0100
Subject: [PATCH 27/31] prepare actix-identity release 0.5.1

---
 actix-identity/CHANGES.md | 3 +++
 actix-identity/Cargo.toml | 2 +-
 actix-identity/README.md  | 4 ++--
 3 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/actix-identity/CHANGES.md b/actix-identity/CHANGES.md
index 984810b36..b1a0c5f31 100644
--- a/actix-identity/CHANGES.md
+++ b/actix-identity/CHANGES.md
@@ -1,6 +1,9 @@
 # Changes
 
 ## Unreleased - 2022-xx-xx
+
+
+## 0.5.1 - 2022-07-11
 - Remove unnecessary dependencies. [#259]
 
 [#259]: https://github.com/actix/actix-extras/pull/259
diff --git a/actix-identity/Cargo.toml b/actix-identity/Cargo.toml
index 2bba40808..1bf888f67 100644
--- a/actix-identity/Cargo.toml
+++ b/actix-identity/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "actix-identity"
-version = "0.5.0"
+version = "0.5.1"
 authors = [
     "Nikolay Kim <fafhrd91@gmail.com>",
     "Luca Palmieri <rust@lpalmieri.com>",
diff --git a/actix-identity/README.md b/actix-identity/README.md
index 3e1e85253..1e77552a2 100644
--- a/actix-identity/README.md
+++ b/actix-identity/README.md
@@ -3,9 +3,9 @@
 > Identity service for actix-web framework.
 
 [![crates.io](https://img.shields.io/crates/v/actix-identity?label=latest)](https://crates.io/crates/actix-identity)
-[![Documentation](https://docs.rs/actix-identity/badge.svg?version=0.5.0)](https://docs.rs/actix-identity/0.5.0)
+[![Documentation](https://docs.rs/actix-identity/badge.svg?version=0.5.1)](https://docs.rs/actix-identity/0.5.1)
 ![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-identity)
-[![Dependency Status](https://deps.rs/crate/actix-identity/0.5.0/status.svg)](https://deps.rs/crate/actix-identity/0.5.0)
+[![Dependency Status](https://deps.rs/crate/actix-identity/0.5.1/status.svg)](https://deps.rs/crate/actix-identity/0.5.1)
 
 ## Documentation & community resources
 

From 1089faaf932ad6c57d35766c7340ecf18cef5fd4 Mon Sep 17 00:00:00 2001
From: Luca Palmieri <lpalmieri@truelayer.com>
Date: Tue, 19 Jul 2022 01:31:31 +0100
Subject: [PATCH 28/31] [actix-identity] Fix visit deadline (#263)

---
 actix-identity/CHANGES.md                       |  4 +++-
 actix-identity/src/identity.rs                  | 17 +++++++++++++----
 actix-identity/src/middleware.rs                |  8 ++++++++
 actix-identity/tests/integration/integration.rs | 17 +++++++++++++++++
 4 files changed, 41 insertions(+), 5 deletions(-)

diff --git a/actix-identity/CHANGES.md b/actix-identity/CHANGES.md
index b1a0c5f31..6a2355a14 100644
--- a/actix-identity/CHANGES.md
+++ b/actix-identity/CHANGES.md
@@ -1,7 +1,9 @@
 # Changes
 
 ## Unreleased - 2022-xx-xx
-
+- Fix visit deadline. [#263]
+ 
+[#263]: https://github.com/actix/actix-extras/pull/263
 
 ## 0.5.1 - 2022-07-11
 - Remove unnecessary dependencies. [#259]
diff --git a/actix-identity/src/identity.rs b/actix-identity/src/identity.rs
index 7a95a8d0b..0b6342736 100644
--- a/actix-identity/src/identity.rs
+++ b/actix-identity/src/identity.rs
@@ -152,10 +152,13 @@ impl Identity {
     pub fn login(ext: &Extensions, id: String) -> Result<Self, anyhow::Error> {
         let inner = IdentityInner::extract(ext);
         inner.session.insert(ID_KEY, id)?;
-        inner.session.insert(
-            LOGIN_UNIX_TIMESTAMP_KEY,
-            OffsetDateTime::now_utc().unix_timestamp(),
-        )?;
+        let now = OffsetDateTime::now_utc().unix_timestamp();
+        if inner.is_login_deadline_enabled {
+            inner.session.insert(LOGIN_UNIX_TIMESTAMP_KEY, now)?;
+        }
+        if inner.is_visit_deadline_enabled {
+            inner.session.insert(LAST_VISIT_UNIX_TIMESTAMP_KEY, now)?;
+        }
         inner.session.renew();
         Ok(Self(inner))
     }
@@ -220,6 +223,12 @@ impl Identity {
             .transpose()
             .map_err(anyhow::Error::from)
     }
+
+    pub(crate) fn set_last_visited_at(&self) -> Result<(), anyhow::Error> {
+        let now = OffsetDateTime::now_utc().unix_timestamp();
+        self.0.session.insert(LAST_VISIT_UNIX_TIMESTAMP_KEY, now)?;
+        Ok(())
+    }
 }
 
 /// Extractor implementation for [`Identity`].
diff --git a/actix-identity/src/middleware.rs b/actix-identity/src/middleware.rs
index d281f481c..893e623f6 100644
--- a/actix-identity/src/middleware.rs
+++ b/actix-identity/src/middleware.rs
@@ -162,6 +162,14 @@ fn enforce_policies(req: &ServiceRequest, configuration: &Configuration) {
         ) {
             identity.logout();
             return;
+        } else {
+            if let Err(err) = identity.set_last_visited_at() {
+                tracing::warn!(
+                    error.display = %err,
+                    error.debug = ?err,
+                    "Failed to set the last visited timestamp on `Identity` for an incoming request."
+                );
+            }
         }
     }
 }
diff --git a/actix-identity/tests/integration/integration.rs b/actix-identity/tests/integration/integration.rs
index 71d6dc5ba..9753d4e2d 100644
--- a/actix-identity/tests/integration/integration.rs
+++ b/actix-identity/tests/integration/integration.rs
@@ -147,6 +147,23 @@ async fn login_deadline_does_not_log_users_out_before_their_time() {
     assert_eq!(body.user_id, Some(user_id));
 }
 
+#[actix_web::test]
+async fn visit_deadline_does_not_log_users_out_before_their_time() {
+    // 1 hour
+    let visit_deadline = Duration::from_secs(60 * 60);
+    let app = TestApp::spawn_with_config(
+        IdentityMiddleware::builder().visit_deadline(Some(visit_deadline)),
+    );
+    let user_id = user_id();
+
+    // Log-in
+    let body = app.post_login(user_id.clone()).await;
+    assert_eq!(body.user_id, Some(user_id.clone()));
+
+    let body = app.get_current().await;
+    assert_eq!(body.user_id, Some(user_id));
+}
+
 #[actix_web::test]
 async fn user_is_logged_out_when_visit_deadline_is_elapsed() {
     let visit_deadline = Duration::from_millis(10);

From 553c2bfb92695552cce70fc6da2c6b0b77a2f251 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Tue, 19 Jul 2022 01:33:53 +0100
Subject: [PATCH 29/31] prepare actix-identity release 0.5.2

---
 actix-identity/CHANGES.md | 6 +++++-
 actix-identity/Cargo.toml | 2 +-
 actix-identity/README.md  | 4 ++--
 3 files changed, 8 insertions(+), 4 deletions(-)

diff --git a/actix-identity/CHANGES.md b/actix-identity/CHANGES.md
index 6a2355a14..1487b3129 100644
--- a/actix-identity/CHANGES.md
+++ b/actix-identity/CHANGES.md
@@ -1,10 +1,14 @@
 # Changes
 
 ## Unreleased - 2022-xx-xx
+
+
+## 0.5.2 - 2022-07-19
 - Fix visit deadline. [#263]
- 
+
 [#263]: https://github.com/actix/actix-extras/pull/263
 
+
 ## 0.5.1 - 2022-07-11
 - Remove unnecessary dependencies. [#259]
 
diff --git a/actix-identity/Cargo.toml b/actix-identity/Cargo.toml
index 1bf888f67..fd82e024e 100644
--- a/actix-identity/Cargo.toml
+++ b/actix-identity/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "actix-identity"
-version = "0.5.1"
+version = "0.5.2"
 authors = [
     "Nikolay Kim <fafhrd91@gmail.com>",
     "Luca Palmieri <rust@lpalmieri.com>",
diff --git a/actix-identity/README.md b/actix-identity/README.md
index 1e77552a2..53386d936 100644
--- a/actix-identity/README.md
+++ b/actix-identity/README.md
@@ -3,9 +3,9 @@
 > Identity service for actix-web framework.
 
 [![crates.io](https://img.shields.io/crates/v/actix-identity?label=latest)](https://crates.io/crates/actix-identity)
-[![Documentation](https://docs.rs/actix-identity/badge.svg?version=0.5.1)](https://docs.rs/actix-identity/0.5.1)
+[![Documentation](https://docs.rs/actix-identity/badge.svg?version=0.5.2)](https://docs.rs/actix-identity/0.5.2)
 ![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-identity)
-[![Dependency Status](https://deps.rs/crate/actix-identity/0.5.1/status.svg)](https://deps.rs/crate/actix-identity/0.5.1)
+[![Dependency Status](https://deps.rs/crate/actix-identity/0.5.2/status.svg)](https://deps.rs/crate/actix-identity/0.5.2)
 
 ## Documentation & community resources
 

From 417c06b00e0a97fc7163181805892fbf0f44bf79 Mon Sep 17 00:00:00 2001
From: Roland <rolandma@outlook.com>
Date: Tue, 19 Jul 2022 08:40:01 +0800
Subject: [PATCH 30/31] fix: broken stream when authentication failed (#260)

* fix: broken http stream when authentication failed

Signed-off-by: Roland Ma <rolandma@outlook.com>

* remove unchanged

Signed-off-by: Roland Ma <rolandma@outlook.com>

* Update CHANGES.md

* Update CHANGES.md

* Update CHANGES.md

Co-authored-by: Rob Ede <robjtede@icloud.com>
---
 actix-web-httpauth/CHANGES.md             |  3 ++
 actix-web-httpauth/examples/middleware.rs |  5 ++-
 actix-web-httpauth/examples/with-cors.rs  |  2 +-
 actix-web-httpauth/src/middleware.rs      | 37 ++++++++++++-----------
 4 files changed, 28 insertions(+), 19 deletions(-)

diff --git a/actix-web-httpauth/CHANGES.md b/actix-web-httpauth/CHANGES.md
index 86a6b5c25..d7bdbcbbd 100644
--- a/actix-web-httpauth/CHANGES.md
+++ b/actix-web-httpauth/CHANGES.md
@@ -1,8 +1,11 @@
 # Changes
 
 ## Unreleased - 2022-xx-xx
+- Auth validator functions now need to return `(Error, ServiceRequest)` in error cases. [#260]
 - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
 
+[#260]: https://github.com/actix/actix-extras/pull/260
+
 
 ## 0.6.0 - 2022-03-01
 - Update `actix-web` dependency to `4`.
diff --git a/actix-web-httpauth/examples/middleware.rs b/actix-web-httpauth/examples/middleware.rs
index 4df4c0341..487f96346 100644
--- a/actix-web-httpauth/examples/middleware.rs
+++ b/actix-web-httpauth/examples/middleware.rs
@@ -3,7 +3,10 @@ use actix_web::{middleware, web, App, Error, HttpServer};
 use actix_web_httpauth::extractors::basic::BasicAuth;
 use actix_web_httpauth::middleware::HttpAuthentication;
 
-async fn validator(req: ServiceRequest, _credentials: BasicAuth) -> Result<ServiceRequest, Error> {
+async fn validator(
+    req: ServiceRequest,
+    _credentials: BasicAuth,
+) -> Result<ServiceRequest, (Error, ServiceRequest)> {
     Ok(req)
 }
 
diff --git a/actix-web-httpauth/examples/with-cors.rs b/actix-web-httpauth/examples/with-cors.rs
index eb2f890ca..4f034a295 100644
--- a/actix-web-httpauth/examples/with-cors.rs
+++ b/actix-web-httpauth/examples/with-cors.rs
@@ -5,7 +5,7 @@ use actix_web_httpauth::{extractors::bearer::BearerAuth, middleware::HttpAuthent
 async fn ok_validator(
     req: ServiceRequest,
     credentials: BearerAuth,
-) -> Result<ServiceRequest, Error> {
+) -> Result<ServiceRequest, (Error, ServiceRequest)> {
     eprintln!("{:?}", credentials);
     Ok(req)
 }
diff --git a/actix-web-httpauth/src/middleware.rs b/actix-web-httpauth/src/middleware.rs
index 16b0004f4..68a41ea8b 100644
--- a/actix-web-httpauth/src/middleware.rs
+++ b/actix-web-httpauth/src/middleware.rs
@@ -39,7 +39,7 @@ impl<T, F, O> HttpAuthentication<T, F>
 where
     T: AuthExtractor,
     F: Fn(ServiceRequest, T) -> O,
-    O: Future<Output = Result<ServiceRequest, Error>>,
+    O: Future<Output = Result<ServiceRequest, (Error, ServiceRequest)>>,
 {
     /// Construct `HttpAuthentication` middleware with the provided auth extractor `T` and
     /// validation callback `F`.
@@ -54,7 +54,7 @@ where
 impl<F, O> HttpAuthentication<basic::BasicAuth, F>
 where
     F: Fn(ServiceRequest, basic::BasicAuth) -> O,
-    O: Future<Output = Result<ServiceRequest, Error>>,
+    O: Future<Output = Result<ServiceRequest, (Error, ServiceRequest)>>,
 {
     /// Construct `HttpAuthentication` middleware for the HTTP "Basic" authentication scheme.
     ///
@@ -70,7 +70,7 @@ where
     /// async fn validator(
     ///     req: ServiceRequest,
     ///     credentials: BasicAuth,
-    /// ) -> Result<ServiceRequest, Error> {
+    /// ) -> Result<ServiceRequest, (Error, ServiceRequest)> {
     ///     // All users are great and more than welcome!
     ///     Ok(req)
     /// }
@@ -85,7 +85,7 @@ where
 impl<F, O> HttpAuthentication<bearer::BearerAuth, F>
 where
     F: Fn(ServiceRequest, bearer::BearerAuth) -> O,
-    O: Future<Output = Result<ServiceRequest, Error>>,
+    O: Future<Output = Result<ServiceRequest, (Error, ServiceRequest)>>,
 {
     /// Construct `HttpAuthentication` middleware for the HTTP "Bearer" authentication scheme.
     ///
@@ -96,7 +96,7 @@ where
     /// # use actix_web_httpauth::middleware::HttpAuthentication;
     /// # use actix_web_httpauth::extractors::bearer::{Config, BearerAuth};
     /// # use actix_web_httpauth::extractors::{AuthenticationError, AuthExtractorConfig};
-    /// async fn validator(req: ServiceRequest, credentials: BearerAuth) -> Result<ServiceRequest, Error> {
+    /// async fn validator(req: ServiceRequest, credentials: BearerAuth) -> Result<ServiceRequest, (Error, ServiceRequest)> {
     ///     if credentials.token() == "mF_9.B5f-4.1JqM" {
     ///         Ok(req)
     ///     } else {
@@ -105,7 +105,7 @@ where
     ///             .unwrap_or_else(Default::default)
     ///             .scope("urn:example:channel=HBO&urn:example:rating=G,PG-13");
     ///
-    ///         Err(AuthenticationError::from(config).into())
+    ///         Err((AuthenticationError::from(config).into(), req))
     ///     }
     /// }
     ///
@@ -121,7 +121,7 @@ where
     S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
     S::Future: 'static,
     F: Fn(ServiceRequest, T) -> O + 'static,
-    O: Future<Output = Result<ServiceRequest, Error>> + 'static,
+    O: Future<Output = Result<ServiceRequest, (Error, ServiceRequest)>> + 'static,
     T: AuthExtractor + 'static,
     B: MessageBody + 'static,
 {
@@ -155,7 +155,7 @@ where
     S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
     S::Future: 'static,
     F: Fn(ServiceRequest, T) -> O + 'static,
-    O: Future<Output = Result<ServiceRequest, Error>> + 'static,
+    O: Future<Output = Result<ServiceRequest, (Error, ServiceRequest)>> + 'static,
     T: AuthExtractor + 'static,
     B: MessageBody + 'static,
 {
@@ -178,9 +178,12 @@ where
                 }
             };
 
-            // TODO: alter to remove ? operator; an error response is required for downstream
-            // middleware to do their thing (eg. cors adding headers)
-            let req = process_fn(req, credentials).await?;
+            let req = match process_fn(req, credentials).await {
+                Ok(req) => req,
+                Err((err, req)) => {
+                    return Ok(req.error_response(err).map_into_right_body());
+                }
+            };
 
             service.call(req).await.map(|res| res.map_into_left_body())
         }
@@ -362,10 +365,10 @@ mod tests {
     #[actix_web::test]
     async fn test_middleware_works_with_app() {
         async fn validator(
-            _req: ServiceRequest,
+            req: ServiceRequest,
             _credentials: BasicAuth,
-        ) -> Result<ServiceRequest, actix_web::Error> {
-            Err(ErrorForbidden("You are not welcome!"))
+        ) -> Result<ServiceRequest, (actix_web::Error, ServiceRequest)> {
+            Err((ErrorForbidden("You are not welcome!"), req))
         }
         let middleware = HttpAuthentication::basic(validator);
 
@@ -387,10 +390,10 @@ mod tests {
     #[actix_web::test]
     async fn test_middleware_works_with_scope() {
         async fn validator(
-            _req: ServiceRequest,
+            req: ServiceRequest,
             _credentials: BasicAuth,
-        ) -> Result<ServiceRequest, actix_web::Error> {
-            Err(ErrorForbidden("You are not welcome!"))
+        ) -> Result<ServiceRequest, (actix_web::Error, ServiceRequest)> {
+            Err((ErrorForbidden("You are not welcome!"), req))
         }
         let middleware = actix_web::middleware::Compat::new(HttpAuthentication::basic(validator));
 

From fbae63d07f92648bc80dd4e13c939a3959404a7c Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Tue, 19 Jul 2022 01:52:04 +0100
Subject: [PATCH 31/31] prepare actix-web-httpauth release 0.7.0

---
 actix-web-httpauth/CHANGES.md | 3 +++
 actix-web-httpauth/Cargo.toml | 2 +-
 actix-web-httpauth/README.md  | 4 ++--
 3 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/actix-web-httpauth/CHANGES.md b/actix-web-httpauth/CHANGES.md
index d7bdbcbbd..1b72cee94 100644
--- a/actix-web-httpauth/CHANGES.md
+++ b/actix-web-httpauth/CHANGES.md
@@ -1,6 +1,9 @@
 # Changes
 
 ## Unreleased - 2022-xx-xx
+
+
+## 0.7.0 - 2022-07-19
 - Auth validator functions now need to return `(Error, ServiceRequest)` in error cases. [#260]
 - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
 
diff --git a/actix-web-httpauth/Cargo.toml b/actix-web-httpauth/Cargo.toml
index cebb395bf..98bc14721 100644
--- a/actix-web-httpauth/Cargo.toml
+++ b/actix-web-httpauth/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "actix-web-httpauth"
-version = "0.6.0"
+version = "0.7.0"
 authors = [
     "svartalf <self@svartalf.info>",
     "Yuki Okushi <huyuumi.dev@gmail.com>",
diff --git a/actix-web-httpauth/README.md b/actix-web-httpauth/README.md
index 6b7f0b21d..1d05158b8 100644
--- a/actix-web-httpauth/README.md
+++ b/actix-web-httpauth/README.md
@@ -3,9 +3,9 @@
 > HTTP authentication schemes for [actix-web](https://github.com/actix/actix-web).
 
 [![crates.io](https://img.shields.io/crates/v/actix-web-httpauth?label=latest)](https://crates.io/crates/actix-web-httpauth)
-[![Documentation](https://docs.rs/actix-web-httpauth/badge.svg?version=0.6.0)](https://docs.rs/actix-web-httpauth/0.6.0)
+[![Documentation](https://docs.rs/actix-web-httpauth/badge.svg?version=0.7.0)](https://docs.rs/actix-web-httpauth/0.7.0)
 ![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-web-httpauth)
-[![Dependency Status](https://deps.rs/crate/actix-web-httpauth/0.6.0/status.svg)](https://deps.rs/crate/actix-web-httpauth/0.6.0)
+[![Dependency Status](https://deps.rs/crate/actix-web-httpauth/0.7.0/status.svg)](https://deps.rs/crate/actix-web-httpauth/0.7.0)
 
 ## Documentation & Resources