Add more actix-jwt-session docs
This commit is contained in:
parent
2852f89895
commit
ecf3c3a344
@ -2,6 +2,8 @@
|
||||
name = "actix-jwt-session"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Full featured JWT session managment for actix"
|
||||
license = "MIT"
|
||||
|
||||
[features]
|
||||
default = ['use-redis']
|
||||
@ -21,7 +23,7 @@ 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"] }
|
||||
uuid = { version = "1.4.1", features = ["v4", "serde"] }
|
||||
|
||||
[[test]]
|
||||
name = "ensure_redis_flow"
|
||||
|
@ -38,9 +38,9 @@
|
||||
//! redis.clone(),
|
||||
//! vec![
|
||||
//! // Check if header "Authorization" exists and contains Bearer with encoded JWT
|
||||
//! Box::new(HeaderExtractor::new()),
|
||||
//! // Check if cookie "Authorization" exists and contains encoded JWT
|
||||
//! Box::new(CookieExtractor::new()),
|
||||
//! Box::new(HeaderExtractor::new("Authorization")),
|
||||
//! // Check if cookie "jwt" exists and contains encoded JWT
|
||||
//! Box::new(CookieExtractor::new("jwt")),
|
||||
//! ]
|
||||
//! );
|
||||
//!
|
||||
@ -70,8 +70,22 @@
|
||||
//! fn subject(&self) -> &str { &self.subject }
|
||||
//! }
|
||||
//!
|
||||
//! #[get("/access-storage")]
|
||||
//! async fn storage_access(session_store: Data<SessionStorage<AppClaims>>) -> HttpResponse {
|
||||
//! #[derive(Clone, PartialEq, Serialize, Deserialize)]
|
||||
//! pub struct SessionData {
|
||||
//! id: uuid::Uuid,
|
||||
//! subject: String,
|
||||
//! }
|
||||
//!
|
||||
//! #[actix_web::post("/access-storage")]
|
||||
//! async fn storage_access(
|
||||
//! session_store: Data<SessionStorage<AppClaims>>,
|
||||
//! p: actix_web::web::Json<SessionData>,
|
||||
//! ) -> HttpResponse {
|
||||
//! let p = p.into_inner();
|
||||
//! session_store.store(AppClaims {
|
||||
//! id: p.id,
|
||||
//! subject: p.subject,
|
||||
//! }, std::time::Duration::from_secs(60 * 60 * 24 * 14) ).await.unwrap();
|
||||
//! HttpResponse::Ok().body("")
|
||||
//! }
|
||||
//!
|
||||
@ -92,7 +106,7 @@
|
||||
//! encoding_key: EncodingKey,
|
||||
//! decoding_key: DecodingKey,
|
||||
//! }
|
||||
//!
|
||||
//!
|
||||
//! impl JwtSigningKeys {
|
||||
//! fn generate() -> Result<Self, Box<dyn std::error::Error>> {
|
||||
//! let doc = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new())?;
|
||||
@ -116,13 +130,19 @@ use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub static HEADER_NAME: &str = "Authorization";
|
||||
/// Default authorization header is "Authorization"
|
||||
pub static DEFAULT_HEADER_NAME: &str = "Authorization";
|
||||
|
||||
/// Serializable and storable struct which represent JWT claims
|
||||
///
|
||||
/// * 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 {
|
||||
fn jti(&self) -> uuid::Uuid;
|
||||
fn subject(&self) -> &str;
|
||||
}
|
||||
|
||||
/// Session related errors
|
||||
#[derive(Debug, thiserror::Error, PartialEq)]
|
||||
pub enum Error {
|
||||
#[error("Failed to obtain redis connection")]
|
||||
@ -154,13 +174,37 @@ impl actix_web::ResponseError for Error {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extractable user session which requires presence of JWT in request.
|
||||
/// If there's no JWT endpoint which requires this structure will automatically returns `401`.
|
||||
///
|
||||
/// Examples:
|
||||
///
|
||||
/// ```
|
||||
/// use actix_web::get;
|
||||
/// use actix_web::HttpResponse;
|
||||
/// use actix_jwt_session::Authenticated;
|
||||
///
|
||||
/// # #[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 }
|
||||
/// # }
|
||||
///
|
||||
/// // If there's no JWT in request server will automatically returns 401
|
||||
/// #[get("/session")]
|
||||
/// async fn read_session(session: Authenticated<Claims>) -> HttpResponse {
|
||||
/// let encoded = session.encode().unwrap(); // JWT as encrypted string
|
||||
/// HttpResponse::Ok().finish()
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
#[cfg_attr(feature = "serde-transparent", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde-transparent", serde(transparent))]
|
||||
pub struct Authenticated<T> {
|
||||
claims: Arc<T>,
|
||||
jwt_encoding_key: Arc<EncodingKey>,
|
||||
algorithm: Algorithm,
|
||||
pub claims: Arc<T>,
|
||||
pub jwt_encoding_key: Arc<EncodingKey>,
|
||||
pub algorithm: Algorithm,
|
||||
}
|
||||
|
||||
impl<T> std::ops::Deref for Authenticated<T> {
|
||||
@ -172,6 +216,7 @@ impl<T> std::ops::Deref for Authenticated<T> {
|
||||
}
|
||||
|
||||
impl<T: Claims> Authenticated<T> {
|
||||
/// Encode claims as JWT encrypted string
|
||||
pub fn encode(&self) -> Result<String, jsonwebtoken::errors::Error> {
|
||||
encode(
|
||||
&jsonwebtoken::Header::new(self.algorithm),
|
||||
@ -197,6 +242,31 @@ impl<T: Claims> FromRequest for Authenticated<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Similar to [Authenticated] but JWT is optional
|
||||
///
|
||||
/// Examples:
|
||||
///
|
||||
/// ```
|
||||
/// use actix_web::get;
|
||||
/// use actix_web::HttpResponse;
|
||||
/// use actix_jwt_session::MaybeAuthenticated;
|
||||
///
|
||||
/// # #[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 }
|
||||
/// # }
|
||||
///
|
||||
/// // If there's no JWT in request server will NOT automatically returns 401
|
||||
/// #[get("/session")]
|
||||
/// async fn read_session(session: MaybeAuthenticated<Claims>) -> HttpResponse {
|
||||
/// if let Some(session) = session.into_option() {
|
||||
/// // handle authenticated request
|
||||
/// }
|
||||
/// HttpResponse::Ok().finish()
|
||||
/// }
|
||||
/// ```
|
||||
pub struct MaybeAuthenticated<ClaimsType: Claims>(Option<Authenticated<ClaimsType>>);
|
||||
|
||||
impl<ClaimsType: Claims> MaybeAuthenticated<ClaimsType> {
|
||||
@ -204,6 +274,8 @@ impl<ClaimsType: Claims> MaybeAuthenticated<ClaimsType> {
|
||||
self.0.is_some()
|
||||
}
|
||||
|
||||
/// Transform extractor to simple [Option] with [Some] containing [Authenticated] as value.
|
||||
/// This allow to handle signed in request and encrypt claims if needed
|
||||
pub fn into_option(self) -> Option<Authenticated<ClaimsType>> {
|
||||
self.0
|
||||
}
|
||||
@ -233,33 +305,42 @@ impl<T: Claims> FromRequest for MaybeAuthenticated<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows to customize where and how sessions are stored in persistant storage.
|
||||
/// By default redis can be used to store sesions but it's possible and easy to use memcached or
|
||||
/// postgresql.
|
||||
#[async_trait(?Send)]
|
||||
pub trait TokenStorage: Send + Sync {
|
||||
type ClaimsType: Claims;
|
||||
|
||||
/// Load claims from storage or returns [Error] if record does not exists or there was other
|
||||
/// error while trying to fetch data from storage.
|
||||
async fn get_from_jti(self: Arc<Self>, jti: uuid::Uuid) -> Result<Self::ClaimsType, Error>;
|
||||
|
||||
/// Save claims in storage in a way claims can be loaded from database using `jti` as [uuid::Uuid] (JWT ID)
|
||||
async fn set_by_jti(
|
||||
self: Arc<Self>,
|
||||
claims: Self::ClaimsType,
|
||||
exp: std::time::Duration,
|
||||
) -> Result<(), Error>;
|
||||
|
||||
/// Erase claims from storage. You may ignore if claims does not exists in storage.
|
||||
/// Redis implementation returns [Error::NotFound] if record does not exists.
|
||||
async fn remove_by_jti(self: Arc<Self>, jti: Uuid) -> Result<(), Error>;
|
||||
|
||||
fn jwt_encoding_key(&self) -> Arc<EncodingKey>;
|
||||
|
||||
fn algorithm(&self) -> Algorithm;
|
||||
}
|
||||
|
||||
/// Allow to save, read and remove session from storage.
|
||||
#[derive(Clone)]
|
||||
pub struct SessionStorage<ClaimsType: Claims>(Arc<dyn TokenStorage<ClaimsType = ClaimsType>>);
|
||||
pub struct SessionStorage<ClaimsType: Claims> {
|
||||
storage: Arc<dyn TokenStorage<ClaimsType = ClaimsType>>,
|
||||
jwt_encoding_key: Arc<EncodingKey>,
|
||||
algorithm: Algorithm,
|
||||
}
|
||||
|
||||
impl<ClaimsType: Claims> std::ops::Deref for SessionStorage<ClaimsType> {
|
||||
type Target = Arc<dyn TokenStorage<ClaimsType = ClaimsType>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
&self.storage
|
||||
}
|
||||
}
|
||||
|
||||
@ -269,13 +350,16 @@ impl<ClaimsType: Claims> SessionStorage<ClaimsType> {
|
||||
claims: ClaimsType,
|
||||
exp: std::time::Duration,
|
||||
) -> Result<(), Error> {
|
||||
self.0.clone().set_by_jti(claims, exp).await
|
||||
self.storage.clone().set_by_jti(claims, exp).await
|
||||
}
|
||||
|
||||
/// Load claims from storage or returns [Error] if record does not exists or there was other
|
||||
/// error while trying to fetch data from storage.
|
||||
pub async fn get_from_jti(&self, jti: uuid::Uuid) -> Result<ClaimsType, Error> {
|
||||
self.0.clone().get_from_jti(jti).await
|
||||
self.storage.clone().get_from_jti(jti).await
|
||||
}
|
||||
|
||||
/// Save claims in storage in a way claims can be loaded from database using `jti` as [uuid::Uuid] (JWT ID)
|
||||
pub async fn store(
|
||||
&self,
|
||||
claims: ClaimsType,
|
||||
@ -284,18 +368,71 @@ impl<ClaimsType: Claims> SessionStorage<ClaimsType> {
|
||||
self.set_by_jti(claims.clone(), exp).await?;
|
||||
Ok(Authenticated {
|
||||
claims: Arc::new(claims),
|
||||
jwt_encoding_key: self.0.jwt_encoding_key(),
|
||||
algorithm: self.algorithm(),
|
||||
jwt_encoding_key: self.jwt_encoding_key.clone(),
|
||||
algorithm: self.algorithm,
|
||||
})
|
||||
}
|
||||
|
||||
/// Erase claims from storage. You may ignore if claims does not exists in storage.
|
||||
/// Redis implementation returns [Error::NotFound] if record does not exists.
|
||||
pub async fn erase(&self, jti: Uuid) -> Result<(), Error> {
|
||||
self.0.clone().remove_by_jti(jti).await
|
||||
self.storage.clone().remove_by_jti(jti).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait allowing to extract JWt token from [actix_web::dev::ServiceRequest]
|
||||
///
|
||||
/// Two extractor are implemented by default
|
||||
/// * [HeaderExtractor] which is best for any PWA or micro services requests
|
||||
/// * [CookieExtractor] which is best for simple server with session stored in cookie
|
||||
///
|
||||
/// It's possible to implement GraphQL, JSON payload or query using `req.extract::<JSON<YourStruct>>()` if this is needed.
|
||||
///
|
||||
/// All implementation can use [SessionExtractor::decode] method for decoding raw JWT string into
|
||||
/// Claims and then [SessionExtractor::validate] to validate claims agains session stored in [SessionStorage]
|
||||
#[async_trait(?Send)]
|
||||
pub trait Extractor<ClaimsType: Claims>: Send + Sync + 'static {
|
||||
pub trait SessionExtractor<ClaimsType: Claims>: Send + Sync + 'static {
|
||||
/// Extract claims from [actix_web::dev::ServiceRequest]
|
||||
///
|
||||
/// Examples:
|
||||
///
|
||||
/// ```
|
||||
/// use actix_web::dev::ServiceRequest;
|
||||
/// use jsonwebtoken::*;
|
||||
/// use actix_jwt_session::{Extractor, Authenticated, Error, SessionStorage};
|
||||
/// use std::sync::Arc;
|
||||
/// use actix_web::HttpMessage;
|
||||
/// # #[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 }
|
||||
/// # }
|
||||
///
|
||||
/// #[derive(Debug, Clone, Copy, Default)]
|
||||
/// struct ExampleExtractor;
|
||||
///
|
||||
/// #[async_trait::async_trait(?Send)]
|
||||
/// impl Extractor<Claims> for ExampleExtractor {
|
||||
/// async fn extract_jwt(
|
||||
/// &self,
|
||||
/// req: &ServiceRequest,
|
||||
/// jwt_encoding_key: Arc<EncodingKey>,
|
||||
/// jwt_decoding_key: Arc<DecodingKey>,
|
||||
/// algorithm: Algorithm,
|
||||
/// storage: SessionStorage<Claims>,
|
||||
/// ) -> Result<(), Error> {
|
||||
/// if req.peer_addr().unwrap().ip().is_multicast() {
|
||||
/// req.extensions_mut().insert(Authenticated {
|
||||
/// claims: Arc::new(Claims { id: uuid::Uuid::default(), sub: "HUB".into() }),
|
||||
/// jwt_encoding_key,
|
||||
/// algorithm,
|
||||
/// });
|
||||
/// }
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
async fn extract_jwt(
|
||||
&self,
|
||||
req: &ServiceRequest,
|
||||
@ -305,6 +442,7 @@ pub trait Extractor<ClaimsType: Claims>: Send + Sync + 'static {
|
||||
storage: SessionStorage<ClaimsType>,
|
||||
) -> Result<(), Error>;
|
||||
|
||||
/// Decode encrypted JWT to structure
|
||||
fn decode(
|
||||
&self,
|
||||
value: &str,
|
||||
@ -319,13 +457,16 @@ pub trait Extractor<ClaimsType: Claims>: Send + Sync + 'static {
|
||||
.map(|t| t.claims)
|
||||
}
|
||||
|
||||
/// Validate JWT Claims agains stored in storage tokens.
|
||||
///
|
||||
/// * Token must exists in storage
|
||||
/// * Token must be exactly the same as token from storage
|
||||
async fn validate(
|
||||
&self,
|
||||
claims: &ClaimsType,
|
||||
storage: SessionStorage<ClaimsType>,
|
||||
) -> Result<(), Error> {
|
||||
let stored = storage
|
||||
.0
|
||||
.clone()
|
||||
.get_from_jti(claims.jti())
|
||||
.await
|
||||
@ -338,15 +479,28 @@ pub trait Extractor<ClaimsType: Claims>: Send + Sync + 'static {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CookieExtractor<ClaimsType>(PhantomData<ClaimsType>);
|
||||
/// Extracts JWT token from HTTP Request cookies. This extractor should be used when you can't set
|
||||
/// your own header, for example when user enters http links to browser and you don't have any
|
||||
/// advanced frontend.
|
||||
///
|
||||
/// This exractor is may be used by PWA application or micro services but [HeaderExtractor] is much
|
||||
/// more suitable for this purpose.
|
||||
pub struct CookieExtractor<ClaimsType> {
|
||||
__ty: PhantomData<ClaimsType>,
|
||||
cookie_name: &'static str,
|
||||
}
|
||||
|
||||
impl<ClaimsType: Claims> CookieExtractor<ClaimsType> {
|
||||
pub fn new() -> Self { Self(Default::default()) }
|
||||
pub fn new(cookie_name: &'static str) -> Self {
|
||||
Self {
|
||||
__ty: Default::default(),
|
||||
cookie_name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl<ClaimsType: Claims> Extractor<ClaimsType> for CookieExtractor<ClaimsType> {
|
||||
impl<ClaimsType: Claims> SessionExtractor<ClaimsType> for CookieExtractor<ClaimsType> {
|
||||
async fn extract_jwt(
|
||||
&self,
|
||||
req: &ServiceRequest,
|
||||
@ -355,7 +509,9 @@ impl<ClaimsType: Claims> Extractor<ClaimsType> for CookieExtractor<ClaimsType> {
|
||||
algorithm: Algorithm,
|
||||
storage: SessionStorage<ClaimsType>,
|
||||
) -> Result<(), Error> {
|
||||
let Some(cookie) = req.cookie(HEADER_NAME) else {return Ok(())};
|
||||
let Some(cookie) = req.cookie(self.cookie_name) else {
|
||||
return Ok(())
|
||||
};
|
||||
let as_str = cookie.value();
|
||||
let decoded_claims = self.decode(as_str, jwt_decoding_key, algorithm)?;
|
||||
self.validate(&decoded_claims, storage).await?;
|
||||
@ -368,15 +524,28 @@ impl<ClaimsType: Claims> Extractor<ClaimsType> for CookieExtractor<ClaimsType> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct HeaderExtractor<ClaimsType>(PhantomData<ClaimsType>);
|
||||
/// Extracts JWT token from HTTP Request headers
|
||||
///
|
||||
/// This exractor is very useful for all PWA application or for micro services
|
||||
/// because you can set your own headers while making http requests.
|
||||
///
|
||||
/// If you want to have users authorized using simple html <a> you should use [CookieExtractor]
|
||||
pub struct HeaderExtractor<ClaimsType> {
|
||||
__ty: PhantomData<ClaimsType>,
|
||||
header_name: &'static str,
|
||||
}
|
||||
|
||||
impl<ClaimsType: Claims> HeaderExtractor<ClaimsType> {
|
||||
pub fn new() -> Self { Self(Default::default()) }
|
||||
pub fn new(header_name: &'static str) -> Self {
|
||||
Self {
|
||||
__ty: Default::default(),
|
||||
header_name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl<ClaimsType: Claims> Extractor<ClaimsType> for HeaderExtractor<ClaimsType> {
|
||||
impl<ClaimsType: Claims> SessionExtractor<ClaimsType> for HeaderExtractor<ClaimsType> {
|
||||
async fn extract_jwt(
|
||||
&self,
|
||||
req: &ServiceRequest,
|
||||
@ -387,7 +556,7 @@ impl<ClaimsType: Claims> Extractor<ClaimsType> for HeaderExtractor<ClaimsType> {
|
||||
) -> Result<(), Error> {
|
||||
let Some(authorisation_header) = req
|
||||
.headers()
|
||||
.get(HEADER_NAME)
|
||||
.get(self.header_name)
|
||||
else {
|
||||
return Ok(())
|
||||
};
|
||||
|
@ -1,3 +1,12 @@
|
||||
//! Default session storage which uses async redis requests
|
||||
//!
|
||||
//! Sessions are serialized to binary format and stored using [uuid::Uuid] key as bytes.
|
||||
//! All sessions must have expirations time after which they will be automatically removed by
|
||||
//! redis.
|
||||
//!
|
||||
//! [RedisStorage] is constructed by [RedisMiddlewareFactory] from [redis_async_pool::RedisPool] and shared
|
||||
//! between all [RedisMiddleware] instances.
|
||||
|
||||
use super::*;
|
||||
use actix_web::dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform};
|
||||
use futures_util::future::LocalBoxFuture;
|
||||
@ -7,24 +16,17 @@ use std::marker::PhantomData;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Redis implementation for [TokenStorage]
|
||||
#[derive(Clone)]
|
||||
struct RedisStorage<ClaimsType: Claims> {
|
||||
pool: redis_async_pool::RedisPool,
|
||||
jwt_encoding_key: Arc<EncodingKey>,
|
||||
algorithm: Algorithm,
|
||||
_claims_type_marker: PhantomData<ClaimsType>,
|
||||
}
|
||||
|
||||
impl<ClaimsType: Claims> RedisStorage<ClaimsType> {
|
||||
pub fn new(
|
||||
pool: redis_async_pool::RedisPool,
|
||||
jwt_encoding_key: Arc<EncodingKey>,
|
||||
algorithm: Algorithm,
|
||||
) -> Self {
|
||||
pub fn new(pool: redis_async_pool::RedisPool) -> Self {
|
||||
Self {
|
||||
pool,
|
||||
jwt_encoding_key,
|
||||
algorithm,
|
||||
_claims_type_marker: Default::default(),
|
||||
}
|
||||
}
|
||||
@ -69,14 +71,6 @@ where
|
||||
.map_err(|_| Error::NotFound)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn jwt_encoding_key(&self) -> Arc<EncodingKey> {
|
||||
self.jwt_encoding_key.clone()
|
||||
}
|
||||
|
||||
fn algorithm(&self) -> Algorithm {
|
||||
self.algorithm
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RedisMiddleware<S, ClaimsType>
|
||||
@ -89,7 +83,7 @@ where
|
||||
jwt_decoding_key: Arc<DecodingKey>,
|
||||
algorithm: Algorithm,
|
||||
storage: SessionStorage<ClaimsType>,
|
||||
extractors: Arc<Vec<Box<dyn Extractor<ClaimsType>>>>,
|
||||
extractors: Arc<Vec<Box<dyn SessionExtractor<ClaimsType>>>>,
|
||||
}
|
||||
|
||||
impl<S, B, ClaimsType> Service<ServiceRequest> for RedisMiddleware<S, ClaimsType>
|
||||
@ -116,15 +110,20 @@ where
|
||||
async move {
|
||||
let mut last_error = None;
|
||||
for extractor in extractors.iter() {
|
||||
match extractor.extract_jwt(
|
||||
&req,
|
||||
jwt_encoding_key.clone(),
|
||||
jwt_decoding_key.clone(),
|
||||
algorithm,
|
||||
storage.clone(),
|
||||
).await {
|
||||
match extractor
|
||||
.extract_jwt(
|
||||
&req,
|
||||
jwt_encoding_key.clone(),
|
||||
jwt_decoding_key.clone(),
|
||||
algorithm,
|
||||
storage.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => break,
|
||||
Err(e) => { last_error = Some(e); },
|
||||
Err(e) => {
|
||||
last_error = Some(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
if let Some(e) = last_error {
|
||||
@ -143,7 +142,7 @@ pub struct RedisMiddlewareFactory<ClaimsType: Claims> {
|
||||
jwt_decoding_key: Arc<DecodingKey>,
|
||||
algorithm: Algorithm,
|
||||
storage: SessionStorage<ClaimsType>,
|
||||
extractors: Arc<Vec<Box<dyn Extractor<ClaimsType>>>>,
|
||||
extractors: Arc<Vec<Box<dyn SessionExtractor<ClaimsType>>>>,
|
||||
_claims_type_marker: PhantomData<ClaimsType>,
|
||||
}
|
||||
|
||||
@ -153,15 +152,19 @@ impl<ClaimsType: Claims> RedisMiddlewareFactory<ClaimsType> {
|
||||
jwt_decoding_key: Arc<DecodingKey>,
|
||||
algorithm: Algorithm,
|
||||
pool: redis_async_pool::RedisPool,
|
||||
extractors: Vec<Box<dyn Extractor<ClaimsType>>>,
|
||||
extractors: Vec<Box<dyn SessionExtractor<ClaimsType>>>,
|
||||
) -> Self {
|
||||
let storage = Arc::new(RedisStorage::new(pool, jwt_encoding_key.clone(), algorithm));
|
||||
let storage = Arc::new(RedisStorage::new(pool));
|
||||
|
||||
Self {
|
||||
jwt_encoding_key,
|
||||
jwt_encoding_key: jwt_encoding_key.clone(),
|
||||
jwt_decoding_key,
|
||||
algorithm,
|
||||
storage: SessionStorage(storage),
|
||||
storage: SessionStorage {
|
||||
storage,
|
||||
jwt_encoding_key: jwt_encoding_key.clone(),
|
||||
algorithm,
|
||||
},
|
||||
extractors: Arc::new(extractors),
|
||||
_claims_type_marker: Default::default(),
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_jwt_session::{Authenticated, RedisMiddlewareFactory, HeaderExtractor, SessionStorage};
|
||||
use actix_jwt_session::{
|
||||
Authenticated, HeaderExtractor, RedisMiddlewareFactory, SessionStorage, DEFAULT_HEADER_NAME,
|
||||
};
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::{Data, Json};
|
||||
use actix_web::HttpResponse;
|
||||
@ -47,7 +49,7 @@ async fn not_authenticated() {
|
||||
Arc::new(keys.decoding_key),
|
||||
Algorithm::EdDSA,
|
||||
redis.clone(),
|
||||
vec![Box::new(HeaderExtractor::new())]
|
||||
vec![Box::new(HeaderExtractor::new(DEFAULT_HEADER_NAME))],
|
||||
);
|
||||
|
||||
let app = App::new()
|
||||
@ -80,7 +82,10 @@ async fn not_authenticated() {
|
||||
.await;
|
||||
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
||||
|
||||
let origina_claims = Claims { id: Uuid::new_v4(), subject: "foo".to_string() };
|
||||
let origina_claims = Claims {
|
||||
id: Uuid::new_v4(),
|
||||
subject: "foo".to_string(),
|
||||
};
|
||||
let res = test::call_service(
|
||||
&app,
|
||||
test::TestRequest::default()
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::ops::Add;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_jwt_session::{SessionStorage, CookieExtractor, HeaderExtractor};
|
||||
use actix_jwt_session::{CookieExtractor, HeaderExtractor, SessionStorage, DEFAULT_HEADER_NAME};
|
||||
pub use actix_jwt_session::{Error, RedisMiddlewareFactory};
|
||||
use actix_web::web::{Data, Form, ServiceConfig};
|
||||
use actix_web::{get, post, HttpRequest, HttpResponse};
|
||||
@ -133,7 +133,10 @@ impl SessionConfigurator {
|
||||
Arc::new(jwt_signing_keys.decoding_key),
|
||||
Algorithm::EdDSA,
|
||||
redis,
|
||||
vec![Box::new(CookieExtractor::<Claims>::new()), Box::new(HeaderExtractor::<Claims>::new())]
|
||||
vec![
|
||||
Box::new(CookieExtractor::<Claims>::new(DEFAULT_HEADER_NAME)),
|
||||
Box::new(HeaderExtractor::<Claims>::new(DEFAULT_HEADER_NAME)),
|
||||
],
|
||||
);
|
||||
|
||||
Self {
|
||||
@ -296,12 +299,13 @@ async fn login_inner(
|
||||
}
|
||||
};
|
||||
|
||||
let cookie = actix_web::cookie::Cookie::build(actix_jwt_session::HEADER_NAME, &bearer_token)
|
||||
.http_only(true)
|
||||
.finish();
|
||||
let cookie =
|
||||
actix_web::cookie::Cookie::build(actix_jwt_session::DEFAULT_HEADER_NAME, &bearer_token)
|
||||
.http_only(true)
|
||||
.finish();
|
||||
Ok(HttpResponse::Ok()
|
||||
.append_header((
|
||||
actix_jwt_session::HEADER_NAME,
|
||||
actix_jwt_session::DEFAULT_HEADER_NAME,
|
||||
format!("Bearer {bearer_token}").as_str(),
|
||||
))
|
||||
.cookie(cookie)
|
||||
|
Loading…
Reference in New Issue
Block a user