use std::pin::Pin;
use std::task::{Context, Poll};
use actix_web::{Error, ResponseError};
pub use actix_web::dev::{ServiceRequest,ServiceResponse};
use std::future::{Future, Ready, ready};
use actix_web::dev::{Transform, Service};
use actix_web::body::MessageBody;
use jwks_client::keyset::KeyStore;
use std::env;
use thiserror::Error;
use std::env::VarError;
pub use jwks_client::jwt::{Jwt, Payload, Header};
use std::rc::Rc;
use actix_web::http::StatusCode;
type JwtValidator = fn(&ServiceRequest,&Option<Jwt>)->bool;
#[allow(non_snake_case)]
pub fn CheckJwtValid(req: &ServiceRequest, jwt: &Option<Jwt>) -> bool {
log::debug!("Default JWT validator called {:?} / {:?}", req, jwt);
match jwt {
None => {
false
},
Some(_) => {
true
}
}
}
pub struct JwtAuth {
jwks_url: String,
validator: Rc<JwtValidator>
}
pub struct JwtAuthService<S> {
service: S,
jwks: KeyStore,
validator: Rc<JwtValidator>
}
#[derive(Error,Debug)]
pub enum JwtAuthError {
#[error("No JWKS keystore address specified")]
NoKeystoreSpecified,
#[error("Failed to load JWKS keystore from {0:?}")]
FailedToLoadKeystore(jwks_client::error::Error),
#[error("Bearer authentication token invalid: {0:?}")]
InvalidBearerAuth(jwks_client::error::Error),
#[error("Access to this resource is not authorised")]
Unauthorised
}
impl JwtAuth
{
pub fn new_from_env(validator: JwtValidator) -> Result<Self,JwtAuthError> {
let jwks_url = env::var("JWKS_URL")?;
JwtAuth::new_from_url(validator, jwks_url)
}
pub fn new_from_url(validator: JwtValidator, jwks_url: String) -> Result<Self,JwtAuthError> {
let _jwks = KeyStore::new_from(&jwks_url)?;
Ok(JwtAuth {
jwks_url,
validator: Rc::new(validator)
})
}
}
impl <S,B> Transform<S, ServiceRequest> for JwtAuth
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error=Error>,
B: MessageBody,
B: 'static,
S::Future: 'static
{
type Response = S::Response;
type Error = S::Error;
type Transform = JwtAuthService<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
let jwks_url = self.jwks_url.clone();
ready(match KeyStore::new_from(&jwks_url) {
Ok(jwks) => {
Ok(JwtAuthService {
service,
jwks,
validator: self.validator.clone()
})
}
Err(e) => {
log::error!("Cannot load JWKS keystore from {}: {:?}", jwks_url, e);
Err(())
}
})
}
}
impl <S, B> Service<ServiceRequest> for JwtAuthService<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: MessageBody,
B: 'static
{
type Response = S::Response;
type Error = S::Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
fn poll_ready(&self, ctx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(ctx)
}
fn call(&self, req: ServiceRequest) -> Self::Future {
let authorization = req.headers().get(actix_web::http::header::AUTHORIZATION);
let jwt = {
match authorization {
Some(value) => {
let value_str = value.to_str().unwrap().to_string();
match value_str.strip_prefix("Bearer ") {
Some(token) => {
match self.jwks.verify(&token) {
Ok(jwt) => {
Some(jwt)
}
Err(e) => {
return Box::pin(ready(Err(JwtAuthError::InvalidBearerAuth(e).into())))
}
}
}
_ => {
None
}
}
},
None => {
None
}
}
};
if (self.validator)(&req, &jwt) {
let fut = self.service.call(req);
Box::pin(async move {
let res = fut.await?;
Ok(res)
})
} else {
Box::pin(ready(Err(JwtAuthError::Unauthorised.into())))
}
}
}
impl From<jwks_client::error::Error> for JwtAuthError {
fn from(e: jwks_client::error::Error) -> Self {
JwtAuthError::FailedToLoadKeystore(e)
}
}
impl From<VarError> for JwtAuthError {
fn from(_: VarError) -> Self {
JwtAuthError::NoKeystoreSpecified
}
}
impl ResponseError for JwtAuthError {
fn status_code(&self) -> StatusCode {
match self {
JwtAuthError::NoKeystoreSpecified => StatusCode::INTERNAL_SERVER_ERROR,
JwtAuthError::FailedToLoadKeystore(_) => StatusCode::INTERNAL_SERVER_ERROR,
JwtAuthError::InvalidBearerAuth(_) => StatusCode::UNAUTHORIZED,
JwtAuthError::Unauthorised => StatusCode::UNAUTHORIZED
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_KEYSET: &str = "https://snowgoons.eu.auth0.com/.well-known/jwks.json";
#[actix_rt::test]
async fn test_jwks_url() {
let _middleware = JwtAuth::new_from_url(CheckJwtValid, String::from(TEST_KEYSET)).unwrap();
}
#[actix_rt::test]
#[should_panic]
async fn test_jwks_url_fail() {
let _middleware = JwtAuth::new_from_url(CheckJwtValid, String::from("https://not.here/")).unwrap();
}
#[actix_rt::test]
async fn test_jwks_env() {
env::set_var("JWKS_URL", String::from(TEST_KEYSET));
let _middleware = JwtAuth::new_from_env(CheckJwtValid).unwrap();
}
#[actix_rt::test]
#[should_panic]
async fn test_jwks_env_fail() {
env::remove_var("JWKS_URL");
let _middleware = JwtAuth::new_from_env(CheckJwtValid).unwrap();
}
}