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, refresh: Option, } ``` 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() { // 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::::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::::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) -> 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) -> 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) -> Result { 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, payload: Json, jwt_ttl: Data, refresh_ttl: Data, ) -> Result { 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, auth: Authenticated) -> HttpResponse { let store = store.into_inner(); store.erase::(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) -> HttpResponse { HttpResponse::Ok().json(&*auth) } #[get("/session/refresh")] async fn refresh_session( auth: Authenticated, storage: Data, ) -> 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, } ```