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;