actix_jwt_session/
lib.rs

1//! All in one creating session and session validation library for actix.
2//!
3//! It's designed to extract session using middleware and validate endpoint
4//! simply by using actix-web extractors. Currently you can extract tokens from
5//! Header or Cookie. It's possible to implement Path, Query or Body using
6//! `[ServiceRequest::extract]` but you must have struct to which values will be
7//! extracted so it's easy to do if you have your own fields.
8//!
9//! Example:
10//!
11//! ```
12//! use serde::Deserialize;
13//!
14//! #[derive(Deserialize)]
15//! struct MyJsonBody {
16//!     jwt: Option<String>,
17//!     refresh: Option<String>,
18//! }
19//! ```
20//!
21//! To start with this library you need to create your own `AppClaims` structure
22//! and implement `actix_jwt_session::Claims` trait for it.
23//!
24//! ```
25//! use serde::{Serialize, Deserialize};
26//!
27//! #[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
28//! #[serde(rename_all = "snake_case")]
29//! pub enum Audience {
30//!     Web,
31//! }
32//!
33//! #[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
34//! #[serde(rename_all = "snake_case")]
35//! pub struct Claims {
36//!     #[serde(rename = "exp")]
37//!     pub expiration_time: u64,
38//!     #[serde(rename = "iat")]
39//!     pub issues_at: usize,
40//!     /// Account login
41//!     #[serde(rename = "sub")]
42//!     pub subject: String,
43//!     #[serde(rename = "aud")]
44//!     pub audience: Audience,
45//!     #[serde(rename = "jti")]
46//!     pub jwt_id: uuid::Uuid,
47//!     #[serde(rename = "aci")]
48//!     pub account_id: i32,
49//!     #[serde(rename = "nbf")]
50//!     pub not_before: u64,
51//! }
52//!
53//! impl actix_jwt_session::Claims for Claims {
54//!     fn jti(&self) -> uuid::Uuid {
55//!         self.jwt_id
56//!     }
57//!
58//!     fn subject(&self) -> &str {
59//!         &self.subject
60//!     }
61//! }
62//!
63//! impl Claims {
64//!     pub fn account_id(&self) -> i32 {
65//!         self.account_id
66//!     }
67//! }
68//! ```
69//!
70//! Then you must create middleware factory with session storage. Currently
71//! there's adapter only for redis so we will goes with it in this example.
72//!
73//! * First create connection pool to redis using `redis_async_pool`.
74//! * Next generate or load create jwt signing keys. They are required for
75//!   creating JWT from claims.
76//! * Finally pass keys and algorithm to builder, pass pool and add some
77//!   extractors
78//!
79//! ```
80//! use std::sync::Arc;
81//! use actix_jwt_session::*;
82//!
83//! # async fn create<AppClaims: actix_jwt_session::Claims>() {
84//!     // create redis connection
85//!     let redis = deadpool_redis::Config::from_url("redis://localhost:6379")
86//!         .create_pool(Some(deadpool_redis::Runtime::Tokio1)).unwrap();
87//!  
88//!     // create new [SessionStorage] and [SessionMiddlewareFactory]
89//!     let (storage, factory) = SessionMiddlewareFactory::<AppClaims>::build_ed_dsa()
90//!     // pass redis connection
91//!     .with_redis_pool(redis.clone())
92//!     .with_extractors(
93//!         Extractors::default()
94//!             // Check if header "Authorization" exists and contains Bearer with encoded JWT
95//!             .with_jwt_header("Authorization")
96//!             // Check if cookie "jwt" exists and contains encoded JWT
97//!             .with_jwt_cookie("acx-a")
98//!             .with_refresh_header("ACX-Refresh")
99//!             // Check if cookie "jwt" exists and contains encoded JWT
100//!             .with_refresh_cookie("acx-r")
101//!     )
102//!     .finish();
103//! # }
104//! ```
105//!
106//! As you can see we have there [SessionMiddlewareBuilder::with_refresh_cookie]
107//! and [SessionMiddlewareBuilder::with_refresh_header]. Library uses
108//! internal structure [RefreshToken] which is created and managed internally
109//! without any additional user work.
110//!
111//! This will be used to extend JWT lifetime. This lifetime comes from 2
112//! structures which describe time to live. [JwtTtl] describes how long access
113//! token should be valid, [RefreshToken] describes how long refresh token is
114//! valid. [SessionStorage] allows to extend livetime of both with single call
115//! of [SessionStorage::refresh] and it will change time of creating tokens to
116//! current time.
117//!
118//! ```
119//! use actix_jwt_session::{JwtTtl, RefreshTtl, Duration};
120//!
121//! let jwt_ttl = JwtTtl(Duration::days(14));
122//! let refresh_ttl = RefreshTtl(Duration::days(3 * 31));
123//! ```
124//!
125//! Now you just need to add those structures to [actix_web::App] using
126//! `.app_data` and `.wrap` and you are ready to go. Bellow you have full
127//! example of usage.
128//!
129//! Examples:
130//!
131//! ```no_run
132//! use std::sync::Arc;
133//! use actix_jwt_session::*;
134//! use actix_web::{get, post};
135//! use actix_web::web::{Data, Json};
136//! use actix_web::{HttpResponse, App, HttpServer};
137//! use jsonwebtoken::*;
138//! use serde::{Serialize, Deserialize};
139//!
140//! #[tokio::main]
141//! async fn main() {
142//!     // create redis connection
143//!     let redis = deadpool_redis::Config::from_url("redis://localhost:6379")
144//!         .create_pool(Some(deadpool_redis::Runtime::Tokio1)).unwrap();
145//!  
146//!     let jwt_ttl = JwtTtl(Duration::days(14));
147//!     let refresh_ttl = RefreshTtl(Duration::days(3 * 31));
148//!  
149//!     HttpServer::new(move || {
150//!         App::new()
151//!             .app_data(Data::new( jwt_ttl ))
152//!             .app_data(Data::new( refresh_ttl ))
153//!             .use_jwt::<AppClaims>(
154//!                 Extractors::default()
155//!                     // Check if header "Authorization" exists and contains Bearer with encoded JWT
156//!                     .with_jwt_header(JWT_HEADER_NAME)
157//!                     // Check if cookie JWT exists and contains encoded JWT
158//!                     .with_jwt_cookie(JWT_COOKIE_NAME)
159//!                     .with_refresh_header(REFRESH_HEADER_NAME)
160//!                     // Check if cookie JWT exists and contains encoded JWT
161//!                     .with_refresh_cookie(REFRESH_COOKIE_NAME),
162//!                 Some(redis.clone())
163//!             )
164//!             .app_data(Data::new(redis.clone()))
165//!             .service(must_be_signed_in)
166//!             .service(may_be_signed_in)
167//!             .service(register)
168//!             .service(sign_in)
169//!             .service(sign_out)
170//!             .service(refresh_session)
171//!             .service(session_info)
172//!             .service(root)
173//!     })
174//!     .bind(("0.0.0.0", 8080)).unwrap()
175//!     .run()
176//!     .await.unwrap();
177//! }
178//!
179//! #[derive(Clone, PartialEq, Serialize, Deserialize)]
180//! pub struct SessionData {
181//!     id: uuid::Uuid,
182//!     subject: String,
183//! }
184//!
185//! #[get("/authorized")]
186//! async fn must_be_signed_in(session: Authenticated<AppClaims>) -> HttpResponse {
187//!     use crate::actix_jwt_session::Claims;
188//!     let jit = session.jti();
189//!     HttpResponse::Ok().finish()
190//! }
191//!
192//! #[get("/maybe-authorized")]
193//! async fn may_be_signed_in(session: MaybeAuthenticated<AppClaims>) -> HttpResponse {
194//!     if let Some(session) = session.into_option() {
195//!     }
196//!     HttpResponse::Ok().finish()
197//! }
198//!
199//! #[derive(Deserialize)]
200//! struct SignUpPayload {
201//!     login: String,
202//!     password: String,
203//!     password_confirmation: String,
204//! }
205//!
206//! #[post("/session/sign-up")]
207//! async fn register(payload: Json<SignUpPayload>) -> Result<HttpResponse, actix_web::Error> {
208//!     let payload = payload.into_inner();
209//!     
210//!     // Validate payload
211//!     
212//!     // Save model and return HttpResponse
213//!     let model = AccountModel {
214//!         id: -1,
215//!         login: payload.login,
216//!         // Encrypt password before saving to database
217//!         pass_hash: Hashing::encrypt(&payload.password).unwrap(),
218//!     };
219//!     // Save model
220//!
221//!     # todo!()
222//! }
223//!
224//! #[derive(Deserialize)]
225//! struct SignInPayload {
226//!     login: String,
227//!     password: String,
228//! }
229//!
230//! #[post("/session/sign-in")]
231//! async fn sign_in(
232//!     store: Data<SessionStorage>,
233//!     payload: Json<SignInPayload>,
234//!     jwt_ttl: Data<JwtTtl>,
235//!     refresh_ttl: Data<RefreshTtl>,
236//! ) -> Result<HttpResponse, actix_web::Error> {
237//!     let payload = payload.into_inner();
238//!     let store = store.into_inner();
239//!     let account: AccountModel = {
240//!         /* load account using login */
241//! #         todo!()
242//!     };
243//!     if let Err(e) = Hashing::verify(account.pass_hash.as_str(), payload.password.as_str()) {
244//!         return Ok(HttpResponse::Unauthorized().finish());
245//!     }
246//!     let claims = AppClaims {
247//!          issues_at: OffsetDateTime::now_utc().unix_timestamp() as usize,
248//!          subject: account.login.clone(),
249//!          expiration_time: jwt_ttl.0.as_seconds_f64() as u64,
250//!          audience: Audience::Web,
251//!          jwt_id: uuid::Uuid::new_v4(),
252//!          account_id: account.id,
253//!          not_before: 0,
254//!     };
255//!     let pair = store
256//!         .clone()
257//!         .store(claims, *jwt_ttl.into_inner(), *refresh_ttl.into_inner())
258//!         .await
259//!         .unwrap();
260//!     Ok(HttpResponse::Ok()
261//!         .append_header((JWT_HEADER_NAME, pair.jwt.encode().unwrap()))
262//!         .append_header((REFRESH_HEADER_NAME, pair.refresh.encode().unwrap()))
263//!         .finish())
264//! }
265//!
266//! #[post("/session/sign-out")]
267//! async fn sign_out(store: Data<SessionStorage>, auth: Authenticated<AppClaims>) -> HttpResponse {
268//!     let store = store.into_inner();
269//!     store.erase::<AppClaims>(auth.jwt_id).await.unwrap();
270//!     HttpResponse::Ok()
271//!         .append_header((JWT_HEADER_NAME, ""))
272//!         .append_header((REFRESH_HEADER_NAME, ""))
273//!         .cookie(
274//!             actix_web::cookie::Cookie::build(JWT_COOKIE_NAME, "")
275//!                 .expires(OffsetDateTime::now_utc())
276//!                 .finish(),
277//!         )
278//!         .cookie(
279//!             actix_web::cookie::Cookie::build(REFRESH_COOKIE_NAME, "")
280//!                 .expires(OffsetDateTime::now_utc())
281//!                 .finish(),
282//!         )
283//!         .finish()
284//! }
285//!
286//! #[get("/session/info")]
287//! async fn session_info(auth: Authenticated<AppClaims>) -> HttpResponse {
288//!     HttpResponse::Ok().json(&*auth)
289//! }
290//!
291//! #[get("/session/refresh")]
292//! async fn refresh_session(
293//!     refresh_token: Authenticated<RefreshToken>,
294//!     storage: Data<SessionStorage>,
295//! ) -> HttpResponse {
296//!     let s = storage.into_inner();
297//!     let pair = match s.refresh::<AppClaims>(refresh_token.access_jti()).await {
298//!         Err(e) => {
299//!             tracing::warn!("Failed to refresh token: {e}");
300//!             return HttpResponse::BadRequest().finish();
301//!         }
302//!         Ok(pair) => pair,
303//!     };
304//!
305//!     let encrypted_jwt = match pair.jwt.encode() {
306//!         Ok(text) => text,
307//!         Err(e) => {
308//!             tracing::warn!("Failed to encode claims: {e}");
309//!             return HttpResponse::InternalServerError().finish();
310//!         }
311//!     };
312//!     let encrypted_refresh = match pair.refresh.encode() {
313//!         Err(e) => {
314//!             tracing::warn!("Failed to encode claims: {e}");
315//!             return HttpResponse::InternalServerError().finish();
316//!         }
317//!         Ok(text) => text,
318//!     };
319//!     HttpResponse::Ok()
320//!         .append_header((
321//!             actix_jwt_session::JWT_HEADER_NAME,
322//!             format!("Bearer {encrypted_jwt}").as_str(),
323//!         ))
324//!         .append_header((
325//!             actix_jwt_session::REFRESH_HEADER_NAME,
326//!             format!("Bearer {}", encrypted_refresh).as_str(),
327//!         ))
328//!         .append_header((
329//!             "ACX-JWT-TTL",
330//!             (pair.refresh.issues_at + pair.refresh.refresh_ttl.0).to_string(),
331//!         ))
332//!         .finish()
333//! }
334//!
335//! #[get("/")]
336//! async fn root() -> HttpResponse {
337//!     HttpResponse::Ok().finish()
338//! }
339//!
340//! #[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
341//! #[serde(rename_all = "snake_case")]
342//! pub enum Audience {
343//!     Web,
344//! }
345//!
346//! #[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
347//! #[serde(rename_all = "snake_case")]
348//! pub struct AppClaims {
349//!     #[serde(rename = "exp")]
350//!     pub expiration_time: u64,
351//!     #[serde(rename = "iat")]
352//!     pub issues_at: usize,
353//!     /// Account login
354//!     #[serde(rename = "sub")]
355//!     pub subject: String,
356//!     #[serde(rename = "aud")]
357//!     pub audience: Audience,
358//!     #[serde(rename = "jti")]
359//!     pub jwt_id: uuid::Uuid,
360//!     #[serde(rename = "aci")]
361//!     pub account_id: i32,
362//!     #[serde(rename = "nbf")]
363//!     pub not_before: u64,
364//! }
365//!
366//! impl actix_jwt_session::Claims for AppClaims {
367//!     fn jti(&self) -> uuid::Uuid {
368//!         self.jwt_id
369//!     }
370//!
371//!     fn subject(&self) -> &str {
372//!         &self.subject
373//!     }
374//! }
375//!
376//! impl AppClaims {
377//!     pub fn account_id(&self) -> i32 {
378//!         self.account_id
379//!     }
380//! }
381//!
382//! struct AccountModel {
383//!     id: i32,
384//!     login: String,
385//!     pass_hash: String,
386//! }
387//! ```
388
389use std::borrow::Cow;
390use std::marker::PhantomData;
391use std::sync::Arc;
392
393pub use actix_web::cookie::time::{Duration, OffsetDateTime};
394use actix_web::dev::ServiceRequest;
395use actix_web::{FromRequest, HttpMessage, HttpResponse};
396use async_trait::async_trait;
397use derive_more::{Constructor, Deref};
398pub use jsonwebtoken::Algorithm;
399use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Validation};
400use serde::de::DeserializeOwned;
401use serde::{Deserialize, Serialize};
402pub use uuid::Uuid;
403
404/// This is maximum duration of json web token after which token will be invalid
405/// and depends on implementation removed.
406///
407/// This value should never be lower than 1 second since some implementations
408/// don't accept values lower than 1s.
409#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Deref, Constructor)]
410#[serde(transparent)]
411pub struct JwtTtl(pub Duration);
412
413/// This is maximum duration of refresh token after which token will be invalid
414/// and depends on implementation removed
415///
416/// This value should never be lower than 1 second since some implementations
417/// don't accept values lower than 1s.
418#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Deref, Constructor)]
419#[serde(transparent)]
420pub struct RefreshTtl(pub Duration);
421
422/// Default json web token header name
423///
424/// Examples:
425///
426/// ```
427/// use actix_web::{get, HttpResponse, cookie::Cookie};
428/// use actix_jwt_session::*;
429///
430/// async fn create_response<C: Claims>(pair: Pair<C>) -> HttpResponse {
431///     let jwt_text = pair.jwt.encode().unwrap();
432///     let refresh_text = pair.refresh.encode().unwrap();
433///     HttpResponse::Ok()
434///         .append_header((JWT_HEADER_NAME, jwt_text.as_str()))
435///         .append_header((REFRESH_HEADER_NAME, refresh_text.as_str()))
436///         .cookie(
437///             actix_web::cookie::Cookie::build(JWT_COOKIE_NAME, jwt_text.as_str())
438///                 .finish()
439///         )
440///         .cookie(
441///             actix_web::cookie::Cookie::build(REFRESH_COOKIE_NAME, refresh_text.as_str())
442///                 .finish()
443///         )
444///         .finish()
445/// }
446/// ```
447pub static JWT_HEADER_NAME: &str = "Authorization";
448
449/// Default refresh token header name
450///
451/// Examples:
452///
453/// ```
454/// use actix_web::{get, HttpResponse, cookie::Cookie};
455/// use actix_jwt_session::*;
456///
457/// async fn create_response<C: Claims>(pair: Pair<C>) -> HttpResponse {
458///     let jwt_text = pair.jwt.encode().unwrap();
459///     let refresh_text = pair.refresh.encode().unwrap();
460///     HttpResponse::Ok()
461///         .append_header((JWT_HEADER_NAME, jwt_text.as_str()))
462///         .append_header((REFRESH_HEADER_NAME, refresh_text.as_str()))
463///         .cookie(
464///             actix_web::cookie::Cookie::build(JWT_COOKIE_NAME, jwt_text.as_str())
465///                 .finish()
466///         )
467///         .cookie(
468///             actix_web::cookie::Cookie::build(REFRESH_COOKIE_NAME, refresh_text.as_str())
469///                 .finish()
470///         )
471///         .finish()
472/// }
473/// ```
474pub static REFRESH_HEADER_NAME: &str = "ACX-Refresh";
475
476/// Default json web token cookie name
477///
478/// Examples:
479///
480/// ```
481/// use actix_web::{get, HttpResponse, cookie::Cookie};
482/// use actix_jwt_session::*;
483///
484/// async fn create_response<C: Claims>(pair: Pair<C>) -> HttpResponse {
485///     let jwt_text = pair.jwt.encode().unwrap();
486///     let refresh_text = pair.refresh.encode().unwrap();
487///     HttpResponse::Ok()
488///         .append_header((JWT_HEADER_NAME, jwt_text.as_str()))
489///         .append_header((REFRESH_HEADER_NAME, refresh_text.as_str()))
490///         .cookie(
491///             actix_web::cookie::Cookie::build(JWT_COOKIE_NAME, jwt_text.as_str())
492///                 .finish()
493///         )
494///         .cookie(
495///             actix_web::cookie::Cookie::build(REFRESH_COOKIE_NAME, refresh_text.as_str())
496///                 .finish()
497///         )
498///         .finish()
499/// }
500/// ```
501pub static JWT_COOKIE_NAME: &str = "ACX-Auth";
502
503/// Default refresh token cookie name
504///
505/// Examples:
506///
507/// ```
508/// use actix_web::{get, HttpResponse, cookie::Cookie};
509/// use actix_jwt_session::*;
510///
511/// async fn create_response<C: Claims>(pair: Pair<C>) -> HttpResponse {
512///     let jwt_text = pair.jwt.encode().unwrap();
513///     let refresh_text = pair.refresh.encode().unwrap();
514///     HttpResponse::Ok()
515///         .append_header((JWT_HEADER_NAME, jwt_text.as_str()))
516///         .append_header((REFRESH_HEADER_NAME, refresh_text.as_str()))
517///         .cookie(
518///             actix_web::cookie::Cookie::build(JWT_COOKIE_NAME, jwt_text.as_str())
519///                 .finish()
520///         )
521///         .cookie(
522///             actix_web::cookie::Cookie::build(REFRESH_COOKIE_NAME, refresh_text.as_str())
523///                 .finish()
524///         )
525///         .finish()
526/// }
527/// ```
528pub static REFRESH_COOKIE_NAME: &str = "ACX-Refresh";
529
530/// Serializable and storable struct which represent JWT claims
531///
532/// * It must have JWT ID as [uuid::Uuid]
533/// * It must have subject as a String
534pub trait Claims:
535    PartialEq + DeserializeOwned + Serialize + Clone + Send + Sync + std::fmt::Debug + 'static
536{
537    /// Unique token identifier
538    fn jti(&self) -> uuid::Uuid;
539
540    /// Login, email or other identifier
541    fn subject(&self) -> &str;
542}
543
544/// Internal claims which allows to extend tokens pair livetime
545///
546/// After encoding it can be used as HTTP token send to endpoint, decoded and
547/// extend pair livetime. It's always created while calling
548/// [SessionStorage::store]. If there's any extractor for refresh you can use
549/// this structure as guard for an endpoint.
550///
551/// Example:
552///
553/// ```
554/// use actix_web::{get, HttpResponse};
555/// use actix_web::web::Data;
556/// use actix_jwt_session::*;
557///
558/// #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
559/// pub struct AppClaims { id: uuid::Uuid, sub: String }
560/// impl actix_jwt_session::Claims for AppClaims {
561///     fn jti(&self) -> uuid::Uuid { self.id }
562///     fn subject(&self) -> &str { &self.sub }
563/// }
564///
565/// #[get("/session/refresh")]
566/// async fn refresh_session(
567///     auth: Authenticated<RefreshToken>,
568///     storage: Data<SessionStorage>,
569/// ) -> HttpResponse {
570///     let storage = storage.into_inner();
571///     storage.refresh::<AppClaims>(auth.refresh_jti).await.unwrap();
572///     HttpResponse::Ok().json(&*auth)
573/// }
574/// ```
575#[derive(Debug, Clone, Serialize, Deserialize)]
576pub struct RefreshToken {
577    /// date and time when token was created
578    #[serde(rename = "iat")]
579    pub issues_at: OffsetDateTime,
580
581    /// related JWT unique identifier
582    #[serde(rename = "sub")]
583    access_jti: String,
584
585    /// JWT lifetime
586    pub access_ttl: JwtTtl,
587
588    /// this token unique identifier
589    pub refresh_jti: uuid::Uuid,
590
591    /// this token lifetime
592    pub refresh_ttl: RefreshTtl,
593
594    // REQUIRED
595    /// this token lifetime as integer
596    /// (this field is required by standard)
597    #[serde(rename = "exp")]
598    pub expiration_time: u64,
599
600    /// time before which token is not validate
601    /// (this field is required by standard and always set `0`)
602    #[serde(rename = "nbf")]
603    pub not_before: u64,
604
605    /// target audience
606    /// (this field is required by standard)
607    #[serde(rename = "aud")]
608
609    /// who created this token
610    /// (this field is required by standard)
611    pub audience: String,
612    #[serde(rename = "iss")]
613    pub issuer: String,
614}
615
616impl PartialEq for RefreshToken {
617    fn eq(&self, o: &Self) -> bool {
618        self.access_jti == o.access_jti
619            && self.refresh_jti == o.refresh_jti
620            && self.refresh_ttl == o.refresh_ttl
621            && self.expiration_time == o.expiration_time
622            && self.not_before == o.not_before
623            && self.audience == o.audience
624            && self.issuer == o.issuer
625    }
626}
627
628impl RefreshToken {
629    pub fn is_access_valid(&self) -> bool {
630        self.issues_at + self.access_ttl.0 >= OffsetDateTime::now_utc()
631    }
632
633    pub fn is_refresh_valid(&self) -> bool {
634        self.issues_at + self.refresh_ttl.0 >= OffsetDateTime::now_utc()
635    }
636
637    pub fn access_jti(&self) -> uuid::Uuid {
638        Uuid::parse_str(&self.access_jti).unwrap()
639    }
640}
641
642impl Claims for RefreshToken {
643    fn jti(&self) -> uuid::Uuid {
644        self.refresh_jti
645    }
646    fn subject(&self) -> &str {
647        "refresh-token"
648    }
649}
650
651/// JSON Web Token and internally created refresh token.
652///
653/// Both should be encoded using [Authenticated::encode] and added to response
654/// as cookie, header or in body.
655pub struct Pair<ClaimsType: Claims> {
656    /// Access token in form of JWT decrypted token
657    pub jwt: Authenticated<ClaimsType>,
658    /// Refresh token in form of JWT decrypted token
659    pub refresh: Authenticated<RefreshToken>,
660}
661
662/// Session related errors
663#[derive(Debug, thiserror::Error, PartialEq, Clone, Copy)]
664pub enum Error {
665    #[error("Failed to obtain redis connection")]
666    RedisConn,
667    #[error("Record not found")]
668    NotFound,
669    #[error("Record malformed")]
670    RecordMalformed,
671    #[error("Invalid session")]
672    InvalidSession,
673    #[error("Claims can't be loaded")]
674    LoadError,
675    #[error("Storage claims and given claims are different")]
676    DontMatch,
677    #[error("Given token in invalid. Can't decode claims")]
678    CantDecode,
679    #[error("No http authentication header")]
680    NoAuthHeader,
681    #[error("Failed to serialize claims")]
682    SerializeFailed,
683    #[error("Unable to write claims to storage")]
684    WriteFailed,
685    #[error("Access token expired")]
686    JWTExpired,
687}
688
689impl actix_web::ResponseError for Error {
690    fn status_code(&self) -> actix_web::http::StatusCode {
691        match self {
692            Self::RedisConn => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
693            _ => actix_web::http::StatusCode::UNAUTHORIZED,
694        }
695    }
696
697    fn error_response(&self) -> actix_web::HttpResponse<actix_web::body::BoxBody> {
698        HttpResponse::build(self.status_code()).body("")
699    }
700}
701
702/// Extractable user session which requires presence of JWT in request.
703/// If there's no JWT endpoint which requires this structure will automatically
704/// returns `401`.
705///
706/// Examples:
707///
708/// ```
709/// use actix_web::get;
710/// use actix_web::HttpResponse;
711/// use actix_jwt_session::Authenticated;
712///
713/// #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
714/// pub struct AppClaims { id: uuid::Uuid, sub: String }
715/// impl actix_jwt_session::Claims for AppClaims {
716///     fn jti(&self) -> uuid::Uuid { self.id }
717///     fn subject(&self) -> &str { &self.sub }
718/// }
719///
720/// // If there's no JWT in request server will automatically returns 401
721/// #[get("/session")]
722/// async fn read_session(session: Authenticated<AppClaims>) -> HttpResponse {
723///     let encoded = session.encode().unwrap(); // JWT as encrypted string
724///     HttpResponse::Ok().finish()
725/// }
726/// ```
727#[derive(Clone)]
728pub struct Authenticated<T> {
729    pub claims: Arc<T>,
730    pub jwt_encoding_key: Arc<EncodingKey>,
731    pub algorithm: Algorithm,
732}
733
734impl<T> std::ops::Deref for Authenticated<T> {
735    type Target = T;
736
737    fn deref(&self) -> &Self::Target {
738        &self.claims
739    }
740}
741
742impl<T: Claims> Authenticated<T> {
743    /// Encode claims as JWT encrypted string
744    pub fn encode(&self) -> Result<String, jsonwebtoken::errors::Error> {
745        encode(
746            &jsonwebtoken::Header::new(self.algorithm),
747            &*self.claims,
748            &self.jwt_encoding_key,
749        )
750    }
751}
752
753impl<T: Claims> FromRequest for Authenticated<T> {
754    type Error = actix_web::error::Error;
755    type Future = std::future::Ready<Result<Self, actix_web::Error>>;
756
757    fn from_request(
758        req: &actix_web::HttpRequest,
759        _payload: &mut actix_web::dev::Payload,
760    ) -> Self::Future {
761        let value = req
762            .extensions_mut()
763            .get::<Authenticated<T>>()
764            .map(Clone::clone);
765        std::future::ready(value.ok_or_else(|| Error::NotFound.into()))
766    }
767}
768
769/// Similar to [Authenticated] but JWT is optional
770///
771/// Examples:
772///
773/// ```
774/// use actix_web::get;
775/// use actix_web::HttpResponse;
776/// use actix_jwt_session::MaybeAuthenticated;
777///
778/// # #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
779/// # pub struct Claims { id: uuid::Uuid, sub: String }
780/// # impl actix_jwt_session::Claims for Claims {
781/// #     fn jti(&self) -> uuid::Uuid { self.id }
782/// #     fn subject(&self) -> &str { &self.sub }
783/// # }
784///
785/// // If there's no JWT in request server will NOT automatically returns 401
786/// #[get("/session")]
787/// async fn read_session(session: MaybeAuthenticated<Claims>) -> HttpResponse {
788///     if let Some(session) = session.into_option() {
789///         // handle authenticated request
790///     }
791///     HttpResponse::Ok().finish()
792/// }
793/// ```
794pub struct MaybeAuthenticated<ClaimsType: Claims>(Option<Authenticated<ClaimsType>>);
795
796impl<ClaimsType: Claims> MaybeAuthenticated<ClaimsType> {
797    pub fn is_authenticated(&self) -> bool {
798        self.0.is_some()
799    }
800
801    /// Transform extractor to simple [Option] with [Some] containing
802    /// [Authenticated] as value. This allow to handle signed in request and
803    /// encrypt claims if needed
804    pub fn into_option(self) -> Option<Authenticated<ClaimsType>> {
805        self.0
806    }
807}
808
809impl<ClaimsType: Claims> std::ops::Deref for MaybeAuthenticated<ClaimsType> {
810    type Target = Option<Authenticated<ClaimsType>>;
811
812    fn deref(&self) -> &Self::Target {
813        &self.0
814    }
815}
816
817impl<T: Claims> FromRequest for MaybeAuthenticated<T> {
818    type Error = actix_web::error::Error;
819    type Future = std::future::Ready<Result<Self, actix_web::Error>>;
820
821    fn from_request(
822        req: &actix_web::HttpRequest,
823        _payload: &mut actix_web::dev::Payload,
824    ) -> Self::Future {
825        let value = req
826            .extensions_mut()
827            .get::<Authenticated<T>>()
828            .map(Clone::clone);
829        std::future::ready(Ok(MaybeAuthenticated(value)))
830    }
831}
832
833/// Allows to customize where and how sessions are stored in persistant storage.
834/// By default redis can be used to store sesions but it's possible and easy to
835/// use memcached or postgresql.
836#[async_trait(?Send)]
837pub trait TokenStorage: Send + Sync {
838    /// Load claims from storage or returns [Error] if record does not exists or
839    /// there was other error while trying to fetch data from storage.
840    async fn get_by_jti(self: Arc<Self>, jti: &[u8]) -> Result<Vec<u8>, Error>;
841
842    /// Save claims in storage in a way claims can be loaded from database using
843    /// `jti` as [uuid::Uuid] (JWT ID)
844    async fn set_by_jti(
845        self: Arc<Self>,
846        jwt_jti: &[u8],
847        refresh_jti: &[u8],
848        bytes: &[u8],
849        exp: Duration,
850    ) -> Result<(), Error>;
851
852    /// Erase claims from storage. You may ignore if claims does not exists in
853    /// storage. Redis implementation returns [Error::NotFound] if record
854    /// does not exists.
855    async fn remove_by_jti(self: Arc<Self>, jti: &[u8]) -> Result<(), Error>;
856}
857
858/// Allow to save, read and remove session from storage.
859#[derive(Clone)]
860pub struct SessionStorage {
861    storage: Arc<dyn TokenStorage>,
862    jwt_encoding_key: Arc<EncodingKey>,
863    algorithm: Algorithm,
864}
865
866impl std::ops::Deref for SessionStorage {
867    type Target = Arc<dyn TokenStorage>;
868
869    fn deref(&self) -> &Self::Target {
870        &self.storage
871    }
872}
873
874#[doc(hidden)]
875/// This structure is saved to session storage (for example Redis)
876/// It's internal structure and should not be used unless you plan to create new
877/// session storage
878#[derive(Serialize, Deserialize, Clone, Debug)]
879pub struct SessionRecord {
880    refresh_jti: uuid::Uuid,
881    jwt_jti: uuid::Uuid,
882    refresh_token: String,
883    jwt: String,
884}
885
886impl SessionRecord {
887    /// Create new record from user claims and generated refresh token
888    ///
889    /// Both claims are serialized to text and saved as a string
890    fn new<ClaimsType: Claims>(claims: ClaimsType, refresh: RefreshToken) -> Result<Self, Error> {
891        let refresh_jti = claims.jti();
892        let jwt_jti = refresh.refresh_jti;
893        let refresh_token = serde_json::to_string(&refresh).map_err(|e| {
894            #[cfg(feature = "use-tracing")]
895            tracing::debug!("Failed to serialize Refresh Token to construct pair: {e:?}");
896            Error::SerializeFailed
897        })?;
898        let jwt = serde_json::to_string(&claims).map_err(|e| {
899            #[cfg(feature = "use-tracing")]
900            tracing::debug!("Failed to serialize JWT from to construct pair {e:?}");
901            Error::SerializeFailed
902        })?;
903        Ok(Self {
904            refresh_jti,
905            jwt_jti,
906            refresh_token,
907            jwt,
908        })
909    }
910
911    /// Deserialize loaded refresh token
912    fn refresh_token(&self) -> Result<RefreshToken, Error> {
913        serde_json::from_str(&self.refresh_token).map_err(|e| {
914            #[cfg(feature = "use-tracing")]
915            tracing::debug!("Failed to deserialize refresh token from pair: {e:?}");
916            Error::RecordMalformed
917        })
918    }
919
920    /// Deserialize field content to structure
921    fn from_field<CT: Claims>(s: &str) -> Result<CT, Error> {
922        serde_json::from_str(s).map_err(|e| {
923            #[cfg(feature = "use-tracing")]
924            tracing::debug!(
925                "Failed to deserialize {} for pair: {e:?}",
926                std::any::type_name::<CT>()
927            );
928            Error::RecordMalformed
929        })
930    }
931
932    /// Serialize refresh token in this record and replace field with generated
933    /// text
934    fn set_refresh_token(&mut self, mut refresh: RefreshToken) -> Result<(), Error> {
935        refresh.expiration_time = refresh.refresh_ttl.0.as_seconds_f64() as u64;
936        let refresh_token = serde_json::to_string(&refresh).map_err(|e| {
937            #[cfg(feature = "use-tracing")]
938            tracing::debug!("Failed to serialize refresh token for pair: {e:?}");
939            Error::SerializeFailed
940        })?;
941        self.refresh_token = refresh_token;
942        Ok(())
943    }
944}
945
946impl SessionStorage {
947    /// Abstraction layer over database holding tokens information
948    ///
949    /// It allows read/write/update/delete operation on tokens
950    pub fn new(
951        storage: Arc<dyn TokenStorage>,
952        jwt_encoding_key: Arc<EncodingKey>,
953        algorithm: Algorithm,
954    ) -> Self {
955        Self {
956            storage,
957            jwt_encoding_key,
958            algorithm,
959        }
960    }
961
962    /// Load claims from storage or returns [Error] if record does not exists or
963    /// there was other error while trying to fetch data from storage.
964    pub async fn find_jwt<ClaimsType: Claims>(&self, jti: uuid::Uuid) -> Result<ClaimsType, Error> {
965        let record = self.load_pair_by_jwt(jti).await?;
966        let refresh_token = record.refresh_token()?;
967        if std::any::type_name::<ClaimsType>() == std::any::type_name::<RefreshToken>() {
968            SessionRecord::from_field(&record.refresh_token)
969        } else {
970            if !refresh_token.is_access_valid() {
971                #[cfg(feature = "use-tracing")]
972                tracing::debug!("JWT expired");
973                return Err(Error::JWTExpired);
974            }
975            SessionRecord::from_field(&record.jwt)
976        }
977    }
978
979    /// Changes [RefreshToken::issues_at] allowing Claims and RefreshToken to be
980    /// accessible longer
981    ///
982    /// Examples:
983    ///
984    /// ```
985    /// use actix_jwt_session::SessionStorage;
986    /// use actix_web::{Error, HttpResponse};
987    ///
988    /// #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
989    /// pub struct AppClaims { id: uuid::Uuid, sub: String }
990    /// impl actix_jwt_session::Claims for AppClaims {
991    ///     fn jti(&self) -> uuid::Uuid { self.id }
992    ///     fn subject(&self) -> &str { &self.sub }
993    /// }
994    ///
995    /// async fn extend_tokens_lifetime(
996    ///     session_storage: SessionStorage,
997    ///     jti: uuid::Uuid
998    /// ) -> Result<HttpResponse, Error> {
999    ///     session_storage.refresh::<AppClaims>(jti).await?;
1000    ///     Ok(HttpResponse::Ok().finish())
1001    /// }
1002    /// ```
1003    pub async fn refresh<ClaimsType: Claims>(
1004        &self,
1005        refresh_jti: uuid::Uuid,
1006    ) -> Result<Pair<ClaimsType>, Error> {
1007        let mut record = self.load_pair_by_refresh(refresh_jti).await?;
1008        let mut refresh_token = record.refresh_token()?;
1009        let ttl = refresh_token.refresh_ttl;
1010        refresh_token.issues_at = OffsetDateTime::now_utc();
1011        record.set_refresh_token(refresh_token)?;
1012        self.store_pair(record.clone(), ttl).await?;
1013
1014        let claims = SessionRecord::from_field::<ClaimsType>(&record.jwt)?;
1015        let refresh = SessionRecord::from_field::<RefreshToken>(&record.refresh_token)?;
1016        Ok(Pair {
1017            jwt: Authenticated {
1018                claims: Arc::new(claims),
1019                jwt_encoding_key: self.jwt_encoding_key.clone(),
1020                algorithm: self.algorithm,
1021            },
1022            refresh: Authenticated {
1023                claims: Arc::new(refresh),
1024                jwt_encoding_key: self.jwt_encoding_key.clone(),
1025                algorithm: self.algorithm,
1026            },
1027        })
1028    }
1029
1030    /// Save claims in storage in a way claims can be loaded from database using
1031    /// `jti` as [uuid::Uuid] (JWT ID)
1032    pub async fn store<ClaimsType: Claims>(
1033        &self,
1034        claims: ClaimsType,
1035        access_ttl: JwtTtl,
1036        refresh_ttl: RefreshTtl,
1037    ) -> Result<Pair<ClaimsType>, Error> {
1038        let now = OffsetDateTime::now_utc();
1039        let refresh = RefreshToken {
1040            refresh_jti: uuid::Uuid::new_v4(),
1041            refresh_ttl,
1042            access_jti: claims.jti().hyphenated().to_string(),
1043            access_ttl,
1044            issues_at: now,
1045            expiration_time: refresh_ttl.0.as_seconds_f64() as u64,
1046            issuer: claims.jti().hyphenated().to_string(),
1047            not_before: 0,
1048            audience: claims.subject().to_string(),
1049        };
1050
1051        let record = SessionRecord::new(claims.clone(), refresh.clone())?;
1052        self.store_pair(record, refresh_ttl).await?;
1053
1054        Ok(Pair {
1055            jwt: Authenticated {
1056                claims: Arc::new(claims),
1057                jwt_encoding_key: self.jwt_encoding_key.clone(),
1058                algorithm: self.algorithm,
1059            },
1060            refresh: Authenticated {
1061                claims: Arc::new(refresh),
1062                jwt_encoding_key: self.jwt_encoding_key.clone(),
1063                algorithm: self.algorithm,
1064            },
1065        })
1066    }
1067
1068    /// Erase claims from storage. You may ignore if claims does not exists in
1069    /// storage. Redis implementation returns [Error::NotFound] if record
1070    /// does not exists.
1071    pub async fn erase<ClaimsType: Claims>(&self, jti: Uuid) -> Result<(), Error> {
1072        let record = self.load_pair_by_jwt(jti).await?;
1073
1074        self.storage
1075            .clone()
1076            .remove_by_jti(record.refresh_jti.as_bytes())
1077            .await?;
1078        self.storage
1079            .clone()
1080            .remove_by_jti(record.jwt_jti.as_bytes())
1081            .await?;
1082
1083        Ok(())
1084    }
1085
1086    /// Write to storage tokens pair as [SessionRecord]
1087    /// This operation allows to load pair using JWT ID and Refresh Token ID
1088    async fn store_pair(
1089        &self,
1090        record: SessionRecord,
1091        refresh_ttl: RefreshTtl,
1092    ) -> Result<(), Error> {
1093        let value = bincode::serialize(&record).map_err(|e| {
1094            #[cfg(feature = "use-tracing")]
1095            tracing::debug!("Serialize pair to bytes failed: {e:?}");
1096            Error::SerializeFailed
1097        })?;
1098
1099        self.storage
1100            .clone()
1101            .set_by_jti(
1102                record.jwt_jti.as_bytes(),
1103                record.refresh_jti.as_bytes(),
1104                &value,
1105                refresh_ttl.0,
1106            )
1107            .await?;
1108
1109        Ok(())
1110    }
1111
1112    /// Load [SessionRecord] as tokens pair from storage using JWT ID (jti)
1113    async fn load_pair_by_jwt(&self, jti: Uuid) -> Result<SessionRecord, Error> {
1114        self.storage
1115            .clone()
1116            .get_by_jti(jti.as_bytes())
1117            .await
1118            .and_then(|bytes| {
1119                bincode::deserialize(&bytes).map_err(|e| {
1120                    #[cfg(feature = "use-tracing")]
1121                    tracing::debug!("Deserialize pair while loading for JWT ID failed: {e:?}");
1122                    Error::RecordMalformed
1123                })
1124            })
1125    }
1126
1127    /// Load [SessionRecord] as tokens pair from storage using Refresh ID (jti)
1128    async fn load_pair_by_refresh(&self, jti: Uuid) -> Result<SessionRecord, Error> {
1129        self.storage
1130            .clone()
1131            .get_by_jti(jti.as_bytes())
1132            .await
1133            .and_then(|bytes| {
1134                bincode::deserialize(&bytes).map_err(|e| {
1135                    #[cfg(feature = "use-tracing")]
1136                    tracing::debug!("Deserialize pair while loading for refresh id failed: {e:?}");
1137                    Error::RecordMalformed
1138                })
1139            })
1140    }
1141}
1142
1143pub mod builder;
1144pub use builder::*;
1145
1146#[cfg(feature = "routes")]
1147pub mod actix_routes;
1148#[cfg(feature = "routes")]
1149pub use actix_routes::configure;
1150
1151mod extractors;
1152pub use extractors::*;
1153
1154/// Load or generate new Ed25519 signing keys.
1155///
1156/// [JwtSigningKeys::load_or_create] should be called only once at the boot of
1157/// the server.
1158///
1159/// If there's any issue during generating new keys or loading exiting one
1160/// application will panic.
1161///
1162/// Examples:
1163///
1164/// ```rust
1165/// use actix_jwt_session::*;
1166///
1167/// pub fn boot_server() {
1168///     let keys = JwtSigningKeys::load_or_create();
1169/// }
1170/// ```
1171pub struct JwtSigningKeys {
1172    pub encoding_key: EncodingKey,
1173    pub decoding_key: DecodingKey,
1174}
1175
1176impl JwtSigningKeys {
1177    /// Loads signing keys from `./config` directory or creates new pair and
1178    /// save it to directory.
1179    ///
1180    /// Pair is composed of encode key and decode key saved in
1181    /// `./config/jwt-encoding.bin` and `./config/jwt-decoding.bin`
1182    /// written as binary file.
1183    ///
1184    /// Decode key can be transform to base64 and shared with clients if this is
1185    /// required.
1186    ///
1187    /// Files must be shared between restarts otherwise all old sessions will be
1188    /// invalidated.
1189    pub fn load_or_create() -> Self {
1190        match Self::load_from_files() {
1191            Ok(s) => s,
1192            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
1193                Self::generate(true).expect("Generating new jwt signing keys must succeed")
1194            }
1195            Err(e) => panic!("Failed to load or generate jwt signing keys: {:?}", e),
1196        }
1197    }
1198
1199    pub fn generate(save: bool) -> Result<Self, Box<dyn std::error::Error>> {
1200        use jsonwebtoken::*;
1201        use ring::rand::SystemRandom;
1202        use ring::signature::{Ed25519KeyPair, KeyPair};
1203
1204        let doc = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new())?;
1205        let keypair = Ed25519KeyPair::from_pkcs8(doc.as_ref())?;
1206        let encoding_key = EncodingKey::from_ed_der(doc.as_ref());
1207        let decoding_key = DecodingKey::from_ed_der(keypair.public_key().as_ref());
1208
1209        if save {
1210            std::fs::write("./config/jwt-encoding.bin", doc.as_ref()).unwrap_or_else(|e| {
1211                panic!("Failed to write ./config/jwt-encoding.bin: {:?}", e);
1212            });
1213            std::fs::write("./config/jwt-decoding.bin", keypair.public_key()).unwrap_or_else(|e| {
1214                panic!("Failed to write ./config/jwt-decoding.bin: {:?}", e);
1215            });
1216        }
1217
1218        Ok(JwtSigningKeys {
1219            encoding_key,
1220            decoding_key,
1221        })
1222    }
1223
1224    pub fn load_from_files() -> std::io::Result<Self> {
1225        use std::io::Read;
1226
1227        use jsonwebtoken::*;
1228
1229        let mut buf = Vec::new();
1230        let mut e = std::fs::File::open("./config/jwt-encoding.bin")?;
1231        e.read_to_end(&mut buf).unwrap_or_else(|e| {
1232            panic!("Failed to read jwt encoding key: {:?}", e);
1233        });
1234        let encoding_key: EncodingKey = EncodingKey::from_ed_der(&buf);
1235
1236        let mut buf = Vec::new();
1237        let mut e = std::fs::File::open("./config/jwt-decoding.bin")?;
1238        e.read_to_end(&mut buf).unwrap_or_else(|e| {
1239            panic!("Failed to read jwt decoding key: {:?}", e);
1240        });
1241        let decoding_key = DecodingKey::from_ed_der(&buf);
1242        Ok(Self {
1243            encoding_key,
1244            decoding_key,
1245        })
1246    }
1247}
1248
1249#[macro_export]
1250macro_rules! bad_ttl {
1251    ($ttl: expr, $min: expr, $panic_msg: expr) => {
1252        if $ttl < $min {
1253            #[cfg(feature = "use-tracing")]
1254            tracing::warn!(
1255                "Expiration time is bellow 1s. This is not allowed for redis server. Overriding!"
1256            );
1257            if cfg!(feature = "panic-bad-ttl") {
1258                panic!($panic_msg);
1259            } else if cfg!(feature = "override-bad-ttl") {
1260                $ttl = $min;
1261            }
1262        }
1263    };
1264}
1265
1266mod middleware;
1267pub use middleware::*;
1268
1269#[cfg(feature = "redis")]
1270mod redis_adapter;
1271#[allow(unused_imports)]
1272#[cfg(feature = "redis")]
1273pub use redis_adapter::*;
1274#[cfg(feature = "hashing")]
1275mod hashing;
1276#[cfg(feature = "hashing")]
1277pub use hashing::*;