actix_jwt_authc/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2//! This crate provides an Actix Web middleware that supports authentication of requests based
3//! on JWTs, with support for JWT invalidation without incurring a per-request performance hit of
4//! making IO calls to an external datastore.
5//!
6//! # Example
7//!
8//! The example below demonstrates `Bearer` authentication. For a more expansive example showing
9//! sessions-based authenticated sessions, refer to examples/inmemory.rs.
10//!
11//! ```
12//! use std::collections::HashSet;
13//! use std::ops::Add;
14//! use std::sync::Arc;
15//! use std::time::Duration;
16//!
17//! use actix_jwt_authc::*;
18//! use actix_http::StatusCode;
19//! use actix_web::web::Data;
20//! use actix_web::dev::{Service, ServiceResponse};
21//! use actix_web::{get, test, App, HttpResponse};
22//! use dashmap::DashSet;
23//! use futures::channel::{mpsc, mpsc::{channel, Sender}};
24//! use futures::SinkExt;
25//! use futures::stream::Stream;
26//! use jsonwebtoken::*;
27//! use ring::rand::SystemRandom;
28//! use ring::signature::{Ed25519KeyPair, KeyPair};
29//! use serde::{Deserialize, Serialize};
30//! use time::ext::*;
31//! use time::OffsetDateTime;
32//! use uuid::Uuid;
33//! use tokio::sync::Mutex;
34//! # #[cfg(feature = "tracing")]
35//! # use tracing::error;
36//!
37//! const JWT_SIGNING_ALGO: Algorithm = Algorithm::EdDSA;
38//!
39//! #[actix_web::main]
40//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
41//!   let jwt_ttl = JWTTtl(1.hours());
42//!   let jwt_signing_keys = JwtSigningKeys::generate()?;
43//!   let validator = Validation::new(JWT_SIGNING_ALGO);
44//!
45//!   let auth_middleware_settings = AuthenticateMiddlewareSettings {
46//!       # #[cfg(feature = "session")]
47//!       # jwt_session_key: Some(JWTSessionKey("jwt-session".to_string())),
48//!       jwt_decoding_key: jwt_signing_keys.decoding_key,
49//!       jwt_authorization_header_prefixes: Some(vec!["Bearer".to_string()]),
50//!       jwt_validator: validator,
51//!   };
52//!
53//!   let (invalidated_jwts_store, stream) = InvalidatedJWTStore::new_with_stream();
54//!   let auth_middleware_factory = AuthenticateMiddlewareFactory::<Claims>::new(
55//!     stream,
56//!     auth_middleware_settings.clone(),
57//!   );
58//!
59//!   /// To instantiate a real running app, consult Actix docs
60//!   let app = {
61//!      test::init_service(
62//!          App::new()
63//!              .app_data(Data::new(invalidated_jwts_store.clone()))
64//!              .app_data(Data::new(jwt_signing_keys.encoding_key.clone()))
65//!              .app_data(Data::new(jwt_ttl.clone()))
66//!              .wrap(auth_middleware_factory.clone())
67//!              .service(login)
68//!              .service(logout)
69//!              .service(session_info)
70//!       )
71//!     }.await;
72//!
73//!   let unauthenticated_session_req = test::TestRequest::get().uri("/session").to_request();
74//!   let unauthenticated_resp = test::call_service(&app, unauthenticated_session_req).await;
75//!   assert_eq!(StatusCode::UNAUTHORIZED, unauthenticated_resp.status());
76//!
77//!   let login_resp = {
78//!     let req = test::TestRequest::get().uri("/login").to_request();
79//!     test::call_service(&app, req).await
80//!   };
81//!   let login_response: LoginResponse = test::read_body_json(login_resp).await;
82//!   let (login_response, session_req) = {
83//!     let req = test::TestRequest::get().uri("/session").insert_header((
84//!       "Authorization",
85//!       format!("Bearer {}", login_response.bearer_token),
86//!     ));
87//!    (login_response, req)
88//!   };
89//!   let session_resp = test::call_service(&app, session_req.to_request()).await;
90//!   assert_eq!(StatusCode::OK, session_resp.status());
91//!   let session_response: Authenticated<Claims> = test::read_body_json(session_resp).await;
92//!   assert_eq!(login_response.claims, session_response.claims);
93//!
94//!   let logout_req = test::TestRequest::get().uri("/logout").insert_header((
95//!     "Authorization",
96//!     format!("Bearer {}", login_response.bearer_token),
97//!   ));
98//!   let logout_resp = test::call_service(&app, logout_req.to_request()).await;
99//!   assert_eq!(StatusCode::OK, logout_resp.status());
100//!   assert!(invalidated_jwts_store.store.contains(&JWT(login_response.bearer_token.clone())));
101//!
102//!   // Wait until middleware reloads invalidated JWTs from central store
103//!   tokio::time::sleep(Duration::from_millis(100)).await;
104//!
105//!   let session_resp_after_logout = {
106//!     let req = test::TestRequest::get().uri("/session").insert_header((
107//!       "Authorization",
108//!       format!("Bearer {}", login_response.bearer_token),
109//!     ));
110//!     let resp: actix_web::Error = app.call(req.to_request()).await.err().unwrap();
111//!     ServiceResponse::new(
112//!       test::TestRequest::get().uri("/session").to_http_request(),
113//!       resp.error_response(),
114//!     )
115//!   };
116//!   assert_eq!(StatusCode::UNAUTHORIZED, session_resp_after_logout.status());
117//!   Ok(())
118//! }
119//!
120//! #[get("/login")]
121//! async fn login(
122//!     jwt_encoding_key: Data<EncodingKey>,
123//!     jwt_ttl: Data<JWTTtl>
124//! ) -> Result<HttpResponse, Error> {
125//!     let sub = format!("{}", Uuid::new_v4().as_u128());
126//!     let iat = OffsetDateTime::now_utc().unix_timestamp() as usize;
127//!     let expires_at = OffsetDateTime::now_utc().add(jwt_ttl.0);
128//!     let exp = expires_at.unix_timestamp() as usize;
129//!
130//!     let jwt_claims = Claims { iat, exp, sub };
131//!     let jwt_token = encode(
132//!         &Header::new(JWT_SIGNING_ALGO),
133//!         &jwt_claims,
134//!         &jwt_encoding_key,
135//!     )
136//!     .map_err(|_| Error::InternalError)?;
137//!     let login_response = LoginResponse {
138//!         bearer_token: jwt_token,
139//!         claims: jwt_claims,
140//!     };
141//!
142//!     Ok(HttpResponse::Ok().json(login_response))
143//! }
144//!
145//! #[get("/session")]
146//! async fn session_info(authenticated: Authenticated<Claims>) -> Result<HttpResponse, Error> {
147//!     Ok(HttpResponse::Ok().json(authenticated))
148//! }
149//!
150//! #[get("/logout")]
151//! async fn logout(
152//!     invalidated_jwts: Data<InvalidatedJWTStore>,
153//!     authenticated: Authenticated<Claims>
154//! ) -> Result<HttpResponse, Error> {
155//!     invalidated_jwts.add_to_invalidated(authenticated).await;
156//!     Ok(HttpResponse::Ok().json(EmptyResponse {}))
157//! }
158//!
159//! #[derive(Clone)]
160//! struct InvalidatedJWTStore {
161//!     store: Arc<DashSet<JWT>>,
162//!     tx: Arc<Mutex<Sender<InvalidatedTokensEvent>>>,
163//! }
164//!
165//! impl InvalidatedJWTStore {
166//!
167//!     /// Returns a [InvalidatedJWTStore] with a Stream of [InvalidatedTokensEvent]s
168//!     fn new_with_stream() -> (InvalidatedJWTStore, impl Stream<Item = InvalidatedTokensEvent>) {
169//!         let invalidated = Arc::new(DashSet::new());
170//!         let (tx, rx) = mpsc::channel(100);
171//!         let tx_to_hold = Arc::new(Mutex::new(tx));
172//!         (
173//!             InvalidatedJWTStore {
174//!                 store: invalidated,
175//!                 tx: tx_to_hold,
176//!             },
177//!             rx,
178//!         )
179//!     }
180//!
181//!     async fn add_to_invalidated(&self, authenticated: Authenticated<Claims>) {
182//!         self.store.insert(authenticated.jwt.clone());
183//!         let mut tx = self.tx.lock().await;
184//!         if let Err(_e) = tx
185//!             .send(InvalidatedTokensEvent::Add(authenticated.jwt))
186//!             .await
187//!         {
188//!             #[cfg(feature = "tracing")]
189//!             error!(error = ?_e, "Failed to send update on adding to invalidated")
190//!         }
191//!     }
192//! }
193//!
194//! struct JwtSigningKeys {
195//!   encoding_key: EncodingKey,
196//!   decoding_key: DecodingKey,
197//! }
198//!
199//! impl JwtSigningKeys {
200//!     fn generate() -> Result<Self, Box<dyn std::error::Error>> {
201//!         let doc = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new())?;
202//!         let keypair = Ed25519KeyPair::from_pkcs8(doc.as_ref())?;
203//!         let encoding_key = EncodingKey::from_ed_der(doc.as_ref());
204//!         let decoding_key = DecodingKey::from_ed_der(keypair.public_key().as_ref());
205//!         Ok(JwtSigningKeys {
206//!             encoding_key,
207//!             decoding_key,
208//!         })
209//!     }
210//! }
211//!
212//! #[derive(Clone, Copy)]
213//! struct JWTTtl(time::Duration);
214//!
215//! #[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
216//! struct Claims {
217//!     exp: usize,
218//!     iat: usize,
219//!     sub: String,
220//! }
221//!
222//! #[derive(Serialize, Deserialize)]
223//! struct EmptyResponse {}
224//!
225//! #[derive(Debug, Serialize, Deserialize)]
226//! struct LoginResponse {
227//!     bearer_token: String,
228//!     claims: Claims,
229//! }
230//! ```
231
232pub use authentication::*;
233pub use errors::*;
234
235mod authentication;
236mod errors;