create_rust_app/auth/extractors/
auth_actixweb.rs

1use super::auth::Auth;
2use crate::auth::{permissions::Permission, AccessTokenClaims};
3use actix_web::dev::Payload;
4use actix_web::error::ResponseError;
5use actix_web::http::StatusCode;
6use actix_web::{FromRequest, HttpRequest, HttpResponse};
7use derive_more::{Display, Error};
8use futures::future::{ready, Ready};
9use jsonwebtoken::decode;
10use jsonwebtoken::DecodingKey;
11use jsonwebtoken::Validation;
12use serde_json::json;
13use std::collections::HashSet;
14
15#[derive(Debug, Display, Error)]
16#[display(fmt = "Unauthorized ({status:?}), reason: {reason:?}")]
17/// custom error type for Authorization related errors
18pub struct AuthError {
19    reason: String,
20    status: StatusCode,
21}
22
23impl AuthError {
24    #[must_use]
25    pub const fn new(reason: String, status: StatusCode) -> Self {
26        Self { reason, status }
27    }
28
29    #[must_use]
30    pub const fn reason(reason: String) -> Self {
31        Self {
32            reason,
33            status: StatusCode::UNAUTHORIZED,
34        }
35    }
36}
37
38impl ResponseError for AuthError {
39    /// builds an [`HttpResponse`] for [`self`](`AuthError`)
40    fn error_response(&self) -> HttpResponse {
41        HttpResponse::build(self.status_code()).body(
42            json!({
43              "message": self.reason.as_str()
44            })
45            .to_string(),
46        )
47    }
48
49    /// return the [`StatusCode`] associated with an [`AuthError`]
50    fn status_code(&self) -> StatusCode {
51        StatusCode::UNAUTHORIZED
52    }
53}
54
55impl FromRequest for Auth {
56    type Future = Ready<Result<Self, Self::Error>>;
57    type Error = AuthError;
58
59    /// extracts [`Auth`] from the given [`req`](`HttpRequest`)
60    fn from_request(req: &HttpRequest, _payload: &mut Payload) -> <Self as FromRequest>::Future {
61        let access_token_str = match req.headers().get("Authorization").map(|h| h.to_str()) {
62            Some(Ok(auth_header)) if auth_header.starts_with("Bearer ") => auth_header,
63            Some(_) => {
64                return ready(Err(AuthError::reason(
65                    "Invalid authorization header".to_string(),
66                )))
67            }
68            None => {
69                return ready(Err(AuthError::reason(
70                    "Authorization header required".to_string(),
71                )))
72            }
73        };
74
75        let access_token = match decode::<AccessTokenClaims>(
76            access_token_str.trim_start_matches("Bearer "),
77            &DecodingKey::from_secret(std::env::var("SECRET_KEY").unwrap().as_ref()),
78            &Validation::default(),
79        ) {
80            Ok(token) if token.claims.token_type.eq_ignore_ascii_case("access_token") => token,
81            _ => return ready(Err(AuthError::reason("Invalid access token".to_string()))),
82        };
83
84        let user_id = access_token.claims.sub;
85        let permissions: HashSet<Permission> =
86            access_token.claims.permissions.iter().cloned().collect();
87        let roles: HashSet<String> = access_token.claims.roles.iter().cloned().collect();
88
89        ready(Ok(Self {
90            user_id,
91            roles,
92            permissions,
93        }))
94    }
95}