Improve JWT

This commit is contained in:
eraden 2023-08-28 14:57:31 +02:00
parent 7fa7fcda2b
commit 7fe4b285ce
9 changed files with 92 additions and 38 deletions

1
Cargo.lock generated
View File

@ -195,6 +195,7 @@ dependencies = [
"serde",
"thiserror",
"tokio 1.30.0",
"tracing",
"uuid",
]

View File

@ -6,9 +6,9 @@ description = "Full featured JWT session managment for actix"
license = "MIT"
[features]
default = ['use-redis']
default = ['use-redis', 'use-tracing']
use-redis = ["redis", "redis-async-pool"]
serde-transparent = []
use-tracing = ['tracing']
[dependencies]
actix-web = "4"
@ -25,6 +25,7 @@ ring = "0.16.20"
serde = { version = "1.0.183", features = ["derive"] }
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"] }
[[test]]

View File

@ -144,7 +144,9 @@ pub static REFRESH_PARAM_NAME: &str = "refresh_token";
///
/// * It must have JWT ID as [uuid::Uuid]
/// * It must have subject as a String
pub trait Claims: PartialEq + DeserializeOwned + Serialize + Clone + Send + Sync + 'static {
pub trait Claims:
PartialEq + DeserializeOwned + Serialize + Clone + Send + Sync + std::fmt::Debug + 'static
{
fn jti(&self) -> uuid::Uuid;
fn subject(&self) -> &str;
}
@ -233,8 +235,6 @@ impl actix_web::ResponseError for Error {
/// }
/// ```
#[derive(Clone)]
#[cfg_attr(feature = "serde-transparent", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde-transparent", serde(transparent))]
pub struct Authenticated<T> {
pub claims: Arc<T>,
pub jwt_encoding_key: Arc<EncodingKey>,
@ -391,8 +391,16 @@ impl SessionRecord {
fn new<ClaimsType: Claims>(claims: ClaimsType, refresh: RefreshToken) -> Result<Self, Error> {
let refresh_jti = claims.jti();
let jwt_jti = refresh.refresh_jti;
let refresh_token = bincode::serialize(&refresh).map_err(|_| Error::SerializeFailed)?;
let jwt = bincode::serialize(&claims).map_err(|_| Error::SerializeFailed)?;
let refresh_token = bincode::serialize(&refresh).map_err(|e| {
#[cfg(feature = "use-tracing")]
tracing::debug!("Failed to serialize Refresh Token to construct pair: {e:?}");
Error::SerializeFailed
})?;
let jwt = bincode::serialize(&claims).map_err(|e| {
#[cfg(feature = "use-tracing")]
tracing::debug!("Failed to serialize JWT from to construct pair {e:?}");
Error::SerializeFailed
})?;
Ok(Self {
refresh_jti,
jwt_jti,
@ -402,13 +410,25 @@ impl SessionRecord {
}
fn refresh_token(&self) -> Result<RefreshToken, Error> {
bincode::deserialize(&self.refresh_token).map_err(|_| Error::RecordMalformed)
bincode::deserialize(&self.refresh_token).map_err(|e| {
#[cfg(feature = "use-tracing")]
tracing::debug!("Failed to deserialize refresh token from pair: {e:?}");
Error::RecordMalformed
})
}
fn jwt_token<ClaimsType: Claims>(&self) -> Result<ClaimsType, Error> {
bincode::deserialize(&self.jwt).map_err(|_| Error::RecordMalformed)
bincode::deserialize(&self.jwt).map_err(|e| {
#[cfg(feature = "use-tracing")]
tracing::debug!("Failed to serialize JWT for pair: {e:?}");
Error::RecordMalformed
})
}
fn set_refresh_token(&mut self, refresh: RefreshToken) -> Result<(), Error> {
let refresh_token = bincode::serialize(&refresh).map_err(|_| Error::SerializeFailed)?;
let refresh_token = bincode::serialize(&refresh).map_err(|e| {
#[cfg(feature = "use-tracing")]
tracing::debug!("Failed to serialize refresh token for pair: {e:?}");
Error::SerializeFailed
})?;
self.refresh_token = refresh_token;
Ok(())
}
@ -434,6 +454,8 @@ impl<ClaimsType: Claims> SessionStorage<ClaimsType> {
let record = self.load_pair_by_jwt(jti).await?;
let refresh_token = record.refresh_token()?;
if refresh_token.iat + refresh_token.access_exp < std::time::SystemTime::now() {
#[cfg(feature = "use-tracing")]
tracing::debug!("JWT expired");
return Err(Error::JWTExpired);
}
record.jwt_token()
@ -528,7 +550,11 @@ impl<ClaimsType: Claims> SessionStorage<ClaimsType> {
record: SessionRecord,
exp: std::time::Duration,
) -> Result<(), Error> {
let value = bincode::serialize(&record).map_err(|_| Error::SerializeFailed)?;
let value = bincode::serialize(&record).map_err(|e| {
#[cfg(feature = "use-tracing")]
tracing::debug!("Serialize pair to bytes failed: {e:?}");
Error::SerializeFailed
})?;
self.storage
.clone()
@ -549,7 +575,13 @@ impl<ClaimsType: Claims> SessionStorage<ClaimsType> {
.clone()
.get_by_jti(jti.as_bytes())
.await
.and_then(|bytes| bincode::deserialize(&bytes).map_err(|_| Error::RecordMalformed))
.and_then(|bytes| {
bincode::deserialize(&bytes).map_err(|e| {
#[cfg(feature = "use-tracing")]
tracing::debug!("Deserialize pair while loading for JWT ID failed: {e:?}");
Error::RecordMalformed
})
})
}
/// Load [SessionRecord] as tokens pair from storage using Refresh ID (jti)
@ -558,7 +590,13 @@ impl<ClaimsType: Claims> SessionStorage<ClaimsType> {
.clone()
.get_by_jti(jti.as_bytes())
.await
.and_then(|bytes| bincode::deserialize(&bytes).map_err(|_| Error::RecordMalformed))
.and_then(|bytes| {
bincode::deserialize(&bytes).map_err(|e| {
#[cfg(feature = "use-tracing")]
tracing::debug!("Deserialize pair while loading for refresh id failed: {e:?}");
Error::RecordMalformed
})
})
}
}
@ -644,8 +682,9 @@ pub trait SessionExtractor<ClaimsType: Claims>: Send + Sync + 'static {
algorithm: Algorithm,
) -> Result<ClaimsType, Error> {
decode::<ClaimsType>(value, &*jwt_decoding_key, &Validation::new(algorithm))
.map_err(|_e| {
// let error_message = e.to_string();
.map_err(|e| {
#[cfg(feature = "use-tracing")]
tracing::debug!("Failed to decode claims: {e:?}");
Error::InvalidSession
})
.map(|t| t.claims)
@ -660,13 +699,15 @@ pub trait SessionExtractor<ClaimsType: Claims>: Send + Sync + 'static {
claims: &ClaimsType,
storage: SessionStorage<ClaimsType>,
) -> Result<(), Error> {
let stored = storage
.clone()
.find_jwt(claims.jti())
.await
.map_err(|_| Error::InvalidSession)?;
let stored = storage.clone().find_jwt(claims.jti()).await.map_err(|e| {
#[cfg(feature = "use-tracing")]
tracing::debug!("Failed to load claims from storage: {e:?}");
Error::InvalidSession
})?;
if &stored != claims {
#[cfg(feature = "use-tracing")]
tracing::debug!("{claims:?} != {stored:?}");
return Err(Error::InvalidSession);
}
Ok(())

View File

@ -44,6 +44,7 @@ pub fn translations(l10n: &mut oswilno_view::TranslationStorage) {
async fn root() -> HttpResponse {
HttpResponse::SeeOther()
.append_header(("Location", "/parking-spaces/all"))
.append_header(("HX-Retarget", "main"))
.body("")
}
@ -80,14 +81,16 @@ async fn all_parking_spaces(
..Default::default()
},
};
HttpResponse::Ok().body(
if is_partial(&req) {
main.render()
} else {
Layout { main }.render()
}
.unwrap(),
)
HttpResponse::Ok()
.append_header(("HX-Retarget", "main"))
.body(
if is_partial(&req) {
main.render()
} else {
Layout { main }.render()
}
.unwrap(),
)
}
#[post("/search")]

View File

@ -60,13 +60,12 @@ async fn main() -> std::io::Result<()> {
.wrap(middleware::Logger::default())
.wrap(session_factory)
.app_data(Data::new(pq.clone()))
// .app_data(Data::new(redis.clone()))
.app_data(Data::new(l10n.clone()))
.configure(oswilno_parking_space::mount)
.configure(oswilno_admin::mount)
.configure(web_assets::mount)
.configure(oswilno_view::mount)
.configure(|c| session_config.app_data(c))
.configure(|c| session_config.mount(c))
})
.bind(("0.0.0.0", 8080))?
.run()

View File

@ -1,7 +1,9 @@
use std::ops::Add;
use std::sync::Arc;
use actix_jwt_session::{CookieExtractor, HeaderExtractor, SessionStorage, JWT_HEADER_NAME, JwtSigningKeys};
use actix_jwt_session::{
CookieExtractor, HeaderExtractor, JwtSigningKeys, SessionStorage, JWT_HEADER_NAME,
};
pub use actix_jwt_session::{Error, RedisMiddlewareFactory};
use actix_web::web::{Data, Form, ServiceConfig};
use actix_web::{get, post, HttpRequest, HttpResponse};
@ -85,9 +87,9 @@ pub struct SessionConfigurator {
}
impl SessionConfigurator {
pub fn app_data(self, config: &mut ServiceConfig) {
pub fn mount(self, config: &mut ServiceConfig) {
config
.app_data(self.session_storage)
.app_data(Data::new(self.session_storage))
.app_data(self.jwt_ttl)
.app_data(self.refresh_ttl)
.service(login)
@ -333,6 +335,7 @@ async fn login_inner(
))
.append_header(("Location", "/"))
.append_header(("HX-Redirect", "/"))
.append_header(("HX-Retarget", "main"))
.cookie(jwt_cookie)
.cookie(refresh_cookie)
.body(""))

View File

@ -3,7 +3,7 @@
{% for error in errors.global() %}
<oswilno-error>{{error|t(lang,t)}}</oswilno-error>
{% endfor %}
<form hx-post="/register" hx-target="#main-view">
<form hx-post="/register" hx-target="#main-view" hx-replace-url="true" hx-headers='{"Accept":"text/html-partial"}'>
<div class="mb-4">
<label for="login" class="block mb-2 text-sm text-gray-600">{{"Login"|t(lang,t)}}</label>
<input

View File

@ -3,7 +3,7 @@
{% for error in errors.global() %}
<oswilno-error>{{error|t(lang,t)}}</oswilno-error>
{% endfor %}
<form hx-post="/login" hx-target="#main-view">
<form hx-post="/login" hx-target="#main-view" hx-headers='{"Accept":"text/html-partial"}' hx-replace-url="true">
<div class="mb-4">
<label for="login" class="block mb-2 text-sm text-gray-600">Login</label>
<input id="login" name="login" value="{{ form.login }}" class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-500" />

View File

@ -2,6 +2,8 @@ import './elements/oswilno-price.js';
import './elements/oswilno-error.js';
import("https://unpkg.com/htmx.org@1.9.4/dist/htmx.min.js");
const AUTH_HEADER = 'ACX-Authorization';
const REFRESH_HEADER = '';
const body = document.body;
body.addEventListener('htmx:beforeOnLoad', function (evt) {
const detail = evt.detail;
@ -9,12 +11,15 @@ body.addEventListener('htmx:beforeOnLoad', function (evt) {
const status = xhr.status;
const successful = detail.successful;
console.log(successful, xhr.getResponseHeader('Authorization'));
if (status === 200) {
const bearer = xhr.getResponseHeader('Authorization');
const bearer = xhr.getResponseHeader(AUTH_HEADER);
if (bearer) {
localStorage.setItem('jwt', bearer.replace(/^Bearer /i, ''));
}
const refresh = xhr.getResponseHeader(REFRESH_HEADER);
if (refresh) {
localStorage.setItem('refresh', bearer.replace(/^Bearer /i, ''));
}
} else if (status === 401) {
localStorage.removeItem('jwt');
}
@ -25,7 +30,8 @@ body.addEventListener('htmx:beforeOnLoad', function (evt) {
});
body.addEventListener('htmx:configRequest', function (evt) {
if (localStorage.getItem('jwt')) {
evt.detail.headers.Authorization = 'Bearer ' + (localStorage.getItem('jwt') || '');
evt.detail.headers[AUTH_HEADER] = 'Bearer ' + (localStorage.getItem('jwt') || '');
evt.detail.headers[REFRESH_HEADER] = 'Bearer ' + (localStorage.getItem('refresh') || '');
}
});