autoendpoint/extractors/
authorization_check.rs

1use crate::auth::sign_with_key;
2use crate::error::{ApiError, ApiErrorKind};
3use crate::headers::util::get_header;
4use crate::server::AppState;
5use actix_web::dev::Payload;
6use actix_web::{web::Data, FromRequest, HttpRequest};
7use futures::future::LocalBoxFuture;
8use futures::FutureExt;
9use openssl::error::ErrorStack;
10use uuid::Uuid;
11
12use autopush_common::util::user_agent::UserAgentInfo;
13
14/// Verifies the request authorization via the authorization header.
15///
16/// The expected token is the HMAC-SHA256 hash of the UAID, signed with one of
17/// the available keys (allows for key rotation).
18/// NOTE: This is *ONLY* for internal calls that require authorization and should
19///      NOT be used by calls that are using VAPID authentication (e.g.
20///      subscription provider endpoints)
21pub struct AuthorizationCheck {
22    pub user_agent: UserAgentInfo,
23}
24
25impl AuthorizationCheck {
26    pub fn generate_token(auth_key: &str, user: &Uuid) -> Result<String, ErrorStack> {
27        sign_with_key(auth_key.as_bytes(), user.as_simple().to_string().as_bytes())
28    }
29
30    pub fn validate_token(
31        token: &str,
32        uaid: &Uuid,
33        auth_keys: &[String],
34        user_agent: UserAgentInfo,
35    ) -> Result<Self, ApiError> {
36        // Check the token against the expected token for each key
37        for key in auth_keys {
38            let expected_token =
39                sign_with_key(key.as_bytes(), uaid.as_simple().to_string().as_bytes())
40                    .map_err(ApiErrorKind::RegistrationSecretHash)?;
41
42            debug!("expected: {:?}, recv'd {:?}", &expected_token, &token);
43            if expected_token.len() == token.len()
44                && openssl::memcmp::eq(expected_token.as_bytes(), token.as_bytes())
45            {
46                return Ok(Self { user_agent });
47            }
48        }
49        Err(ApiErrorKind::InvalidLocalAuth("incorrect auth token".to_owned()).into())
50    }
51}
52
53impl FromRequest for AuthorizationCheck {
54    type Error = ApiError;
55    type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>;
56
57    fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
58        let req = req.clone();
59
60        async move {
61            let uaid = req
62                .match_info()
63                .get("uaid")
64                .expect("{uaid} must be part of the path")
65                .parse::<Uuid>()
66                .map_err(|_| ApiErrorKind::NoUser)?;
67            let state: Data<AppState> = Data::extract(&req)
68                .into_inner()
69                .expect("No server state found");
70            let auth_header = get_header(&req, "Authorization")
71                .ok_or_else(|| ApiErrorKind::InvalidLocalAuth("missing auth header".to_owned()))?;
72            let token = get_token_from_auth_header(auth_header)
73                .ok_or_else(|| ApiErrorKind::InvalidLocalAuth("missing auth token".to_owned()))?;
74            let user_agent = UserAgentInfo::from(&req);
75
76            Self::validate_token(token, &uaid, &state.settings.auth_keys(), user_agent)
77        }
78        .boxed_local()
79    }
80}
81
82/// Get the token from a bearer authorization header
83fn get_token_from_auth_header(header: &str) -> Option<&str> {
84    let mut split = header.splitn(2, ' ');
85    let scheme = split.next()?;
86
87    // An error in the android push component code uses "webpush" to identify
88    // the local authorization header. We need to allow for that.
89    if !["bearer", "webpush"].contains(&scheme.to_lowercase().as_str()) {
90        return None;
91    }
92
93    split.next()
94}
95
96#[cfg(test)]
97mod test {
98
99    use crate::error::ApiResult;
100    use autopush_common::util::user_agent::UserAgentInfo;
101
102    use super::*;
103
104    #[test]
105    fn test_signature() -> ApiResult<()> {
106        // hopefully no-op type test to check locally generated tokens.
107        let uaid: Uuid = "729e5104f5f04abc9196085340317dea".parse().unwrap();
108        let auth_keys = ["HJVPy4ZwF4Yz_JdvXTL8hRcwIhv742vC60Tg5Ycrvw8=".to_owned()].to_vec();
109        let token = AuthorizationCheck::generate_token(auth_keys.first().unwrap(), &uaid).unwrap();
110
111        AuthorizationCheck::validate_token(&token, &uaid, &auth_keys, UserAgentInfo::default())?;
112        Ok(())
113    }
114
115    #[test]
116    fn test_legacy_signature() -> ApiResult<()> {
117        // check a previously generated python token.
118        // original python uaids are lower case all hex.
119        let uaid: Uuid = "729e5104f5f04abc9196085340317dea".parse().unwrap();
120        // Auth keys are strings. don't run through base64!
121        let auth_keys = ["HJVPy4ZwF4Yz_JdvXTL8hRcwIhv742vC60Tg5Ycrvw8=".to_owned()].to_vec();
122        // the following token was generated using the old python application.
123        let legacy_token = "f694963453adf5dedcc379bbdd6900d692b6e09f1c91f44169bfcd2f941bf36c";
124        // pop the firstkey off of the auth_key list.
125        let selected = auth_keys.first().unwrap();
126        let token = AuthorizationCheck::generate_token(selected, &uaid).unwrap();
127
128        assert_eq!(&token, legacy_token);
129        Ok(())
130    }
131
132    #[test]
133    fn test_token_extractor() -> ApiResult<()> {
134        let uaid: Uuid = "729e5104f5f04abc9196085340317dea".parse().unwrap();
135        let auth_keys = ["HJVPy4ZwF4Yz_JdvXTL8hRcwIhv742vC60Tg5Ycrvw8=".to_owned()].to_vec();
136        let token = AuthorizationCheck::generate_token(auth_keys.first().unwrap(), &uaid).unwrap();
137
138        assert!(get_token_from_auth_header(&format!("bearer {}", &token)).is_some());
139        assert!(get_token_from_auth_header(&format!("webpush {}", &token)).is_some());
140        assert!(get_token_from_auth_header(&format!("random {}", &token)).is_none());
141        Ok(())
142    }
143}