actix_firebase_auth/
impls.rs

1use actix_web::error::InternalError;
2use actix_web::http::{header, StatusCode};
3use actix_web::{dev, http::header::Header, web, FromRequest, HttpRequest};
4use actix_web::{HttpResponse, ResponseError};
5use actix_web_httpauth::headers::authorization::{Authorization, Bearer};
6use futures::future::{err, ok, Ready};
7
8use crate::jwk::{PublicKeysError, VerificationError};
9use crate::{Error, FirebaseAuth, FirebaseUser};
10
11fn status_code_from_http_err(err: &reqwest::Error) -> StatusCode {
12    let code = err.status().map_or_else(|| 500, |s| s.as_u16());
13    StatusCode::from_u16(code).unwrap_or(StatusCode::BAD_GATEWAY) // Use BAD_GATEWAY for upstream fetch failures
14}
15
16impl ResponseError for Error {
17    fn error_response(&self) -> HttpResponse {
18        HttpResponse::build(self.status_code()).json(self.to_string())
19    }
20
21    fn status_code(&self) -> StatusCode {
22        match self {
23            Error::PublicKeysError(err) => match err {
24                PublicKeysError::FetchPublicKeys(http_err)
25                | PublicKeysError::PublicKeyParseError(http_err) => {
26                    status_code_from_http_err(http_err)
27                }
28
29                PublicKeysError::MissingCacheControlHeader
30                | PublicKeysError::MissingMaxAgeDirective
31                | PublicKeysError::EmptyMaxAgeDirective
32                | PublicKeysError::InvalidMaxAgeValue => {
33                    // Indicates a misconfigured or invalid response from the identity provider
34                    StatusCode::INTERNAL_SERVER_ERROR
35                }
36            },
37
38            Error::VerificationError(err) => match err {
39                VerificationError::InvalidSignature => {
40                    // Token is invalid or tampered with
41                    StatusCode::UNAUTHORIZED
42                }
43                VerificationError::InvalidKeyAlgorithm => {
44                    // Token uses unsupported algorithm – client bug or attacker
45                    StatusCode::BAD_REQUEST
46                }
47                VerificationError::InvalidToken => {
48                    // Token is malformed or structurally invalid
49                    StatusCode::BAD_REQUEST
50                }
51                VerificationError::NoKidHeader => {
52                    // Token doesn't specify which key was used to sign – malformed
53                    StatusCode::BAD_REQUEST
54                }
55                VerificationError::NoMatchingKid => {
56                    // Token specifies a `kid` for which we have no key – likely expired key
57                    StatusCode::UNAUTHORIZED
58                }
59                VerificationError::CannotDecodePublicKeys => {
60                    // Server failed to decode key set
61                    StatusCode::INTERNAL_SERVER_ERROR
62                }
63                VerificationError::CannotDecodeJwt(_) => {
64                    // Server failed to decode key set
65                    StatusCode::UNAUTHORIZED
66                }
67            },
68        }
69    }
70}
71
72impl FromRequest for FirebaseUser {
73    type Error = actix_web::Error;
74    type Future = Ready<Result<Self, Self::Error>>;
75
76    fn from_request(req: &HttpRequest, _: &mut dev::Payload) -> Self::Future {
77        let firebase_auth = req
78            .app_data::<web::Data<FirebaseAuth>>()
79            .expect("FirebaseAuth should be initialized in application data");
80
81        let bearer = match Authorization::<Bearer>::parse(req) {
82            Ok(header) => header.into_scheme(),
83            Err(_) => {
84                // Per RFC 7235, a 401 Unauthorized response MUST be returned when the
85                // Authorization header is missing, malformed, or uses an unsupported scheme.
86                //
87                // Actix defaults to 400 Bad Request for parsing failures, which is incorrect
88                // in the context of authentication. We explicitly return 401 and include a
89                // WWW-Authenticate header to guide the client on how to authenticate.
90                return err(missing_or_malformed_auth_header());
91            }
92        };
93
94        let id_token = bearer.token();
95
96        match firebase_auth.verify(id_token) {
97            Ok(user) => ok(user),
98            Err(crate::Error::VerificationError(
99                VerificationError::CannotDecodePublicKeys,
100            )) => err(internal_token_verification_error()),
101            Err(other) => err(invalid_token_error(&other)),
102        }
103    }
104}
105
106fn internal_token_verification_error() -> actix_web::Error {
107    let response = HttpResponse::InternalServerError()
108        .body("Internal error during token verification");
109
110    InternalError::from_response("token_verification_failure", response).into()
111}
112
113fn missing_or_malformed_auth_header() -> actix_web::Error {
114    unauthorized_with_www_authenticate(
115        "invalid_request",
116        "Authorization header missing or not using Bearer scheme",
117        "Authorization header is missing or malformed",
118    )
119}
120
121fn invalid_token_error(err: &crate::Error) -> actix_web::Error {
122    unauthorized_with_www_authenticate(
123        "invalid_token",
124        &err.to_string(),
125        format!("Failed to verify Firebase ID token: {err}"),
126    )
127}
128
129/// Constructs a generic `actix_web::Error` with a `WWW-Authenticate` header if needed.
130fn unauthorized_with_www_authenticate(
131    www_error_code: &str,
132    www_error_description: &str,
133    body: impl Into<String>,
134) -> actix_web::Error {
135    let header_value = format!(
136        r#"Bearer realm="firebase", error="{www_error_code}", error_description="{www_error_description}""#
137    );
138
139    let response = HttpResponse::Unauthorized()
140        .insert_header((header::WWW_AUTHENTICATE, header_value))
141        .body(body.into());
142
143    InternalError::from_response("auth_error", response).into()
144}