use actix_web::http::StatusCode; use actix_web::http::header::{HeaderName, HeaderValue}; use actix_web::{HttpResponse, ResponseError}; use serde::{Deserialize, Serialize}; pub type JwtResult = std::result::Result; #[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'\\')) }