jwt_actix/
lib.rs

1/* auth.rs
2 *
3 * Developed by Tim Walls <tim.walls@snowgoons.com>
4 * Copyright (c) All Rights Reserved, Tim Walls
5 */
6/**
7 * Documentation comment for this file.
8 */
9// Imports ===================================================================
10use std::pin::Pin;
11use std::task::{Context, Poll};
12
13use actix_web::{Error, ResponseError};
14pub use actix_web::dev::{ServiceRequest,ServiceResponse};
15use std::future::{Future, Ready, ready};
16use actix_web::dev::{Transform, Service};
17use actix_web::body::MessageBody;
18use jwks_client::keyset::KeyStore;
19use std::env;
20use thiserror::Error;
21use std::env::VarError;
22pub use jwks_client::jwt::{Jwt, Payload, Header};
23use std::rc::Rc;
24
25use actix_web::http::StatusCode;
26
27
28// Declarations ==============================================================
29/**
30 * Type definition for functions that will, given a request and a JWT, return
31 * `true` if the request should be allowed to continue for processing, or `false`
32 * otherwise.
33 */
34type JwtValidator = fn(&ServiceRequest,&Option<Jwt>)->bool;
35
36/**
37 * A simple validator function that simply returns true if the request had
38 * a valid (that is, it exists, and the signature was checked) JWT.  It does
39 * not check any claims or any other details within the token.
40 */
41#[allow(non_snake_case)]
42pub fn CheckJwtValid(req: &ServiceRequest, jwt: &Option<Jwt>) -> bool {
43  log::debug!("Default JWT validator called {:?} / {:?}", req, jwt);
44
45  match jwt {
46    None => {
47      false
48    },
49    Some(_) => {
50      true
51    }
52  }
53}
54
55/**
56 * JWT validating middleware for Actix-Web.
57 */
58pub struct JwtAuth {
59  jwks_url: String,
60  validator: Rc<JwtValidator>
61}
62
63pub struct JwtAuthService<S> {
64  service: S,
65  jwks: KeyStore,
66  validator: Rc<JwtValidator>
67}
68
69#[derive(Error,Debug)]
70pub enum JwtAuthError {
71  #[error("No JWKS keystore address specified")]
72  NoKeystoreSpecified,
73
74  #[error("Failed to load JWKS keystore from {0:?}")]
75  FailedToLoadKeystore(jwks_client::error::Error),
76
77  #[error("Bearer authentication token invalid: {0:?}")]
78  InvalidBearerAuth(jwks_client::error::Error),
79
80  #[error("Access to this resource is not authorised")]
81  Unauthorised
82}
83
84// Code ======================================================================
85impl JwtAuth
86{
87  /**
88   * Create a new instance of JwtAuth.  The URL for the keystore must be
89   * provided in the environment variable `JWKS_URL` at runtime.
90   *
91   * A validator function of type `JwtValidator` must be provided.  For every
92   * request, this will be called with the request and token information, and
93   * the function will determine whether the request should be processed
94   * (`true`) or not (`false`).
95   */
96  pub fn new_from_env(validator: JwtValidator) -> Result<Self,JwtAuthError> {
97    let jwks_url = env::var("JWKS_URL")?;
98
99    JwtAuth::new_from_url(validator, jwks_url)
100  }
101
102  /**
103   * Create a new instance of JwtAuth.  The keystore for validating token
104   * signatures will be downloaded from the given `jwks_url`.
105   *
106   * A validator function of type `JwtValidator` must be provided.  For every
107   * request, this will be called with the request and token information, and
108   * the function will determine whether the request should be processed
109   * (`true`) or not (`false`).
110   */
111  pub fn new_from_url(validator: JwtValidator, jwks_url: String) -> Result<Self,JwtAuthError> {
112
113    // Even though we don't use it now, I want to fail-fast, so I check now
114    // if I can download the keystore
115    let _jwks = KeyStore::new_from(&jwks_url)?;
116
117    Ok(JwtAuth {
118      jwks_url,
119      validator: Rc::new(validator)
120    })
121  }
122}
123
124impl <S,B> Transform<S, ServiceRequest> for JwtAuth
125where
126  S: Service<ServiceRequest, Response = ServiceResponse<B>, Error=Error>,
127  B: MessageBody,
128  B: 'static,
129  S::Future: 'static
130{
131  type Response = S::Response;
132  type Error = S::Error;
133  type Transform = JwtAuthService<S>;
134  type InitError = ();
135  type Future = Ready<Result<Self::Transform, Self::InitError>>;
136
137  fn new_transform(&self, service: S) -> Self::Future {
138    let jwks_url = self.jwks_url.clone();
139
140    ready(match KeyStore::new_from(&jwks_url) {
141      Ok(jwks) => {
142        Ok(JwtAuthService {
143          service,
144          jwks,
145          validator: self.validator.clone()
146        })
147      }
148      Err(e) => {
149        log::error!("Cannot load JWKS keystore from {}: {:?}", jwks_url, e);
150        Err(())
151      }
152    })
153
154
155  }
156}
157
158impl <S, B> Service<ServiceRequest> for JwtAuthService<S>
159where
160  S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
161  S::Future: 'static,
162  B: MessageBody,
163  B: 'static
164{
165  type Response = S::Response;
166  type Error = S::Error;
167  type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
168
169  fn poll_ready(&self, ctx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
170    self.service.poll_ready(ctx)
171  }
172
173  fn call(&self, req: ServiceRequest) -> Self::Future {
174    let authorization = req.headers().get(actix_web::http::header::AUTHORIZATION);
175
176    let jwt = {
177      match authorization {
178        Some(value) => {
179
180          let value_str = value.to_str().unwrap().to_string();
181
182          match value_str.strip_prefix("Bearer ") {
183            Some(token) => {
184              match self.jwks.verify(&token) {
185                Ok(jwt) => {
186                  Some(jwt)
187                }
188                Err(e) => {
189                  return Box::pin(ready(Err(JwtAuthError::InvalidBearerAuth(e).into())))
190                }
191              }
192            }
193            _ => {
194              None
195            }
196          }
197        },
198        None => {
199          None
200        }
201      }
202    };
203
204    // OK, if we got this far, we have a possibly validated JWT (or None in
205    // its stead, if it wasn't present or didn't validate)
206    if (self.validator)(&req, &jwt) {
207      let fut = self.service.call(req);
208      Box::pin(async move {
209        let res = fut.await?;
210
211        Ok(res)
212      })
213    } else {
214      Box::pin(ready(Err(JwtAuthError::Unauthorised.into())))
215    }
216  }
217}
218
219
220impl From<jwks_client::error::Error> for JwtAuthError {
221  fn from(e: jwks_client::error::Error) -> Self {
222    JwtAuthError::FailedToLoadKeystore(e)
223  }
224}
225
226impl From<VarError> for JwtAuthError {
227  fn from(_: VarError) -> Self {
228    JwtAuthError::NoKeystoreSpecified
229  }
230}
231
232impl ResponseError for JwtAuthError {
233  fn status_code(&self) -> StatusCode {
234    match self {
235      JwtAuthError::NoKeystoreSpecified => StatusCode::INTERNAL_SERVER_ERROR,
236      JwtAuthError::FailedToLoadKeystore(_) => StatusCode::INTERNAL_SERVER_ERROR,
237      JwtAuthError::InvalidBearerAuth(_) => StatusCode::UNAUTHORIZED,
238      JwtAuthError::Unauthorised => StatusCode::UNAUTHORIZED
239    }
240  }
241}
242
243// Tests =====================================================================
244#[cfg(test)]
245mod tests {
246  use super::*;
247
248  const TEST_KEYSET: &str = "https://snowgoons.eu.auth0.com/.well-known/jwks.json";
249
250  #[actix_rt::test]
251  async fn test_jwks_url() {
252    let _middleware = JwtAuth::new_from_url(CheckJwtValid, String::from(TEST_KEYSET)).unwrap();
253  }
254
255  #[actix_rt::test]
256  #[should_panic]
257  async fn test_jwks_url_fail() {
258    let _middleware = JwtAuth::new_from_url(CheckJwtValid, String::from("https://not.here/")).unwrap();
259  }
260
261  #[actix_rt::test]
262  async fn test_jwks_env() {
263    env::set_var("JWKS_URL", String::from(TEST_KEYSET));
264
265    let _middleware = JwtAuth::new_from_env(CheckJwtValid).unwrap();
266  }
267
268  #[actix_rt::test]
269  #[should_panic]
270  async fn test_jwks_env_fail() {
271    env::remove_var("JWKS_URL");
272
273    let _middleware = JwtAuth::new_from_env(CheckJwtValid).unwrap();
274  }
275}