Skip to main content

objectstore_server/extractors/
service.rs

1use axum::extract::FromRequestParts;
2use axum::http::{header, request::Parts};
3
4use crate::auth::{AuthAwareService, AuthContext};
5use crate::endpoints::common::ApiError;
6use crate::state::ServiceState;
7
8const BEARER_PREFIX: &str = "Bearer ";
9
10/// Custom header for Objectstore authentication. Checked before the standard
11/// `Authorization` header so that proxy setups (e.g. Django) can use
12/// `Authorization` for their own auth while forwarding an Objectstore token in
13/// this header.
14const OBJECTSTORE_AUTH_HEADER: &str = "x-os-auth";
15
16impl FromRequestParts<ServiceState> for AuthAwareService {
17    type Rejection = ApiError;
18
19    async fn from_request_parts(
20        parts: &mut Parts,
21        state: &ServiceState,
22    ) -> Result<Self, Self::Rejection> {
23        let encoded_token = parts
24            .headers
25            .get(OBJECTSTORE_AUTH_HEADER)
26            .or_else(|| parts.headers.get(header::AUTHORIZATION))
27            .and_then(|v| v.to_str().ok())
28            .and_then(strip_bearer);
29
30        let enforce = state.config.auth.enforce;
31        // Attempt to decode / verify the JWT, logging failure
32        let auth_result = AuthContext::from_encoded_jwt(encoded_token, &state.key_directory)
33            .inspect_err(|err| err.log(None, None, enforce));
34
35        // If auth enforcement is enabled, `from_encoded_jwt()` must have succeeded.
36        // If auth enforcement is disabled, we'll pass the context along if it succeeded but will
37        // still proceed with `None` if it failed.
38        let auth_context = match enforce {
39            true => Some(auth_result?),
40            false => auth_result.ok(),
41        };
42
43        AuthAwareService::new(
44            state.service.clone(),
45            auth_context,
46            state.config.auth.enforce,
47        )
48    }
49}
50
51fn strip_bearer(header_value: &str) -> Option<&str> {
52    let (prefix, tail) = header_value.split_at_checked(BEARER_PREFIX.len())?;
53    if prefix.eq_ignore_ascii_case(BEARER_PREFIX) {
54        Some(tail)
55    } else {
56        None
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn test_strip_bearer() {
66        // Prefix matches
67        assert_eq!(strip_bearer("Bearer tokenvalue"), Some("tokenvalue"));
68        assert_eq!(strip_bearer("bearer tokenvalue"), Some("tokenvalue"));
69        assert_eq!(strip_bearer("BEARER tokenvalue"), Some("tokenvalue"));
70
71        // Prefix doesn't match
72        assert_eq!(strip_bearer("Token tokenvalue"), None);
73        assert_eq!(strip_bearer("Bearer"), None);
74
75        // No character boundary at end of expected prefix
76        assert_eq!(strip_bearer("Bearer⚠️tokenvalue"), None);
77    }
78}