#[cfg(feature = "jwt-auth")]
use {
crate::headers::{CACHE_CONTROL, HeaderValue, WWW_AUTHENTICATE, cache_control::NO_STORE},
crate::middleware::{HttpContext, Middleware, NextFn},
crate::{
App, HttpResult,
error::Error,
http::StatusCode,
routing::{Route, RouteGroup},
status,
},
std::{future::Future, sync::Arc},
};
#[cfg(feature = "jwt-auth")]
pub use {
algorithm::Algorithm,
authenticated::Authenticated,
authorizer::{Authorizer, permissions, predicate, role, roles},
bearer::{Bearer, BearerAuthConfig, BearerTokenService},
claims::AuthClaims,
decoding_key::DecodingKey,
encoding_key::EncodingKey,
};
#[cfg(feature = "jwt-auth")]
use jsonwebtoken::errors::{Error as JwtError, ErrorKind};
#[cfg(feature = "jwt-derive")]
pub use volga_macros::Claims;
#[cfg(feature = "basic-auth")]
pub use basic::Basic;
#[cfg(feature = "jwt-auth")]
pub mod algorithm;
#[cfg(feature = "jwt-auth")]
pub mod authenticated;
#[cfg(feature = "jwt-auth")]
pub mod authorizer;
#[cfg(feature = "basic-auth")]
pub mod basic;
#[cfg(feature = "jwt-auth")]
pub mod bearer;
#[cfg(feature = "jwt-auth")]
pub mod claims;
#[cfg(feature = "jwt-auth")]
pub mod decoding_key;
#[cfg(feature = "jwt-auth")]
pub mod encoding_key;
#[cfg(feature = "jwt-auth")]
pub(crate) mod pem;
#[cfg(feature = "jwt-auth")]
impl From<JwtError> for Error {
#[inline]
fn from(err: JwtError) -> Self {
let kind = err.kind();
let status_code = map_jwt_error_to_status(kind);
Error::from_parts(status_code, None, err)
}
}
#[cfg(feature = "jwt-auth")]
fn map_jwt_error_to_status(err: &ErrorKind) -> StatusCode {
use ErrorKind::*;
match err {
ExpiredSignature
| InvalidToken
| InvalidIssuer
| InvalidAudience
| InvalidSubject
| InvalidSignature
| MissingRequiredClaim(_)
| ImmatureSignature
| InvalidAlgorithmName
| InvalidAlgorithm => StatusCode::UNAUTHORIZED,
Base64(_) | Json(_) | Utf8(_) | InvalidKeyFormat => StatusCode::BAD_REQUEST,
_ => StatusCode::INTERNAL_SERVER_ERROR,
}
}
#[cfg(feature = "jwt-auth")]
fn build_www_authenticate(err: &ErrorKind, resource_metadata_url: Option<&str>) -> String {
use ErrorKind::*;
let base = match err {
ExpiredSignature => {
r#"Bearer error="invalid_token", error_description="Token has expired""#
}
InvalidSignature => {
r#"Bearer error="invalid_token", error_description="Invalid signature""#
}
InvalidToken => {
r#"Bearer error="invalid_token", error_description="Token is malformed or invalid""#
}
ImmatureSignature => {
r#"Bearer error="invalid_token", error_description="Token is not valid yet (nbf)""#
}
MissingRequiredClaim(_) => {
r#"Bearer error="invalid_token", error_description="Missing required claim""#
}
InvalidIssuer => {
r#"Bearer error="invalid_token", error_description="Invalid issuer (iss)""#
}
InvalidAudience => {
r#"Bearer error="invalid_token", error_description="Invalid audience (aud)""#
}
InvalidSubject => {
r#"Bearer error="invalid_token", error_description="Invalid subject (sub)""#
}
InvalidAlgorithm | InvalidAlgorithmName => {
r#"Bearer error="invalid_token", error_description="Invalid algorithm""#
}
Base64(_) => {
r#"Bearer error="invalid_request", error_description="Token is not properly base64-encoded""#
}
Json(_) => {
r#"Bearer error="invalid_request", error_description="Token payload is not valid JSON""#
}
Utf8(_) => {
r#"Bearer error="invalid_request", error_description="Token contains invalid UTF-8 characters""#
}
InvalidKeyFormat => {
r#"Bearer error="invalid_request", error_description="Invalid key format""#
}
_ => r#"Bearer error="server_error", error_description="Internal token processing error""#,
};
match resource_metadata_url {
Some(url) => format!(r#"{base}, resource_metadata="{url}""#),
None => base.to_string(),
}
}
#[cfg(feature = "jwt-auth")]
impl App {
pub fn with_bearer_auth<F>(mut self, config: F) -> Self
where
F: FnOnce(BearerAuthConfig) -> BearerAuthConfig,
{
self.auth_config = Some(config(Default::default()));
self
}
pub fn authorize<C: AuthClaims + Send + Sync + 'static>(
&mut self,
authorizer: Authorizer<C>,
) -> &mut Self {
self.ensure_bearer_auth_configured();
self.attach(Authorize::new(authorizer))
}
fn ensure_bearer_auth_configured(&self) {
let config = match &self.auth_config {
Some(config) => config,
_ => panic!("Bearer Auth is not configured"),
};
config
.decoding_key()
.expect("Bearer Auth security key is not configured");
}
}
#[cfg(feature = "jwt-auth")]
impl<'a> Route<'a> {
pub fn authorize<C: AuthClaims + Send + Sync + 'static>(
self,
authorizer: Authorizer<C>,
) -> Self {
self.ensure_bearer_auth_configured();
self.attach(Authorize::new(authorizer))
}
}
#[cfg(feature = "jwt-auth")]
impl<'a> RouteGroup<'a> {
pub fn authorize<C: AuthClaims + Send + Sync + 'static>(
&mut self,
authorizer: Authorizer<C>,
) -> &mut Self {
self.app.ensure_bearer_auth_configured();
self.attach(Authorize::new(authorizer))
}
}
#[cfg(feature = "jwt-auth")]
struct Authorize<C>
where
C: AuthClaims + Send + Sync + 'static,
{
authorizer: Arc<Authorizer<C>>,
}
#[cfg(feature = "jwt-auth")]
impl<C> Authorize<C>
where
C: AuthClaims + Send + Sync + 'static,
{
fn new(a: Authorizer<C>) -> Self {
Self {
authorizer: Arc::new(a),
}
}
}
#[cfg(feature = "jwt-auth")]
impl<C> Middleware for Authorize<C>
where
C: AuthClaims + Send + Sync + 'static,
{
#[inline]
fn call(
&self,
ctx: HttpContext,
next: NextFn,
) -> impl Future<Output = HttpResult> + Send + 'static {
authorize_impl(Arc::clone(&self.authorizer), ctx, next)
}
}
#[cfg(feature = "jwt-auth")]
fn authorize_impl<C>(
authorizer: Arc<Authorizer<C>>,
mut ctx: HttpContext,
next: NextFn,
) -> impl Future<Output = HttpResult>
where
C: AuthClaims + Send + Sync + 'static,
{
let authorizer = authorizer.clone();
async move {
if should_reject_for_https(&ctx)? {
return status!(400; [
(WWW_AUTHENTICATE, r#"Bearer error="invalid_request", error_description="HTTPS required""#)
]);
}
let bearer = resolve_bearer(&ctx)?;
let bts: BearerTokenService = ctx.extract()?;
let resp = match bts.decode(bearer.clone()) {
Ok(claims) if authorizer.validate(&claims) => {
if bts.strip_token_from_request {
stash_bearer(&mut ctx, bearer);
ctx.request_mut()
.headers_mut()
.remove(crate::headers::AUTHORIZATION);
}
ctx.request_mut()
.extensions_mut()
.insert(Authenticated(claims));
next(ctx).await
}
Ok(_) => {
let metadata_url = bts.resource_metadata_url.as_deref();
status!(403; [
(WWW_AUTHENTICATE, authorizer::default_error_msg(metadata_url))
])
}
Err(err) => {
let metadata_url = bts.resource_metadata_url.as_deref();
let www_authenticate = err
.into_inner()
.downcast_ref::<JwtError>()
.map(|e| build_www_authenticate(e.kind(), metadata_url))
.unwrap_or_else(|| authorizer::default_error_msg(metadata_url));
status!(403; [
(WWW_AUTHENTICATE, www_authenticate)
])
}
};
resp.map(|mut resp| {
resp.headers_mut()
.insert(CACHE_CONTROL, HeaderValue::from_static(NO_STORE));
resp
})
}
}
#[cfg(feature = "jwt-auth")]
fn resolve_bearer(ctx: &HttpContext) -> Result<Bearer, Error> {
use crate::http::request_scope::HttpRequestScope;
if let Some(scope) = ctx.request().extensions().get::<HttpRequestScope>()
&& let Some(bearer) = scope.bearer.as_ref()
{
return Ok(bearer.clone());
}
ctx.extract::<Bearer>()
}
#[cfg(feature = "jwt-auth")]
fn stash_bearer(ctx: &mut HttpContext, bearer: Bearer) {
use crate::http::request_scope::HttpRequestScope;
if let Some(scope) = ctx
.request_mut()
.extensions_mut()
.get_mut::<HttpRequestScope>()
&& scope.bearer.is_none()
{
scope.bearer = Some(bearer);
}
}
#[cfg(feature = "jwt-auth")]
fn should_reject_for_https(ctx: &HttpContext) -> Result<bool, Error> {
let bts: BearerTokenService = ctx.extract()?;
if !bts.require_https || bts.tls_enabled {
return Ok(false);
}
let client_ip: crate::ClientIp = ctx.extract()?;
Ok(!client_ip.into_inner().ip().is_loopback())
}
#[cfg(all(test, feature = "jwt-auth"))]
mod tests {
use super::*;
use crate::http::StatusCode;
use jsonwebtoken::errors::{Error as JwtError, ErrorKind};
#[test]
fn it_maps_expired_signature_to_unauthorized() {
let status = map_jwt_error_to_status(&ErrorKind::ExpiredSignature);
assert_eq!(status, StatusCode::UNAUTHORIZED);
}
#[test]
fn it_maps_invalid_token_to_unauthorized() {
let status = map_jwt_error_to_status(&ErrorKind::InvalidToken);
assert_eq!(status, StatusCode::UNAUTHORIZED);
}
#[test]
fn it_maps_invalid_issuer_to_unauthorized() {
let status = map_jwt_error_to_status(&ErrorKind::InvalidIssuer);
assert_eq!(status, StatusCode::UNAUTHORIZED);
}
#[test]
fn it_maps_invalid_audience_to_unauthorized() {
let status = map_jwt_error_to_status(&ErrorKind::InvalidAudience);
assert_eq!(status, StatusCode::UNAUTHORIZED);
}
#[test]
fn it_maps_invalid_subject_to_unauthorized() {
let status = map_jwt_error_to_status(&ErrorKind::InvalidSubject);
assert_eq!(status, StatusCode::UNAUTHORIZED);
}
#[test]
fn it_maps_invalid_signature_to_unauthorized() {
let status = map_jwt_error_to_status(&ErrorKind::InvalidSignature);
assert_eq!(status, StatusCode::UNAUTHORIZED);
}
#[test]
fn it_maps_missing_required_claim_to_unauthorized() {
let status = map_jwt_error_to_status(&ErrorKind::MissingRequiredClaim("test".to_string()));
assert_eq!(status, StatusCode::UNAUTHORIZED);
}
#[test]
fn it_maps_immature_signature_to_unauthorized() {
let status = map_jwt_error_to_status(&ErrorKind::ImmatureSignature);
assert_eq!(status, StatusCode::UNAUTHORIZED);
}
#[test]
fn it_maps_invalid_algorithm_name_to_unauthorized() {
let status = map_jwt_error_to_status(&ErrorKind::InvalidAlgorithmName);
assert_eq!(status, StatusCode::UNAUTHORIZED);
}
#[test]
fn it_maps_invalid_algorithm_to_unauthorized() {
let status = map_jwt_error_to_status(&ErrorKind::InvalidAlgorithm);
assert_eq!(status, StatusCode::UNAUTHORIZED);
}
#[test]
fn it_maps_base64_error_to_bad_request() {
let status =
map_jwt_error_to_status(&ErrorKind::Base64(base64::DecodeError::InvalidByte(0, 0)));
assert_eq!(status, StatusCode::BAD_REQUEST);
}
#[test]
fn it_maps_json_error_to_bad_request() {
let json_result: Result<serde_json::Value, _> = serde_json::from_str("invalid json");
let json_error = json_result.unwrap_err();
let status = map_jwt_error_to_status(&ErrorKind::Json(Arc::from(json_error)));
assert_eq!(status, StatusCode::BAD_REQUEST);
}
#[test]
fn it_maps_utf8_error_to_bad_request() {
let invalid_utf8_bytes = vec![0, 159, 146, 150];
let utf8_error = String::from_utf8(invalid_utf8_bytes).unwrap_err();
let status = map_jwt_error_to_status(&ErrorKind::Utf8(utf8_error));
assert_eq!(status, StatusCode::BAD_REQUEST);
}
#[test]
fn it_maps_invalid_key_format_to_bad_request() {
let status = map_jwt_error_to_status(&ErrorKind::InvalidKeyFormat);
assert_eq!(status, StatusCode::BAD_REQUEST);
}
#[test]
fn it_maps_expired_signature_to_www_authenticate() {
let www_auth = build_www_authenticate(&ErrorKind::ExpiredSignature, None);
assert_eq!(
www_auth,
r#"Bearer error="invalid_token", error_description="Token has expired""#
);
}
#[test]
fn it_maps_invalid_signature_to_www_authenticate() {
let www_auth = build_www_authenticate(&ErrorKind::InvalidSignature, None);
assert_eq!(
www_auth,
r#"Bearer error="invalid_token", error_description="Invalid signature""#
);
}
#[test]
fn it_maps_invalid_token_to_www_authenticate() {
let www_auth = build_www_authenticate(&ErrorKind::InvalidToken, None);
assert_eq!(
www_auth,
r#"Bearer error="invalid_token", error_description="Token is malformed or invalid""#
);
}
#[test]
fn it_maps_immature_signature_to_www_authenticate() {
let www_auth = build_www_authenticate(&ErrorKind::ImmatureSignature, None);
assert_eq!(
www_auth,
r#"Bearer error="invalid_token", error_description="Token is not valid yet (nbf)""#
);
}
#[test]
fn it_maps_missing_required_claim_to_www_authenticate() {
let www_auth =
build_www_authenticate(&ErrorKind::MissingRequiredClaim("test".to_string()), None);
assert_eq!(
www_auth,
r#"Bearer error="invalid_token", error_description="Missing required claim""#
);
}
#[test]
fn it_maps_invalid_issuer_to_www_authenticate() {
let www_auth = build_www_authenticate(&ErrorKind::InvalidIssuer, None);
assert_eq!(
www_auth,
r#"Bearer error="invalid_token", error_description="Invalid issuer (iss)""#
);
}
#[test]
fn it_maps_invalid_audience_to_www_authenticate() {
let www_auth = build_www_authenticate(&ErrorKind::InvalidAudience, None);
assert_eq!(
www_auth,
r#"Bearer error="invalid_token", error_description="Invalid audience (aud)""#
);
}
#[test]
fn it_maps_invalid_subject_to_www_authenticate() {
let www_auth = build_www_authenticate(&ErrorKind::InvalidSubject, None);
assert_eq!(
www_auth,
r#"Bearer error="invalid_token", error_description="Invalid subject (sub)""#
);
}
#[test]
fn it_maps_invalid_algorithm_to_www_authenticate() {
let www_auth = build_www_authenticate(&ErrorKind::InvalidAlgorithm, None);
assert_eq!(
www_auth,
r#"Bearer error="invalid_token", error_description="Invalid algorithm""#
);
}
#[test]
fn it_maps_invalid_algorithm_name_to_www_authenticate() {
let www_auth = build_www_authenticate(&ErrorKind::InvalidAlgorithmName, None);
assert_eq!(
www_auth,
r#"Bearer error="invalid_token", error_description="Invalid algorithm""#
);
}
#[test]
fn it_maps_base64_error_to_www_authenticate() {
let www_auth = build_www_authenticate(
&ErrorKind::Base64(base64::DecodeError::InvalidByte(0, 0)),
None,
);
assert_eq!(
www_auth,
r#"Bearer error="invalid_request", error_description="Token is not properly base64-encoded""#
);
}
#[test]
fn it_maps_json_error_to_www_authenticate() {
let json_result: Result<serde_json::Value, _> = serde_json::from_str("invalid json");
let json_error = json_result.unwrap_err();
let www_auth = build_www_authenticate(&ErrorKind::Json(Arc::from(json_error)), None);
assert_eq!(
www_auth,
r#"Bearer error="invalid_request", error_description="Token payload is not valid JSON""#
);
}
#[test]
fn it_maps_utf8_error_to_www_authenticate() {
let invalid_utf8_bytes = vec![0, 159, 146, 150];
let utf8_error = String::from_utf8(invalid_utf8_bytes).unwrap_err();
let www_auth = build_www_authenticate(&ErrorKind::Utf8(utf8_error), None);
assert_eq!(
www_auth,
r#"Bearer error="invalid_request", error_description="Token contains invalid UTF-8 characters""#
);
}
#[test]
fn it_maps_invalid_key_format_to_www_authenticate() {
let www_auth = build_www_authenticate(&ErrorKind::InvalidKeyFormat, None);
assert_eq!(
www_auth,
r#"Bearer error="invalid_request", error_description="Invalid key format""#
);
}
#[test]
fn it_appends_resource_metadata_url_to_challenge() {
let www_auth = build_www_authenticate(
&ErrorKind::ExpiredSignature,
Some("https://api.example.com/.well-known/oauth-protected-resource"),
);
assert_eq!(
www_auth,
r#"Bearer error="invalid_token", error_description="Token has expired", resource_metadata="https://api.example.com/.well-known/oauth-protected-resource""#
);
}
#[test]
fn it_default_error_msg_without_metadata_url() {
let www_auth = authorizer::default_error_msg(None);
assert_eq!(
www_auth,
r#"Bearer error="insufficient_scope" error_description="User does not have required role or permission""#
);
}
#[test]
fn it_default_error_msg_appends_metadata_url() {
let www_auth = authorizer::default_error_msg(Some(
"https://api.example.com/.well-known/oauth-protected-resource",
));
assert_eq!(
www_auth,
r#"Bearer error="insufficient_scope" error_description="User does not have required role or permission", resource_metadata="https://api.example.com/.well-known/oauth-protected-resource""#
);
}
#[test]
fn it_converts_jwt_error_to_error_with_expired_signature() {
let jwt_error = JwtError::from(ErrorKind::ExpiredSignature);
let error: Error = jwt_error.into();
assert_eq!(error.status, StatusCode::UNAUTHORIZED);
assert!(error.instance.is_none());
}
#[test]
fn it_converts_jwt_error_to_error_with_invalid_token() {
let jwt_error = JwtError::from(ErrorKind::InvalidToken);
let error: Error = jwt_error.into();
assert_eq!(error.status, StatusCode::UNAUTHORIZED);
assert!(error.instance.is_none());
}
#[test]
fn it_converts_jwt_error_to_error_with_base64_error() {
let jwt_error = JwtError::from(ErrorKind::Base64(base64::DecodeError::InvalidByte(0, 0)));
let error: Error = jwt_error.into();
assert_eq!(error.status, StatusCode::BAD_REQUEST);
assert!(error.instance.is_none());
}
#[test]
fn it_converts_jwt_error_to_error_with_json_error() {
let json_result: Result<serde_json::Value, _> = serde_json::from_str("invalid json");
let json_error = json_result.unwrap_err();
let jwt_error = JwtError::from(ErrorKind::Json(Arc::from(json_error)));
let error: Error = jwt_error.into();
assert_eq!(error.status, StatusCode::BAD_REQUEST);
assert!(error.instance.is_none());
}
#[test]
fn it_converts_jwt_error_to_error_with_invalid_key_format() {
let jwt_error = JwtError::from(ErrorKind::InvalidKeyFormat);
let error: Error = jwt_error.into();
assert_eq!(error.status, StatusCode::BAD_REQUEST);
assert!(error.instance.is_none());
}
}