Expand description
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:
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.
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
use std::sync::Arc;
use actix_jwt_session::*;
// create redis connection
let redis = deadpool_redis::Config::from_url("redis://localhost:6379")
.create_pool(Some(deadpool_redis::Runtime::Tokio1)).unwrap();
// create new [SessionStorage] and [SessionMiddlewareFactory]
let (storage, factory) = SessionMiddlewareFactory::<AppClaims>::build_ed_dsa()
// pass redis connection
.with_redis_pool(redis.clone())
.with_extractors(
Extractors::default()
// 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.
use actix_jwt_session::{JwtTtl, RefreshTtl, Duration};
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:
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() {
// create redis connection
let redis = deadpool_redis::Config::from_url("redis://localhost:6379")
.create_pool(Some(deadpool_redis::Runtime::Tokio1)).unwrap();
let jwt_ttl = JwtTtl(Duration::days(14));
let refresh_ttl = RefreshTtl(Duration::days(3 * 31));
HttpServer::new(move || {
App::new()
.app_data(Data::new( jwt_ttl ))
.app_data(Data::new( refresh_ttl ))
.use_jwt::<AppClaims>(
Extractors::default()
// 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),
Some(redis.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
}
#[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 */
};
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(
refresh_token: Authenticated<RefreshToken>,
storage: Data<SessionStorage>,
) -> HttpResponse {
let s = storage.into_inner();
let pair = match s.refresh::<AppClaims>(refresh_token.access_jti()).await {
Err(e) => {
tracing::warn!("Failed to refresh token: {e}");
return HttpResponse::BadRequest().finish();
}
Ok(pair) => pair,
};
let encrypted_jwt = match pair.jwt.encode() {
Ok(text) => text,
Err(e) => {
tracing::warn!("Failed to encode claims: {e}");
return HttpResponse::InternalServerError().finish();
}
};
let encrypted_refresh = match pair.refresh.encode() {
Err(e) => {
tracing::warn!("Failed to encode claims: {e}");
return HttpResponse::InternalServerError().finish();
}
Ok(text) => text,
};
HttpResponse::Ok()
.append_header((
actix_jwt_session::JWT_HEADER_NAME,
format!("Bearer {encrypted_jwt}").as_str(),
))
.append_header((
actix_jwt_session::REFRESH_HEADER_NAME,
format!("Bearer {}", encrypted_refresh).as_str(),
))
.append_header((
"ACX-JWT-TTL",
(pair.refresh.issues_at + pair.refresh.refresh_ttl.0).to_string(),
))
.finish()
}
#[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,
}
Re-exports§
pub use actix_routes::configure;
pub use deadpool_redis;
pub use builder::*;
Modules§
Macros§
Structs§
- Authenticated
- Extractable user session which requires presence of JWT in request.
If there’s no JWT endpoint which requires this structure will automatically
returns
401
. - Cookie
Extractor - 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.
- Duration
- A span of time with nanosecond precision.
- Extractors
- Hashing
- Encrypting and decrypting password
- Header
Extractor - Extracts JWT token from HTTP Request headers
- Json
Extractor - JwtSigning
Keys - Load or generate new Ed25519 signing keys.
- JwtTtl
- This is maximum duration of json web token after which token will be invalid and depends on implementation removed.
- Maybe
Authenticated - Similar to Authenticated but JWT is optional
- Offset
Date Time - A
PrimitiveDateTime
with aUtcOffset
. - Pair
- JSON Web Token and internally created refresh token.
- Refresh
Token - Internal claims which allows to extend tokens pair livetime
- Refresh
Ttl - This is maximum duration of refresh token after which token will be invalid and depends on implementation removed
- Session
Middleware Builder - Session middleware factory builder
- Session
Middleware Factory - Factory creates middlware for every single request.
- Session
Storage - Allow to save, read and remove session from storage.
- Uuid
- A Universally Unique Identifier (UUID).
Enums§
- Algorithm
- The algorithms supported for signing/verifying JWTs
- Error
- Session related errors
- Extractor
Kind
Statics§
- JWT_
COOKIE_ NAME - Default json web token cookie name
- JWT_
HEADER_ NAME - Default json web token header name
- REFRESH_
COOKIE_ NAME - Default refresh token cookie name
- REFRESH_
HEADER_ NAME - Default refresh token header name
Traits§
- Claims
- Serializable and storable struct which represent JWT claims
- Session
Extractor - Trait allowing to extract JWt token from actix_web::dev::ServiceRequest
- Token
Storage - Allows to customize where and how sessions are stored in persistant storage. By default redis can be used to store sesions but it’s possible and easy to use memcached or postgresql.