graphql_starter/auth/
extractor.rs

1use axum::extract::{FromRequestParts, OptionalFromRequestParts};
2use http::request::Parts;
3
4use super::{AuthErrorCode, AuthState, AuthenticationService, Subject};
5use crate::error::{err, ApiError, MapToErr, OkOrErr, Result};
6
7/// This extractor will authenticate the request by inspecting both the authentication header and cookie.
8///
9/// It also implements [OptionalFromRequestParts] so it can be optionally extracted, returning [None] if there is no
10/// auth header or cookie, but failing if they're present but not valid.
11pub struct Auth<S: Subject>(pub S);
12
13impl<S, St> OptionalFromRequestParts<St> for Auth<S>
14where
15    S: Subject,
16    St: AuthState<S> + Send + Sync,
17{
18    type Rejection = Box<ApiError>;
19
20    async fn from_request_parts(parts: &mut Parts, state: &St) -> Result<Option<Self>, Self::Rejection> {
21        // Extract the auth header (if any)
22        let auth_header_name = state.authn().header_name();
23        let auth_token = parts
24            .headers
25            .get(auth_header_name)
26            .map(|v| {
27                v.to_str().map_err(|err| {
28                    err!(
29                        AuthErrorCode::AuthMalformedAuthHeader {
30                            auth_header: auth_header_name.into(),
31                        },
32                        "Couldn't parse auth header value"
33                    )
34                    .with_source(err)
35                })
36            })
37            .transpose()?
38            .filter(|t| !t.is_empty());
39
40        // Extract the auth cookie (if any)
41        let auth_cookie_name = state.authn().cookie_name();
42        let auth_cookie_value = parts
43            .headers
44            .get(http::header::COOKIE)
45            .map(|v| {
46                v.to_str()
47                    .map_to_err_with(AuthErrorCode::AuthMalformedCookies, "Couldn't parse request cookies")
48            })
49            .transpose()?
50            .and_then(|cookies| {
51                cookies
52                    .split("; ")
53                    .find_map(|cookie| cookie.strip_prefix(&format!("{auth_cookie_name}=")))
54            })
55            .filter(|c| !c.is_empty());
56
57        // Authenticate the subject
58        if auth_token.is_none() && auth_cookie_value.is_none() {
59            Ok(None)
60        } else {
61            let subject = match state.authn().authenticate(auth_token, auth_cookie_value).await {
62                Ok(s) => s,
63                Err(err) => {
64                    let is_invalid_token = err.info().code() == "AUTH_INVALID_TOKEN";
65                    let mut err: Box<ApiError> = err.into();
66                    // If the authentication fails because the token is invalid, remove the auth cookie if set
67                    // If the cookie is HttpOnly, clients are not able to remove it manually when invalid
68                    if auth_cookie_value.is_some() && is_invalid_token {
69                        err = err.with_header(
70                            "Set-Cookie",
71                            format!("{auth_cookie_name}=invalid; Expires=Thu, 01 Jan 1970 00:00:00 GMT"),
72                        );
73                    }
74                    return Err(err);
75                }
76            };
77            tracing::trace!("Authenticated as {subject}");
78            Ok(Some(Self(subject)))
79        }
80    }
81}
82
83impl<S, St> FromRequestParts<St> for Auth<S>
84where
85    S: Subject,
86    St: AuthState<S> + Send + Sync,
87{
88    type Rejection = Box<ApiError>;
89
90    async fn from_request_parts(parts: &mut Parts, state: &St) -> Result<Self, Self::Rejection> {
91        Ok(<Self as OptionalFromRequestParts<St>>::from_request_parts(parts, state)
92            .await?
93            .ok_or_err_with(AuthErrorCode::AuthMissing, "The subject must be authenticated")?)
94    }
95}