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::*;