Add actix-jwt-session working docs
This commit is contained in:
parent
58a0239a05
commit
7fa7fcda2b
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -188,6 +188,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"garde",
|
||||
"jsonwebtoken",
|
||||
"rand 0.8.5",
|
||||
"redis",
|
||||
"redis-async-pool",
|
||||
"ring",
|
||||
|
@ -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"] }
|
||||
|
@ -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")]
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user