Clear workspace
This commit is contained in:
parent
6d721886d0
commit
0b145bec5d
43
Cargo.lock
generated
43
Cargo.lock
generated
@ -40,7 +40,7 @@ dependencies = [
|
|||||||
"actix-admin-macros",
|
"actix-admin-macros",
|
||||||
"actix-files",
|
"actix-files",
|
||||||
"actix-multipart",
|
"actix-multipart",
|
||||||
"actix-session 0.7.2",
|
"actix-session",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -159,26 +159,11 @@ dependencies = [
|
|||||||
"zstd",
|
"zstd",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "actix-jwt-authc"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ed5e2d7e61895ae7e33b8ee818d99e1fe1edd3bf0be5fa04331b66f31dc6e9fe"
|
|
||||||
dependencies = [
|
|
||||||
"actix-session 0.6.2",
|
|
||||||
"actix-web",
|
|
||||||
"derive_more",
|
|
||||||
"futures-util",
|
|
||||||
"jsonwebtoken",
|
|
||||||
"serde",
|
|
||||||
"time 0.3.28",
|
|
||||||
"tokio 1.30.0",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "actix-jwt-session"
|
name = "actix-jwt-session"
|
||||||
version = "1.0.1"
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e57613443370ddec840ba877ee18e955c6c4d3972342ef240f918ba520508f28"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"argon2",
|
"argon2",
|
||||||
@ -188,7 +173,6 @@ dependencies = [
|
|||||||
"futures",
|
"futures",
|
||||||
"futures-lite",
|
"futures-lite",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"garde",
|
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"redis",
|
"redis",
|
||||||
@ -305,24 +289,6 @@ dependencies = [
|
|||||||
"pin-project-lite 0.2.12",
|
"pin-project-lite 0.2.12",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "actix-session"
|
|
||||||
version = "0.6.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0c9138a66462f1e65da829f9c0de81b44a96dfe193a4f19bfea32ee2be312368"
|
|
||||||
dependencies = [
|
|
||||||
"actix-service",
|
|
||||||
"actix-utils",
|
|
||||||
"actix-web",
|
|
||||||
"anyhow",
|
|
||||||
"async-trait",
|
|
||||||
"derive_more",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"time 0.3.28",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "actix-session"
|
name = "actix-session"
|
||||||
version = "0.7.2"
|
version = "0.7.2"
|
||||||
@ -2628,7 +2594,6 @@ name = "oswilno-session"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-http",
|
"actix-http",
|
||||||
"actix-jwt-authc",
|
|
||||||
"actix-jwt-session",
|
"actix-jwt-session",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"argon2",
|
"argon2",
|
||||||
|
@ -6,6 +6,5 @@ members = [
|
|||||||
'./crates/oswilno-parking-space',
|
'./crates/oswilno-parking-space',
|
||||||
'./crates/migration',
|
'./crates/migration',
|
||||||
'./crates/oswilno-actix-admin',
|
'./crates/oswilno-actix-admin',
|
||||||
'./crates/actix-jwt-session',
|
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "actix-jwt-session"
|
|
||||||
version = "1.0.1"
|
|
||||||
edition = "2021"
|
|
||||||
description = "Full featured JWT session managment for actix"
|
|
||||||
license = "MIT"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = ['use-redis', 'use-tracing', 'panic-bad-ttl', 'hashing']
|
|
||||||
use-redis = ["redis", "redis-async-pool"]
|
|
||||||
use-tracing = ['tracing']
|
|
||||||
override-bad-ttl = []
|
|
||||||
panic-bad-ttl = []
|
|
||||||
hashing = ["argon2"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
actix-web = "4"
|
|
||||||
async-trait = "0.1.72"
|
|
||||||
bincode = "1.3.3"
|
|
||||||
futures = "0.3.28"
|
|
||||||
futures-lite = "1.13.0"
|
|
||||||
futures-util = { version = "0.3.28", features = ['async-await'] }
|
|
||||||
jsonwebtoken = "8.3.0"
|
|
||||||
rand = "0.8.5"
|
|
||||||
redis = { version = "0.17", optional = true }
|
|
||||||
redis-async-pool = { version = "0.2.4", optional = true }
|
|
||||||
ring = "0.16.20"
|
|
||||||
serde = { version = "1.0.183", features = ["derive"] }
|
|
||||||
serde_json = "1.0.105"
|
|
||||||
thiserror = "1.0.44"
|
|
||||||
tokio = { version = "1.30.0", features = ["full"] }
|
|
||||||
tracing = { version = "0.1.37", optional = true }
|
|
||||||
uuid = { version = "1.4.1", features = ["v4", "serde"] }
|
|
||||||
argon2 = { version = "0.5.1", optional = true }
|
|
||||||
cookie = "0.17.0"
|
|
||||||
time = { version = "0.3.28", features = ["serde"] }
|
|
||||||
|
|
||||||
[[test]]
|
|
||||||
name = "ensure_redis_flow"
|
|
||||||
path = "./tests/ensure_redis_flow.rs"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
garde = "0.14.0"
|
|
||||||
ring = "0.16.20"
|
|
@ -1,394 +0,0 @@
|
|||||||
![docs.rs](https://img.shields.io/docsrs/actix-jwt-session)
|
|
||||||
|
|
||||||
|
|
||||||
All in one creating session and session validation library for actix.
|
|
||||||
|
|
||||||
It's designed to extract session using middleware and validate endpoint simply by using actix-web extractors.
|
|
||||||
Currently you can extract tokens from Header or Cookie. It's possible to implement Path, Query
|
|
||||||
or Body using `[ServiceRequest::extract]` but you must have struct to which values will be
|
|
||||||
extracted so it's easy to do if you have your own fields.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct MyJsonBody {
|
|
||||||
jwt: Option<String>,
|
|
||||||
refresh: Option<String>,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
To start with this library you need to create your own `AppClaims` structure and implement
|
|
||||||
`actix_jwt_session::Claims` trait for it.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum Audience {
|
|
||||||
Web,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub struct Claims {
|
|
||||||
#[serde(rename = "exp")]
|
|
||||||
pub expiration_time: u64,
|
|
||||||
#[serde(rename = "iat")]
|
|
||||||
pub issues_at: usize,
|
|
||||||
/// Account login
|
|
||||||
#[serde(rename = "sub")]
|
|
||||||
pub subject: String,
|
|
||||||
#[serde(rename = "aud")]
|
|
||||||
pub audience: Audience,
|
|
||||||
#[serde(rename = "jti")]
|
|
||||||
pub jwt_id: uuid::Uuid,
|
|
||||||
#[serde(rename = "aci")]
|
|
||||||
pub account_id: i32,
|
|
||||||
#[serde(rename = "nbf")]
|
|
||||||
pub not_before: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl actix_jwt_session::Claims for Claims {
|
|
||||||
fn jti(&self) -> uuid::Uuid {
|
|
||||||
self.jwt_id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn subject(&self) -> &str {
|
|
||||||
&self.subject
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Claims {
|
|
||||||
pub fn account_id(&self) -> i32 {
|
|
||||||
self.account_id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Then you must create middleware factory with session storage. Currently there's adapter only
|
|
||||||
for redis so we will goes with it in this example.
|
|
||||||
|
|
||||||
* First create connection pool to redis using `redis_async_pool`.
|
|
||||||
* Next generate or load create jwt signing keys. They are required for creating JWT from
|
|
||||||
claims.
|
|
||||||
* Finally pass keys and algorithm to builder, pass pool and add some extractors
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use std::sync::Arc;
|
|
||||||
use actix_jwt_session::*;
|
|
||||||
|
|
||||||
async fn create<AppClaims: actix_jwt_session::Claims>() {
|
|
||||||
// create redis connection
|
|
||||||
let redis = {
|
|
||||||
use redis_async_pool::{RedisConnectionManager, RedisPool};
|
|
||||||
RedisPool::new(
|
|
||||||
RedisConnectionManager::new(
|
|
||||||
redis::Client::open("redis://localhost:6379").expect("Fail to connect to redis"),
|
|
||||||
true,
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
5,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
// load or create new keys in `./config`
|
|
||||||
let keys = JwtSigningKeys::load_or_create();
|
|
||||||
|
|
||||||
// create new [SessionStorage] and [SessionMiddlewareFactory]
|
|
||||||
let (storage, factory) = SessionMiddlewareFactory::<AppClaims>::build(
|
|
||||||
Arc::new(keys.encoding_key),
|
|
||||||
Arc::new(keys.decoding_key),
|
|
||||||
Algorithm::EdDSA
|
|
||||||
)
|
|
||||||
// pass redis connection
|
|
||||||
.with_redis_pool(redis.clone())
|
|
||||||
// Check if header "Authorization" exists and contains Bearer with encoded JWT
|
|
||||||
.with_jwt_header("Authorization")
|
|
||||||
// Check if cookie "jwt" exists and contains encoded JWT
|
|
||||||
.with_jwt_cookie("acx-a")
|
|
||||||
.with_refresh_header("ACX-Refresh")
|
|
||||||
// Check if cookie "jwt" exists and contains encoded JWT
|
|
||||||
.with_refresh_cookie("acx-r")
|
|
||||||
.finish();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
As you can see we have there [SessionMiddlewareBuilder::with_refresh_cookie] and [SessionMiddlewareBuilder::with_refresh_header]. Library uses
|
|
||||||
internal structure [RefreshToken] which is created and managed internally without any additional user work.
|
|
||||||
|
|
||||||
This will be used to extend JWT lifetime. This lifetime comes from 2 structures which describe
|
|
||||||
time to live. [JwtTtl] describes how long access token should be valid, [RefreshToken]
|
|
||||||
describes how long refresh token is valid. [SessionStorage] allows to extend livetime of both
|
|
||||||
with single call of [SessionStorage::refresh] and it will change time of creating tokens to
|
|
||||||
current time.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use actix_jwt_session::{JwtTtl, RefreshTtl, Duration};
|
|
||||||
|
|
||||||
fn example_ttl() {
|
|
||||||
let jwt_ttl = JwtTtl(Duration::days(14));
|
|
||||||
let refresh_ttl = RefreshTtl(Duration::days(3 * 31));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Now you just need to add those structures to [actix_web::App] using `.app_data` and `.wrap` and
|
|
||||||
you are ready to go. Bellow you have full example of usage.
|
|
||||||
|
|
||||||
Examples usage:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use std::sync::Arc;
|
|
||||||
use actix_jwt_session::*;
|
|
||||||
use actix_web::{get, post};
|
|
||||||
use actix_web::web::{Data, Json};
|
|
||||||
use actix_web::{HttpResponse, App, HttpServer};
|
|
||||||
use jsonwebtoken::*;
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let redis = {
|
|
||||||
use redis_async_pool::{RedisConnectionManager, RedisPool};
|
|
||||||
RedisPool::new(
|
|
||||||
RedisConnectionManager::new(
|
|
||||||
redis::Client::open("redis://localhost:6379").expect("Fail to connect to redis"),
|
|
||||||
true,
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
5,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
let keys = JwtSigningKeys::load_or_create();
|
|
||||||
let (storage, factory) = SessionMiddlewareFactory::<AppClaims>::build(
|
|
||||||
Arc::new(keys.encoding_key),
|
|
||||||
Arc::new(keys.decoding_key),
|
|
||||||
Algorithm::EdDSA
|
|
||||||
)
|
|
||||||
.with_redis_pool(redis.clone())
|
|
||||||
// Check if header "Authorization" exists and contains Bearer with encoded JWT
|
|
||||||
.with_jwt_header(JWT_HEADER_NAME)
|
|
||||||
// Check if cookie JWT exists and contains encoded JWT
|
|
||||||
.with_jwt_cookie(JWT_COOKIE_NAME)
|
|
||||||
.with_refresh_header(REFRESH_HEADER_NAME)
|
|
||||||
// Check if cookie JWT exists and contains encoded JWT
|
|
||||||
.with_refresh_cookie(REFRESH_COOKIE_NAME)
|
|
||||||
.finish();
|
|
||||||
let jwt_ttl = JwtTtl(Duration::days(14));
|
|
||||||
let refresh_ttl = RefreshTtl(Duration::days(3 * 31));
|
|
||||||
|
|
||||||
HttpServer::new(move || {
|
|
||||||
App::new()
|
|
||||||
.app_data(Data::new(storage.clone()))
|
|
||||||
.app_data(Data::new( jwt_ttl ))
|
|
||||||
.app_data(Data::new( refresh_ttl ))
|
|
||||||
.wrap(factory.clone())
|
|
||||||
.app_data(Data::new(redis.clone()))
|
|
||||||
.service(must_be_signed_in)
|
|
||||||
.service(may_be_signed_in)
|
|
||||||
.service(register)
|
|
||||||
.service(sign_in)
|
|
||||||
.service(sign_out)
|
|
||||||
.service(refresh_session)
|
|
||||||
.service(session_info)
|
|
||||||
.service(root)
|
|
||||||
})
|
|
||||||
.bind(("0.0.0.0", 8080)).unwrap()
|
|
||||||
.run()
|
|
||||||
.await.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Serialize, Deserialize)]
|
|
||||||
pub struct SessionData {
|
|
||||||
id: uuid::Uuid,
|
|
||||||
subject: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/authorized")]
|
|
||||||
async fn must_be_signed_in(session: Authenticated<AppClaims>) -> HttpResponse {
|
|
||||||
use crate::actix_jwt_session::Claims;
|
|
||||||
let jit = session.jti();
|
|
||||||
HttpResponse::Ok().finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/maybe-authorized")]
|
|
||||||
async fn may_be_signed_in(session: MaybeAuthenticated<AppClaims>) -> HttpResponse {
|
|
||||||
if let Some(session) = session.into_option() {
|
|
||||||
}
|
|
||||||
HttpResponse::Ok().finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct SignUpPayload {
|
|
||||||
login: String,
|
|
||||||
password: String,
|
|
||||||
password_confirmation: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/session/sign-up")]
|
|
||||||
async fn register(payload: Json<SignUpPayload>) -> Result<HttpResponse, actix_web::Error> {
|
|
||||||
let payload = payload.into_inner();
|
|
||||||
|
|
||||||
// Validate payload
|
|
||||||
|
|
||||||
// Save model and return HttpResponse
|
|
||||||
let model = AccountModel {
|
|
||||||
id: -1,
|
|
||||||
login: payload.login,
|
|
||||||
// Encrypt password before saving to database
|
|
||||||
pass_hash: Hashing::encrypt(&payload.password).unwrap(),
|
|
||||||
};
|
|
||||||
// Save model
|
|
||||||
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct SignInPayload {
|
|
||||||
login: String,
|
|
||||||
password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/session/sign-in")]
|
|
||||||
async fn sign_in(
|
|
||||||
store: Data<SessionStorage>,
|
|
||||||
payload: Json<SignInPayload>,
|
|
||||||
jwt_ttl: Data<JwtTtl>,
|
|
||||||
refresh_ttl: Data<RefreshTtl>,
|
|
||||||
) -> Result<HttpResponse, actix_web::Error> {
|
|
||||||
let payload = payload.into_inner();
|
|
||||||
let store = store.into_inner();
|
|
||||||
let account: AccountModel = {
|
|
||||||
/* load account using login */
|
|
||||||
todo!()
|
|
||||||
};
|
|
||||||
if let Err(e) = Hashing::verify(account.pass_hash.as_str(), payload.password.as_str()) {
|
|
||||||
return Ok(HttpResponse::Unauthorized().finish());
|
|
||||||
}
|
|
||||||
let claims = AppClaims {
|
|
||||||
issues_at: OffsetDateTime::now_utc().unix_timestamp() as usize,
|
|
||||||
subject: account.login.clone(),
|
|
||||||
expiration_time: jwt_ttl.0.as_seconds_f64() as u64,
|
|
||||||
audience: Audience::Web,
|
|
||||||
jwt_id: uuid::Uuid::new_v4(),
|
|
||||||
account_id: account.id,
|
|
||||||
not_before: 0,
|
|
||||||
};
|
|
||||||
let pair = store
|
|
||||||
.clone()
|
|
||||||
.store(claims, *jwt_ttl.into_inner(), *refresh_ttl.into_inner())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
Ok(HttpResponse::Ok()
|
|
||||||
.append_header((JWT_HEADER_NAME, pair.jwt.encode().unwrap()))
|
|
||||||
.append_header((REFRESH_HEADER_NAME, pair.refresh.encode().unwrap()))
|
|
||||||
.finish())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/session/sign-out")]
|
|
||||||
async fn sign_out(store: Data<SessionStorage>, auth: Authenticated<AppClaims>) -> HttpResponse {
|
|
||||||
let store = store.into_inner();
|
|
||||||
store.erase::<AppClaims>(auth.jwt_id).await.unwrap();
|
|
||||||
HttpResponse::Ok()
|
|
||||||
.append_header((JWT_HEADER_NAME, ""))
|
|
||||||
.append_header((REFRESH_HEADER_NAME, ""))
|
|
||||||
.cookie(
|
|
||||||
actix_web::cookie::Cookie::build(JWT_COOKIE_NAME, "")
|
|
||||||
.expires(OffsetDateTime::now_utc())
|
|
||||||
.finish(),
|
|
||||||
)
|
|
||||||
.cookie(
|
|
||||||
actix_web::cookie::Cookie::build(REFRESH_COOKIE_NAME, "")
|
|
||||||
.expires(OffsetDateTime::now_utc())
|
|
||||||
.finish(),
|
|
||||||
)
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/session/info")]
|
|
||||||
async fn session_info(auth: Authenticated<AppClaims>) -> HttpResponse {
|
|
||||||
HttpResponse::Ok().json(&*auth)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/session/refresh")]
|
|
||||||
async fn refresh_session(
|
|
||||||
auth: Authenticated<RefreshToken>,
|
|
||||||
storage: Data<SessionStorage>,
|
|
||||||
) -> HttpResponse {
|
|
||||||
let storage = storage.into_inner();
|
|
||||||
storage.refresh(auth.refresh_jti).await.unwrap();
|
|
||||||
HttpResponse::Ok().json(&*auth)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/")]
|
|
||||||
async fn root() -> HttpResponse {
|
|
||||||
HttpResponse::Ok().finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum Audience {
|
|
||||||
Web,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub struct AppClaims {
|
|
||||||
#[serde(rename = "exp")]
|
|
||||||
pub expiration_time: u64,
|
|
||||||
#[serde(rename = "iat")]
|
|
||||||
pub issues_at: usize,
|
|
||||||
/// Account login
|
|
||||||
#[serde(rename = "sub")]
|
|
||||||
pub subject: String,
|
|
||||||
#[serde(rename = "aud")]
|
|
||||||
pub audience: Audience,
|
|
||||||
#[serde(rename = "jti")]
|
|
||||||
pub jwt_id: uuid::Uuid,
|
|
||||||
#[serde(rename = "aci")]
|
|
||||||
pub account_id: i32,
|
|
||||||
#[serde(rename = "nbf")]
|
|
||||||
pub not_before: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl actix_jwt_session::Claims for AppClaims {
|
|
||||||
fn jti(&self) -> uuid::Uuid {
|
|
||||||
self.jwt_id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn subject(&self) -> &str {
|
|
||||||
&self.subject
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppClaims {
|
|
||||||
pub fn account_id(&self) -> i32 {
|
|
||||||
self.account_id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AccountModel {
|
|
||||||
id: i32,
|
|
||||||
login: String,
|
|
||||||
pass_hash: String,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
# Changelog:
|
|
||||||
|
|
||||||
1.0.0
|
|
||||||
|
|
||||||
* Factory is created using builder pattern
|
|
||||||
* JSON Web Token has automatically created Refresh Token
|
|
||||||
* Higher abstraction layers for Middleware, MiddlewareFactory and SessionStorage
|
|
||||||
* Build-in hashing functions
|
|
||||||
* Build-in TTL structures
|
|
||||||
* Documentation
|
|
||||||
|
|
||||||
1.0.1
|
|
||||||
|
|
||||||
* Returns new pair after refresh lifetime
|
|
@ -1,208 +0,0 @@
|
|||||||
//! Allow to create own session extractor and extract from cookie or header.
|
|
||||||
|
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
/// Trait allowing to extract JWt token from [actix_web::dev::ServiceRequest]
|
|
||||||
///
|
|
||||||
/// Two extractor are implemented by default
|
|
||||||
/// * [HeaderExtractor] which is best for any PWA or micro services requests
|
|
||||||
/// * [CookieExtractor] which is best for simple server with session stored in cookie
|
|
||||||
///
|
|
||||||
/// It's possible to implement GraphQL, JSON payload or query using `req.extract::<JSON<YourStruct>>()` if this is needed.
|
|
||||||
///
|
|
||||||
/// All implementation can use [SessionExtractor::decode] method for decoding raw JWT string into
|
|
||||||
/// Claims and then [SessionExtractor::validate] to validate claims agains session stored in [SessionStorage]
|
|
||||||
#[async_trait(?Send)]
|
|
||||||
pub trait SessionExtractor<ClaimsType: Claims>: Send + Sync + 'static {
|
|
||||||
/// Extract claims from [actix_web::dev::ServiceRequest]
|
|
||||||
///
|
|
||||||
/// Examples:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use actix_web::dev::ServiceRequest;
|
|
||||||
/// use jsonwebtoken::*;
|
|
||||||
/// use actix_jwt_session::*;
|
|
||||||
/// use std::sync::Arc;
|
|
||||||
/// use actix_web::HttpMessage;
|
|
||||||
/// use std::borrow::Cow;
|
|
||||||
///
|
|
||||||
/// # #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
|
||||||
/// # pub struct Claims { id: uuid::Uuid, sub: String }
|
|
||||||
/// # impl actix_jwt_session::Claims for Claims {
|
|
||||||
/// # fn jti(&self) -> uuid::Uuid { self.id }
|
|
||||||
/// # fn subject(&self) -> &str { &self.sub }
|
|
||||||
/// # }
|
|
||||||
///
|
|
||||||
/// #[derive(Debug, Clone, Copy, Default)]
|
|
||||||
/// struct ExampleExtractor;
|
|
||||||
///
|
|
||||||
/// #[async_trait::async_trait(?Send)]
|
|
||||||
/// impl SessionExtractor<Claims> for ExampleExtractor {
|
|
||||||
/// async fn extract_claims(
|
|
||||||
/// &self,
|
|
||||||
/// req: &mut ServiceRequest,
|
|
||||||
/// jwt_encoding_key: Arc<EncodingKey>,
|
|
||||||
/// jwt_decoding_key: Arc<DecodingKey>,
|
|
||||||
/// algorithm: Algorithm,
|
|
||||||
/// storage: SessionStorage,
|
|
||||||
/// ) -> Result<(), Error> {
|
|
||||||
/// if req.peer_addr().unwrap().ip().is_multicast() {
|
|
||||||
/// req.extensions_mut().insert(Authenticated {
|
|
||||||
/// claims: Arc::new(Claims { id: uuid::Uuid::default(), sub: "HUB".into() }),
|
|
||||||
/// jwt_encoding_key,
|
|
||||||
/// algorithm,
|
|
||||||
/// });
|
|
||||||
/// }
|
|
||||||
/// Ok(())
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// async fn extract_token_text<'req>(&self, req: &'req mut ServiceRequest) -> Option<Cow<'req, str>> { None }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
async fn extract_claims(
|
|
||||||
&self,
|
|
||||||
req: &mut ServiceRequest,
|
|
||||||
jwt_encoding_key: Arc<EncodingKey>,
|
|
||||||
jwt_decoding_key: Arc<DecodingKey>,
|
|
||||||
algorithm: Algorithm,
|
|
||||||
storage: SessionStorage,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let Some(as_str) = self.extract_token_text(req).await else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let decoded_claims = self.decode(&as_str, jwt_decoding_key, algorithm)?;
|
|
||||||
self.validate(&decoded_claims, storage).await?;
|
|
||||||
req.extensions_mut().insert(Authenticated {
|
|
||||||
claims: Arc::new(decoded_claims),
|
|
||||||
jwt_encoding_key,
|
|
||||||
algorithm,
|
|
||||||
});
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decode encrypted JWT to structure
|
|
||||||
fn decode(
|
|
||||||
&self,
|
|
||||||
value: &str,
|
|
||||||
jwt_decoding_key: Arc<DecodingKey>,
|
|
||||||
algorithm: Algorithm,
|
|
||||||
) -> Result<ClaimsType, Error> {
|
|
||||||
let mut validation = Validation::new(algorithm);
|
|
||||||
validation.validate_exp = false;
|
|
||||||
validation.validate_nbf = false;
|
|
||||||
validation.leeway = 0;
|
|
||||||
validation.required_spec_claims.clear();
|
|
||||||
|
|
||||||
decode::<ClaimsType>(value, &jwt_decoding_key, &validation)
|
|
||||||
.map_err(|e| {
|
|
||||||
#[cfg(feature = "use-tracing")]
|
|
||||||
tracing::debug!("Failed to decode claims: {e:?}. {e}");
|
|
||||||
Error::CantDecode
|
|
||||||
})
|
|
||||||
.map(|t| t.claims)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validate JWT Claims agains stored in storage tokens.
|
|
||||||
///
|
|
||||||
/// * Token must exists in storage
|
|
||||||
/// * Token must be exactly the same as token from storage
|
|
||||||
async fn validate(&self, claims: &ClaimsType, storage: SessionStorage) -> Result<(), Error> {
|
|
||||||
let stored = storage
|
|
||||||
.clone()
|
|
||||||
.find_jwt::<ClaimsType>(claims.jti())
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
#[cfg(feature = "use-tracing")]
|
|
||||||
tracing::debug!(
|
|
||||||
"Failed to load {} from storage: {e:?}",
|
|
||||||
std::any::type_name::<ClaimsType>()
|
|
||||||
);
|
|
||||||
Error::LoadError
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if &stored != claims {
|
|
||||||
#[cfg(feature = "use-tracing")]
|
|
||||||
tracing::debug!("{claims:?} != {stored:?}");
|
|
||||||
Err(Error::DontMatch)
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lookup for session data as a string in [actix_web::dev::ServiceRequest]
|
|
||||||
///
|
|
||||||
/// If there's no token data in request you should returns `None`. This is not considered as an
|
|
||||||
/// error and until endpoint requires `Authenticated` this will not results in `401`.
|
|
||||||
async fn extract_token_text<'req>(
|
|
||||||
&self,
|
|
||||||
req: &'req mut ServiceRequest,
|
|
||||||
) -> Option<Cow<'req, str>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extracts JWT token from HTTP Request cookies. This extractor should be used when you can't set
|
|
||||||
/// your own header, for example when user enters http links to browser and you don't have any
|
|
||||||
/// advanced frontend.
|
|
||||||
///
|
|
||||||
/// This exractor is may be used by PWA application or micro services but [HeaderExtractor] is much
|
|
||||||
/// more suitable for this purpose.
|
|
||||||
pub struct CookieExtractor<ClaimsType> {
|
|
||||||
__ty: PhantomData<ClaimsType>,
|
|
||||||
cookie_name: &'static str,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<ClaimsType: Claims> CookieExtractor<ClaimsType> {
|
|
||||||
/// Creates new cookie extractor.
|
|
||||||
/// It will extract token data from cookie with given name
|
|
||||||
pub fn new(cookie_name: &'static str) -> Self {
|
|
||||||
Self {
|
|
||||||
__ty: Default::default(),
|
|
||||||
cookie_name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait(?Send)]
|
|
||||||
impl<ClaimsType: Claims> SessionExtractor<ClaimsType> for CookieExtractor<ClaimsType> {
|
|
||||||
async fn extract_token_text<'req>(
|
|
||||||
&self,
|
|
||||||
req: &'req mut ServiceRequest,
|
|
||||||
) -> Option<Cow<'req, str>> {
|
|
||||||
req.cookie(self.cookie_name)
|
|
||||||
.map(|c| c.value().to_string().into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extracts JWT token from HTTP Request headers
|
|
||||||
///
|
|
||||||
/// This exractor is very useful for all PWA application or for micro services
|
|
||||||
/// because you can set your own headers while making http requests.
|
|
||||||
///
|
|
||||||
/// If you want to have users authorized using simple html anchor (tag A) you should use [CookieExtractor]
|
|
||||||
pub struct HeaderExtractor<ClaimsType> {
|
|
||||||
__ty: PhantomData<ClaimsType>,
|
|
||||||
header_name: &'static str,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<ClaimsType: Claims> HeaderExtractor<ClaimsType> {
|
|
||||||
/// Creates new header extractor.
|
|
||||||
/// It will extract token data from header with given name
|
|
||||||
pub fn new(header_name: &'static str) -> Self {
|
|
||||||
Self {
|
|
||||||
__ty: Default::default(),
|
|
||||||
header_name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait(?Send)]
|
|
||||||
impl<ClaimsType: Claims> SessionExtractor<ClaimsType> for HeaderExtractor<ClaimsType> {
|
|
||||||
async fn extract_token_text<'req>(
|
|
||||||
&self,
|
|
||||||
req: &'req mut ServiceRequest,
|
|
||||||
) -> Option<Cow<'req, str>> {
|
|
||||||
req.headers()
|
|
||||||
.get(self.header_name)
|
|
||||||
.and_then(|h| h.to_str().ok())
|
|
||||||
.map(|h| h.to_owned().into())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
//! Encrypting and decrypting password
|
|
||||||
//!
|
|
||||||
//! This module is available by default or by enabling `hashing` feature.
|
|
||||||
//! Library docs covers using it in context of `register` and `sign in`.
|
|
||||||
|
|
||||||
use argon2::{
|
|
||||||
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
|
|
||||||
Argon2,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Encrypting and decrypting password
|
|
||||||
pub struct Hashing;
|
|
||||||
|
|
||||||
impl Hashing {
|
|
||||||
/// Takes password and returns encrypted hash with random salt
|
|
||||||
pub fn encrypt(password: &str) -> argon2::password_hash::Result<String> {
|
|
||||||
let salt = SaltString::generate(&mut OsRng);
|
|
||||||
let argon2 = Argon2::default();
|
|
||||||
argon2
|
|
||||||
.hash_password(password.as_bytes(), &salt)
|
|
||||||
.map(|hash| hash.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Takes password hash and password and validates it.
|
|
||||||
pub fn verify(password_hash: &str, password: &str) -> argon2::password_hash::Result<()> {
|
|
||||||
let parsed_hash = PasswordHash::new(password_hash)?;
|
|
||||||
Argon2::default().verify_password(password.as_bytes(), &parsed_hash)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn check_always_random_salt() {
|
|
||||||
let pass = "ahs9dya8tsd7fa8tsa86tT&^R%^DS^%ARS&A";
|
|
||||||
let hash = Hashing::encrypt(pass).unwrap();
|
|
||||||
assert!(Hashing::verify(hash.as_str(), pass).is_ok());
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -1,295 +0,0 @@
|
|||||||
//! Create session storage and build middleware factory
|
|
||||||
|
|
||||||
use crate::*;
|
|
||||||
pub use actix_web::cookie::time::{Duration, OffsetDateTime};
|
|
||||||
use actix_web::dev::Transform;
|
|
||||||
use actix_web::dev::{forward_ready, Service, ServiceRequest, ServiceResponse};
|
|
||||||
use futures_util::future::LocalBoxFuture;
|
|
||||||
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey};
|
|
||||||
use std::future::{ready, Ready};
|
|
||||||
use std::rc::Rc;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
/// Session middleware factory builder
|
|
||||||
///
|
|
||||||
/// It should be constructed with [SessionMiddlewareFactory::build].
|
|
||||||
pub struct SessionMiddlewareBuilder<ClaimsType: Claims> {
|
|
||||||
pub(crate) jwt_encoding_key: Arc<EncodingKey>,
|
|
||||||
pub(crate) jwt_decoding_key: Arc<DecodingKey>,
|
|
||||||
pub(crate) algorithm: Algorithm,
|
|
||||||
pub(crate) storage: Option<SessionStorage>,
|
|
||||||
pub(crate) jwt_extractors: Vec<Box<dyn SessionExtractor<ClaimsType>>>,
|
|
||||||
pub(crate) refresh_extractors: Vec<Box<dyn SessionExtractor<RefreshToken>>>,
|
|
||||||
}
|
|
||||||
impl<ClaimsType: Claims> SessionMiddlewareBuilder<ClaimsType> {
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub(crate) fn new(
|
|
||||||
jwt_encoding_key: Arc<EncodingKey>,
|
|
||||||
jwt_decoding_key: Arc<DecodingKey>,
|
|
||||||
algorithm: Algorithm,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
jwt_encoding_key: jwt_encoding_key.clone(),
|
|
||||||
jwt_decoding_key,
|
|
||||||
algorithm,
|
|
||||||
storage: None,
|
|
||||||
jwt_extractors: vec![],
|
|
||||||
refresh_extractors: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set session storage to given instance. Good if for some reason you need to share 1 storage
|
|
||||||
/// with multiple instances of session middleware
|
|
||||||
#[must_use]
|
|
||||||
pub fn with_storage(mut self, storage: SessionStorage) -> Self {
|
|
||||||
self.storage = Some(storage);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add cookie extractor for refresh token.
|
|
||||||
#[must_use]
|
|
||||||
pub fn with_refresh_cookie(mut self, name: &'static str) -> Self {
|
|
||||||
self.refresh_extractors
|
|
||||||
.push(Box::new(CookieExtractor::<RefreshToken>::new(name)));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add header extractor for refresh token.
|
|
||||||
#[must_use]
|
|
||||||
pub fn with_refresh_header(mut self, name: &'static str) -> Self {
|
|
||||||
self.refresh_extractors
|
|
||||||
.push(Box::new(HeaderExtractor::<RefreshToken>::new(name)));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add cookie extractor for json web token.
|
|
||||||
#[must_use]
|
|
||||||
pub fn with_jwt_cookie(mut self, name: &'static str) -> Self {
|
|
||||||
self.jwt_extractors
|
|
||||||
.push(Box::new(CookieExtractor::<ClaimsType>::new(name)));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add header extractor for json web token.
|
|
||||||
#[must_use]
|
|
||||||
pub fn with_jwt_header(mut self, name: &'static str) -> Self {
|
|
||||||
self.jwt_extractors
|
|
||||||
.push(Box::new(HeaderExtractor::<ClaimsType>::new(name)));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds middleware factory and returns session storage with factory
|
|
||||||
pub fn finish(self) -> (SessionStorage, SessionMiddlewareFactory<ClaimsType>) {
|
|
||||||
let Self {
|
|
||||||
storage,
|
|
||||||
jwt_encoding_key,
|
|
||||||
jwt_decoding_key,
|
|
||||||
algorithm,
|
|
||||||
jwt_extractors,
|
|
||||||
refresh_extractors,
|
|
||||||
..
|
|
||||||
} = self;
|
|
||||||
let storage = storage
|
|
||||||
.expect("Session storage must be constracted from pool or set from existing storage");
|
|
||||||
(
|
|
||||||
storage.clone(),
|
|
||||||
SessionMiddlewareFactory {
|
|
||||||
jwt_encoding_key,
|
|
||||||
jwt_decoding_key,
|
|
||||||
algorithm,
|
|
||||||
storage,
|
|
||||||
jwt_extractors: Arc::new(jwt_extractors),
|
|
||||||
refresh_extractors: Arc::new(refresh_extractors),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Factory creates middlware for every single request.
|
|
||||||
///
|
|
||||||
/// All fields here are immutable and have atomic access and only pointer is copied so are very cheap
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use std::sync::Arc;
|
|
||||||
/// use actix_jwt_session::*;
|
|
||||||
///
|
|
||||||
/// # async fn create<AppClaims: actix_jwt_session::Claims>() {
|
|
||||||
/// // create redis connection
|
|
||||||
/// let redis = {
|
|
||||||
/// use redis_async_pool::{RedisConnectionManager, RedisPool};
|
|
||||||
/// RedisPool::new(
|
|
||||||
/// RedisConnectionManager::new(
|
|
||||||
/// redis::Client::open("redis://localhost:6379").expect("Fail to connect to redis"),
|
|
||||||
/// true,
|
|
||||||
/// None,
|
|
||||||
/// ),
|
|
||||||
/// 5,
|
|
||||||
/// )
|
|
||||||
/// };
|
|
||||||
///
|
|
||||||
/// // load or create new keys in `./config`
|
|
||||||
/// let keys = JwtSigningKeys::load_or_create();
|
|
||||||
///
|
|
||||||
/// // create new [SessionStorage] and [SessionMiddlewareFactory]
|
|
||||||
/// let (storage, factory) = SessionMiddlewareFactory::<AppClaims>::build(
|
|
||||||
/// Arc::new(keys.encoding_key),
|
|
||||||
/// Arc::new(keys.decoding_key),
|
|
||||||
/// Algorithm::EdDSA
|
|
||||||
/// )
|
|
||||||
/// // pass redis connection
|
|
||||||
/// .with_redis_pool(redis.clone())
|
|
||||||
/// // Check if header "Authorization" exists and contains Bearer with encoded JWT
|
|
||||||
/// .with_jwt_header("Authorization")
|
|
||||||
/// // Check if cookie "jwt" exists and contains encoded JWT
|
|
||||||
/// .with_jwt_cookie("acx-a")
|
|
||||||
/// .with_refresh_header("ACX-Refresh")
|
|
||||||
/// // Check if cookie "jwt" exists and contains encoded JWT
|
|
||||||
/// .with_refresh_cookie("acx-r")
|
|
||||||
/// .finish();
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct SessionMiddlewareFactory<ClaimsType: Claims> {
|
|
||||||
pub(crate) jwt_encoding_key: Arc<EncodingKey>,
|
|
||||||
pub(crate) jwt_decoding_key: Arc<DecodingKey>,
|
|
||||||
pub(crate) algorithm: Algorithm,
|
|
||||||
pub(crate) storage: SessionStorage,
|
|
||||||
pub(crate) jwt_extractors: Arc<Vec<Box<dyn SessionExtractor<ClaimsType>>>>,
|
|
||||||
pub(crate) refresh_extractors: Arc<Vec<Box<dyn SessionExtractor<RefreshToken>>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<ClaimsType: Claims> SessionMiddlewareFactory<ClaimsType> {
|
|
||||||
pub fn build(
|
|
||||||
jwt_encoding_key: Arc<EncodingKey>,
|
|
||||||
jwt_decoding_key: Arc<DecodingKey>,
|
|
||||||
algorithm: Algorithm,
|
|
||||||
) -> SessionMiddlewareBuilder<ClaimsType> {
|
|
||||||
SessionMiddlewareBuilder::new(jwt_encoding_key, jwt_decoding_key, algorithm)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S, B, ClaimsType> Transform<S, ServiceRequest> for SessionMiddlewareFactory<ClaimsType>
|
|
||||||
where
|
|
||||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
|
|
||||||
ClaimsType: Claims,
|
|
||||||
{
|
|
||||||
type Response = ServiceResponse<B>;
|
|
||||||
type Error = actix_web::Error;
|
|
||||||
type Transform = SessionMiddleware<S, ClaimsType>;
|
|
||||||
type InitError = ();
|
|
||||||
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
|
||||||
|
|
||||||
fn new_transform(&self, service: S) -> Self::Future {
|
|
||||||
ready(Ok(SessionMiddleware {
|
|
||||||
service: Rc::new(service),
|
|
||||||
storage: self.storage.clone(),
|
|
||||||
jwt_encoding_key: self.jwt_encoding_key.clone(),
|
|
||||||
jwt_decoding_key: self.jwt_decoding_key.clone(),
|
|
||||||
algorithm: self.algorithm,
|
|
||||||
jwt_extractors: self.jwt_extractors.clone(),
|
|
||||||
refresh_extractors: self.refresh_extractors.clone(),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub struct SessionMiddleware<S, ClaimsType>
|
|
||||||
where
|
|
||||||
ClaimsType: Claims,
|
|
||||||
{
|
|
||||||
pub(crate) service: Rc<S>,
|
|
||||||
pub(crate) jwt_encoding_key: Arc<EncodingKey>,
|
|
||||||
pub(crate) jwt_decoding_key: Arc<DecodingKey>,
|
|
||||||
pub(crate) algorithm: Algorithm,
|
|
||||||
pub(crate) storage: SessionStorage,
|
|
||||||
pub(crate) jwt_extractors: Arc<Vec<Box<dyn SessionExtractor<ClaimsType>>>>,
|
|
||||||
pub(crate) refresh_extractors: Arc<Vec<Box<dyn SessionExtractor<RefreshToken>>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S, ClaimsType: Claims> SessionMiddleware<S, ClaimsType> {
|
|
||||||
async fn extract_token<C: Claims>(
|
|
||||||
req: &mut ServiceRequest,
|
|
||||||
jwt_encoding_key: Arc<EncodingKey>,
|
|
||||||
jwt_decoding_key: Arc<DecodingKey>,
|
|
||||||
algorithm: Algorithm,
|
|
||||||
storage: SessionStorage,
|
|
||||||
extractors: &[Box<dyn SessionExtractor<C>>],
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let mut last_error = None;
|
|
||||||
for extractor in extractors.iter() {
|
|
||||||
match extractor
|
|
||||||
.extract_claims(
|
|
||||||
req,
|
|
||||||
jwt_encoding_key.clone(),
|
|
||||||
jwt_decoding_key.clone(),
|
|
||||||
algorithm,
|
|
||||||
storage.clone(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(_) => break,
|
|
||||||
Err(e) => {
|
|
||||||
last_error = Some(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if let Some(e) = last_error {
|
|
||||||
return Err(e)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S, B, ClaimsType> Service<ServiceRequest> for SessionMiddleware<S, ClaimsType>
|
|
||||||
where
|
|
||||||
ClaimsType: Claims,
|
|
||||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
|
|
||||||
{
|
|
||||||
type Response = ServiceResponse<B>;
|
|
||||||
type Error = actix_web::Error;
|
|
||||||
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
|
||||||
|
|
||||||
forward_ready!(service);
|
|
||||||
|
|
||||||
fn call(&self, mut req: ServiceRequest) -> Self::Future {
|
|
||||||
use futures_lite::FutureExt;
|
|
||||||
|
|
||||||
let svc = self.service.clone();
|
|
||||||
let jwt_decoding_key = self.jwt_decoding_key.clone();
|
|
||||||
let jwt_encoding_key = self.jwt_encoding_key.clone();
|
|
||||||
let algorithm = self.algorithm;
|
|
||||||
let storage = self.storage.clone();
|
|
||||||
let jwt_extractors = self.jwt_extractors.clone();
|
|
||||||
let refresh_extractors = self.refresh_extractors.clone();
|
|
||||||
|
|
||||||
async move {
|
|
||||||
if !jwt_extractors.is_empty() {
|
|
||||||
Self::extract_token(
|
|
||||||
&mut req,
|
|
||||||
jwt_encoding_key.clone(),
|
|
||||||
jwt_decoding_key.clone(),
|
|
||||||
algorithm,
|
|
||||||
storage.clone(),
|
|
||||||
&jwt_extractors,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
if !refresh_extractors.is_empty() {
|
|
||||||
Self::extract_token(
|
|
||||||
&mut req,
|
|
||||||
jwt_encoding_key,
|
|
||||||
jwt_decoding_key,
|
|
||||||
algorithm,
|
|
||||||
storage,
|
|
||||||
&refresh_extractors,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
let res = svc.call(req).await?;
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
.boxed_local()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,195 +0,0 @@
|
|||||||
//! Default session storage which uses async redis requests
|
|
||||||
//!
|
|
||||||
//! Sessions are serialized to binary format and stored using [uuid::Uuid] key as bytes.
|
|
||||||
//! All sessions must have expirations time after which they will be automatically removed by
|
|
||||||
//! redis.
|
|
||||||
//!
|
|
||||||
//! [RedisStorage] is constructed by [RedisMiddlewareFactory] from [redis_async_pool::RedisPool] and shared
|
|
||||||
//! between all [RedisMiddleware] instances.
|
|
||||||
|
|
||||||
use crate::*;
|
|
||||||
use redis::aio::ConnectionLike;
|
|
||||||
use redis::AsyncCommands;
|
|
||||||
use std::marker::PhantomData;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
/// Redis implementation for [TokenStorage]
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct RedisStorage<ClaimsType: Claims> {
|
|
||||||
pool: redis_async_pool::RedisPool,
|
|
||||||
_claims_type_marker: PhantomData<ClaimsType>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<ClaimsType: Claims> RedisStorage<ClaimsType> {
|
|
||||||
pub fn new(pool: redis_async_pool::RedisPool) -> Self {
|
|
||||||
Self {
|
|
||||||
pool,
|
|
||||||
_claims_type_marker: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl<ClaimsType> TokenStorage for RedisStorage<ClaimsType>
|
|
||||||
where
|
|
||||||
ClaimsType: Claims,
|
|
||||||
{
|
|
||||||
async fn get_by_jti(self: Arc<Self>, jti: &[u8]) -> Result<Vec<u8>, Error> {
|
|
||||||
let pool = self.pool.clone();
|
|
||||||
let mut conn = pool.get().await.map_err(|e| {
|
|
||||||
#[cfg(feature = "use-tracing")]
|
|
||||||
tracing::error!("Unable to obtain redis connection: {e}");
|
|
||||||
Error::RedisConn
|
|
||||||
})?;
|
|
||||||
conn.get::<_, Vec<u8>>(jti).await.map_err(|e| {
|
|
||||||
#[cfg(feature = "use-tracing")]
|
|
||||||
tracing::error!("Session record not found in redis: {e}");
|
|
||||||
Error::NotFound
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn set_by_jti(
|
|
||||||
self: Arc<Self>,
|
|
||||||
jwt_jti: &[u8],
|
|
||||||
refresh_jti: &[u8],
|
|
||||||
bytes: &[u8],
|
|
||||||
mut exp: Duration,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
bad_ttl!(
|
|
||||||
exp,
|
|
||||||
Duration::seconds(1),
|
|
||||||
"Expiration time is bellow 1s. This is not allowed for redis server."
|
|
||||||
);
|
|
||||||
let pool = self.pool.clone();
|
|
||||||
let mut conn = pool.get().await.map_err(|e| {
|
|
||||||
#[cfg(feature = "use-tracing")]
|
|
||||||
tracing::error!("Unable to obtain redis connection: {e}");
|
|
||||||
Error::RedisConn
|
|
||||||
})?;
|
|
||||||
let mut pipeline = redis::Pipeline::new();
|
|
||||||
pipeline
|
|
||||||
.set_ex(jwt_jti, bytes, exp.as_seconds_f32() as usize)
|
|
||||||
.set_ex(refresh_jti, bytes, exp.as_seconds_f32() as usize);
|
|
||||||
conn.req_packed_commands(&pipeline, 0, 2)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
#[cfg(feature = "use-tracing")]
|
|
||||||
tracing::error!("Failed to save session in redis: {e}");
|
|
||||||
Error::WriteFailed
|
|
||||||
})?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn remove_by_jti(self: Arc<Self>, jti: &[u8]) -> Result<(), Error> {
|
|
||||||
let pool = self.pool.clone();
|
|
||||||
let mut conn = pool.get().await.map_err(|e| {
|
|
||||||
#[cfg(feature = "use-tracing")]
|
|
||||||
tracing::error!("Unable to obtain redis connection: {e}");
|
|
||||||
Error::RedisConn
|
|
||||||
})?;
|
|
||||||
conn.del(jti).await.map_err(|e| {
|
|
||||||
#[cfg(feature = "use-tracing")]
|
|
||||||
tracing::error!("Session record can't be removed from redis: {e}");
|
|
||||||
Error::NotFound
|
|
||||||
})?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<ClaimsType: Claims> SessionMiddlewareBuilder<ClaimsType> {
|
|
||||||
#[must_use]
|
|
||||||
pub fn with_redis_pool(mut self, pool: redis_async_pool::RedisPool) -> Self {
|
|
||||||
let storage = Arc::new(RedisStorage::<ClaimsType>::new(pool));
|
|
||||||
let storage = SessionStorage::new(storage, self.jwt_encoding_key.clone(), self.algorithm);
|
|
||||||
self.storage = Some(storage);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use actix_web::cookie::time::*;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use std::ops::Add;
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub struct Claims {
|
|
||||||
#[serde(rename = "exp")]
|
|
||||||
pub expires_at: usize,
|
|
||||||
#[serde(rename = "iat")]
|
|
||||||
pub issues_at: usize,
|
|
||||||
/// Account login
|
|
||||||
#[serde(rename = "sub")]
|
|
||||||
pub subject: String,
|
|
||||||
#[serde(rename = "aud")]
|
|
||||||
pub audience: String,
|
|
||||||
#[serde(rename = "jti")]
|
|
||||||
pub jwt_id: uuid::Uuid,
|
|
||||||
#[serde(rename = "aci")]
|
|
||||||
pub account_id: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl crate::Claims for Claims {
|
|
||||||
fn jti(&self) -> uuid::Uuid {
|
|
||||||
self.jwt_id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn subject(&self) -> &str {
|
|
||||||
&self.subject
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn create_storage() -> (SessionStorage, SessionMiddlewareFactory<Claims>) {
|
|
||||||
let redis = {
|
|
||||||
use redis_async_pool::{RedisConnectionManager, RedisPool};
|
|
||||||
RedisPool::new(
|
|
||||||
RedisConnectionManager::new(
|
|
||||||
redis::Client::open("redis://localhost:6379")
|
|
||||||
.expect("Fail to connect to redis"),
|
|
||||||
true,
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
5,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
let jwt_signing_keys = JwtSigningKeys::generate(false).unwrap();
|
|
||||||
SessionMiddlewareFactory::<Claims>::build(
|
|
||||||
Arc::new(jwt_signing_keys.encoding_key),
|
|
||||||
Arc::new(jwt_signing_keys.decoding_key),
|
|
||||||
Algorithm::EdDSA,
|
|
||||||
)
|
|
||||||
.with_redis_pool(redis)
|
|
||||||
.with_refresh_cookie(REFRESH_COOKIE_NAME)
|
|
||||||
.with_refresh_header(REFRESH_HEADER_NAME)
|
|
||||||
.with_jwt_cookie(JWT_COOKIE_NAME)
|
|
||||||
.with_jwt_header(JWT_HEADER_NAME)
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn check_encode() {
|
|
||||||
let (store, _) = create_storage().await;
|
|
||||||
let jwt_exp = JwtTtl(Duration::days(31));
|
|
||||||
let refresh_exp = RefreshTtl(Duration::days(31));
|
|
||||||
|
|
||||||
let original = Claims {
|
|
||||||
subject: "me".into(),
|
|
||||||
expires_at: OffsetDateTime::now_utc()
|
|
||||||
.add(Duration::days(31))
|
|
||||||
.unix_timestamp() as usize,
|
|
||||||
issues_at: OffsetDateTime::now_utc().unix_timestamp() as usize,
|
|
||||||
audience: "web".into(),
|
|
||||||
jwt_id: Uuid::new_v4(),
|
|
||||||
account_id: 24234,
|
|
||||||
};
|
|
||||||
|
|
||||||
store
|
|
||||||
.store(original.clone(), jwt_exp, refresh_exp)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let loaded = store.find_jwt(original.jwt_id).await.unwrap();
|
|
||||||
assert_eq!(original, loaded);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,260 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use actix_jwt_session::*;
|
|
||||||
use actix_web::dev::ServiceResponse;
|
|
||||||
use actix_web::http::{Method, StatusCode};
|
|
||||||
use actix_web::web::{Data, Json};
|
|
||||||
use actix_web::HttpResponse;
|
|
||||||
use actix_web::{get, post};
|
|
||||||
use actix_web::{http::header::ContentType, test, App};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
||||||
struct Claims {
|
|
||||||
id: Uuid,
|
|
||||||
subject: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl actix_jwt_session::Claims for Claims {
|
|
||||||
fn jti(&self) -> Uuid {
|
|
||||||
self.id
|
|
||||||
}
|
|
||||||
fn subject(&self) -> &str {
|
|
||||||
&self.subject
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
|
||||||
async fn full_flow() {
|
|
||||||
let redis = {
|
|
||||||
use redis_async_pool::{RedisConnectionManager, RedisPool};
|
|
||||||
RedisPool::new(
|
|
||||||
RedisConnectionManager::new(
|
|
||||||
redis::Client::open("redis://localhost:6379").expect("Fail to connect to redis"),
|
|
||||||
true,
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
5,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
let keys = JwtSigningKeys::generate(false).unwrap();
|
|
||||||
let (storage, factory) = SessionMiddlewareFactory::<Claims>::build(
|
|
||||||
Arc::new(keys.encoding_key),
|
|
||||||
Arc::new(keys.decoding_key),
|
|
||||||
Algorithm::EdDSA,
|
|
||||||
)
|
|
||||||
.with_redis_pool(redis.clone())
|
|
||||||
.with_jwt_header(JWT_HEADER_NAME)
|
|
||||||
.with_refresh_header(REFRESH_HEADER_NAME)
|
|
||||||
.with_jwt_cookie(JWT_COOKIE_NAME)
|
|
||||||
.with_refresh_cookie(REFRESH_COOKIE_NAME)
|
|
||||||
.finish();
|
|
||||||
|
|
||||||
let app = App::new()
|
|
||||||
.app_data(Data::new(storage.clone()))
|
|
||||||
.wrap(factory.clone())
|
|
||||||
.app_data(Data::new(redis.clone()))
|
|
||||||
.app_data(Data::new(JwtTtl(Duration::seconds(1))))
|
|
||||||
.app_data(Data::new(RefreshTtl(Duration::seconds(30))))
|
|
||||||
.service(sign_in)
|
|
||||||
.service(sign_out)
|
|
||||||
.service(session)
|
|
||||||
.service(refresh_session)
|
|
||||||
.service(root);
|
|
||||||
|
|
||||||
let app = actix_web::test::init_service(app).await;
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
// Assert authorization is ignored when token is not needed
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
let res = test::call_service(
|
|
||||||
&app,
|
|
||||||
test::TestRequest::default()
|
|
||||||
.insert_header(ContentType::plaintext())
|
|
||||||
.to_request(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
assert!(res.status().is_success());
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
// Assert signed out when active session
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
let res = test::call_service(&app, session_request("", "").to_request()).await;
|
|
||||||
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
|
||||||
|
|
||||||
let origina_claims = Claims {
|
|
||||||
id: Uuid::new_v4(),
|
|
||||||
subject: "foo".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// ----------------------------------------------
|
|
||||||
// Create session
|
|
||||||
// ----------------------------------------------
|
|
||||||
println!("-> Creating session");
|
|
||||||
let res = test::call_service(
|
|
||||||
&app,
|
|
||||||
test::TestRequest::default()
|
|
||||||
.uri("/session/sign-in")
|
|
||||||
.method(actix_web::http::Method::POST)
|
|
||||||
.insert_header(ContentType::json())
|
|
||||||
.set_json(&origina_claims)
|
|
||||||
.to_request(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
assert_eq!(res.status(), StatusCode::OK);
|
|
||||||
println!(" <- OK");
|
|
||||||
|
|
||||||
let auth_bearer = res
|
|
||||||
.headers()
|
|
||||||
.get(JWT_HEADER_NAME)
|
|
||||||
.unwrap()
|
|
||||||
.to_str()
|
|
||||||
.unwrap();
|
|
||||||
let refresh_bearer = res
|
|
||||||
.headers()
|
|
||||||
.get(REFRESH_HEADER_NAME)
|
|
||||||
.unwrap()
|
|
||||||
.to_str()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// ----------------------------------------------
|
|
||||||
// Assert signed in
|
|
||||||
// ----------------------------------------------
|
|
||||||
println!("-> Assert signed in");
|
|
||||||
let res = test::call_service(
|
|
||||||
&app,
|
|
||||||
session_request(&auth_bearer, &refresh_bearer).to_request(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
assert_eq!(res.status(), StatusCode::OK);
|
|
||||||
println!(" <- OK");
|
|
||||||
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
|
||||||
|
|
||||||
// ----------------------------------------------
|
|
||||||
// Access Token TTL expires
|
|
||||||
// ----------------------------------------------
|
|
||||||
println!("-> Access Token TTL expires");
|
|
||||||
let res = test::try_call_service(
|
|
||||||
&app,
|
|
||||||
session_request(&auth_bearer, &refresh_bearer).to_request(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
expect_invalid_session(res);
|
|
||||||
println!(" <- OK");
|
|
||||||
|
|
||||||
// ----------------------------------------------
|
|
||||||
// Refresh token
|
|
||||||
// ----------------------------------------------
|
|
||||||
println!("-> Refresh token");
|
|
||||||
let res = test::call_service(
|
|
||||||
&app,
|
|
||||||
test::TestRequest::default()
|
|
||||||
.uri("/session/refresh")
|
|
||||||
.method(Method::GET)
|
|
||||||
.insert_header((REFRESH_HEADER_NAME, refresh_bearer))
|
|
||||||
.to_request(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
assert_eq!(res.status(), StatusCode::OK);
|
|
||||||
println!(" <- OK");
|
|
||||||
|
|
||||||
// ----------------------------------------------
|
|
||||||
// Logout
|
|
||||||
// ----------------------------------------------
|
|
||||||
println!("-> Logout");
|
|
||||||
let res = test::call_service(
|
|
||||||
&app,
|
|
||||||
test::TestRequest::default()
|
|
||||||
.uri("/session/sign-out")
|
|
||||||
.method(Method::POST)
|
|
||||||
.insert_header((JWT_HEADER_NAME, auth_bearer))
|
|
||||||
.insert_header((REFRESH_HEADER_NAME, refresh_bearer))
|
|
||||||
.to_request(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
assert_eq!(res.status(), StatusCode::OK);
|
|
||||||
println!(" <- OK");
|
|
||||||
|
|
||||||
// --------------------------------------------------------------
|
|
||||||
// Assert signed out - session destroyed
|
|
||||||
// --------------------------------------------------------------
|
|
||||||
println!("-> Assert signed out - session destroyed");
|
|
||||||
let res = test::try_call_service(
|
|
||||||
&app,
|
|
||||||
session_request(&auth_bearer, &refresh_bearer).to_request(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
expect_invalid_session(res);
|
|
||||||
println!(" <- OK");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/session/sign-in")]
|
|
||||||
async fn sign_in(
|
|
||||||
store: Data<SessionStorage>,
|
|
||||||
claims: Json<Claims>,
|
|
||||||
jwt_ttl: Data<JwtTtl>,
|
|
||||||
refresh_ttl: Data<RefreshTtl>,
|
|
||||||
) -> Result<HttpResponse, actix_web::Error> {
|
|
||||||
let claims = claims.into_inner();
|
|
||||||
let store = store.into_inner();
|
|
||||||
let pair = store
|
|
||||||
.clone()
|
|
||||||
.store(claims, *jwt_ttl.into_inner(), *refresh_ttl.into_inner())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
Ok(HttpResponse::Ok()
|
|
||||||
.append_header((JWT_HEADER_NAME, pair.jwt.encode().unwrap()))
|
|
||||||
.append_header((REFRESH_HEADER_NAME, pair.refresh.encode().unwrap()))
|
|
||||||
.finish())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/session/sign-out")]
|
|
||||||
async fn sign_out(store: Data<SessionStorage>, auth: Authenticated<Claims>) -> HttpResponse {
|
|
||||||
let store = store.into_inner();
|
|
||||||
store.erase::<Claims>(auth.id).await.unwrap();
|
|
||||||
HttpResponse::Ok().finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/session/info")]
|
|
||||||
async fn session(auth: Authenticated<Claims>) -> HttpResponse {
|
|
||||||
HttpResponse::Ok().json(&*auth)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/session/refresh")]
|
|
||||||
async fn refresh_session(
|
|
||||||
auth: Authenticated<RefreshToken>,
|
|
||||||
storage: Data<SessionStorage>,
|
|
||||||
) -> HttpResponse {
|
|
||||||
let storage = storage.into_inner();
|
|
||||||
storage.refresh::<Claims>(auth.refresh_jti).await.unwrap();
|
|
||||||
HttpResponse::Ok().json(&*auth)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/")]
|
|
||||||
async fn root() -> HttpResponse {
|
|
||||||
HttpResponse::Ok().finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn session_request(auth_bearer: &str, refresh_bearer: &str) -> actix_web::test::TestRequest {
|
|
||||||
let req = test::TestRequest::default()
|
|
||||||
.uri("/session/info")
|
|
||||||
.method(Method::GET);
|
|
||||||
if !auth_bearer.is_empty() {
|
|
||||||
req.insert_header((JWT_HEADER_NAME, auth_bearer))
|
|
||||||
.insert_header((REFRESH_HEADER_NAME, refresh_bearer))
|
|
||||||
} else {
|
|
||||||
req
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expect_invalid_session(res: Result<ServiceResponse, actix_web::Error>) {
|
|
||||||
let err = res
|
|
||||||
.expect_err("Must be unauthorized")
|
|
||||||
.as_error::<actix_jwt_session::Error>()
|
|
||||||
.expect("Must be authorization error")
|
|
||||||
.clone();
|
|
||||||
assert_eq!(err, actix_jwt_session::Error::LoadError);
|
|
||||||
}
|
|
@ -5,7 +5,6 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-http = "3.3.1"
|
actix-http = "3.3.1"
|
||||||
actix-jwt-authc = { version = "0.2.0", features = ["tracing", "session"] }
|
|
||||||
actix-web = "4.3.1"
|
actix-web = "4.3.1"
|
||||||
argon2 = "0.5.1"
|
argon2 = "0.5.1"
|
||||||
askama = { version = "0.12.0", features = ["serde", "with-actix-web", "comrak", "mime"] }
|
askama = { version = "0.12.0", features = ["serde", "with-actix-web", "comrak", "mime"] }
|
||||||
@ -17,7 +16,7 @@ garde = { version = "0.14.0", features = ["derive"] }
|
|||||||
jsonwebtoken = "8.3.0"
|
jsonwebtoken = "8.3.0"
|
||||||
oswilno-contract = { path = "../oswilno-contract" }
|
oswilno-contract = { path = "../oswilno-contract" }
|
||||||
oswilno-view = { path = "../oswilno-view" }
|
oswilno-view = { path = "../oswilno-view" }
|
||||||
actix-jwt-session = { path = "../actix-jwt-session", features = ["use-redis"] }
|
actix-jwt-session = { version = "*", features = ["use-redis"] }
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
redis = { version = "0.17" }
|
redis = { version = "0.17" }
|
||||||
redis-async-pool = "0.2.4"
|
redis-async-pool = "0.2.4"
|
||||||
|
@ -10,4 +10,4 @@ askama_actix = { version = "0.14.0" }
|
|||||||
futures-core = "0.3.28"
|
futures-core = "0.3.28"
|
||||||
garde = { version = "0.14.0", features = ["derive"] }
|
garde = { version = "0.14.0", features = ["derive"] }
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
actix-jwt-session = { path = "../actix-jwt-session" }
|
actix-jwt-session = { version = "*" }
|
||||||
|
Loading…
Reference in New Issue
Block a user