mirror of
https://github.com/actix/actix-extras.git
synced 2025-03-16 02:13:05 +01:00
Merge branch 'master' into master
This commit is contained in:
commit
cb1fbefbb2
@ -1,4 +1,4 @@
|
|||||||
name: CI (master only)
|
name: CI (post-merge)
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@ -34,6 +34,9 @@ jobs:
|
|||||||
profile: minimal
|
profile: minimal
|
||||||
override: true
|
override: true
|
||||||
|
|
||||||
|
- name: Install cargo-hack
|
||||||
|
uses: taiki-e/install-action@cargo-hack
|
||||||
|
|
||||||
- name: Generate Cargo.lock
|
- name: Generate Cargo.lock
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
@ -41,12 +44,6 @@ jobs:
|
|||||||
- name: Cache Dependencies
|
- name: Cache Dependencies
|
||||||
uses: Swatinem/rust-cache@v1.2.0
|
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
|
- name: check minimal
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with: { command: ci-min }
|
with: { command: ci-min }
|
||||||
@ -92,6 +89,9 @@ jobs:
|
|||||||
profile: minimal
|
profile: minimal
|
||||||
override: true
|
override: true
|
||||||
|
|
||||||
|
- name: Install cargo-hack
|
||||||
|
uses: taiki-e/install-action@cargo-hack
|
||||||
|
|
||||||
- name: Generate Cargo.lock
|
- name: Generate Cargo.lock
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
@ -99,12 +99,6 @@ jobs:
|
|||||||
- name: Cache Dependencies
|
- name: Cache Dependencies
|
||||||
uses: Swatinem/rust-cache@v1.2.0
|
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
|
- name: check minimal
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with: { command: ci-min }
|
with: { command: ci-min }
|
22
.github/workflows/ci.yml
vendored
22
.github/workflows/ci.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
|||||||
target:
|
target:
|
||||||
- { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu }
|
- { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu }
|
||||||
version:
|
version:
|
||||||
- 1.54.0 # MSRV
|
- 1.57 # MSRV
|
||||||
- stable
|
- stable
|
||||||
|
|
||||||
name: ${{ matrix.target.name }} / ${{ matrix.version }}
|
name: ${{ matrix.target.name }} / ${{ matrix.version }}
|
||||||
@ -42,6 +42,9 @@ jobs:
|
|||||||
profile: minimal
|
profile: minimal
|
||||||
override: true
|
override: true
|
||||||
|
|
||||||
|
- name: Install cargo-hack
|
||||||
|
uses: taiki-e/install-action@cargo-hack
|
||||||
|
|
||||||
- name: Generate Cargo.lock
|
- name: Generate Cargo.lock
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
@ -49,12 +52,6 @@ jobs:
|
|||||||
- name: Cache Dependencies
|
- name: Cache Dependencies
|
||||||
uses: Swatinem/rust-cache@v1.2.0
|
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
|
- name: check minimal
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with: { command: ci-min }
|
with: { command: ci-min }
|
||||||
@ -85,7 +82,7 @@ jobs:
|
|||||||
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
|
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
|
||||||
- { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc }
|
- { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc }
|
||||||
version:
|
version:
|
||||||
- 1.54.0 # MSRV
|
- 1.57 # MSRV
|
||||||
- stable
|
- stable
|
||||||
|
|
||||||
name: ${{ matrix.target.name }} / ${{ matrix.version }}
|
name: ${{ matrix.target.name }} / ${{ matrix.version }}
|
||||||
@ -101,6 +98,9 @@ jobs:
|
|||||||
profile: minimal
|
profile: minimal
|
||||||
override: true
|
override: true
|
||||||
|
|
||||||
|
- name: Install cargo-hack
|
||||||
|
uses: taiki-e/install-action@cargo-hack
|
||||||
|
|
||||||
- name: Generate Cargo.lock
|
- name: Generate Cargo.lock
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
@ -108,12 +108,6 @@ jobs:
|
|||||||
- name: Cache Dependencies
|
- name: Cache Dependencies
|
||||||
uses: Swatinem/rust-cache@v1.2.0
|
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
|
- name: check minimal
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with: { command: ci-min }
|
with: { command: ci-min }
|
||||||
|
@ -10,9 +10,6 @@ members = [
|
|||||||
"actix-web-httpauth",
|
"actix-web-httpauth",
|
||||||
]
|
]
|
||||||
|
|
||||||
# TODO: move this example to examples repo
|
|
||||||
# "actix-protobuf/examples/prost-example",
|
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
actix-cors = { path = "./actix-cors" }
|
actix-cors = { path = "./actix-cors" }
|
||||||
actix-identity = { path = "./actix-identity" }
|
actix-identity = { path = "./actix-identity" }
|
||||||
|
72
README.md
72
README.md
@ -25,21 +25,22 @@
|
|||||||
|
|
||||||
These crates are provided by the community.
|
These crates are provided by the community.
|
||||||
|
|
||||||
| Crate | | |
|
| Crate | | |
|
||||||
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
||||||
| [actix-web-lab] | [](https://crates.io/crates/actix-web-lab) [](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] | [][actix-web-lab] [](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] | [](https://crates.io/crates/actix-form-data) [](https://deps.rs/crate/actix-form-data/0.6.2) | Rate-limiting backed by form-data. |
|
| [actix-multipart-extract] | [][actix-multipart-extract] [](https://deps.rs/crate/actix-multipart-extract/0.1.4) | Better multipart form support for Actix Web. |
|
||||||
| [actix-governor] | [](https://crates.io/crates/actix-governor) [](https://deps.rs/crate/actix-governor/0.3.0) | Rate-limiting backed by governor. |
|
| [actix-form-data] | [][actix-form-data] [](https://deps.rs/crate/actix-form-data/0.6.2) | Rate-limiting backed by form-data. |
|
||||||
| [actix-casbin] | [](https://crates.io/crates/actix-casbin) [](https://deps.rs/crate/actix-casbin/0.4.2) | Authorization library that supports access control models like ACL, RBAC & ABAC. |
|
| [actix-governor] | [][actix-governor] [](https://deps.rs/crate/actix-governor/0.3.0) | Rate-limiting backed by governor. |
|
||||||
| [actix-ip-filter] | [](https://crates.io/crates/actix-ip-filter) [](https://deps.rs/crate/actix-ip-filter/0.3.1) | IP address filter. Supports glob patterns. |
|
| [actix-casbin] | [][actix-casbin] [](https://deps.rs/crate/actix-casbin/0.4.2) | Authorization library that supports access control models like ACL, RBAC & ABAC. |
|
||||||
| [actix-web-static-files] | [](https://crates.io/crates/actix-web-static-files) [](https://deps.rs/crate/actix-web-static-files/4.0.0) | Static files as embedded resources. |
|
| [actix-ip-filter] | [][actix-ip-filter] [](https://deps.rs/crate/actix-ip-filter/0.3.1) | IP address filter. Supports glob patterns. |
|
||||||
| [actix-web-grants] | [](https://crates.io/crates/actix-web-grants) [](https://deps.rs/crate/actix-web-grants/3.0.0-beta.6) | Extension for validating user authorities. |
|
| [actix-web-static-files] | [][actix-web-static-files] [](https://deps.rs/crate/actix-web-static-files/4.0.0) | Static files as embedded resources. |
|
||||||
| [aliri_actix] | [](https://crates.io/crates/aliri_actix) [](https://deps.rs/crate/aliri_actix/0.6.0) | Endpoint authorization and authentication using scoped OAuth2 JWT tokens. |
|
| [actix-web-grants] | [][actix-web-grants] [](https://deps.rs/crate/actix-web-grants/3.0.1) | Extension for validating user authorities. |
|
||||||
| [actix-web-flash-messages] | [](https://crates.io/crates/actix-web-flash-messages) [](https://deps.rs/crate/actix-web-flash-messages/0.3.2) | Support for flash messages/one-time notifications in `actix-web`. |
|
| [aliri_actix] | [][aliri_actix] [](https://deps.rs/crate/aliri_actix/0.7.0) | Endpoint authorization and authentication using scoped OAuth2 JWT tokens. |
|
||||||
| [awmp] | [](https://crates.io/crates/awmp) [](https://deps.rs/crate/awmp/0.8.1) | An easy to use wrapper around multipart fields for Actix Web. |
|
| [actix-web-flash-messages] | [][actix-web-flash-messages] [](https://deps.rs/crate/actix-web-flash-messages/0.4.1) | Support for flash messages/one-time notifications in `actix-web`. |
|
||||||
| [tracing-actix-web] | [](https://crates.io/crates/tracing-actix-web) [](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. |
|
| [awmp] | [][awmp] [](https://deps.rs/crate/awmp/0.8.1) | An easy to use wrapper around multipart fields for Actix Web. |
|
||||||
| [actix-ws] | [](https://crates.io/crates/actix-ws) [](https://deps.rs/crate/actix-ws/0.2.5) | Actor-less Websockets for the Actix Runtime. |
|
| [tracing-actix-web] | [][tracing-actix-web] [](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-hash] | [](https://crates.io/crates/actix-hash) [](https://deps.rs/crate/actix-hash/0.3.0) | Hashing utilities for Actix Web. |
|
| [actix-ws] | [][actix-ws] [](https://deps.rs/crate/actix-ws/0.2.5) | Actor-less WebSockets for the Actix Runtime. |
|
||||||
|
| [actix-hash] | [][actix-hash] [](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.
|
To add a crate to this list, submit a pull request.
|
||||||
|
|
||||||
@ -48,23 +49,24 @@ To add a crate to this list, submit a pull request.
|
|||||||
[actix]: https://github.com/actix/actix
|
[actix]: https://github.com/actix/actix
|
||||||
[actix web]: https://github.com/actix/actix-web
|
[actix web]: https://github.com/actix/actix-web
|
||||||
[actix-extras]: https://github.com/actix/actix-extras
|
[actix-extras]: https://github.com/actix/actix-extras
|
||||||
[actix-cors]: actix-cors
|
[actix-cors]: ./actix-cors
|
||||||
[actix-identity]: actix-identity
|
[actix-identity]: ./actix-identity
|
||||||
[actix-limitation]: actix-limitation
|
[actix-limitation]: ./actix-limitation
|
||||||
[actix-protobuf]: actix-protobuf
|
[actix-protobuf]: ./actix-protobuf
|
||||||
[actix-redis]: actix-redis
|
[actix-redis]: ./actix-redis
|
||||||
[actix-session]: actix-session
|
[actix-session]: ./actix-session
|
||||||
[actix-web-httpauth]: actix-web-httpauth
|
[actix-web-httpauth]: ./actix-web-httpauth
|
||||||
[actix-web-lab]: https://github.com/robjtede/actix-web-lab/tree/main/actix-web-lab
|
[actix-web-lab]: https://crates.io/crates/actix-web-lab
|
||||||
[actix-form-data]: https://git.asonix.dog/asonix/actix-form-data
|
[actix-multipart-extract]: https://crates.io/crates/actix-multipart-extract
|
||||||
[actix-casbin]: https://github.com/casbin-rs/actix-casbin
|
[actix-form-data]: https://crates.io/crates/actix-form-data
|
||||||
[actix-ip-filter]: https://github.com/jhen0409/actix-ip-filter
|
[actix-casbin]: https://crates.io/crates/actix-casbin
|
||||||
[actix-web-static-files]: https://github.com/kilork/actix-web-static-files
|
[actix-ip-filter]: https://crates.io/crates/actix-ip-filter
|
||||||
[actix-web-grants]: https://github.com/DDtKey/actix-web-grants
|
[actix-web-static-files]: https://crates.io/crates/actix-web-static-files
|
||||||
[actix-web-flash-messages]: https://github.com/LukeMathWalker/actix-web-flash-messages
|
[actix-web-grants]: https://crates.io/crates/actix-web-grants
|
||||||
[actix-governor]: https://github.com/AaronErhardt/actix-governor
|
[actix-web-flash-messages]: https://crates.io/crates/actix-web-flash-messages
|
||||||
[aliri_actix]: https://github.com/neoeinstein/aliri
|
[actix-governor]: https://crates.io/crates/actix-governor
|
||||||
[awmp]: https://github.com/kardeiz/awmp
|
[aliri_actix]: https://crates.io/crates/aliri_actix
|
||||||
[tracing-actix-web]: https://github.com/LukeMathWalker/tracing-actix-web
|
[awmp]: https://crates.io/crates/awmp
|
||||||
[actix-ws]: https://git.asonix.dog/asonix/actix-actorless-websockets
|
[tracing-actix-web]: https://crates.io/crates/tracing-actix-web
|
||||||
[actix-hash]: https://github.com/robjtede/actix-web-lab/tree/main/actix-hash
|
[actix-ws]: https://crates.io/crates/actix-ws
|
||||||
|
[actix-hash]: https://crates.io/crates/actix-hash
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# Changes
|
# 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
|
## 0.6.1 - 2022-03-07
|
||||||
|
@ -11,4 +11,4 @@
|
|||||||
|
|
||||||
- [API Documentation](https://docs.rs/actix-cors)
|
- [API Documentation](https://docs.rs/actix-cors)
|
||||||
- [Example Project](https://github.com/actix/examples/tree/master/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
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/// An enum signifying that some of type `T` is allowed, or `All` (anything is allowed).
|
/// 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> {
|
pub enum AllOrSome<T> {
|
||||||
/// Everything is allowed. Usually equivalent to the `*` value.
|
/// Everything is allowed. Usually equivalent to the `*` value.
|
||||||
All,
|
All,
|
||||||
|
@ -1,6 +1,54 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2021-xx-xx
|
## 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]
|
||||||
|
|
||||||
|
[#259]: https://github.com/actix/actix-extras/pull/259
|
||||||
|
|
||||||
|
|
||||||
|
## 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` 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]
|
||||||
|
- 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;
|
||||||
|
|
||||||
|
#[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
|
## 0.4.0 - 2022-03-01
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-identity"
|
name = "actix-identity"
|
||||||
version = "0.4.0"
|
version = "0.5.2"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
authors = [
|
||||||
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
|
"Luca Palmieri <rust@lpalmieri.com>",
|
||||||
|
]
|
||||||
description = "Identity service for Actix Web"
|
description = "Identity service for Actix Web"
|
||||||
keywords = ["actix", "auth", "identity", "web", "security"]
|
keywords = ["actix", "auth", "identity", "web", "security"]
|
||||||
homepage = "https://actix.rs"
|
homepage = "https://actix.rs"
|
||||||
@ -15,14 +18,20 @@ path = "src/lib.rs"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-service = "2"
|
actix-service = "2"
|
||||||
|
actix-session = "0.7"
|
||||||
actix-utils = "3"
|
actix-utils = "3"
|
||||||
actix-web = { version = "4", default-features = false, features = ["cookies", "secure-cookies"] }
|
actix-web = { version = "4", default-features = false, features = ["cookies", "secure-cookies"] }
|
||||||
|
|
||||||
futures-util = { version = "0.3.7", default-features = false }
|
anyhow = "1"
|
||||||
|
futures-core = "0.3.7"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
||||||
time = "0.3"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[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-web = { version = "4", default_features = false, features = ["macros", "cookies", "secure-cookies"] }
|
||||||
|
actix-session = { version = "0.7", features = ["redis-rs-session", "cookie-session"] }
|
||||||
|
|
||||||
|
env_logger = "0.9"
|
||||||
|
reqwest = { version = "0.11", default_features = false, features = ["cookies", "json"] }
|
||||||
|
uuid = { version = "1", features = ["v4"] }
|
||||||
|
@ -3,11 +3,11 @@
|
|||||||
> Identity service for actix-web framework.
|
> Identity service for actix-web framework.
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-identity)
|
[](https://crates.io/crates/actix-identity)
|
||||||
[](https://docs.rs/actix-identity/0.4.0)
|
[](https://docs.rs/actix-identity/0.5.2)
|
||||||

|

|
||||||
[](https://deps.rs/crate/actix-identity/0.4.0)
|
[](https://deps.rs/crate/actix-identity/0.5.2)
|
||||||
|
|
||||||
## Documentation & community resources
|
## Documentation & community resources
|
||||||
|
|
||||||
* [API Documentation](https://docs.rs/actix-identity)
|
* [API Documentation](https://docs.rs/actix-identity)
|
||||||
* Minimum Supported Rust Version (MSRV): 1.54
|
* Minimum Supported Rust Version (MSRV): 1.57
|
||||||
|
84
actix-identity/examples/identity.rs
Normal file
84
actix-identity/examples/identity.rs
Normal file
@ -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()
|
||||||
|
}
|
101
actix-identity/src/config.rs
Normal file
101
actix-identity/src/config.rs
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,89 +1,247 @@
|
|||||||
|
use actix_session::Session;
|
||||||
use actix_utils::future::{ready, Ready};
|
use actix_utils::future::{ready, Ready};
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
|
cookie::time::OffsetDateTime,
|
||||||
dev::{Extensions, Payload},
|
dev::{Extensions, Payload},
|
||||||
Error, FromRequest, HttpMessage as _, HttpRequest,
|
http::StatusCode,
|
||||||
|
Error, FromRequest, HttpMessage, HttpRequest, HttpResponse,
|
||||||
};
|
};
|
||||||
|
use anyhow::{anyhow, Context};
|
||||||
|
|
||||||
pub(crate) struct IdentityItem {
|
use crate::config::LogoutBehaviour;
|
||||||
pub(crate) id: Option<String>,
|
|
||||||
pub(crate) changed: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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;
|
/// use actix_identity::Identity;
|
||||||
///
|
///
|
||||||
/// #[get("/")]
|
/// #[get("/")]
|
||||||
/// async fn index(id: Identity) -> impl Responder {
|
/// async fn index(user: Option<Identity>) -> impl Responder {
|
||||||
/// // access request identity
|
/// if let Some(user) = user {
|
||||||
/// if let Some(id) = id.identity() {
|
/// format!("Welcome! {}", user.id().unwrap())
|
||||||
/// format!("Welcome! {}", id)
|
|
||||||
/// } else {
|
/// } else {
|
||||||
/// "Welcome Anonymous!".to_owned()
|
/// "Welcome Anonymous!".to_owned()
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// #[post("/login")]
|
/// #[post("/login")]
|
||||||
/// async fn login(id: Identity) -> impl Responder {
|
/// async fn login(request: HttpRequest) -> impl Responder {
|
||||||
/// // remember identity
|
/// Identity::login(&request.extensions(), "User1".into());
|
||||||
/// id.remember("User1".to_owned());
|
|
||||||
///
|
|
||||||
/// HttpResponse::Ok()
|
/// HttpResponse::Ok()
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// #[post("/logout")]
|
/// #[post("/logout")]
|
||||||
/// async fn logout(id: Identity) -> impl Responder {
|
/// async fn logout(user: Identity) -> impl Responder {
|
||||||
/// // remove identity
|
/// user.logout();
|
||||||
/// id.forget();
|
|
||||||
///
|
|
||||||
/// HttpResponse::Ok()
|
/// 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;
|
/// use actix_identity::Identity;
|
||||||
///
|
///
|
||||||
/// #[get("/")]
|
/// #[get("/")]
|
||||||
/// async fn index(id: Identity) -> impl Responder {
|
/// async fn index(user: Option<Identity>) -> impl Responder {
|
||||||
/// // access request identity
|
/// if let Some(user) = user {
|
||||||
/// if let Some(id) = id.identity() {
|
/// HttpResponse::Ok().finish()
|
||||||
/// format!("Welcome! {}", id)
|
/// } 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)?;
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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`].
|
||||||
|
///
|
||||||
|
/// # 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 {
|
/// } else {
|
||||||
/// "Welcome Anonymous!".to_owned()
|
/// "Welcome Anonymous!".to_owned()
|
||||||
/// }
|
/// }
|
||||||
@ -91,10 +249,17 @@ impl Identity {
|
|||||||
/// ```
|
/// ```
|
||||||
impl FromRequest for Identity {
|
impl FromRequest for Identity {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Future = Ready<Result<Identity, Error>>;
|
type Future = Ready<Result<Self, Self::Error>>;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
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)
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
27
actix-identity/src/identity_ext.rs
Normal file
27
actix-identity/src/identity_ext.rs
Normal file
@ -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())
|
||||||
|
}
|
||||||
|
}
|
@ -1,163 +1,100 @@
|
|||||||
//! Opinionated request identity service for Actix Web apps.
|
//! Identity management for Actix Web.
|
||||||
//!
|
//!
|
||||||
//! [`IdentityService`] middleware can be used with different policies types to store
|
//! `actix-identity` can be used to track identity of a user across multiple requests. It is built
|
||||||
//! identity information.
|
//! 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("/")]
|
//! #[get("/")]
|
||||||
//! async fn index(id: Identity) -> String {
|
//! async fn index(user: Option<Identity>) -> impl Responder {
|
||||||
//! // access request identity
|
//! if let Some(user) = user {
|
||||||
//! if let Some(id) = id.identity() {
|
//! format!("Welcome! {}", user.id().unwrap())
|
||||||
//! format!("Welcome! {}", id)
|
|
||||||
//! } else {
|
//! } else {
|
||||||
//! "Welcome Anonymous!".to_owned()
|
//! "Welcome Anonymous!".to_owned()
|
||||||
//! }
|
//! }
|
||||||
//! }
|
//! }
|
||||||
//!
|
//!
|
||||||
//! #[post("/login")]
|
//! #[post("/login")]
|
||||||
//! async fn login(id: Identity) -> HttpResponse {
|
//! async fn login(request: HttpRequest) -> impl Responder {
|
||||||
//! // remember identity
|
//! // Some kind of authentication should happen here
|
||||||
//! id.remember("User1".to_owned());
|
//! // e.g. password-based, biometric, etc.
|
||||||
//! HttpResponse::Ok().finish()
|
//! // [...]
|
||||||
|
//!
|
||||||
|
//! // attach a verified user identity to the active session
|
||||||
|
//! Identity::login(&request.extensions(), "User1".into()).unwrap();
|
||||||
|
//!
|
||||||
|
//! HttpResponse::Ok()
|
||||||
//! }
|
//! }
|
||||||
//!
|
//!
|
||||||
//! #[post("/logout")]
|
//! #[post("/logout")]
|
||||||
//! async fn logout(id: Identity) -> HttpResponse {
|
//! async fn logout(user: Identity) -> impl Responder {
|
||||||
//! // remove identity
|
//! user.logout();
|
||||||
//! id.forget();
|
//! HttpResponse::Ok()
|
||||||
//! HttpResponse::Ok().finish()
|
|
||||||
//! }
|
//! }
|
||||||
//!
|
|
||||||
//! 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)]
|
#![forbid(unsafe_code)]
|
||||||
|
#![deny(rust_2018_idioms, nonstandard_style, missing_docs)]
|
||||||
#![warn(future_incompatible)]
|
#![warn(future_incompatible)]
|
||||||
|
|
||||||
use std::future::Future;
|
pub mod config;
|
||||||
|
|
||||||
use actix_web::{
|
|
||||||
dev::{ServiceRequest, ServiceResponse},
|
|
||||||
Error, HttpMessage, Result,
|
|
||||||
};
|
|
||||||
|
|
||||||
mod cookie;
|
|
||||||
mod identity;
|
mod identity;
|
||||||
|
mod identity_ext;
|
||||||
mod middleware;
|
mod middleware;
|
||||||
|
|
||||||
pub use self::cookie::CookieIdentityPolicy;
|
|
||||||
pub use self::identity::Identity;
|
pub use self::identity::Identity;
|
||||||
pub use self::middleware::IdentityService;
|
pub use self::identity_ext::IdentityExt;
|
||||||
|
pub use self::middleware::IdentityMiddleware;
|
||||||
/// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,171 +1,258 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use actix_session::SessionExt;
|
||||||
use actix_utils::future::{ready, Ready};
|
use actix_utils::future::{ready, Ready};
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
body::{EitherBody, MessageBody},
|
body::MessageBody,
|
||||||
|
cookie::time::{format_description::well_known::Rfc3339, OffsetDateTime},
|
||||||
dev::{Service, ServiceRequest, ServiceResponse, Transform},
|
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 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;
|
#[derive(Default, Clone)]
|
||||||
/// use actix_identity::{CookieIdentityPolicy, IdentityService};
|
pub struct IdentityMiddleware {
|
||||||
///
|
configuration: Rc<Configuration>,
|
||||||
/// // 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>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> IdentityService<T> {
|
impl IdentityMiddleware {
|
||||||
/// Create new identity service with specified backend.
|
pub(crate) fn new(configuration: Configuration) -> Self {
|
||||||
pub fn new(backend: T) -> Self {
|
Self {
|
||||||
IdentityService {
|
configuration: Rc::new(configuration),
|
||||||
backend: Rc::new(backend),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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
|
where
|
||||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
T: IdentityPolicy,
|
|
||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
{
|
{
|
||||||
type Response = ServiceResponse<EitherBody<B>>;
|
type Response = ServiceResponse<B>;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
type Transform = InnerIdentityMiddleware<S>;
|
||||||
type InitError = ();
|
type InitError = ();
|
||||||
type Transform = IdentityServiceMiddleware<S, T>;
|
|
||||||
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||||
|
|
||||||
fn new_transform(&self, service: S) -> Self::Future {
|
fn new_transform(&self, service: S) -> Self::Future {
|
||||||
ready(Ok(IdentityServiceMiddleware {
|
ready(Ok(InnerIdentityMiddleware {
|
||||||
backend: self.backend.clone(),
|
|
||||||
service: Rc::new(service),
|
service: Rc::new(service),
|
||||||
|
configuration: Rc::clone(&self.configuration),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct IdentityServiceMiddleware<S, T> {
|
#[doc(hidden)]
|
||||||
pub(crate) service: Rc<S>,
|
pub struct InnerIdentityMiddleware<S> {
|
||||||
pub(crate) backend: Rc<T>,
|
service: Rc<S>,
|
||||||
|
configuration: Rc<Configuration>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S, T> Clone for IdentityServiceMiddleware<S, T> {
|
impl<S> Clone for InnerIdentityMiddleware<S> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
backend: Rc::clone(&self.backend),
|
|
||||||
service: Rc::clone(&self.service),
|
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
|
where
|
||||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
T: IdentityPolicy,
|
|
||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
{
|
{
|
||||||
type Response = ServiceResponse<EitherBody<B>>;
|
type Response = ServiceResponse<B>;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||||
|
|
||||||
actix_service::forward_ready!(service);
|
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 srv = Rc::clone(&self.service);
|
||||||
let backend = Rc::clone(&self.backend);
|
let configuration = Rc::clone(&self.configuration);
|
||||||
let fut = self.backend.from_request(&mut req);
|
Box::pin(async move {
|
||||||
|
let identity_inner = IdentityInner {
|
||||||
async move {
|
session: req.get_session(),
|
||||||
match fut.await {
|
logout_behaviour: configuration.on_logout.clone(),
|
||||||
Ok(id) => {
|
is_login_deadline_enabled: configuration.login_deadline.is_some(),
|
||||||
req.extensions_mut()
|
is_visit_deadline_enabled: configuration.visit_deadline.is_some(),
|
||||||
.insert(IdentityItem { id, changed: false });
|
};
|
||||||
|
req.extensions_mut().insert(identity_inner);
|
||||||
let mut res = srv.call(req).await?;
|
enforce_policies(&req, &configuration);
|
||||||
let id = res.request().extensions_mut().remove::<IdentityItem>();
|
srv.call(req).await
|
||||||
|
})
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
// easier to scan with returns where they are
|
||||||
mod tests {
|
// especially if the function body were to evolve in the future
|
||||||
use std::{rc::Rc, time::Duration};
|
#[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;
|
if !must_extract_identity {
|
||||||
use actix_web::{dev, error, test, Error, Result};
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
use super::*;
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
#[actix_web::test]
|
if let Some(login_deadline) = configuration.login_deadline {
|
||||||
async fn test_borrowed_mut_error() {
|
if matches!(
|
||||||
use actix_utils::future::{ok, Ready};
|
enforce_login_deadline(&identity, login_deadline),
|
||||||
use futures_util::future::lazy;
|
PolicyDecision::LogOut
|
||||||
|
) {
|
||||||
|
identity.logout();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct Ident;
|
if let Some(visit_deadline) = configuration.visit_deadline {
|
||||||
impl IdentityPolicy for Ident {
|
if matches!(
|
||||||
type Future = Ready<Result<Option<String>, Error>>;
|
enforce_visit_deadline(&identity, visit_deadline),
|
||||||
type ResponseFuture = Ready<Result<(), Error>>;
|
PolicyDecision::LogOut
|
||||||
|
) {
|
||||||
fn from_request(&self, _: &mut dev::ServiceRequest) -> Self::Future {
|
identity.logout();
|
||||||
ok(Some("test".to_string()))
|
return;
|
||||||
}
|
} else {
|
||||||
|
if let Err(err) = identity.set_last_visited_at() {
|
||||||
fn to_response<B>(
|
tracing::warn!(
|
||||||
&self,
|
error.display = %err,
|
||||||
_: Option<String>,
|
error.debug = ?err,
|
||||||
_: bool,
|
"Failed to set the last visited timestamp on `Identity` for an incoming request."
|
||||||
_: &mut dev::ServiceResponse<B>,
|
);
|
||||||
) -> Self::ResponseFuture {
|
|
||||||
ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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"))
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
17
actix-identity/tests/integration/fixtures.rs
Normal file
17
actix-identity/tests/integration/fixtures.rs
Normal file
@ -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()
|
||||||
|
}
|
185
actix-identity/tests/integration/integration.rs
Normal file
185
actix-identity/tests/integration/integration.rs
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
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 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);
|
||||||
|
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);
|
||||||
|
}
|
3
actix-identity/tests/integration/main.rs
Normal file
3
actix-identity/tests/integration/main.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub mod fixtures;
|
||||||
|
mod integration;
|
||||||
|
pub mod test_app;
|
186
actix-identity/tests/integration/test_app.rs
Normal file
186
actix-identity/tests/integration/test_app.rs
Normal file
@ -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()
|
||||||
|
}
|
@ -3,6 +3,12 @@
|
|||||||
## Unreleased - 2022-xx-xx
|
## 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`.
|
||||||
|
|
||||||
|
|
||||||
## 0.2.0 - 2022-03-22
|
## 0.2.0 - 2022-03-22
|
||||||
- Update Actix Web dependency to v4 ecosystem. [#229]
|
- Update Actix Web dependency to v4 ecosystem. [#229]
|
||||||
- Update Tokio dependencies to v1 ecosystem. [#229]
|
- Update Tokio dependencies to v1 ecosystem. [#229]
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-limitation"
|
name = "actix-limitation"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
authors = [
|
authors = [
|
||||||
"0xmad <0xmad@users.noreply.github.com>",
|
"0xmad <0xmad@users.noreply.github.com>",
|
||||||
"Rob Ede <robjtede@icloud.com>",
|
"Rob Ede <robjtede@icloud.com>",
|
||||||
@ -13,7 +13,7 @@ license = "MIT OR Apache-2.0"
|
|||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-session = "0.5"
|
actix-session = "0.7"
|
||||||
actix-utils = "3"
|
actix-utils = "3"
|
||||||
actix-web = { version = "4", default-features = false }
|
actix-web = { version = "4", default-features = false }
|
||||||
|
|
||||||
@ -25,5 +25,5 @@ time = "0.3"
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-web = "4"
|
actix-web = "4"
|
||||||
uuid = { version = "0.8", features = ["v4"] }
|
|
||||||
static_assertions = "1"
|
static_assertions = "1"
|
||||||
|
uuid = { version = "1", features = ["v4"] }
|
||||||
|
@ -4,16 +4,16 @@
|
|||||||
> Originally based on <https://github.com/fnichol/limitation>.
|
> Originally based on <https://github.com/fnichol/limitation>.
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-limitation)
|
[](https://crates.io/crates/actix-limitation)
|
||||||
[](https://docs.rs/actix-limitation/0.2.0)
|
[](https://docs.rs/actix-limitation/0.3.0)
|
||||||

|

|
||||||
[](https://deps.rs/crate/actix-limitation/0.2.0)
|
[](https://deps.rs/crate/actix-limitation/0.3.0)
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = "4"
|
actix-web = "4"
|
||||||
actix-limitation = "0.1.4"
|
actix-limitation = "0.3"
|
||||||
```
|
```
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
@ -4,17 +4,17 @@ use redis::Client;
|
|||||||
|
|
||||||
use crate::{errors::Error, Limiter};
|
use crate::{errors::Error, Limiter};
|
||||||
|
|
||||||
/// Rate limit builder.
|
/// Rate limiter builder.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Builder<'a> {
|
pub struct Builder {
|
||||||
pub(crate) redis_url: &'a str,
|
pub(crate) redis_url: String,
|
||||||
pub(crate) limit: usize,
|
pub(crate) limit: usize,
|
||||||
pub(crate) period: Duration,
|
pub(crate) period: Duration,
|
||||||
pub(crate) cookie_name: Cow<'static, str>,
|
pub(crate) cookie_name: Cow<'static, str>,
|
||||||
pub(crate) session_key: Cow<'static, str>,
|
pub(crate) session_key: Cow<'static, str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Builder<'_> {
|
impl Builder {
|
||||||
/// Set upper limit.
|
/// Set upper limit.
|
||||||
pub fn limit(&mut self, limit: usize) -> &mut Self {
|
pub fn limit(&mut self, limit: usize) -> &mut Self {
|
||||||
self.limit = limit;
|
self.limit = limit;
|
||||||
@ -45,7 +45,7 @@ impl Builder<'_> {
|
|||||||
/// **synchronous** operation.
|
/// **synchronous** operation.
|
||||||
pub fn build(&self) -> Result<Limiter, Error> {
|
pub fn build(&self) -> Result<Limiter, Error> {
|
||||||
Ok(Limiter {
|
Ok(Limiter {
|
||||||
client: Client::open(self.redis_url)?,
|
client: Client::open(self.redis_url.as_str())?,
|
||||||
limit: self.limit,
|
limit: self.limit,
|
||||||
period: self.period,
|
period: self.period,
|
||||||
cookie_name: self.cookie_name.clone(),
|
cookie_name: self.cookie_name.clone(),
|
||||||
@ -63,7 +63,7 @@ mod tests {
|
|||||||
let redis_url = "redis://127.0.0.1";
|
let redis_url = "redis://127.0.0.1";
|
||||||
let period = Duration::from_secs(10);
|
let period = Duration::from_secs(10);
|
||||||
let builder = Builder {
|
let builder = Builder {
|
||||||
redis_url,
|
redis_url: redis_url.to_owned(),
|
||||||
limit: 100,
|
limit: 100,
|
||||||
period,
|
period,
|
||||||
cookie_name: Cow::Owned("session".to_string()),
|
cookie_name: Cow::Owned("session".to_string()),
|
||||||
@ -82,7 +82,7 @@ mod tests {
|
|||||||
let redis_url = "redis://127.0.0.1";
|
let redis_url = "redis://127.0.0.1";
|
||||||
let period = Duration::from_secs(20);
|
let period = Duration::from_secs(20);
|
||||||
let mut builder = Builder {
|
let mut builder = Builder {
|
||||||
redis_url,
|
redis_url: redis_url.to_owned(),
|
||||||
limit: 100,
|
limit: 100,
|
||||||
period: Duration::from_secs(10),
|
period: Duration::from_secs(10),
|
||||||
session_key: Cow::Borrowed("key"),
|
session_key: Cow::Borrowed("key"),
|
||||||
@ -109,7 +109,7 @@ mod tests {
|
|||||||
let redis_url = "127.0.0.1";
|
let redis_url = "127.0.0.1";
|
||||||
let period = Duration::from_secs(20);
|
let period = Duration::from_secs(20);
|
||||||
let mut builder = Builder {
|
let mut builder = Builder {
|
||||||
redis_url,
|
redis_url: redis_url.to_owned(),
|
||||||
limit: 100,
|
limit: 100,
|
||||||
period: Duration::from_secs(10),
|
period: Duration::from_secs(10),
|
||||||
session_key: Cow::Borrowed("key"),
|
session_key: Cow::Borrowed("key"),
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
//! ```toml
|
//! ```toml
|
||||||
//! [dependencies]
|
//! [dependencies]
|
||||||
//! actix-web = "4"
|
//! actix-web = "4"
|
||||||
//! actix-limitation = "0.1.4"
|
#![doc = concat!("actix-limitation = \"", env!("CARGO_PKG_VERSION_MAJOR"), ".", env!("CARGO_PKG_VERSION_MINOR"),"\"")]
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! ```no_run
|
//! ```no_run
|
||||||
@ -34,7 +34,7 @@
|
|||||||
//! .app_data(limiter.clone())
|
//! .app_data(limiter.clone())
|
||||||
//! .service(index)
|
//! .service(index)
|
||||||
//! })
|
//! })
|
||||||
//! .bind("127.0.0.1:8080")?
|
//! .bind(("127.0.0.1", 8080))?
|
||||||
//! .run()
|
//! .run()
|
||||||
//! .await
|
//! .await
|
||||||
//! }
|
//! }
|
||||||
@ -73,7 +73,7 @@ pub const DEFAULT_COOKIE_NAME: &str = "sid";
|
|||||||
pub const DEFAULT_SESSION_KEY: &str = "rate-api-id";
|
pub const DEFAULT_SESSION_KEY: &str = "rate-api-id";
|
||||||
|
|
||||||
/// Rate limiter.
|
/// Rate limiter.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Limiter {
|
pub struct Limiter {
|
||||||
client: Client,
|
client: Client,
|
||||||
limit: usize,
|
limit: usize,
|
||||||
@ -88,9 +88,9 @@ impl Limiter {
|
|||||||
/// See [`redis-rs` docs](https://docs.rs/redis/0.21/redis/#connection-parameters) on connection
|
/// See [`redis-rs` docs](https://docs.rs/redis/0.21/redis/#connection-parameters) on connection
|
||||||
/// parameters for how to set the Redis URL.
|
/// parameters for how to set the Redis URL.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn builder(redis_url: &str) -> Builder<'_> {
|
pub fn builder(redis_url: impl Into<String>) -> Builder {
|
||||||
Builder {
|
Builder {
|
||||||
redis_url,
|
redis_url: redis_url.into(),
|
||||||
limit: DEFAULT_REQUEST_LIMIT,
|
limit: DEFAULT_REQUEST_LIMIT,
|
||||||
period: Duration::from_secs(DEFAULT_PERIOD_SECS),
|
period: Duration::from_secs(DEFAULT_PERIOD_SECS),
|
||||||
cookie_name: Cow::Borrowed(DEFAULT_COOKIE_NAME),
|
cookie_name: Cow::Borrowed(DEFAULT_COOKIE_NAME),
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use std::{future::Future, pin::Pin, rc::Rc};
|
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_utils::future::{ok, Ready};
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
body::EitherBody,
|
body::EitherBody,
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2021-xx-xx
|
## 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.
|
||||||
|
|
||||||
|
|
||||||
## 0.7.0 - 2022-03-01
|
## 0.7.0 - 2022-03-01
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-protobuf"
|
name = "actix-protobuf"
|
||||||
version = "0.7.0"
|
version = "0.8.0"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
authors = [
|
authors = [
|
||||||
"kingxsp <jin.hb.zh@outlook.com>",
|
"kingxsp <jin.hb.zh@outlook.com>",
|
||||||
"Yuki Okushi <huyuumi.dev@gmail.com>"
|
"Yuki Okushi <huyuumi.dev@gmail.com>",
|
||||||
]
|
]
|
||||||
description = "Protobuf support for Actix web"
|
description = "Protobuf support for Actix Web"
|
||||||
keywords = ["actix", "protobuf", "protocol", "rpc"]
|
keywords = ["actix", "web", "protobuf", "protocol", "rpc"]
|
||||||
homepage = "https://actix.rs"
|
homepage = "https://actix.rs"
|
||||||
repository = "https://github.com/actix/actix-extras.git"
|
repository = "https://github.com/actix/actix-extras.git"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
exclude = [".cargo/config", "/examples/**"]
|
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "actix_protobuf"
|
name = "actix_protobuf"
|
||||||
@ -21,8 +20,8 @@ path = "src/lib.rs"
|
|||||||
actix-web = { version = "4", default_features = false }
|
actix-web = { version = "4", default_features = false }
|
||||||
derive_more = "0.99.5"
|
derive_more = "0.99.5"
|
||||||
futures-util = { version = "0.3.7", default-features = false }
|
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]
|
[dev-dependencies]
|
||||||
actix-web = { version = "4", default_features = false, features = ["macros"] }
|
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"] }
|
||||||
|
@ -3,15 +3,15 @@
|
|||||||
> Protobuf support for Actix Web.
|
> Protobuf support for Actix Web.
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-protobuf)
|
[](https://crates.io/crates/actix-protobuf)
|
||||||
[](https://docs.rs/actix-protobuf/0.7.0)
|
[](https://docs.rs/actix-protobuf/0.8.0)
|
||||||

|

|
||||||
[](https://deps.rs/crate/actix-protobuf/0.7.0)
|
[](https://deps.rs/crate/actix-protobuf/0.8.0)
|
||||||
|
|
||||||
## Documentation & Resources
|
## Documentation & Resources
|
||||||
|
|
||||||
- [API Documentation](https://docs.rs/actix-protobuf)
|
- [API Documentation](https://docs.rs/actix-protobuf)
|
||||||
- [Example Project](https://github.com/actix/examples/tree/master/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
|
## Example
|
||||||
|
|
||||||
|
@ -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"] }
|
|
@ -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()
|
|
@ -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
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
syntax = "proto3";
|
|
||||||
|
|
||||||
message MyObj {
|
|
||||||
int32 number = 1;
|
|
||||||
string name = 2;
|
|
||||||
}
|
|
@ -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)
|
|
@ -1,6 +1,13 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2021-xx-xx
|
## 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`.
|
||||||
|
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
||||||
|
|
||||||
|
|
||||||
## 0.11.0 - 2022-03-15
|
## 0.11.0 - 2022-03-15
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-redis"
|
name = "actix-redis"
|
||||||
version = "0.11.0"
|
version = "0.12.0"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||||
description = "Redis integration for Actix"
|
description = "Redis integration for Actix"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
keywords = ["actix", "redis", "async", "session"]
|
keywords = ["actix", "redis", "async"]
|
||||||
homepage = "https://actix.rs"
|
homepage = "https://actix.rs"
|
||||||
repository = "https://github.com/actix/actix-extras.git"
|
repository = "https://github.com/actix/actix-extras.git"
|
||||||
categories = ["network-programming", "asynchronous"]
|
categories = ["network-programming", "asynchronous"]
|
||||||
exclude = [".cargo/config"]
|
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
all-features = true
|
||||||
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "actix_redis"
|
name = "actix_redis"
|
||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
@ -22,7 +25,7 @@ default = ["web"]
|
|||||||
web = ["actix-web"]
|
web = ["actix-web"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix = { version = "0.12", default-features = false }
|
actix = { version = "0.13", default-features = false }
|
||||||
actix-rt = { version = "2.1", default-features = false }
|
actix-rt = { version = "2.1", default-features = false }
|
||||||
actix-service = "2"
|
actix-service = "2"
|
||||||
actix-tls = { version = "3", default-features = false, features = ["connect"] }
|
actix-tls = { version = "3", default-features = false, features = ["connect"] }
|
||||||
@ -31,10 +34,10 @@ log = "0.4.6"
|
|||||||
backoff = "0.4.0"
|
backoff = "0.4.0"
|
||||||
derive_more = "0.99.5"
|
derive_more = "0.99.5"
|
||||||
futures-core = { version = "0.3.7", default-features = false }
|
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"
|
time = "0.3"
|
||||||
tokio = { version = "1.13.1", features = ["sync"] }
|
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 }
|
actix-web = { version = "4", default_features = false, optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
@ -3,12 +3,12 @@
|
|||||||
> Redis integration for Actix.
|
> Redis integration for Actix.
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-redis)
|
[](https://crates.io/crates/actix-redis)
|
||||||
[](https://docs.rs/actix-redis/0.11.0)
|
[](https://docs.rs/actix-redis/0.12.0)
|
||||||

|

|
||||||
[](https://deps.rs/crate/actix-redis/0.11.0)
|
[](https://deps.rs/crate/actix-redis/0.12.0)
|
||||||
|
|
||||||
## Documentation & Resources
|
## Documentation & Resources
|
||||||
|
|
||||||
- [API Documentation](https://docs.rs/actix-redis)
|
- [API Documentation](https://docs.rs/actix-redis)
|
||||||
- [Example Project](https://github.com/actix/examples/tree/master/auth/redis-session)
|
- [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
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
//! Redis integration for `actix`.
|
//! Redis integration for `actix`.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||||
#![warn(future_incompatible)]
|
#![warn(future_incompatible)]
|
||||||
|
|
||||||
|
@ -6,6 +6,23 @@
|
|||||||
- Add an `empty()` function to `Session` for simple testing
|
- Add an `empty()` function to `Session` for simple testing
|
||||||
|
|
||||||
|
|
||||||
|
## 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]
|
||||||
|
- 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]
|
||||||
|
- 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
|
||||||
|
|
||||||
|
|
||||||
## 0.6.2 - 2022-03-25
|
## 0.6.2 - 2022-03-25
|
||||||
- Implement `SessionExt` for `GuardContext`. [#234]
|
- Implement `SessionExt` for `GuardContext`. [#234]
|
||||||
- `RedisSessionStore` will prevent connection timeouts from causing user-visible errors. [#235]
|
- `RedisSessionStore` will prevent connection timeouts from causing user-visible errors. [#235]
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-session"
|
name = "actix-session"
|
||||||
version = "0.6.2"
|
version = "0.7.0"
|
||||||
authors = [
|
authors = [
|
||||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
"Luca Palmieri <rust@lpalmieri.com>",
|
"Luca Palmieri <rust@lpalmieri.com>",
|
||||||
@ -38,12 +38,11 @@ derive_more = "0.99.5"
|
|||||||
rand = { version = "0.8", optional = true }
|
rand = { version = "0.8", optional = true }
|
||||||
serde = { version = "1" }
|
serde = { version = "1" }
|
||||||
serde_json = { version = "1" }
|
serde_json = { version = "1" }
|
||||||
time = "0.3"
|
|
||||||
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
||||||
|
|
||||||
# redis-actor-session
|
# redis-actor-session
|
||||||
actix = { version = "0.12.0", default-features = false, optional = true }
|
actix = { version = "0.13", default-features = false, optional = true }
|
||||||
actix-redis = { version = "0.11.0", optional = true }
|
actix-redis = { version = "0.12", optional = true }
|
||||||
futures-core = { version = "0.3.7", default-features = false, optional = true }
|
futures-core = { version = "0.3.7", default-features = false, optional = true }
|
||||||
|
|
||||||
# redis-rs-session
|
# redis-rs-session
|
||||||
|
@ -3,13 +3,12 @@
|
|||||||
> Session management for Actix Web applications.
|
> Session management for Actix Web applications.
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-session)
|
[](https://crates.io/crates/actix-session)
|
||||||
[](https://docs.rs/actix-session/0.6.2)
|
[](https://docs.rs/actix-session/0.7.0)
|
||||||

|

|
||||||
[](https://deps.rs/crate/actix-session/0.6.2)
|
[](https://deps.rs/crate/actix-session/0.7.0)
|
||||||
|
|
||||||
|
|
||||||
## Documentation & Resources
|
## Documentation & Resources
|
||||||
|
|
||||||
- [API Documentation](https://docs.rs/actix-session)
|
- [API Documentation](https://docs.rs/actix-session)
|
||||||
- [Example Projects](https://github.com/actix/examples/tree/master/auth/cookie-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
|
||||||
|
369
actix-session/src/config.rs
Normal file
369
actix-session/src/config.rs
Normal file
@ -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(),
|
||||||
|
}
|
||||||
|
}
|
@ -133,38 +133,32 @@
|
|||||||
//! [`RedisSessionStore`]: storage::RedisSessionStore
|
//! [`RedisSessionStore`]: storage::RedisSessionStore
|
||||||
//! [`RedisActorSessionStore`]: storage::RedisActorSessionStore
|
//! [`RedisActorSessionStore`]: storage::RedisActorSessionStore
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||||
#![warn(future_incompatible, missing_docs)]
|
#![warn(future_incompatible, missing_docs)]
|
||||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||||
|
|
||||||
|
pub mod config;
|
||||||
mod middleware;
|
mod middleware;
|
||||||
mod session;
|
mod session;
|
||||||
mod session_ext;
|
mod session_ext;
|
||||||
pub mod storage;
|
pub mod storage;
|
||||||
|
|
||||||
pub use self::middleware::{
|
pub use self::middleware::SessionMiddleware;
|
||||||
CookieContentSecurity, SessionLength, SessionMiddleware, SessionMiddlewareBuilder,
|
pub use self::session::{Session, SessionGetError, SessionInsertError, SessionStatus};
|
||||||
};
|
|
||||||
pub use self::session::{Session, SessionStatus};
|
|
||||||
pub use self::session_ext::SessionExt;
|
pub use self::session_ext::SessionExt;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod test_helpers {
|
pub mod test_helpers {
|
||||||
use actix_web::cookie::Key;
|
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.
|
/// Generate a random cookie signing/encryption key.
|
||||||
pub fn key() -> Key {
|
pub fn key() -> Key {
|
||||||
let signing_key: String = thread_rng()
|
Key::generate()
|
||||||
.sample_iter(&Alphanumeric)
|
|
||||||
.take(64)
|
|
||||||
.map(char::from)
|
|
||||||
.collect();
|
|
||||||
Key::from(signing_key.as_bytes())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A ready-to-go acceptance test suite to verify that sessions behave as expected
|
/// A ready-to-go acceptance test suite to verify that sessions behave as expected
|
||||||
@ -187,6 +181,11 @@ pub mod test_helpers {
|
|||||||
acceptance_tests::basic_workflow(store_builder.clone(), *policy).await;
|
acceptance_tests::basic_workflow(store_builder.clone(), *policy).await;
|
||||||
acceptance_tests::expiration_is_refreshed_on_changes(store_builder.clone(), *policy)
|
acceptance_tests::expiration_is_refreshed_on_changes(store_builder.clone(), *policy)
|
||||||
.await;
|
.await;
|
||||||
|
acceptance_tests::expiration_is_always_refreshed_if_configured_to_refresh_on_every_request(
|
||||||
|
store_builder.clone(),
|
||||||
|
*policy,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
acceptance_tests::complex_workflow(
|
acceptance_tests::complex_workflow(
|
||||||
store_builder.clone(),
|
store_builder.clone(),
|
||||||
is_invalidation_supported,
|
is_invalidation_supported,
|
||||||
@ -199,18 +198,18 @@ pub mod test_helpers {
|
|||||||
|
|
||||||
mod acceptance_tests {
|
mod acceptance_tests {
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
dev::Service,
|
cookie::time,
|
||||||
|
dev::{Service, ServiceResponse},
|
||||||
guard, middleware, test,
|
guard, middleware, test,
|
||||||
web::{self, get, post, resource, Bytes},
|
web::{self, get, post, resource, Bytes},
|
||||||
App, HttpResponse, Result,
|
App, HttpResponse, Result,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use time::Duration;
|
|
||||||
|
|
||||||
|
use crate::config::{CookieContentSecurity, PersistentSession, TtlExtensionPolicy};
|
||||||
use crate::{
|
use crate::{
|
||||||
middleware::SessionLength, storage::SessionStore, test_helpers::key,
|
storage::SessionStore, test_helpers::key, Session, SessionExt, SessionMiddleware,
|
||||||
CookieContentSecurity, Session, SessionExt, SessionMiddleware,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(super) async fn basic_workflow<F, Store>(
|
pub(super) async fn basic_workflow<F, Store>(
|
||||||
@ -228,9 +227,10 @@ pub mod test_helpers {
|
|||||||
.cookie_name("actix-test".into())
|
.cookie_name("actix-test".into())
|
||||||
.cookie_domain(Some("localhost".into()))
|
.cookie_domain(Some("localhost".into()))
|
||||||
.cookie_content_security(policy)
|
.cookie_content_security(policy)
|
||||||
.session_length(SessionLength::Predetermined {
|
.session_lifecycle(
|
||||||
max_session_length: Some(time::Duration::seconds(100)),
|
PersistentSession::default()
|
||||||
})
|
.session_ttl(time::Duration::seconds(100)),
|
||||||
|
)
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
.service(web::resource("/").to(|ses: Session| async move {
|
.service(web::resource("/").to(|ses: Session| async move {
|
||||||
@ -246,12 +246,7 @@ pub mod test_helpers {
|
|||||||
|
|
||||||
let request = test::TestRequest::get().to_request();
|
let request = test::TestRequest::get().to_request();
|
||||||
let response = app.call(request).await.unwrap();
|
let response = app.call(request).await.unwrap();
|
||||||
let cookie = response
|
let cookie = response.get_cookie("actix-test").unwrap().clone();
|
||||||
.response()
|
|
||||||
.cookies()
|
|
||||||
.find(|c| c.name() == "actix-test")
|
|
||||||
.unwrap()
|
|
||||||
.clone();
|
|
||||||
assert_eq!(cookie.path().unwrap(), "/test/");
|
assert_eq!(cookie.path().unwrap(), "/test/");
|
||||||
|
|
||||||
let request = test::TestRequest::with_uri("/test/")
|
let request = test::TestRequest::with_uri("/test/")
|
||||||
@ -261,6 +256,55 @@ pub mod test_helpers {
|
|||||||
assert_eq!(body, Bytes::from_static(b"counter: 100"));
|
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>(
|
pub(super) async fn expiration_is_refreshed_on_changes<F, Store>(
|
||||||
store_builder: F,
|
store_builder: F,
|
||||||
policy: CookieContentSecurity,
|
policy: CookieContentSecurity,
|
||||||
@ -268,14 +312,15 @@ pub mod test_helpers {
|
|||||||
Store: SessionStore + 'static,
|
Store: SessionStore + 'static,
|
||||||
F: Fn() -> Store + Clone + Send + 'static,
|
F: Fn() -> Store + Clone + Send + 'static,
|
||||||
{
|
{
|
||||||
|
let session_ttl = time::Duration::seconds(60);
|
||||||
let app = test::init_service(
|
let app = test::init_service(
|
||||||
App::new()
|
App::new()
|
||||||
.wrap(
|
.wrap(
|
||||||
SessionMiddleware::builder(store_builder(), key())
|
SessionMiddleware::builder(store_builder(), key())
|
||||||
.cookie_content_security(policy)
|
.cookie_content_security(policy)
|
||||||
.session_length(SessionLength::Predetermined {
|
.session_lifecycle(
|
||||||
max_session_length: Some(time::Duration::seconds(60)),
|
PersistentSession::default().session_ttl(session_ttl),
|
||||||
})
|
)
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
.service(web::resource("/").to(|ses: Session| async move {
|
.service(web::resource("/").to(|ses: Session| async move {
|
||||||
@ -288,25 +333,19 @@ pub mod test_helpers {
|
|||||||
|
|
||||||
let request = test::TestRequest::get().to_request();
|
let request = test::TestRequest::get().to_request();
|
||||||
let response = app.call(request).await.unwrap();
|
let response = app.call(request).await.unwrap();
|
||||||
let cookie_1 = response
|
let cookie_1 = response.get_cookie("id").expect("Cookie is set");
|
||||||
.response()
|
assert_eq!(cookie_1.max_age(), Some(session_ttl));
|
||||||
.cookies()
|
|
||||||
.find(|c| c.name() == "id")
|
|
||||||
.expect("Cookie is set");
|
|
||||||
assert_eq!(cookie_1.max_age(), Some(Duration::seconds(60)));
|
|
||||||
|
|
||||||
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();
|
let response = app.call(request).await.unwrap();
|
||||||
assert!(response.response().cookies().next().is_none());
|
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 response = app.call(request).await.unwrap();
|
||||||
let cookie_2 = response
|
let cookie_2 = response.get_cookie("id").expect("Cookie is set");
|
||||||
.response()
|
assert_eq!(cookie_2.max_age(), Some(session_ttl));
|
||||||
.cookies()
|
|
||||||
.find(|c| c.name() == "id")
|
|
||||||
.expect("Cookie is set");
|
|
||||||
assert_eq!(cookie_2.max_age(), Some(Duration::seconds(60)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn guard<F, Store>(store_builder: F, policy: CookieContentSecurity)
|
pub(super) async fn guard<F, Store>(store_builder: F, policy: CookieContentSecurity)
|
||||||
@ -320,9 +359,9 @@ pub mod test_helpers {
|
|||||||
SessionMiddleware::builder(store_builder(), key())
|
SessionMiddleware::builder(store_builder(), key())
|
||||||
.cookie_name("test-session".into())
|
.cookie_name("test-session".into())
|
||||||
.cookie_content_security(policy)
|
.cookie_content_security(policy)
|
||||||
.session_length(SessionLength::Predetermined {
|
.session_lifecycle(
|
||||||
max_session_length: Some(time::Duration::days(7)),
|
PersistentSession::default().session_ttl(time::Duration::days(7)),
|
||||||
})
|
)
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
.wrap(middleware::Logger::default())
|
.wrap(middleware::Logger::default())
|
||||||
@ -402,15 +441,16 @@ pub mod test_helpers {
|
|||||||
Store: SessionStore + 'static,
|
Store: SessionStore + 'static,
|
||||||
F: Fn() -> Store + Clone + Send + 'static,
|
F: Fn() -> Store + Clone + Send + 'static,
|
||||||
{
|
{
|
||||||
|
let session_ttl = time::Duration::days(7);
|
||||||
let srv = actix_test::start(move || {
|
let srv = actix_test::start(move || {
|
||||||
App::new()
|
App::new()
|
||||||
.wrap(
|
.wrap(
|
||||||
SessionMiddleware::builder(store_builder(), key())
|
SessionMiddleware::builder(store_builder(), key())
|
||||||
.cookie_name("test-session".into())
|
.cookie_name("test-session".into())
|
||||||
.cookie_content_security(policy)
|
.cookie_content_security(policy)
|
||||||
.session_length(SessionLength::Predetermined {
|
.session_lifecycle(
|
||||||
max_session_length: Some(time::Duration::days(7)),
|
PersistentSession::default().session_ttl(session_ttl),
|
||||||
})
|
)
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
.wrap(middleware::Logger::default())
|
.wrap(middleware::Logger::default())
|
||||||
@ -456,7 +496,7 @@ pub mod test_helpers {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|c| c.name() == "test-session")
|
.find(|c| c.name() == "test-session")
|
||||||
.unwrap();
|
.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
|
// Step 3: GET index, including session cookie #1 in request
|
||||||
// - set-cookie will *not* be in response
|
// - set-cookie will *not* be in response
|
||||||
@ -494,7 +534,7 @@ pub mod test_helpers {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|c| c.name() == "test-session")
|
.find(|c| c.name() == "test-session")
|
||||||
.unwrap();
|
.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
|
// Step 5: POST to login, including session cookie #2 in request
|
||||||
// - set-cookie actix-session will be in response (session cookie #3)
|
// - set-cookie actix-session will be in response (session cookie #3)
|
||||||
@ -675,5 +715,18 @@ pub mod test_helpers {
|
|||||||
|
|
||||||
Ok(HttpResponse::Ok().body(body))
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,15 +3,18 @@ use std::{collections::HashMap, convert::TryInto, fmt, future::Future, pin::Pin,
|
|||||||
use actix_utils::future::{ready, Ready};
|
use actix_utils::future::{ready, Ready};
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
body::MessageBody,
|
body::MessageBody,
|
||||||
cookie::{Cookie, CookieJar, Key, SameSite},
|
cookie::{Cookie, CookieJar, Key},
|
||||||
dev::{forward_ready, ResponseHead, Service, ServiceRequest, ServiceResponse, Transform},
|
dev::{forward_ready, ResponseHead, Service, ServiceRequest, ServiceResponse, Transform},
|
||||||
http::header::{HeaderValue, SET_COOKIE},
|
http::header::{HeaderValue, SET_COOKIE},
|
||||||
HttpResponse,
|
HttpResponse,
|
||||||
};
|
};
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use time::Duration;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
config::{
|
||||||
|
self, Configuration, CookieConfiguration, CookieContentSecurity, SessionMiddlewareBuilder,
|
||||||
|
TtlExtensionPolicy,
|
||||||
|
},
|
||||||
storage::{LoadError, SessionKey, SessionStore},
|
storage::{LoadError, SessionKey, SessionStore},
|
||||||
Session, SessionStatus,
|
Session, SessionStatus,
|
||||||
};
|
};
|
||||||
@ -66,8 +69,9 @@ use crate::{
|
|||||||
/// If you want to customise use [`builder`](Self::builder) instead of [`new`](Self::new):
|
/// If you want to customise use [`builder`](Self::builder) instead of [`new`](Self::new):
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// use actix_web::{cookie::Key, web, App, HttpServer, HttpResponse, Error};
|
/// use actix_web::{App, cookie::{Key, time}, Error, HttpResponse, HttpServer, web};
|
||||||
/// use actix_session::{Session, SessionMiddleware, storage::RedisActorSessionStore, SessionLength};
|
/// 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.
|
/// // The secret key would usually be read from a configuration file/environment variables.
|
||||||
/// fn get_secret_key() -> Key {
|
/// fn get_secret_key() -> Key {
|
||||||
@ -87,9 +91,10 @@ use crate::{
|
|||||||
/// RedisActorSessionStore::new(redis_connection_string),
|
/// RedisActorSessionStore::new(redis_connection_string),
|
||||||
/// secret_key.clone()
|
/// secret_key.clone()
|
||||||
/// )
|
/// )
|
||||||
/// .session_length(SessionLength::Predetermined {
|
/// .session_lifecycle(
|
||||||
/// max_session_length: Some(time::Duration::days(5)),
|
/// PersistentSession::default()
|
||||||
/// })
|
/// .session_ttl(time::Duration::days(5))
|
||||||
|
/// )
|
||||||
/// .build(),
|
/// .build(),
|
||||||
/// )
|
/// )
|
||||||
/// .default_service(web::to(|| HttpResponse::Ok())))
|
/// .default_service(web::to(|| HttpResponse::Ok())))
|
||||||
@ -114,117 +119,6 @@ pub struct SessionMiddleware<Store: SessionStore> {
|
|||||||
configuration: Rc<Configuration>,
|
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> {
|
impl<Store: SessionStore> SessionMiddleware<Store> {
|
||||||
/// Use [`SessionMiddleware::new`] to initialize the session framework using the default
|
/// Use [`SessionMiddleware::new`] to initialize the session framework using the default
|
||||||
/// parameters.
|
/// parameters.
|
||||||
@ -234,10 +128,7 @@ impl<Store: SessionStore> SessionMiddleware<Store> {
|
|||||||
/// [`SessionStore]);
|
/// [`SessionStore]);
|
||||||
/// - a secret key, to sign or encrypt the content of client-side session cookie.
|
/// - a secret key, to sign or encrypt the content of client-side session cookie.
|
||||||
pub fn new(store: Store, key: Key) -> Self {
|
pub fn new(store: Store, key: Key) -> Self {
|
||||||
Self {
|
Self::builder(store, key).build()
|
||||||
storage_backend: Rc::new(store),
|
|
||||||
configuration: Rc::new(default_configuration(key)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A fluent API to configure [`SessionMiddleware`].
|
/// A fluent API to configure [`SessionMiddleware`].
|
||||||
@ -247,124 +138,13 @@ impl<Store: SessionStore> SessionMiddleware<Store> {
|
|||||||
/// [`SessionStore]);
|
/// [`SessionStore]);
|
||||||
/// - a secret key, to sign or encrypt the content of client-side session cookie.
|
/// - a secret key, to sign or encrypt the content of client-side session cookie.
|
||||||
pub fn builder(store: Store, key: Key) -> SessionMiddlewareBuilder<Store> {
|
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),
|
storage_backend: Rc::new(store),
|
||||||
configuration: default_configuration(key),
|
configuration: Rc::new(configuration),
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -509,16 +289,39 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
SessionStatus::Unchanged => {
|
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)
|
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> {
|
fn extract_session_key(req: &ServiceRequest, config: &CookieConfiguration) -> Option<SessionKey> {
|
||||||
let cookies = req.cookies().ok()?;
|
let cookies = req.cookies().ok()?;
|
||||||
let session_cookie = cookies
|
let session_cookie = cookies
|
||||||
|
@ -1,16 +1,20 @@
|
|||||||
use std::{
|
use std::{
|
||||||
cell::{Ref, RefCell},
|
cell::{Ref, RefCell},
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
|
error::Error as StdError,
|
||||||
mem,
|
mem,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use actix_utils::future::{ready, Ready};
|
use actix_utils::future::{ready, Ready};
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
|
body::BoxBody,
|
||||||
dev::{Extensions, Payload, ServiceRequest, ServiceResponse},
|
dev::{Extensions, Payload, ServiceRequest, ServiceResponse},
|
||||||
error::Error,
|
error::Error,
|
||||||
FromRequest, HttpMessage, HttpRequest,
|
FromRequest, HttpMessage, HttpRequest, HttpResponse, ResponseError,
|
||||||
};
|
};
|
||||||
|
use anyhow::Context;
|
||||||
|
use derive_more::{Display, From};
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
|
|
||||||
/// The primary interface to access and modify session state.
|
/// The primary interface to access and modify session state.
|
||||||
@ -38,6 +42,7 @@ use serde::{de::DeserializeOwned, Serialize};
|
|||||||
/// [`SessionExt`].
|
/// [`SessionExt`].
|
||||||
///
|
///
|
||||||
/// [`SessionExt`]: crate::SessionExt
|
/// [`SessionExt`]: crate::SessionExt
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct Session(Rc<RefCell<SessionInner>>);
|
pub struct Session(Rc<RefCell<SessionInner>>);
|
||||||
|
|
||||||
/// Status of a [`Session`].
|
/// Status of a [`Session`].
|
||||||
@ -89,9 +94,20 @@ impl Session {
|
|||||||
/// Get a `value` from the session.
|
/// Get a `value` from the session.
|
||||||
///
|
///
|
||||||
/// It returns an error if it fails to deserialize as `T` the JSON value associated with `key`.
|
/// 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) {
|
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 {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
@ -115,17 +131,29 @@ impl Session {
|
|||||||
/// only a reference to the value is taken.
|
/// only a reference to the value is taken.
|
||||||
///
|
///
|
||||||
/// It returns an error if it fails to serialize `value` to JSON.
|
/// It returns an error if it fails to serialize `value` to JSON.
|
||||||
pub fn insert(
|
pub fn insert<T: Serialize>(
|
||||||
&self,
|
&self,
|
||||||
key: impl Into<String>,
|
key: impl Into<String>,
|
||||||
value: impl Serialize,
|
value: T,
|
||||||
) -> Result<(), serde_json::Error> {
|
) -> Result<(), SessionInsertError> {
|
||||||
let mut inner = self.0.borrow_mut();
|
let mut inner = self.0.borrow_mut();
|
||||||
|
|
||||||
if inner.status != SessionStatus::Purged {
|
if inner.status != SessionStatus::Purged {
|
||||||
inner.status = SessionStatus::Changed;
|
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(())
|
Ok(())
|
||||||
@ -147,7 +175,7 @@ impl Session {
|
|||||||
|
|
||||||
/// Remove value from the session and deserialize.
|
/// 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.
|
/// otherwise returns un-deserialized JSON string.
|
||||||
pub fn remove_as<T: DeserializeOwned>(&self, key: &str) -> Option<Result<T, String>> {
|
pub fn remove_as<T: DeserializeOwned>(&self, key: &str) -> Option<Result<T, String>> {
|
||||||
self.remove(key)
|
self.remove(key)
|
||||||
@ -155,7 +183,7 @@ impl Session {
|
|||||||
Ok(val) => Ok(val),
|
Ok(val) => Ok(val),
|
||||||
Err(_err) => {
|
Err(_err) => {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"removed value (key: {}) could not be deserialized as {}",
|
"Removed value (key: {}) could not be deserialized as {}",
|
||||||
key,
|
key,
|
||||||
std::any::type_name::<T>()
|
std::any::type_name::<T>()
|
||||||
);
|
);
|
||||||
@ -206,9 +234,9 @@ impl Session {
|
|||||||
|
|
||||||
/// Returns session status and iterator of key-value pairs of changes.
|
/// 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,
|
/// This is a destructive operation - the session state is removed from the request extensions
|
||||||
/// leaving behind a new empty map. It should only be used when the session is being finalised (i.e.
|
/// typemap, leaving behind a new empty map. It should only be used when the session is being
|
||||||
/// in `SessionMiddleware`).
|
/// finalised (i.e. in `SessionMiddleware`).
|
||||||
pub(crate) fn get_changes<B>(
|
pub(crate) fn get_changes<B>(
|
||||||
res: &mut ServiceResponse<B>,
|
res: &mut ServiceResponse<B>,
|
||||||
) -> (SessionStatus, HashMap<String, String>) {
|
) -> (SessionStatus, HashMap<String, String>) {
|
||||||
@ -265,3 +293,37 @@ impl FromRequest for Session {
|
|||||||
ready(Ok(Session::get_session(&mut *req.extensions_mut())))
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
|
|
||||||
use time::Duration;
|
use actix_web::cookie::time::Duration;
|
||||||
|
use anyhow::Error;
|
||||||
|
|
||||||
use super::SessionKey;
|
use super::SessionKey;
|
||||||
use crate::storage::{
|
use crate::storage::{
|
||||||
@ -34,9 +35,9 @@ use crate::storage::{
|
|||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// # Limitations
|
/// # Limitations
|
||||||
/// Cookies are subject to size limits - we require session keys to be shorter than 4096 bytes. This
|
/// Cookies are subject to size limits so we require session keys to be shorter than 4096 bytes.
|
||||||
/// translates into a limit on the maximum size of the session state when using cookies as storage
|
/// This translates into a limit on the maximum size of the session state when using cookies as
|
||||||
/// backend.
|
/// storage backend.
|
||||||
///
|
///
|
||||||
/// The session cookie can always be inspected by end users via the developer tools exposed by their
|
/// 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
|
/// 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
|
/// There is no way to invalidate a session before its natural expiry when using cookies as the
|
||||||
/// storage backend.
|
/// storage backend.
|
||||||
///
|
///
|
||||||
/// [`CookieContentSecurity::Private`]: crate::CookieContentSecurity::Private
|
/// [`CookieContentSecurity::Private`]: crate::config::CookieContentSecurity::Private
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "cookie-session")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "cookie-session")))]
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
#[non_exhaustive]
|
#[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> {
|
async fn delete(&self, _session_key: &SessionKey) -> Result<(), anyhow::Error> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use actix_web::cookie::time::Duration;
|
||||||
use derive_more::Display;
|
use derive_more::Display;
|
||||||
use time::Duration;
|
|
||||||
|
|
||||||
use super::SessionKey;
|
use super::SessionKey;
|
||||||
|
|
||||||
@ -36,6 +36,13 @@ pub trait SessionStore {
|
|||||||
ttl: &Duration,
|
ttl: &Duration,
|
||||||
) -> Result<SessionKey, UpdateError>;
|
) -> 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.
|
/// Deletes a session from the store.
|
||||||
async fn delete(&self, session_key: &SessionKey) -> Result<(), anyhow::Error>;
|
async fn delete(&self, session_key: &SessionKey) -> Result<(), anyhow::Error>;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use actix::Addr;
|
use actix::Addr;
|
||||||
use actix_redis::{resp_array, Command, RedisActor, RespValue};
|
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 super::SessionKey;
|
||||||
use crate::storage::{
|
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> {
|
async fn delete(&self, session_key: &SessionKey) -> Result<(), anyhow::Error> {
|
||||||
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
|
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
|
||||||
|
|
||||||
@ -258,9 +277,11 @@ impl SessionStore for RedisActorSessionStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod tests {
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use actix_web::cookie::time::Duration;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::test_helpers::acceptance_test_suite;
|
use crate::test_helpers::acceptance_test_suite;
|
||||||
|
|
||||||
@ -286,7 +307,7 @@ mod test {
|
|||||||
let session_key = generate_session_key();
|
let session_key = generate_session_key();
|
||||||
let initial_session_key = session_key.as_ref().to_owned();
|
let initial_session_key = session_key.as_ref().to_owned();
|
||||||
let updated_session_key = store
|
let updated_session_key = store
|
||||||
.update(session_key, HashMap::new(), &time::Duration::seconds(1))
|
.update(session_key, HashMap::new(), &Duration::seconds(1))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_ne!(initial_session_key, updated_session_key.as_ref());
|
assert_ne!(initial_session_key, updated_session_key.as_ref());
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
use std::sync::Arc;
|
use std::{convert::TryInto, sync::Arc};
|
||||||
|
|
||||||
use redis::{aio::ConnectionManager, Cmd, FromRedisValue, RedisResult, Value};
|
use actix_web::cookie::time::Duration;
|
||||||
use time::{self, Duration};
|
use anyhow::{Context, Error};
|
||||||
|
use redis::{aio::ConnectionManager, AsyncCommands, Cmd, FromRedisValue, RedisResult, Value};
|
||||||
|
|
||||||
use super::SessionKey;
|
use super::SessionKey;
|
||||||
use crate::storage::{
|
use crate::storage::{
|
||||||
@ -28,6 +29,7 @@ use crate::storage::{
|
|||||||
/// let secret_key = get_secret_key();
|
/// let secret_key = get_secret_key();
|
||||||
/// let redis_connection_string = "redis://127.0.0.1:6379";
|
/// let redis_connection_string = "redis://127.0.0.1:6379";
|
||||||
/// let store = RedisSessionStore::new(redis_connection_string).await.unwrap();
|
/// let store = RedisSessionStore::new(redis_connection_string).await.unwrap();
|
||||||
|
///
|
||||||
/// HttpServer::new(move ||
|
/// HttpServer::new(move ||
|
||||||
/// App::new()
|
/// App::new()
|
||||||
/// .wrap(SessionMiddleware::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> {
|
async fn delete(&self, session_key: &SessionKey) -> Result<(), anyhow::Error> {
|
||||||
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
|
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
|
||||||
self.execute_command(redis::cmd("DEL").arg(&[&cache_key]))
|
self.execute_command(redis::cmd("DEL").arg(&[&cache_key]))
|
||||||
@ -272,9 +289,10 @@ impl RedisSessionStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod tests {
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use actix_web::cookie::time;
|
||||||
use redis::AsyncCommands;
|
use redis::AsyncCommands;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -2,16 +2,15 @@ use std::convert::TryFrom;
|
|||||||
|
|
||||||
use derive_more::{Display, From};
|
use derive_more::{Display, From};
|
||||||
|
|
||||||
/// A session key, the string stored in a client-side cookie to associate a user
|
/// A session key, the string stored in a client-side cookie to associate a user with its session
|
||||||
/// with its session state on the backend.
|
/// state on the backend.
|
||||||
///
|
///
|
||||||
/// ## Validation
|
/// # Validation
|
||||||
///
|
/// Session keys are stored as cookies, therefore they cannot be arbitrary long. Session keys are
|
||||||
/// Session keys are stored as cookies, therefore they cannot be arbitrary long.
|
/// required to be smaller than 4064 bytes.
|
||||||
/// We require session keys to be smaller than 4064 bytes.
|
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// use std::convert::TryInto;
|
/// # use std::convert::TryInto;
|
||||||
/// use actix_session::storage::SessionKey;
|
/// use actix_session::storage::SessionKey;
|
||||||
///
|
///
|
||||||
/// let key: String = std::iter::repeat('a').take(4065).collect();
|
/// let key: String = std::iter::repeat('a').take(4065).collect();
|
||||||
@ -24,15 +23,15 @@ pub struct SessionKey(String);
|
|||||||
impl TryFrom<String> for SessionKey {
|
impl TryFrom<String> for SessionKey {
|
||||||
type Error = InvalidSessionKeyError;
|
type Error = InvalidSessionKeyError;
|
||||||
|
|
||||||
fn try_from(v: String) -> Result<Self, Self::Error> {
|
fn try_from(val: String) -> Result<Self, Self::Error> {
|
||||||
if v.len() > 4064 {
|
if val.len() > 4064 {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
"The session key is bigger than 4064 bytes, the upper limit on cookie content."
|
"The session key is bigger than 4064 bytes, the upper limit on cookie content."
|
||||||
)
|
)
|
||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(SessionKey(v))
|
Ok(SessionKey(val))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,8 +42,8 @@ impl AsRef<str> for SessionKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl From<SessionKey> for String {
|
impl From<SessionKey> for String {
|
||||||
fn from(k: SessionKey) -> Self {
|
fn from(key: SessionKey) -> Self {
|
||||||
k.0
|
key.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,6 +71,10 @@ impl SessionStore for MockStore {
|
|||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn update_ttl(&self, _session_key: &SessionKey, _ttl: &Duration) -> Result<(), Error> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
async fn delete(&self, _session_key: &SessionKey) -> Result<(), Error> {
|
async fn delete(&self, _session_key: &SessionKey) -> Result<(), Error> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2021-xx-xx
|
## 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.
|
||||||
|
|
||||||
|
[#260]: https://github.com/actix/actix-extras/pull/260
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0 - 2022-03-01
|
## 0.6.0 - 2022-03-01
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-web-httpauth"
|
name = "actix-web-httpauth"
|
||||||
version = "0.6.0"
|
version = "0.7.0"
|
||||||
authors = [
|
authors = [
|
||||||
"svartalf <self@svartalf.info>",
|
"svartalf <self@svartalf.info>",
|
||||||
"Yuki Okushi <huyuumi.dev@gmail.com>",
|
"Yuki Okushi <huyuumi.dev@gmail.com>",
|
||||||
|
@ -3,14 +3,14 @@
|
|||||||
> HTTP authentication schemes for [actix-web](https://github.com/actix/actix-web).
|
> HTTP authentication schemes for [actix-web](https://github.com/actix/actix-web).
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-web-httpauth)
|
[](https://crates.io/crates/actix-web-httpauth)
|
||||||
[](https://docs.rs/actix-web-httpauth/0.6.0)
|
[](https://docs.rs/actix-web-httpauth/0.7.0)
|
||||||

|

|
||||||
[](https://deps.rs/crate/actix-web-httpauth/0.6.0)
|
[](https://deps.rs/crate/actix-web-httpauth/0.7.0)
|
||||||
|
|
||||||
## Documentation & Resources
|
## Documentation & Resources
|
||||||
|
|
||||||
- [API Documentation](https://docs.rs/actix-web-httpauth/)
|
- [API Documentation](https://docs.rs/actix-web-httpauth/)
|
||||||
- Minimum Supported Rust Version (MSRV): 1.54
|
- Minimum Supported Rust Version (MSRV): 1.57
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Typed [Authorization] and [WWW-Authenticate] headers
|
- Typed [Authorization] and [WWW-Authenticate] headers
|
||||||
|
@ -3,7 +3,10 @@ use actix_web::{middleware, web, App, Error, HttpServer};
|
|||||||
use actix_web_httpauth::extractors::basic::BasicAuth;
|
use actix_web_httpauth::extractors::basic::BasicAuth;
|
||||||
use actix_web_httpauth::middleware::HttpAuthentication;
|
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)
|
Ok(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ use actix_web_httpauth::{extractors::bearer::BearerAuth, middleware::HttpAuthent
|
|||||||
async fn ok_validator(
|
async fn ok_validator(
|
||||||
req: ServiceRequest,
|
req: ServiceRequest,
|
||||||
credentials: BearerAuth,
|
credentials: BearerAuth,
|
||||||
) -> Result<ServiceRequest, Error> {
|
) -> Result<ServiceRequest, (Error, ServiceRequest)> {
|
||||||
eprintln!("{:?}", credentials);
|
eprintln!("{:?}", credentials);
|
||||||
Ok(req)
|
Ok(req)
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@ impl<T, F, O> HttpAuthentication<T, F>
|
|||||||
where
|
where
|
||||||
T: AuthExtractor,
|
T: AuthExtractor,
|
||||||
F: Fn(ServiceRequest, T) -> O,
|
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
|
/// Construct `HttpAuthentication` middleware with the provided auth extractor `T` and
|
||||||
/// validation callback `F`.
|
/// validation callback `F`.
|
||||||
@ -54,7 +54,7 @@ where
|
|||||||
impl<F, O> HttpAuthentication<basic::BasicAuth, F>
|
impl<F, O> HttpAuthentication<basic::BasicAuth, F>
|
||||||
where
|
where
|
||||||
F: Fn(ServiceRequest, basic::BasicAuth) -> O,
|
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.
|
/// Construct `HttpAuthentication` middleware for the HTTP "Basic" authentication scheme.
|
||||||
///
|
///
|
||||||
@ -70,7 +70,7 @@ where
|
|||||||
/// async fn validator(
|
/// async fn validator(
|
||||||
/// req: ServiceRequest,
|
/// req: ServiceRequest,
|
||||||
/// credentials: BasicAuth,
|
/// credentials: BasicAuth,
|
||||||
/// ) -> Result<ServiceRequest, Error> {
|
/// ) -> Result<ServiceRequest, (Error, ServiceRequest)> {
|
||||||
/// // All users are great and more than welcome!
|
/// // All users are great and more than welcome!
|
||||||
/// Ok(req)
|
/// Ok(req)
|
||||||
/// }
|
/// }
|
||||||
@ -85,7 +85,7 @@ where
|
|||||||
impl<F, O> HttpAuthentication<bearer::BearerAuth, F>
|
impl<F, O> HttpAuthentication<bearer::BearerAuth, F>
|
||||||
where
|
where
|
||||||
F: Fn(ServiceRequest, bearer::BearerAuth) -> O,
|
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.
|
/// Construct `HttpAuthentication` middleware for the HTTP "Bearer" authentication scheme.
|
||||||
///
|
///
|
||||||
@ -96,7 +96,7 @@ where
|
|||||||
/// # use actix_web_httpauth::middleware::HttpAuthentication;
|
/// # use actix_web_httpauth::middleware::HttpAuthentication;
|
||||||
/// # use actix_web_httpauth::extractors::bearer::{Config, BearerAuth};
|
/// # use actix_web_httpauth::extractors::bearer::{Config, BearerAuth};
|
||||||
/// # use actix_web_httpauth::extractors::{AuthenticationError, AuthExtractorConfig};
|
/// # 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" {
|
/// if credentials.token() == "mF_9.B5f-4.1JqM" {
|
||||||
/// Ok(req)
|
/// Ok(req)
|
||||||
/// } else {
|
/// } else {
|
||||||
@ -105,7 +105,7 @@ where
|
|||||||
/// .unwrap_or_else(Default::default)
|
/// .unwrap_or_else(Default::default)
|
||||||
/// .scope("urn:example:channel=HBO&urn:example:rating=G,PG-13");
|
/// .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: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
F: Fn(ServiceRequest, T) -> O + '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,
|
T: AuthExtractor + 'static,
|
||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
{
|
{
|
||||||
@ -155,7 +155,7 @@ where
|
|||||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
F: Fn(ServiceRequest, T) -> O + '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,
|
T: AuthExtractor + 'static,
|
||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
{
|
{
|
||||||
@ -178,9 +178,12 @@ where
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: alter to remove ? operator; an error response is required for downstream
|
let req = match process_fn(req, credentials).await {
|
||||||
// middleware to do their thing (eg. cors adding headers)
|
Ok(req) => req,
|
||||||
let req = process_fn(req, credentials).await?;
|
Err((err, req)) => {
|
||||||
|
return Ok(req.error_response(err).map_into_right_body());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
service.call(req).await.map(|res| res.map_into_left_body())
|
service.call(req).await.map(|res| res.map_into_left_body())
|
||||||
}
|
}
|
||||||
@ -362,10 +365,10 @@ mod tests {
|
|||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
async fn test_middleware_works_with_app() {
|
async fn test_middleware_works_with_app() {
|
||||||
async fn validator(
|
async fn validator(
|
||||||
_req: ServiceRequest,
|
req: ServiceRequest,
|
||||||
_credentials: BasicAuth,
|
_credentials: BasicAuth,
|
||||||
) -> Result<ServiceRequest, actix_web::Error> {
|
) -> Result<ServiceRequest, (actix_web::Error, ServiceRequest)> {
|
||||||
Err(ErrorForbidden("You are not welcome!"))
|
Err((ErrorForbidden("You are not welcome!"), req))
|
||||||
}
|
}
|
||||||
let middleware = HttpAuthentication::basic(validator);
|
let middleware = HttpAuthentication::basic(validator);
|
||||||
|
|
||||||
@ -387,10 +390,10 @@ mod tests {
|
|||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
async fn test_middleware_works_with_scope() {
|
async fn test_middleware_works_with_scope() {
|
||||||
async fn validator(
|
async fn validator(
|
||||||
_req: ServiceRequest,
|
req: ServiceRequest,
|
||||||
_credentials: BasicAuth,
|
_credentials: BasicAuth,
|
||||||
) -> Result<ServiceRequest, actix_web::Error> {
|
) -> Result<ServiceRequest, (actix_web::Error, ServiceRequest)> {
|
||||||
Err(ErrorForbidden("You are not welcome!"))
|
Err((ErrorForbidden("You are not welcome!"), req))
|
||||||
}
|
}
|
||||||
let middleware = actix_web::middleware::Compat::new(HttpAuthentication::basic(validator));
|
let middleware = actix_web::middleware::Compat::new(HttpAuthentication::basic(validator));
|
||||||
|
|
||||||
|
@ -1 +1 @@
|
|||||||
msrv = "1.54.0"
|
msrv = "1.57"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user