Crate actix_jwt_session

Source
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§

actix_routes
builder

Macros§

bad_ttl

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.
CookieExtractor
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
HeaderExtractor
Extracts JWT token from HTTP Request headers
JsonExtractor
JwtSigningKeys
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.
MaybeAuthenticated
Similar to Authenticated but JWT is optional
OffsetDateTime
A PrimitiveDateTime with a UtcOffset.
Pair
JSON Web Token and internally created refresh token.
RefreshToken
Internal claims which allows to extend tokens pair livetime
RefreshTtl
This is maximum duration of refresh token after which token will be invalid and depends on implementation removed
SessionMiddlewareBuilder
Session middleware factory builder
SessionMiddlewareFactory
Factory creates middlware for every single request.
SessionStorage
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
ExtractorKind

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
SessionExtractor
Trait allowing to extract JWt token from actix_web::dev::ServiceRequest
TokenStorage
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.