dd4650a
use actix_web::http::StatusCode;
use actix_web::http::header::{HeaderName, HeaderValue};
use actix_web::{HttpResponse, ResponseError};
use serde::{Deserialize, Serialize};
pub type JwtResult<T> = std::result::Result<T, JwtError>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum JwtError {
#[error("credential has been revoked")]
Revoked,
#[error("authorization token is missing")]
Missing,
#[error("authentication required")]
Redirect(String),
#[error("authorization token is malformed")]
Malformed,
#[error("token references an unknown signing key")]
UnknownKey,
#[error("token signing key is not supported")]
UnsupportedKey,
#[error("token signature is invalid")]
BadSignature,
#[error("token has expired")]
Expired,
#[error("token issuer is not accepted")]
BadIssuer,
#[error("token audience is not accepted")]
WrongAudience,
#[error("token scope is insufficient")]
InsufficientScope,
#[error("jwt configuration is invalid: {0}")]
Config(String),
#[error("internal error: {0}")]
Internal(String),
}
impl JwtError {
pub fn status_code(&self) -> StatusCode {
match self {
Self::Redirect(_) => StatusCode::FOUND,
Self::InsufficientScope => StatusCode::FORBIDDEN,
Self::Config(_) | Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
_ => StatusCode::UNAUTHORIZED,
}
}
pub fn code(&self) -> &'static str {
match self {
Self::Revoked => "token_revoked",
Self::Missing => "token_missing",
Self::Redirect(_) => "authentication_required",
Self::Malformed => "token_malformed",
Self::UnknownKey => "token_key_unknown",
Self::UnsupportedKey => "token_key_unsupported",
Self::BadSignature => "token_signature_invalid",
Self::Expired => "token_expired",
Self::BadIssuer => "token_issuer_invalid",
Self::WrongAudience => "token_audience_invalid",
Self::InsufficientScope => "insufficient_scope",
Self::Config(_) => "jwt_config_invalid",
Self::Internal(_) => "internal",
}
}
pub fn is_internal(&self) -> bool {
self.status_code().is_server_error()
}
pub fn message(&self) -> String {
if self.is_internal() {
"internal server error".to_owned()
} else {
self.to_string()
}
}
pub fn body(&self) -> ErrorBody {
ErrorBody {
code: self.code().to_owned(),
message: self.message(),
}
}
}
impl ResponseError for JwtError {
fn status_code(&self) -> StatusCode {
JwtError::status_code(self)
}
fn error_response(&self) -> HttpResponse {
if let Self::Redirect(location) = self
&& HeaderValue::from_str(location).is_ok()
&& is_safe_location(location)
{
return HttpResponse::Found()
.append_header((HeaderName::from_static("location"), location.clone()))
.finish();
}
if matches!(self, Self::Redirect(_)) {
return HttpResponse::Found()
.append_header((HeaderName::from_static("location"), "/"))
.finish();
}
let status = self.status_code();
let mut response = HttpResponse::build(status);
if status == StatusCode::UNAUTHORIZED {
response.insert_header((
HeaderName::from_static("www-authenticate"),
HeaderValue::from_static("Bearer"),
));
}
response.json(self.body())
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ErrorBody {
pub code: String,
pub message: String,
}
/// 只允许站内绝对路径:挡掉 `//host`、`/\host` 这类协议相对写法。
fn is_safe_location(location: &str) -> bool {
let bytes = location.as_bytes();
bytes.first() == Some(&b'/') && !matches!(bytes.get(1), Some(b'/') | Some(b'\\'))
}