Returns new pair after refresh session

This commit is contained in:
eraden 2023-09-01 11:37:24 +02:00
parent 0666c74068
commit 96ddd2c51b
7 changed files with 127 additions and 32 deletions

2
Cargo.lock generated
View File

@ -178,7 +178,7 @@ dependencies = [
[[package]]
name = "actix-jwt-session"
version = "1.0.0"
version = "1.0.1"
dependencies = [
"actix-web",
"argon2",

View File

@ -1,6 +1,6 @@
[package]
name = "actix-jwt-session"
version = "1.0.0"
version = "1.0.1"
edition = "2021"
description = "Full featured JWT session managment for actix"
license = "MIT"

View File

@ -385,3 +385,7 @@ struct AccountModel {
* Build-in hashing functions
* Build-in TTL structures
* Documentation
1.0.1
* Returns new pair after refresh lifetime

View File

@ -311,12 +311,46 @@
//!
//! #[get("/session/refresh")]
//! async fn refresh_session(
//! auth: Authenticated<RefreshToken>,
//! refresh_token: Authenticated<RefreshToken>,
//! storage: Data<SessionStorage>,
//! ) -> HttpResponse {
//! let storage = storage.into_inner();
//! storage.refresh(auth.refresh_jti).await.unwrap();
//! HttpResponse::Ok().json(&*auth)
//! 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("/")]
@ -540,13 +574,20 @@ pub trait Claims:
/// use actix_web::web::Data;
/// use actix_jwt_session::*;
///
/// #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
/// pub struct AppClaims { id: uuid::Uuid, sub: String }
/// impl actix_jwt_session::Claims for AppClaims {
/// fn jti(&self) -> uuid::Uuid { self.id }
/// fn subject(&self) -> &str { &self.sub }
/// }
///
/// #[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();
/// storage.refresh::<AppClaims>(auth.refresh_jti).await.unwrap();
/// HttpResponse::Ok().json(&*auth)
/// }
/// ```
@ -687,16 +728,16 @@ impl actix_web::ResponseError for Error {
/// use actix_web::HttpResponse;
/// use actix_jwt_session::Authenticated;
///
/// # #[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, PartialEq, serde::Serialize, serde::Deserialize)]
/// pub struct AppClaims { id: uuid::Uuid, sub: String }
/// impl actix_jwt_session::Claims for AppClaims {
/// fn jti(&self) -> uuid::Uuid { self.id }
/// fn subject(&self) -> &str { &self.sub }
/// }
///
/// // If there's no JWT in request server will automatically returns 401
/// #[get("/session")]
/// async fn read_session(session: Authenticated<Claims>) -> HttpResponse {
/// async fn read_session(session: Authenticated<AppClaims>) -> HttpResponse {
/// let encoded = session.encode().unwrap(); // JWT as encrypted string
/// HttpResponse::Ok().finish()
/// }
@ -955,30 +996,47 @@ impl SessionStorage {
/// ```
/// use actix_jwt_session::SessionStorage;
/// use actix_web::{Error, HttpResponse};
/// # #[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, PartialEq, serde::Serialize, serde::Deserialize)]
/// pub struct AppClaims { id: uuid::Uuid, sub: String }
/// impl actix_jwt_session::Claims for AppClaims {
/// fn jti(&self) -> uuid::Uuid { self.id }
/// fn subject(&self) -> &str { &self.sub }
/// }
///
/// async fn extend_tokens_lifetime(
/// session_storage: SessionStorage,
/// jti: uuid::Uuid
/// ) -> Result<HttpResponse, Error> {
/// session_storage.refresh(jti).await?;
/// session_storage.refresh::<AppClaims>(jti).await?;
/// Ok(HttpResponse::Ok().finish())
/// }
/// ```
pub async fn refresh(&self, refresh_jti: uuid::Uuid) -> Result<(), Error> {
pub async fn refresh<ClaimsType: Claims>(
&self,
refresh_jti: uuid::Uuid,
) -> Result<Pair<ClaimsType>, Error> {
let mut record = self.load_pair_by_refresh(refresh_jti).await?;
let mut refresh_token = record.refresh_token()?;
let ttl = refresh_token.refresh_ttl;
refresh_token.issues_at = OffsetDateTime::now_utc();
record.set_refresh_token(refresh_token)?;
self.store_pair(record, ttl).await?;
self.store_pair(record.clone(), ttl).await?;
Ok(())
let claims = SessionRecord::from_field::<ClaimsType>(&record.jwt)?;
let refresh = SessionRecord::from_field::<RefreshToken>(&record.refresh_token)?;
Ok(Pair {
jwt: Authenticated {
claims: Arc::new(claims),
jwt_encoding_key: self.jwt_encoding_key.clone(),
algorithm: self.algorithm,
},
refresh: Authenticated {
claims: Arc::new(refresh),
jwt_encoding_key: self.jwt_encoding_key.clone(),
algorithm: self.algorithm,
},
})
}
/// Save claims in storage in a way claims can be loaded from database using `jti` as [uuid::Uuid] (JWT ID)

View File

@ -11,7 +11,7 @@ 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>,
@ -108,7 +108,7 @@ impl<ClaimsType: Claims> SessionMiddlewareBuilder<ClaimsType> {
/// 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:
///
/// ```
@ -128,7 +128,7 @@ impl<ClaimsType: Claims> SessionMiddlewareBuilder<ClaimsType> {
/// 5,
/// )
/// };
///
///
/// // load or create new keys in `./config`
/// let keys = JwtSigningKeys::load_or_create();
///

View File

@ -229,7 +229,7 @@ async fn refresh_session(
storage: Data<SessionStorage>,
) -> HttpResponse {
let storage = storage.into_inner();
storage.refresh(auth.refresh_jti).await.unwrap();
storage.refresh::<Claims>(auth.refresh_jti).await.unwrap();
HttpResponse::Ok().json(&*auth)
}

View File

@ -300,6 +300,10 @@ async fn login_inner(
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(),
))
.append_header(("HX-Redirect", "/"))
.append_header(("HX-Retarget", "main"))
.cookie(
@ -350,13 +354,42 @@ async fn refresh_token(
storage: Data<SessionStorage>,
) -> HttpResponse {
let s = storage.into_inner();
match s.refresh(refresh_token.access_jti()).await {
let pair = match s.refresh::<Claims>(refresh_token.access_jti()).await {
Err(e) => {
tracing::warn!("Failed to refresh token: {e}");
HttpResponse::BadRequest().finish()
return HttpResponse::BadRequest().finish();
}
Ok(_) => HttpResponse::Ok().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()
}
#[autometrics]