Add actix-jwt-session working docs

This commit is contained in:
eraden 2023-08-24 22:16:40 +02:00
parent 58a0239a05
commit 7fa7fcda2b
4 changed files with 112 additions and 51 deletions

1
Cargo.lock generated
View File

@ -188,6 +188,7 @@ dependencies = [
"futures-util",
"garde",
"jsonwebtoken",
"rand 0.8.5",
"redis",
"redis-async-pool",
"ring",

View File

@ -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"] }

View File

@ -439,6 +439,28 @@ impl<ClaimsType: Claims> SessionStorage<ClaimsType> {
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<Claims>,
/// jti: uuid::Uuid
/// ) -> Result<HttpResponse, Error> {
/// 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<ClaimsType: Claims> SessionStorage<ClaimsType> {
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<ClaimsType: Claims> SessionStorage<ClaimsType> {
Ok(())
}
/// Load [SessionRecord] as tokens pair from storage using JWT ID (jti)
async fn load_pair_by_jwt(&self, jti: Uuid) -> Result<SessionRecord, Error> {
self.storage
.clone()
@ -525,6 +551,8 @@ impl<ClaimsType: Claims> SessionStorage<ClaimsType> {
.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<SessionRecord, Error> {
self.storage
.clone()
@ -706,6 +734,82 @@ impl<ClaimsType: Claims> SessionExtractor<ClaimsType> for HeaderExtractor<Claims
}
}
/// Load or generate new Ed25519 signing keys.
///
/// [JwtSigningKeys::load_or_create] should be called only once at the boot of the server.
///
/// If there's any issue during generating new keys or loading exiting one application will panic.
///
/// Examples:
///
/// ```rust
/// use actix_jwt_session::*;
///
/// pub fn boot_server() {
/// let keys = JwtSigningKeys::load_or_create();
/// }
/// ```
pub struct JwtSigningKeys {
pub encoding_key: EncodingKey,
pub decoding_key: DecodingKey,
}
impl JwtSigningKeys {
pub 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!("Failed to load or generate jwt signing keys: {:?}", e),
}
}
fn generate() -> Result<Self, Box<dyn std::error::Error>> {
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<Self> {
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")]

View File

@ -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<Self, Box<dyn std::error::Error>> {
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<Self> {
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,
})
}
}