Improve JWT
This commit is contained in:
parent
7fa7fcda2b
commit
7fe4b285ce
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -195,6 +195,7 @@ dependencies = [
|
||||
"serde",
|
||||
"thiserror",
|
||||
"tokio 1.30.0",
|
||||
"tracing",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
|
@ -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]]
|
||||
|
@ -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(())
|
||||
|
@ -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")]
|
||||
|
@ -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()
|
||||
|
@ -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(""))
|
||||
|
@ -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
|
||||
|
@ -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" />
|
||||
|
@ -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') || '');
|
||||
}
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user