From 7fa7fcda2be3f3d3fbb9e6ee6518c8836ba2801d Mon Sep 17 00:00:00 2001 From: eraden Date: Thu, 24 Aug 2023 22:16:40 +0200 Subject: [PATCH] Add actix-jwt-session working docs --- Cargo.lock | 1 + crates/actix-jwt-session/Cargo.toml | 2 + crates/actix-jwt-session/src/lib.rs | 104 ++++++++++++++++++++++++++++ crates/oswilno-session/src/lib.rs | 56 ++------------- 4 files changed, 112 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 035a746..ea93536 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -188,6 +188,7 @@ dependencies = [ "futures-util", "garde", "jsonwebtoken", + "rand 0.8.5", "redis", "redis-async-pool", "ring", diff --git a/crates/actix-jwt-session/Cargo.toml b/crates/actix-jwt-session/Cargo.toml index 5aa4a10..93ccfb5 100644 --- a/crates/actix-jwt-session/Cargo.toml +++ b/crates/actix-jwt-session/Cargo.toml @@ -18,8 +18,10 @@ futures = "0.3.28" futures-lite = "1.13.0" futures-util = { version = "0.3.28", features = ['async-await'] } jsonwebtoken = "8.3.0" +rand = "0.8.5" redis = { version = "0.17", optional = true } redis-async-pool = { version = "0.2.4", optional = true } +ring = "0.16.20" serde = { version = "1.0.183", features = ["derive"] } thiserror = "1.0.44" tokio = { version = "1.30.0", features = ["full"] } diff --git a/crates/actix-jwt-session/src/lib.rs b/crates/actix-jwt-session/src/lib.rs index e5147cb..245fece 100644 --- a/crates/actix-jwt-session/src/lib.rs +++ b/crates/actix-jwt-session/src/lib.rs @@ -439,6 +439,28 @@ impl SessionStorage { record.jwt_token() } + /// Changes [RefreshToken::iat] allowing Claims and RefreshToken to be accessible longer + /// + /// Examples: + /// + /// ``` + /// 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 } + /// # } + /// + /// async fn extend_tokens_lifetime( + /// session_storage: SessionStorage, + /// jti: uuid::Uuid + /// ) -> Result { + /// session_storage.refresh(jti).await?; + /// HttpResponse::Ok().finish() + /// } + /// ``` pub async fn refresh(&self, refresh_jti: uuid::Uuid) -> Result<(), Error> { let mut record = self.load_pair_by_refresh(refresh_jti).await?; let mut refresh_token = record.refresh_token()?; @@ -499,6 +521,8 @@ impl SessionStorage { Ok(()) } + /// Write to storage tokens pair as [SessionRecord] + /// This operation allows to load pair using JWT ID and Refresh Token ID async fn store_pair( &self, record: SessionRecord, @@ -518,6 +542,8 @@ impl SessionStorage { Ok(()) } + + /// Load [SessionRecord] as tokens pair from storage using JWT ID (jti) async fn load_pair_by_jwt(&self, jti: Uuid) -> Result { self.storage .clone() @@ -525,6 +551,8 @@ impl SessionStorage { .await .and_then(|bytes| bincode::deserialize(&bytes).map_err(|_| Error::RecordMalformed)) } + + /// Load [SessionRecord] as tokens pair from storage using Refresh ID (jti) async fn load_pair_by_refresh(&self, jti: Uuid) -> Result { self.storage .clone() @@ -706,6 +734,82 @@ impl SessionExtractor for HeaderExtractor Self { + match Self::load_from_files() { + Ok(s) => s, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Self::generate().unwrap(), + Err(e) => panic!("Failed to load or generate jwt signing keys: {:?}", e), + } + } + + fn generate() -> Result> { + use jsonwebtoken::*; + use ring::rand::SystemRandom; + use ring::signature::{Ed25519KeyPair, KeyPair}; + + let doc = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new())?; + let keypair = Ed25519KeyPair::from_pkcs8(doc.as_ref())?; + let encoding_key = EncodingKey::from_ed_der(doc.as_ref()); + let decoding_key = DecodingKey::from_ed_der(keypair.public_key().as_ref()); + + std::fs::write("./config/jwt-encoding.bin", doc.as_ref()).unwrap_or_else(|e| { + panic!("Failed to write ./config/jwt-encoding.bin: {:?}", e); + }); + std::fs::write("./config/jwt-decoding.bin", keypair.public_key()).unwrap_or_else(|e| { + panic!("Failed to write ./config/jwt-decoding.bin: {:?}", e); + }); + + Ok(JwtSigningKeys { + encoding_key, + decoding_key, + }) + } + + fn load_from_files() -> std::io::Result { + use jsonwebtoken::*; + use std::io::Read; + + let mut buf = Vec::new(); + let mut e = std::fs::File::open("./config/jwt-encoding.bin")?; + e.read_to_end(&mut buf).unwrap_or_else(|e| { + panic!("Failed to read jwt encoding key: {:?}", e); + }); + let encoding_key: EncodingKey = EncodingKey::from_ed_der(&buf); + + let mut buf = Vec::new(); + let mut e = std::fs::File::open("./config/jwt-decoding.bin")?; + e.read_to_end(&mut buf).unwrap_or_else(|e| { + panic!("Failed to read jwt decoding key: {:?}", e); + }); + let decoding_key = DecodingKey::from_ed_der(&buf); + Ok(Self { + encoding_key, + decoding_key, + }) + } +} + #[cfg(feature = "redis")] mod redis_adapter; #[cfg(feature = "redis")] diff --git a/crates/oswilno-session/src/lib.rs b/crates/oswilno-session/src/lib.rs index 94fd68d..7078839 100644 --- a/crates/oswilno-session/src/lib.rs +++ b/crates/oswilno-session/src/lib.rs @@ -1,8 +1,7 @@ -use std::io::Read; use std::ops::Add; use std::sync::Arc; -use actix_jwt_session::{CookieExtractor, HeaderExtractor, SessionStorage, JWT_HEADER_NAME}; +use actix_jwt_session::{CookieExtractor, HeaderExtractor, SessionStorage, JWT_HEADER_NAME, JwtSigningKeys}; pub use actix_jwt_session::{Error, RedisMiddlewareFactory}; use actix_web::web::{Data, Form, ServiceConfig}; use actix_web::{get, post, HttpRequest, HttpResponse}; @@ -11,8 +10,6 @@ use autometrics::autometrics; use garde::Validate; use jsonwebtoken::*; use oswilno_view::{Blank, Errors, Lang, Layout, Main, MainOpts, TranslationStorage}; -use ring::rand::SystemRandom; -use ring::signature::{Ed25519KeyPair, KeyPair}; use sea_orm::DatabaseConnection; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; @@ -293,7 +290,10 @@ async fn login_inner( jwt_id: uuid::Uuid::new_v4(), account_id: account.id, }; - let jwt_token = match redis.store(jwt_claims.clone(), jwt_ttl.0, refresh_ttl.0).await { + let jwt_token = match redis + .store(jwt_claims.clone(), jwt_ttl.0, refresh_ttl.0) + .await + { Err(e) => { tracing::warn!("Failed to set sign-in claims in redis: {e}"); errors.push_global("Failed to sign in. Please try later"); @@ -571,49 +571,3 @@ async fn register_internal( .append_header(("Accept", "text/html-partial")) .body("")) } - -pub struct JwtSigningKeys { - encoding_key: EncodingKey, - decoding_key: DecodingKey, -} - -impl JwtSigningKeys { - fn load_or_create() -> Self { - match Self::load_from_files() { - Ok(s) => s, - Err(e) if e.kind() == std::io::ErrorKind::NotFound => Self::generate().unwrap(), - Err(e) => panic!("{:?}", e), - } - } - - fn generate() -> Result> { - let doc = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new())?; - let keypair = Ed25519KeyPair::from_pkcs8(doc.as_ref())?; - let encoding_key = EncodingKey::from_ed_der(doc.as_ref()); - let decoding_key = DecodingKey::from_ed_der(keypair.public_key().as_ref()); - - std::fs::write("./config/jwt-encoding.bin", doc.as_ref()).unwrap(); - std::fs::write("./config/jwt-decoding.bin", keypair.public_key()).unwrap(); - - Ok(JwtSigningKeys { - encoding_key, - decoding_key, - }) - } - - fn load_from_files() -> std::io::Result { - let mut buf = Vec::new(); - let mut e = std::fs::File::open("./config/jwt-encoding.bin")?; - e.read_to_end(&mut buf).unwrap(); - let encoding_key: EncodingKey = EncodingKey::from_ed_der(&buf); - - let mut buf = Vec::new(); - let mut e = std::fs::File::open("./config/jwt-decoding.bin")?; - e.read_to_end(&mut buf).unwrap(); - let decoding_key = DecodingKey::from_ed_der(&buf); - Ok(Self { - encoding_key, - decoding_key, - }) - } -}