#![cfg_attr(test, allow(clippy::unwrap_used))]
#![doc(html_favicon_url = "https://salvo.rs/favicon-32x32.png")]
#![doc(html_logo_url = "https://salvo.rs/images/logo.svg")]
#![cfg_attr(docsrs, feature(doc_cfg))]
#[cfg(not(any(feature = "aws-lc-rs", feature = "ring")))]
compile_error!(
"salvo-jwt-auth requires a crypto provider; enable either the `aws-lc-rs` (default) or `ring` feature"
);
use std::fmt::{self, Debug, Formatter};
use std::marker::PhantomData;
#[doc(no_inline)]
pub use jsonwebtoken::{
Algorithm, DecodingKey, TokenData, Validation, decode, errors::Error as JwtError,
};
#[cfg(feature = "oidc")]
use salvo_core::http::StatusCode;
use salvo_core::http::{Method, Request, Response, StatusError};
use salvo_core::{Depot, FlowCtrl, Handler, async_trait};
use serde::de::DeserializeOwned;
use thiserror::Error;
pub fn install_crypto_provider() -> Result<(), &'static jsonwebtoken::crypto::CryptoProvider> {
#[cfg(feature = "aws-lc-rs")]
{
jsonwebtoken::crypto::aws_lc::DEFAULT_PROVIDER.install_default()
}
#[cfg(all(not(feature = "aws-lc-rs"), feature = "ring"))]
{
jsonwebtoken::crypto::rust_crypto::DEFAULT_PROVIDER.install_default()
}
#[cfg(not(any(feature = "aws-lc-rs", feature = "ring")))]
unreachable!("a crypto provider feature is required")
}
mod finder;
pub use finder::{CookieFinder, FormFinder, HeaderFinder, JwtTokenFinder, QueryFinder};
mod decoder;
pub use decoder::{ConstDecoder, JwtAuthDecoder};
use salvo_core::cfg_feature;
cfg_feature! {
#![feature = "oidc"]
pub mod oidc;
pub use oidc::OidcDecoder;
}
pub const JWT_AUTH_DATA_KEY: &str = "::salvo::jwt_auth::auth_data";
pub const JWT_AUTH_STATE_KEY: &str = "::salvo::jwt_auth::auth_state";
pub const JWT_AUTH_TOKEN_KEY: &str = "::salvo::jwt_auth::auth_token";
pub const JWT_AUTH_ERROR_KEY: &str = "::salvo::jwt_auth::auth_error";
const STANDARD_METHODS: [Method; 10] = [
Method::GET,
Method::POST,
Method::PUT,
Method::DELETE,
Method::HEAD,
Method::OPTIONS,
Method::CONNECT,
Method::PATCH,
Method::TRACE,
Method::QUERY,
];
#[derive(Debug, Error)]
pub enum JwtAuthError {
#[cfg(feature = "oidc")]
#[cfg_attr(docsrs, doc(cfg(feature = "oidc")))]
#[error("ClientError")]
ClientError(#[from] hyper_util::client::legacy::Error),
#[cfg(feature = "oidc")]
#[cfg_attr(docsrs, doc(cfg(feature = "oidc")))]
#[error("HyperError")]
Hyper(#[from] salvo_core::hyper::Error),
#[error("InvalidUri")]
InvalidUri(#[from] salvo_core::http::uri::InvalidUri),
#[error("Serde error")]
SerdeError(#[from] serde_json::Error),
#[error("Failed to discover OIDC configuration")]
DiscoverError,
#[cfg(feature = "oidc")]
#[cfg_attr(docsrs, doc(cfg(feature = "oidc")))]
#[error("Failed to load native root certificates for OIDC HTTP client: {0}")]
NativeRootCerts(String),
#[cfg(feature = "oidc")]
#[cfg_attr(docsrs, doc(cfg(feature = "oidc")))]
#[error("OIDC endpoint returned non-success status: {0}")]
OidcHttpStatus(StatusCode),
#[cfg(feature = "oidc")]
#[cfg_attr(docsrs, doc(cfg(feature = "oidc")))]
#[error("OIDC endpoint request timed out")]
OidcHttpTimeout,
#[cfg(feature = "oidc")]
#[cfg_attr(docsrs, doc(cfg(feature = "oidc")))]
#[error("OIDC response body read failed: {0}")]
OidcBodyRead(#[source] Box<dyn std::error::Error + Send + Sync>),
#[cfg(feature = "oidc")]
#[cfg_attr(docsrs, doc(cfg(feature = "oidc")))]
#[error("OIDC response exceeded {0} bytes")]
OidcResponseTooLarge(usize),
#[cfg(feature = "oidc")]
#[cfg_attr(docsrs, doc(cfg(feature = "oidc")))]
#[error("OIDC audience must be configured")]
MissingOidcAudience,
#[error("Decoding of JWKS error")]
DecodeError(#[from] base64::DecodeError),
#[error("JWK has missing or unsupported verification parameters")]
InvalidJwk,
#[error("Issuer URL invalid")]
IssuerParseError,
#[error("JWT Is Invalid")]
ValidationFailed(#[from] jsonwebtoken::errors::Error),
#[error("Token was unable to be validated due to cache expiration")]
CacheError,
#[error("Token did not contain a KID field")]
MissingKid,
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum JwtAuthState {
Authorized,
Unauthorized,
Forbidden,
}
pub trait JwtAuthDepotExt {
fn jwt_auth_token(&self) -> Option<&str>;
fn jwt_auth_data<C>(&self) -> Option<&TokenData<C>>
where
C: DeserializeOwned + Send + Sync + 'static;
fn jwt_auth_state(&self) -> JwtAuthState;
fn jwt_auth_error(&self) -> Option<&JwtError>;
}
impl JwtAuthDepotExt for Depot {
#[inline]
fn jwt_auth_token(&self) -> Option<&str> {
self.get::<String>(JWT_AUTH_TOKEN_KEY).map(|v| &**v).ok()
}
#[inline]
fn jwt_auth_data<C>(&self) -> Option<&TokenData<C>>
where
C: DeserializeOwned + Send + Sync + 'static,
{
self.get(JWT_AUTH_DATA_KEY).ok()
}
#[inline]
fn jwt_auth_state(&self) -> JwtAuthState {
self.get(JWT_AUTH_STATE_KEY)
.ok()
.cloned()
.unwrap_or(JwtAuthState::Unauthorized)
}
#[inline]
fn jwt_auth_error(&self) -> Option<&JwtError> {
self.get(JWT_AUTH_ERROR_KEY).ok()
}
}
#[non_exhaustive]
pub struct JwtAuth<C, D> {
pub force_passed: bool,
_claims: PhantomData<C>,
pub decoder: D,
pub finders: Vec<Box<dyn JwtTokenFinder>>,
}
impl<C, D> Debug for JwtAuth<C, D>
where
C: DeserializeOwned + Send + Sync + 'static,
D: JwtAuthDecoder + Send + Sync + 'static,
{
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("JwtAuth")
.field("force_passed", &self.force_passed)
.finish()
}
}
impl<C, D> JwtAuth<C, D>
where
C: DeserializeOwned + Send + Sync + 'static,
D: JwtAuthDecoder + Send + Sync + 'static,
{
#[inline]
#[must_use]
pub fn new(decoder: D) -> Self {
Self {
force_passed: false,
decoder,
_claims: PhantomData::<C>,
finders: vec![Box::new(HeaderFinder::new())],
}
}
#[inline]
#[must_use]
pub fn force_passed(mut self, force_passed: bool) -> Self {
self.force_passed = force_passed;
self
}
#[inline]
pub fn decoder_mut(&mut self) -> &mut D {
&mut self.decoder
}
#[inline]
pub fn finders_mut(&mut self) -> &mut Vec<Box<dyn JwtTokenFinder>> {
&mut self.finders
}
#[inline]
#[must_use]
pub fn finders(mut self, finders: Vec<Box<dyn JwtTokenFinder>>) -> Self {
self.finders = finders;
self
}
async fn find_token(&self, req: &mut Request) -> Option<String> {
for finder in &self.finders {
if let Some(token) = finder.find_token(req).await {
return Some(token);
}
}
None
}
}
#[async_trait]
impl<C, D> Handler for JwtAuth<C, D>
where
C: DeserializeOwned + Clone + Send + Sync + 'static,
D: JwtAuthDecoder + Send + Sync + 'static,
{
async fn handle(
&self,
req: &mut Request,
depot: &mut Depot,
res: &mut Response,
ctrl: &mut FlowCtrl,
) {
for key in [
JWT_AUTH_DATA_KEY,
JWT_AUTH_STATE_KEY,
JWT_AUTH_TOKEN_KEY,
JWT_AUTH_ERROR_KEY,
] {
depot.remove(key);
}
let token = self.find_token(req).await;
if let Some(token) = token {
match self.decoder.decode::<C>(&token, depot).await {
Ok(data) => {
depot.insert(JWT_AUTH_DATA_KEY, data);
depot.insert(JWT_AUTH_STATE_KEY, JwtAuthState::Authorized);
depot.insert(JWT_AUTH_TOKEN_KEY, token);
}
Err(e) => {
tracing::info!(error = ?e, "jwt auth error");
depot.insert(JWT_AUTH_STATE_KEY, JwtAuthState::Forbidden);
depot.insert(JWT_AUTH_ERROR_KEY, e);
if !self.force_passed {
res.render(StatusError::forbidden());
ctrl.skip_rest();
}
}
}
} else {
depot.insert(JWT_AUTH_STATE_KEY, JwtAuthState::Unauthorized);
if !self.force_passed {
res.render(StatusError::unauthorized());
ctrl.skip_rest();
}
}
}
}
#[cfg(test)]
mod tests {
use jsonwebtoken::EncodingKey;
use salvo_core::prelude::*;
use salvo_core::test::{ResponseExt, TestClient};
use serde::{Deserialize, Serialize};
use time::{Duration, OffsetDateTime};
use super::*;
#[derive(Serialize, Deserialize, Clone, Debug)]
struct JwtClaims {
user: String,
exp: i64,
}
fn encode_test_token<T: Serialize>(claims: &T, key: &EncodingKey) -> String {
let _ = install_crypto_provider();
jsonwebtoken::encode(&jsonwebtoken::Header::default(), claims, key).unwrap()
}
fn create_test_token(secret: &[u8], exp_days: i64) -> String {
let claim = JwtClaims {
user: "test_user".into(),
exp: (OffsetDateTime::now_utc() + Duration::days(exp_days)).unix_timestamp(),
};
encode_test_token(&claim, &EncodingKey::from_secret(secret))
}
#[tokio::test]
async fn repeated_authentication_replaces_all_depot_state() {
#[handler]
async fn inspect(depot: &mut Depot) -> String {
serde_json::json!({
"state": format!("{:?}", depot.jwt_auth_state()),
"has_data": depot.jwt_auth_data::<JwtClaims>().is_some(),
"has_token": depot.jwt_auth_token().is_some(),
"has_error": depot.jwt_auth_error().is_some(),
})
.to_string()
}
let outer: JwtAuth<JwtClaims, _> = JwtAuth::new(ConstDecoder::from_secret(b"outer"))
.finders(vec![Box::new(
HeaderFinder::new().header_names(vec!["x-outer-token".parse().unwrap()]),
)])
.force_passed(true);
let inner: JwtAuth<JwtClaims, _> = JwtAuth::new(ConstDecoder::from_secret(b"inner"))
.finders(vec![Box::new(
HeaderFinder::new().header_names(vec!["x-inner-token".parse().unwrap()]),
)])
.force_passed(true);
let service = Service::new(Router::new().hoop(outer).hoop(inner).goal(inspect));
let outer_token = create_test_token(b"outer", 1);
let inner_token = create_test_token(b"inner", 1);
for (outer, inner, state, has_data, has_error) in [
(Some(&outer_token), None, "Unauthorized", false, false),
(
Some(&outer_token),
Some(&outer_token),
"Forbidden",
false,
true,
),
(
Some(&inner_token),
Some(&inner_token),
"Authorized",
true,
false,
),
(Some(&inner_token), None, "Unauthorized", false, false),
(None, Some(&inner_token), "Authorized", true, false),
(
Some(&outer_token),
Some(&inner_token),
"Authorized",
true,
false,
),
] {
let mut request = TestClient::get("http://localhost/");
if let Some(token) = outer {
request = request.add_header("x-outer-token", format!("Bearer {token}"), true);
}
if let Some(token) = inner {
request = request.add_header("x-inner-token", format!("Bearer {token}"), true);
}
let mut response = request.send(&service).await;
let body: serde_json::Value =
serde_json::from_str(&response.take_string().await.unwrap()).unwrap();
assert_eq!(body["state"], state);
assert_eq!(body["has_data"], has_data, "{body}");
assert_eq!(body["has_token"], has_data, "{body}");
assert_eq!(body["has_error"], has_error, "{body}");
}
}
#[tokio::test]
async fn repeated_authentication_clears_other_claims_and_error_types() {
#[derive(Clone, Deserialize)]
struct OtherClaims {
#[serde(rename = "user")]
_user: String,
}
#[handler]
async fn seed_state(depot: &mut Depot) {
depot.insert(
JWT_AUTH_DATA_KEY,
TokenData {
header: jsonwebtoken::Header::default(),
claims: OtherClaims {
_user: "outer".into(),
},
},
);
depot.insert(JWT_AUTH_ERROR_KEY, std::io::Error::other("outer error"));
depot.insert(JWT_AUTH_TOKEN_KEY, "outer token".to_owned());
depot.insert(JWT_AUTH_STATE_KEY, JwtAuthState::Authorized);
}
#[handler]
async fn inspect(depot: &mut Depot) -> &'static str {
assert!(!depot.contains_key(JWT_AUTH_DATA_KEY));
assert!(!depot.contains_key(JWT_AUTH_ERROR_KEY));
assert!(!depot.contains_key(JWT_AUTH_TOKEN_KEY));
assert_eq!(depot.jwt_auth_state(), JwtAuthState::Unauthorized);
"anonymous"
}
let auth: JwtAuth<JwtClaims, _> =
JwtAuth::new(ConstDecoder::from_secret(b"inner")).force_passed(true);
let service = Service::new(Router::new().hoop(seed_state).hoop(auth).goal(inspect));
let mut response = TestClient::get("http://localhost/").send(&service).await;
assert_eq!(response.take_string().await.unwrap(), "anonymous");
}
#[test]
fn test_const_decoder_new() {
let key = DecodingKey::from_secret(b"test_secret");
let decoder = ConstDecoder::new(key);
assert!(format!("{decoder:?}").contains("ConstDecoder"));
}
#[test]
fn test_const_decoder_with_validation() {
let key = DecodingKey::from_secret(b"test_secret");
let mut validation = Validation::default();
validation.validate_exp = false;
let decoder = ConstDecoder::with_validation(key, validation);
assert!(format!("{decoder:?}").contains("ConstDecoder"));
}
#[test]
fn test_const_decoder_from_secret() {
let decoder = ConstDecoder::from_secret(b"my_secret_key");
assert!(format!("{decoder:?}").contains("ConstDecoder"));
}
#[test]
fn test_const_decoder_from_base64_secret() {
let decoder = ConstDecoder::from_base64_secret("dGVzdF9zZWNyZXQ=");
assert!(decoder.is_ok());
let decoder = ConstDecoder::from_base64_secret("not valid base64!!!");
assert!(decoder.is_err());
}
#[tokio::test]
async fn test_const_decoder_decode_valid_token() {
let secret = b"test_secret_key";
let decoder = ConstDecoder::from_secret(secret);
let token = create_test_token(secret, 1);
let mut depot = Depot::new();
let result = decoder.decode::<JwtClaims>(&token, &mut depot).await;
assert!(result.is_ok());
let token_data = result.unwrap();
assert_eq!(token_data.claims.user, "test_user");
}
#[tokio::test]
async fn test_const_decoder_decode_invalid_token() {
let decoder = ConstDecoder::from_secret(b"correct_secret");
let token = create_test_token(b"wrong_secret", 1);
let mut depot = Depot::new();
let result = decoder.decode::<JwtClaims>(&token, &mut depot).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_const_decoder_decode_expired_token() {
let secret = b"test_secret_key";
let decoder = ConstDecoder::from_secret(secret);
let token = create_test_token(secret, -1);
let mut depot = Depot::new();
let result = decoder.decode::<JwtClaims>(&token, &mut depot).await;
assert!(result.is_err());
}
#[test]
fn test_header_finder_new() {
let finder = HeaderFinder::new();
assert!(finder.allowed_methods.is_none());
assert_eq!(finder.header_names.len(), 1); }
#[tokio::test]
async fn test_header_finder_allows_query_by_default() {
let finder = HeaderFinder::new();
let mut req = TestClient::query("http://local.host/search")
.add_header("Authorization", "Bearer token", true)
.build();
assert_eq!(finder.find_token(&mut req).await.as_deref(), Some("token"));
}
#[test]
fn test_header_finder_allowed_methods() {
let finder = HeaderFinder::new().allowed_methods(vec![Method::GET, Method::POST]);
assert_eq!(finder.allowed_methods.as_ref().unwrap().len(), 2);
assert!(
finder
.allowed_methods
.as_ref()
.unwrap()
.contains(&Method::GET)
);
assert!(
finder
.allowed_methods
.as_ref()
.unwrap()
.contains(&Method::POST)
);
}
#[test]
fn test_header_finder_header_names() {
use salvo_core::http::header::HeaderName;
let custom_header = HeaderName::from_static("x-custom-auth");
let finder = HeaderFinder::new().header_names(vec![custom_header.clone()]);
assert_eq!(finder.header_names.len(), 1);
assert_eq!(finder.header_names[0], custom_header);
}
#[test]
fn test_header_finder_mut_methods() {
let mut finder = HeaderFinder::new();
finder.allowed_methods_mut().push(Method::CONNECT);
finder.header_names_mut().clear();
assert!(finder.header_names.is_empty());
}
#[test]
fn test_query_finder_new() {
let finder = QueryFinder::new("token");
assert_eq!(finder.query_name, "token");
assert!(finder.allowed_methods.is_none());
}
#[test]
fn test_query_finder_with_static_str() {
let finder = QueryFinder::new("access_token");
assert_eq!(finder.query_name, "access_token");
}
#[test]
fn test_query_finder_with_string() {
let finder = QueryFinder::new(String::from("jwt"));
assert_eq!(finder.query_name, "jwt");
}
#[test]
fn test_query_finder_allowed_methods() {
let finder = QueryFinder::new("token").allowed_methods(vec![Method::GET]);
assert_eq!(finder.allowed_methods.as_ref().unwrap().len(), 1);
assert!(
finder
.allowed_methods
.as_ref()
.unwrap()
.contains(&Method::GET)
);
}
#[test]
fn test_cookie_finder_new() {
let finder = CookieFinder::new("session");
assert_eq!(finder.cookie_name, "session");
assert!(finder.allowed_methods.is_none());
}
#[test]
fn test_cookie_finder_allowed_methods() {
let finder = CookieFinder::new("jwt").allowed_methods(vec![Method::GET, Method::POST]);
assert_eq!(finder.allowed_methods.as_ref().unwrap().len(), 2);
}
#[test]
fn test_cookie_finder_mut_methods() {
let mut finder = CookieFinder::new("token");
finder.allowed_methods_mut().clear();
assert!(finder.allowed_methods.as_ref().unwrap().is_empty());
}
#[test]
fn test_form_finder_new() {
let finder = FormFinder::new("token");
assert_eq!(finder.field_name, "token");
assert!(finder.allowed_methods.is_none());
}
#[test]
fn test_form_finder_allowed_methods() {
let finder = FormFinder::new("access_token").allowed_methods(vec![Method::POST]);
assert_eq!(finder.allowed_methods.as_ref().unwrap().len(), 1);
assert!(
finder
.allowed_methods
.as_ref()
.unwrap()
.contains(&Method::POST)
);
}
#[test]
fn test_jwt_auth_state_eq() {
assert_eq!(JwtAuthState::Authorized, JwtAuthState::Authorized);
assert_eq!(JwtAuthState::Unauthorized, JwtAuthState::Unauthorized);
assert_eq!(JwtAuthState::Forbidden, JwtAuthState::Forbidden);
assert_ne!(JwtAuthState::Authorized, JwtAuthState::Forbidden);
}
#[test]
fn test_jwt_auth_state_clone() {
let state = JwtAuthState::Authorized;
let cloned = state;
assert_eq!(state, cloned);
}
#[test]
fn test_jwt_auth_state_debug() {
assert!(format!("{:?}", JwtAuthState::Authorized).contains("Authorized"));
assert!(format!("{:?}", JwtAuthState::Unauthorized).contains("Unauthorized"));
assert!(format!("{:?}", JwtAuthState::Forbidden).contains("Forbidden"));
}
#[test]
fn test_depot_jwt_auth_state_default() {
let depot = Depot::new();
assert_eq!(depot.jwt_auth_state(), JwtAuthState::Unauthorized);
}
#[test]
fn test_depot_jwt_auth_token_none() {
let depot = Depot::new();
assert!(depot.jwt_auth_token().is_none());
}
#[test]
fn test_depot_jwt_auth_data_none() {
let depot = Depot::new();
assert!(depot.jwt_auth_data::<JwtClaims>().is_none());
}
#[test]
fn test_depot_jwt_auth_error_none() {
let depot = Depot::new();
assert!(depot.jwt_auth_error().is_none());
}
#[test]
fn test_depot_with_jwt_auth_state() {
let mut depot = Depot::new();
depot.insert(JWT_AUTH_STATE_KEY, JwtAuthState::Authorized);
assert_eq!(depot.jwt_auth_state(), JwtAuthState::Authorized);
}
#[test]
fn test_depot_with_jwt_auth_token() {
let mut depot = Depot::new();
depot.insert(JWT_AUTH_TOKEN_KEY, String::from("test_token_value"));
assert_eq!(depot.jwt_auth_token(), Some("test_token_value"));
}
#[test]
fn test_jwt_auth_new() {
let decoder = ConstDecoder::from_secret(b"secret");
let auth: JwtAuth<JwtClaims, _> = JwtAuth::new(decoder);
assert!(!auth.force_passed);
assert_eq!(auth.finders.len(), 1); }
#[test]
fn test_jwt_auth_force_passed() {
let decoder = ConstDecoder::from_secret(b"secret");
let auth: JwtAuth<JwtClaims, _> = JwtAuth::new(decoder).force_passed(true);
assert!(auth.force_passed);
}
#[test]
fn test_jwt_auth_finders() {
let decoder = ConstDecoder::from_secret(b"secret");
let auth: JwtAuth<JwtClaims, _> = JwtAuth::new(decoder).finders(vec![
Box::new(HeaderFinder::new()),
Box::new(QueryFinder::new("token")),
]);
assert_eq!(auth.finders.len(), 2);
}
#[test]
fn test_jwt_auth_finders_mut() {
let decoder = ConstDecoder::from_secret(b"secret");
let mut auth: JwtAuth<JwtClaims, _> = JwtAuth::new(decoder);
auth.finders_mut().push(Box::new(CookieFinder::new("jwt")));
assert_eq!(auth.finders.len(), 2);
}
#[test]
fn test_jwt_auth_decoder_mut() {
let decoder = ConstDecoder::from_secret(b"secret");
let mut auth: JwtAuth<JwtClaims, _> = JwtAuth::new(decoder);
let _ = auth.decoder_mut(); }
#[test]
fn test_jwt_auth_debug() {
let decoder = ConstDecoder::from_secret(b"secret");
let auth: JwtAuth<JwtClaims, _> = JwtAuth::new(decoder);
let debug_str = format!("{auth:?}");
assert!(debug_str.contains("JwtAuth"));
assert!(debug_str.contains("force_passed"));
}
#[tokio::test]
async fn test_jwt_auth_header_authorization() {
let auth_handler: JwtAuth<JwtClaims, ConstDecoder> =
JwtAuth::new(ConstDecoder::from_secret(b"ABCDEF")).finders(vec![
Box::new(HeaderFinder::new()),
Box::new(QueryFinder::new("jwt_token")),
Box::new(CookieFinder::new("jwt_token")),
]);
#[handler]
async fn hello() -> &'static str {
"hello"
}
let router = Router::new()
.hoop(auth_handler)
.push(Router::with_path("hello").get(hello));
let service = Service::new(router);
async fn access(service: &Service, token: &str) -> String {
TestClient::get("http://127.0.0.1:5801/hello")
.add_header("Authorization", format!("Bearer {token}"), true)
.send(service)
.await
.take_string()
.await
.unwrap()
}
let claim = JwtClaims {
user: "root".into(),
exp: (OffsetDateTime::now_utc() + Duration::days(1)).unix_timestamp(),
};
let token = encode_test_token(&claim, &EncodingKey::from_secret(b"ABCDEF"));
let content = access(&service, &token).await;
assert!(content.contains("hello"));
let content = TestClient::get(format!("http://127.0.0.1:5801/hello?jwt_token={token}"))
.send(&service)
.await
.take_string()
.await
.unwrap();
assert!(content.contains("hello"));
let content = TestClient::get("http://127.0.0.1:5801/hello")
.add_header("Cookie", format!("jwt_token={token}"), true)
.send(&service)
.await
.take_string()
.await
.unwrap();
assert!(content.contains("hello"));
let token = encode_test_token(&claim, &EncodingKey::from_secret(b"ABCDEFG"));
let content = access(&service, &token).await;
assert!(content.contains("Forbidden"));
}
#[tokio::test]
async fn test_jwt_auth_unauthorized_no_token() {
let auth_handler: JwtAuth<JwtClaims, ConstDecoder> =
JwtAuth::new(ConstDecoder::from_secret(b"SECRET"));
#[handler]
async fn hello() -> &'static str {
"hello"
}
let router = Router::new()
.hoop(auth_handler)
.push(Router::with_path("hello").get(hello));
let service = Service::new(router);
let content = TestClient::get("http://127.0.0.1:5801/hello")
.send(&service)
.await
.take_string()
.await
.unwrap();
assert!(content.contains("Unauthorized"));
}
#[tokio::test]
async fn test_jwt_auth_force_passed_no_token() {
let auth_handler: JwtAuth<JwtClaims, ConstDecoder> =
JwtAuth::new(ConstDecoder::from_secret(b"SECRET")).force_passed(true);
#[handler]
async fn hello(depot: &mut Depot) -> String {
format!("{:?}", depot.jwt_auth_state())
}
let router = Router::new()
.hoop(auth_handler)
.push(Router::with_path("hello").get(hello));
let service = Service::new(router);
let content = TestClient::get("http://127.0.0.1:5801/hello")
.send(&service)
.await
.take_string()
.await
.unwrap();
assert!(content.contains("Unauthorized"));
}
#[tokio::test]
async fn test_jwt_auth_force_passed_invalid_token() {
let auth_handler: JwtAuth<JwtClaims, ConstDecoder> =
JwtAuth::new(ConstDecoder::from_secret(b"SECRET")).force_passed(true);
#[handler]
async fn hello(depot: &mut Depot) -> String {
format!("{:?}", depot.jwt_auth_state())
}
let router = Router::new()
.hoop(auth_handler)
.push(Router::with_path("hello").get(hello));
let service = Service::new(router);
let content = TestClient::get("http://127.0.0.1:5801/hello")
.add_header("Authorization", "Bearer invalid_token", true)
.send(&service)
.await
.take_string()
.await
.unwrap();
assert!(content.contains("Forbidden"));
}
#[tokio::test]
async fn test_jwt_auth_depot_data_access() {
let auth_handler: JwtAuth<JwtClaims, ConstDecoder> =
JwtAuth::new(ConstDecoder::from_secret(b"SECRET"));
#[handler]
async fn hello(depot: &mut Depot) -> String {
match depot.jwt_auth_data::<JwtClaims>() {
Some(data) => format!("user:{}", data.claims.user),
None => "no_data".to_owned(),
}
}
let router = Router::new()
.hoop(auth_handler)
.push(Router::with_path("hello").get(hello));
let service = Service::new(router);
let claim = JwtClaims {
user: "admin".into(),
exp: (OffsetDateTime::now_utc() + Duration::days(1)).unix_timestamp(),
};
let token = encode_test_token(&claim, &EncodingKey::from_secret(b"SECRET"));
let content = TestClient::get("http://127.0.0.1:5801/hello")
.add_header("Authorization", format!("Bearer {token}"), true)
.send(&service)
.await
.take_string()
.await
.unwrap();
assert!(content.contains("user:admin"));
}
#[tokio::test]
async fn test_jwt_auth_proxy_authorization_header_requires_explicit_opt_in() {
use salvo_core::http::header::{AUTHORIZATION, PROXY_AUTHORIZATION};
let auth_handler: JwtAuth<JwtClaims, ConstDecoder> =
JwtAuth::new(ConstDecoder::from_secret(b"SECRET"));
#[handler]
async fn hello() -> &'static str {
"hello"
}
let router = Router::new()
.hoop(auth_handler)
.push(Router::with_path("hello").get(hello));
let service = Service::new(router);
let claim = JwtClaims {
user: "proxy_user".into(),
exp: (OffsetDateTime::now_utc() + Duration::days(1)).unix_timestamp(),
};
let token = encode_test_token(&claim, &EncodingKey::from_secret(b"SECRET"));
let content = TestClient::get("http://127.0.0.1:5801/hello")
.add_header("Proxy-Authorization", format!("Bearer {token}"), true)
.send(&service)
.await
.take_string()
.await
.unwrap();
assert!(content.contains("Unauthorized"));
let auth_handler: JwtAuth<JwtClaims, ConstDecoder> =
JwtAuth::new(ConstDecoder::from_secret(b"SECRET")).finders(vec![Box::new(
HeaderFinder::new().header_names(vec![AUTHORIZATION, PROXY_AUTHORIZATION]),
)]);
let router = Router::new()
.hoop(auth_handler)
.push(Router::with_path("hello").get(hello));
let service = Service::new(router);
let content = TestClient::get("http://127.0.0.1:5801/hello")
.add_header("Proxy-Authorization", format!("Bearer {token}"), true)
.send(&service)
.await
.take_string()
.await
.unwrap();
assert!(content.contains("hello"));
}
#[tokio::test]
async fn test_header_finder_non_bearer_ignored() {
let auth_handler: JwtAuth<JwtClaims, ConstDecoder> =
JwtAuth::new(ConstDecoder::from_secret(b"SECRET"));
#[handler]
async fn hello() -> &'static str {
"hello"
}
let router = Router::new()
.hoop(auth_handler)
.push(Router::with_path("hello").get(hello));
let service = Service::new(router);
let content = TestClient::get("http://127.0.0.1:5801/hello")
.add_header("Authorization", "Basic dXNlcjpwYXNz", true)
.send(&service)
.await
.take_string()
.await
.unwrap();
assert!(content.contains("Unauthorized"));
}
#[tokio::test]
async fn test_header_finder_method_filtering() {
let auth_handler: JwtAuth<JwtClaims, ConstDecoder> =
JwtAuth::new(ConstDecoder::from_secret(b"SECRET")).finders(vec![Box::new(
HeaderFinder::new().allowed_methods(vec![Method::POST]),
)]);
#[handler]
async fn hello() -> &'static str {
"hello"
}
let router = Router::new()
.hoop(auth_handler)
.push(Router::with_path("hello").get(hello).post(hello));
let service = Service::new(router);
let claim = JwtClaims {
user: "test".into(),
exp: (OffsetDateTime::now_utc() + Duration::days(1)).unix_timestamp(),
};
let token = encode_test_token(&claim, &EncodingKey::from_secret(b"SECRET"));
let content = TestClient::get("http://127.0.0.1:5801/hello")
.add_header("Authorization", format!("Bearer {token}"), true)
.send(&service)
.await
.take_string()
.await
.unwrap();
assert!(content.contains("Unauthorized"));
let content = TestClient::post("http://127.0.0.1:5801/hello")
.add_header("Authorization", format!("Bearer {token}"), true)
.send(&service)
.await
.take_string()
.await
.unwrap();
assert!(content.contains("hello"));
}
}