Compare commits

...

2 Commits

Author SHA1 Message Date
6692df9aeb New JWT implementation 2023-08-11 15:25:36 +02:00
26dea34054 New JWT implementation 2023-08-11 15:25:26 +02:00
8 changed files with 309 additions and 54 deletions

116
Cargo.lock generated
View File

@ -26,9 +26,9 @@ dependencies = [
"log",
"once_cell",
"parking_lot 0.12.1",
"pin-project-lite 0.2.9",
"pin-project-lite 0.2.12",
"smallvec",
"tokio 1.29.1",
"tokio 1.30.0",
"tokio-util 0.7.3",
]
@ -81,8 +81,8 @@ dependencies = [
"futures-sink",
"log",
"memchr",
"pin-project-lite 0.2.9",
"tokio 1.29.1",
"pin-project-lite 0.2.12",
"tokio 1.30.0",
"tokio-util 0.7.3",
]
@ -106,7 +106,7 @@ dependencies = [
"mime",
"mime_guess",
"percent-encoding",
"pin-project-lite 0.2.9",
"pin-project-lite 0.2.12",
]
[[package]]
@ -149,11 +149,11 @@ dependencies = [
"local-channel",
"mime",
"percent-encoding",
"pin-project-lite 0.2.9",
"pin-project-lite 0.2.12",
"rand 0.8.5",
"sha1 0.10.1",
"smallvec",
"tokio 1.29.1",
"tokio 1.30.0",
"tokio-util 0.7.3",
"tracing",
"zstd",
@ -172,10 +172,29 @@ dependencies = [
"jsonwebtoken",
"serde",
"time 0.3.25",
"tokio 1.29.1",
"tokio 1.30.0",
"tracing",
]
[[package]]
name = "actix-jwt-session"
version = "0.1.0"
dependencies = [
"actix-web",
"async-trait",
"bincode",
"futures",
"futures-lite",
"futures-util",
"jsonwebtoken",
"redis",
"redis-async-pool",
"serde",
"thiserror",
"tokio 1.30.0",
"uuid",
]
[[package]]
name = "actix-macros"
version = "0.2.3"
@ -208,7 +227,7 @@ dependencies = [
"serde_json",
"serde_plain",
"tempfile",
"tokio 1.29.1",
"tokio 1.30.0",
]
[[package]]
@ -246,7 +265,7 @@ checksum = "15265b6b8e2347670eb363c47fc8c75208b4a4994b27192f345fcbe707804f3e"
dependencies = [
"actix-macros",
"futures-core",
"tokio 1.29.1",
"tokio 1.30.0",
]
[[package]]
@ -262,8 +281,8 @@ dependencies = [
"futures-util",
"mio 0.8.8",
"num_cpus",
"socket2",
"tokio 1.29.1",
"socket2 0.4.9",
"tokio 1.30.0",
"tracing",
]
@ -275,7 +294,7 @@ checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a"
dependencies = [
"futures-core",
"paste",
"pin-project-lite 0.2.9",
"pin-project-lite 0.2.12",
]
[[package]]
@ -320,7 +339,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e491cbaac2e7fc788dfff99ff48ef317e23b3cf63dbaf7aaab6418f40f92aa94"
dependencies = [
"local-waker",
"pin-project-lite 0.2.9",
"pin-project-lite 0.2.12",
]
[[package]]
@ -353,13 +372,13 @@ dependencies = [
"log",
"mime",
"once_cell",
"pin-project-lite 0.2.9",
"pin-project-lite 0.2.12",
"regex",
"serde",
"serde_json",
"serde_urlencoded",
"smallvec",
"socket2",
"socket2 0.4.9",
"time 0.3.25",
"url",
]
@ -640,7 +659,7 @@ dependencies = [
"blocking",
"futures-lite",
"once_cell",
"tokio 1.29.1",
"tokio 1.30.0",
]
[[package]]
@ -658,7 +677,7 @@ dependencies = [
"parking",
"polling",
"slab",
"socket2",
"socket2 0.4.9",
"waker-fn",
"winapi 0.3.9",
]
@ -693,7 +712,7 @@ dependencies = [
"log",
"memchr",
"once_cell",
"pin-project-lite 0.2.9",
"pin-project-lite 0.2.12",
"pin-utils",
"slab",
"wasm-bindgen-futures",
@ -707,7 +726,7 @@ checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
dependencies = [
"async-stream-impl",
"futures-core",
"pin-project-lite 0.2.9",
"pin-project-lite 0.2.12",
]
[[package]]
@ -1186,7 +1205,7 @@ dependencies = [
"bytes 1.1.0",
"futures-core",
"memchr",
"pin-project-lite 0.2.9",
"pin-project-lite 0.2.12",
"tokio 0.2.25",
]
@ -1666,7 +1685,7 @@ dependencies = [
"futures-io",
"memchr",
"parking",
"pin-project-lite 0.2.9",
"pin-project-lite 0.2.12",
"waker-fn",
]
@ -1706,7 +1725,7 @@ dependencies = [
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite 0.2.9",
"pin-project-lite 0.2.12",
"pin-utils",
"slab",
]
@ -1837,7 +1856,7 @@ dependencies = [
"http",
"indexmap 1.9.1",
"slab",
"tokio 1.29.1",
"tokio 1.30.0",
"tokio-util 0.7.3",
"tracing",
]
@ -2281,7 +2300,7 @@ dependencies = [
"oswilno-contract",
"sea-orm",
"sea-orm-migration",
"tokio 1.29.1",
"tokio 1.30.0",
]
[[package]]
@ -2515,7 +2534,7 @@ dependencies = [
"sea-orm",
"serde",
"serde_json",
"tokio 1.29.1",
"tokio 1.30.0",
"toml 0.7.6",
"tracing",
"tracing-subscriber",
@ -2577,7 +2596,7 @@ dependencies = [
"sea-orm",
"serde",
"serde_json",
"tokio 1.29.1",
"tokio 1.30.0",
]
[[package]]
@ -2604,7 +2623,7 @@ dependencies = [
"sea-orm",
"serde",
"time 0.3.25",
"tokio 1.29.1",
"tokio 1.30.0",
"tracing",
"uuid",
]
@ -2861,9 +2880,9 @@ checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777"
[[package]]
name = "pin-project-lite"
version = "0.2.9"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05"
[[package]]
name = "pin-utils"
@ -3713,6 +3732,16 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "socket2"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877"
dependencies = [
"libc",
"windows-sys 0.48.0",
]
[[package]]
name = "spez"
version = "0.1.2"
@ -3835,7 +3864,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "804d3f245f894e61b1e6263c84b23ca675d96753b5abfd5cc8597d86806e8024"
dependencies = [
"once_cell",
"tokio 1.29.1",
"tokio 1.30.0",
"tokio-rustls",
]
@ -4045,25 +4074,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46409491c9375a693ce7032101970a54f8a2010efb77e13f70788f0d84489e39"
dependencies = [
"autocfg",
"pin-project-lite 0.2.9",
"pin-project-lite 0.2.12",
]
[[package]]
name = "tokio"
version = "1.29.1"
version = "1.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da"
checksum = "2d3ce25f50619af8b0aec2eb23deebe84249e19e2ddd393a6e16e3300a6dadfd"
dependencies = [
"autocfg",
"backtrace",
"bytes 1.1.0",
"libc",
"mio 0.8.8",
"num_cpus",
"parking_lot 0.12.1",
"pin-project-lite 0.2.9",
"pin-project-lite 0.2.12",
"signal-hook-registry",
"socket2",
"socket2 0.5.3",
"tokio-macros",
"windows-sys 0.48.0",
]
@ -4086,7 +4114,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
dependencies = [
"rustls",
"tokio 1.29.1",
"tokio 1.30.0",
"webpki",
]
@ -4097,8 +4125,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df54d54117d6fdc4e4fea40fe1e4e566b3505700e148a6827e59b34b0d2600d9"
dependencies = [
"futures-core",
"pin-project-lite 0.2.9",
"tokio 1.29.1",
"pin-project-lite 0.2.12",
"tokio 1.30.0",
]
[[package]]
@ -4124,8 +4152,8 @@ dependencies = [
"bytes 1.1.0",
"futures-core",
"futures-sink",
"pin-project-lite 0.2.9",
"tokio 1.29.1",
"pin-project-lite 0.2.12",
"tokio 1.30.0",
"tracing",
]
@ -4180,7 +4208,7 @@ checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
dependencies = [
"cfg-if 1.0.0",
"log",
"pin-project-lite 0.2.9",
"pin-project-lite 0.2.12",
"tracing-attributes",
"tracing-core",
]
@ -4542,7 +4570,7 @@ dependencies = [
"askama",
"serde",
"serde_json",
"tokio 1.29.1",
"tokio 1.30.0",
]
[[package]]

View File

@ -6,4 +6,5 @@ members = [
'./crates/oswilno-parking-space',
'./crates/migration',
'./crates/oswilno-actix-admin',
'./crates/actix-jwt-session',
]

View File

@ -0,0 +1,23 @@
[package]
name = "actix-jwt-session"
version = "0.1.0"
edition = "2021"
[features]
default = ['use-redis']
use-redis = ["redis", "redis-async-pool"]
[dependencies]
actix-web = "4"
async-trait = "0.1.72"
bincode = "1.3.3"
futures = "0.3.28"
futures-lite = "1.13.0"
futures-util = { version = "0.3.28", features = ['async-await'] }
jsonwebtoken = "8.3.0"
redis = { version = "0.17", optional = true }
redis-async-pool = { version = "0.2.4", optional = true }
serde = { version = "1.0.183", features = ["derive"] }
thiserror = "1.0.44"
tokio = { version = "1.30.0", features = ["full"] }
uuid = { version = "1.4.1", features = ["v4"] }

View File

@ -0,0 +1,85 @@
use actix_web::dev::ServiceRequest;
use actix_web::HttpMessage;
use jsonwebtoken::{decode, DecodingKey, Validation};
use serde::{de::DeserializeOwned, Serialize};
use std::sync::Arc;
pub trait Claims: PartialEq + DeserializeOwned + Serialize + Clone + Send + Sync + 'static {
fn jti(&self) -> uuid::Uuid;
}
#[derive(Debug, thiserror::Error, PartialEq)]
pub enum Error {
#[error("Failed to obtain redis connection")]
RedisConn,
#[error("Record not found")]
NotFound,
#[error("Record malformed")]
RecordMalformed,
#[error("Invalid session")]
InvalidSession,
#[error("No http authentication header")]
NoAuthHeader,
}
impl actix_web::ResponseError for Error {
fn status_code(&self) -> actix_web::http::StatusCode {
match self {
Self::RedisConn => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
_ => actix_web::http::StatusCode::UNAUTHORIZED,
}
}
}
pub struct Authenticated<T>(Arc<T>);
#[async_trait::async_trait(?Send)]
pub trait TokenStorage {
type ClaimsType: Claims;
async fn get_from_jti(self: Arc<Self>, jti: uuid::Uuid) -> Result<Self::ClaimsType, Error>;
}
struct Extractor;
impl Extractor {
async fn extract_bearer_jwt<ClaimsType: Claims>(
req: &ServiceRequest,
jwt_decoding_key: Arc<DecodingKey>,
jwt_validator: Arc<Validation>,
storage: Arc<dyn TokenStorage<ClaimsType = ClaimsType>>,
) -> Result<(), Error> {
let authorisation_header = req
.headers()
.get("Authorization")
.ok_or(Error::NoAuthHeader)?;
let as_str = authorisation_header
.to_str()
.map_err(|_| Error::NoAuthHeader)?;
let decoded_claims = decode::<ClaimsType>(as_str, &*jwt_decoding_key, &*jwt_validator)
.map_err(|_e| {
// let error_message = e.to_string();
Error::InvalidSession
})?;
let stored = storage
.get_from_jti(decoded_claims.claims.jti())
.await
.map_err(|_| Error::InvalidSession)?;
if stored != decoded_claims.claims {
return Err(Error::InvalidSession);
}
req.extensions_mut()
.insert(Authenticated(Arc::new(decoded_claims.claims)));
Ok(())
}
}
#[cfg(feature = "redis")]
mod redis_adapter;
#[cfg(feature = "redis")]
pub use redis_adapter::*;

View File

@ -0,0 +1,100 @@
use super::*;
use actix_web::dev::{forward_ready, Service, ServiceRequest, ServiceResponse};
use futures_util::future::LocalBoxFuture;
use jsonwebtoken::{DecodingKey, Validation};
use redis::AsyncCommands;
use std::marker::PhantomData;
use std::rc::Rc;
use std::sync::Arc;
#[derive(Clone)]
pub struct RedisStorage<ClaimsType: Claims> {
pool: redis_async_pool::RedisPool,
_claims_type_marker: PhantomData<ClaimsType>,
}
impl<ClaimsType: Claims> RedisStorage<ClaimsType> {
pub fn new(pool: redis_async_pool::RedisPool) -> Self {
Self {
pool,
_claims_type_marker: Default::default(),
}
}
}
#[async_trait::async_trait(?Send)]
impl<ClaimsType> TokenStorage for RedisStorage<ClaimsType>
where
ClaimsType: Claims,
{
type ClaimsType = ClaimsType;
async fn get_from_jti(self: Arc<Self>, jti: uuid::Uuid) -> Result<ClaimsType, Error> {
let pool = self.pool.clone();
let mut conn = pool.get().await.map_err(|_| Error::RedisConn)?;
let val = conn
.get::<_, Vec<u8>>(jti.as_bytes())
.await
.map_err(|_| Error::NotFound)?;
bincode::deserialize(&val).map_err(|_| Error::RecordMalformed)
}
}
pub struct RedisMiddleware<S, ClaimsType>
where
ClaimsType: Claims,
{
_claims_type_marker: std::marker::PhantomData<ClaimsType>,
service: Rc<S>,
jwt_decoding_key: Arc<DecodingKey>,
jwt_validator: Arc<Validation>,
storage: Arc<RedisStorage<ClaimsType>>,
}
impl<S, B, ClaimsType> Service<ServiceRequest> for RedisMiddleware<S, ClaimsType>
where
ClaimsType: Claims,
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
{
type Response = ServiceResponse<B>;
type Error = actix_web::Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
use futures_lite::FutureExt;
let svc = self.service.clone();
let jwt_decoding_key = self.jwt_decoding_key.clone();
let validation = self.jwt_validator.clone();
let storage = self.storage.clone();
async move {
Extractor::extract_bearer_jwt(&req, jwt_decoding_key, validation, storage).await?;
let res = svc.call(req).await?;
Ok(res)
}
.boxed_local()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
struct Out {
id: uuid::Uuid,
}
impl Claims for Out {
fn jti(&self) -> uuid::Uuid {
self.id
}
}
#[tokio::test]
async fn extract() {
}
}

View File

@ -11,9 +11,7 @@ impl MigrationTrait for Migration {
m.alter_table(
Table::alter()
.table(ParkingSpace::ParkingSpaces)
.add_column(
ColumnDef::new(ParkingSpace::Spot)
.integer())
.add_column(ColumnDef::new(ParkingSpace::Spot).integer())
.to_owned(),
)
.await?;

View File

@ -0,0 +1 @@

View File

@ -4,7 +4,7 @@ use std::sync::Arc;
use actix_jwt_authc::*;
use actix_web::web::{Data, Form, ServiceConfig};
use actix_web::{get, post, HttpResponse};
use askama_actix::{Template, TemplateToResponse as _};
use askama_actix::Template;
use autometrics::autometrics;
use futures::channel::{mpsc, mpsc::Sender};
use futures::stream::Stream;
@ -20,6 +20,7 @@ use time::ext::*;
use time::OffsetDateTime;
use tokio::sync::Mutex;
mod extract_session;
mod hashing;
pub use oswilno_view::filters;
@ -30,11 +31,13 @@ pub type UserSession = Authenticated<Claims>;
pub struct JWTTtl(time::Duration);
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
#[serde(rename_all = "snake_case")]
enum Audience {
Web,
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
#[serde(rename_all = "snake_case")]
pub struct Claims {
#[serde(rename = "exp")]
expires_at: usize,
@ -196,7 +199,7 @@ async fn login(
let mut errors = Errors::default();
match login_inner(
jwt_encoding_key,
jwt_ttl,
jwt_ttl.into_inner(),
payload.into_inner(),
db.into_inner(),
redis.into_inner(),
@ -206,7 +209,12 @@ async fn login(
{
Ok(res) => Ok(HttpResponse::Ok().json(res)),
Err(form) => Ok(HttpResponse::Ok().body(
(SignInPartialTemplate { form, lang, t, errors })
(SignInPartialTemplate {
form,
lang,
t,
errors,
})
.render()
.unwrap(),
)),
@ -215,7 +223,7 @@ async fn login(
async fn login_inner(
jwt_encoding_key: Data<EncodingKey>,
jwt_ttl: Data<JWTTtl>,
jwt_ttl: Arc<JWTTtl>,
payload: SignInPayload,
db: Arc<DatabaseConnection>,
redis: Arc<redis_async_pool::RedisPool>,
@ -274,8 +282,13 @@ async fn login_inner(
errors.push_global("Failed to sign in. Please try later");
return Err(payload);
};
if let Err(e) = conn
.set::<'_, _, _, String>(jwt_claims.jwt_id.as_bytes(), bin_value)
.set_ex::<'_, _, _, String>(
jwt_claims.jwt_id.as_bytes(),
bin_value,
jwt_ttl.0.as_seconds_f32() as usize,
)
.await
{
tracing::warn!("Failed to set sign-in claims in redis: {e}");
@ -298,10 +311,16 @@ async fn session_info(authenticated: UserSession) -> Result<HttpResponse, Error>
#[autometrics]
#[get("/logout")]
async fn logout(
invalidated_jwts: Data<InvalidatedJWTStore>,
authenticated: Authenticated<Claims>,
redis: Data<redis_async_pool::RedisPool>,
) -> Result<HttpResponse, Error> {
invalidated_jwts.add_to_invalidated(authenticated).await;
{
use redis::AsyncCommands;
let jwt_id = authenticated.claims.jwt_id;
if let Ok(mut conn) = redis.get().await {
if conn.del::<_, String>(jwt_id.as_bytes()).await.is_err() {}
}
}
Ok(HttpResponse::Ok().json(EmptyResponse {}))
}