]>
Commit | Line | Data |
---|---|---|
ba39ab20 | 1 | //! Provides authentication primitives for the HTTP server |
ba39ab20 | 2 | |
26858dba | 3 | use std::sync::Arc; |
038f3850 DM |
4 | use std::future::Future; |
5 | use std::pin::Pin; | |
6 | ||
7 | use anyhow::format_err; | |
26858dba | 8 | |
98b7d58b DM |
9 | use proxmox::api::UserInformation; |
10 | ||
9eb78407 | 11 | use pbs_tools::ticket::{self, Ticket}; |
6227654a DM |
12 | use pbs_config::{token_shadow, CachedUserInfo}; |
13 | use pbs_api_types::{Authid, Userid}; | |
778c7d95 | 14 | use proxmox_rest_server::{ApiAuth, AuthError, extract_cookie}; |
9eb78407 | 15 | |
ba39ab20 | 16 | use crate::auth_helpers::*; |
ba39ab20 SR |
17 | |
18 | use hyper::header; | |
19 | use percent_encoding::percent_decode_str; | |
20 | ||
26858dba | 21 | struct UserAuthData { |
ba39ab20 SR |
22 | ticket: String, |
23 | csrf_token: Option<String>, | |
24 | } | |
25 | ||
26858dba | 26 | enum AuthData { |
ba39ab20 SR |
27 | User(UserAuthData), |
28 | ApiToken(String), | |
29 | } | |
30 | ||
26858dba SR |
31 | pub struct UserApiAuth {} |
32 | pub fn default_api_auth() -> Arc<UserApiAuth> { | |
33 | Arc::new(UserApiAuth {}) | |
34 | } | |
35 | ||
36 | impl UserApiAuth { | |
37 | fn extract_auth_data(headers: &http::HeaderMap) -> Option<AuthData> { | |
38 | if let Some(raw_cookie) = headers.get(header::COOKIE) { | |
39 | if let Ok(cookie) = raw_cookie.to_str() { | |
778c7d95 | 40 | if let Some(ticket) = extract_cookie(cookie, "PBSAuthCookie") { |
26858dba SR |
41 | let csrf_token = match headers.get("CSRFPreventionToken").map(|v| v.to_str()) { |
42 | Some(Ok(v)) => Some(v.to_owned()), | |
43 | _ => None, | |
44 | }; | |
45 | return Some(AuthData::User(UserAuthData { ticket, csrf_token })); | |
46 | } | |
ba39ab20 SR |
47 | } |
48 | } | |
ba39ab20 | 49 | |
26858dba SR |
50 | match headers.get(header::AUTHORIZATION).map(|v| v.to_str()) { |
51 | Some(Ok(v)) => { | |
52 | if v.starts_with("PBSAPIToken ") || v.starts_with("PBSAPIToken=") { | |
53 | Some(AuthData::ApiToken(v["PBSAPIToken ".len()..].to_owned())) | |
54 | } else { | |
55 | None | |
56 | } | |
ba39ab20 | 57 | } |
26858dba SR |
58 | _ => None, |
59 | } | |
ba39ab20 | 60 | } |
98b7d58b | 61 | |
038f3850 | 62 | async fn check_auth_async( |
26858dba SR |
63 | &self, |
64 | headers: &http::HeaderMap, | |
65 | method: &hyper::Method, | |
98b7d58b | 66 | ) -> Result<(String, Box<dyn UserInformation + Sync + Send>), AuthError> { |
fd6d2438 | 67 | |
038f3850 DM |
68 | // fixme: make all IO async |
69 | ||
fd6d2438 DM |
70 | let user_info = CachedUserInfo::new()?; |
71 | ||
26858dba SR |
72 | let auth_data = Self::extract_auth_data(headers); |
73 | match auth_data { | |
74 | Some(AuthData::User(user_auth_data)) => { | |
75 | let ticket = user_auth_data.ticket.clone(); | |
9eb78407 | 76 | let ticket_lifetime = ticket::TICKET_LIFETIME; |
ba39ab20 | 77 | |
26858dba SR |
78 | let userid: Userid = Ticket::<super::ticket::ApiTicket>::parse(&ticket)? |
79 | .verify_with_time_frame(public_auth_key(), "PBS", None, -300..ticket_lifetime)? | |
80 | .require_full()?; | |
81 | ||
82 | let auth_id = Authid::from(userid.clone()); | |
83 | if !user_info.is_active_auth_id(&auth_id) { | |
84 | return Err(format_err!("user account disabled or expired.").into()); | |
ba39ab20 | 85 | } |
ba39ab20 | 86 | |
26858dba SR |
87 | if method != hyper::Method::GET { |
88 | if let Some(csrf_token) = &user_auth_data.csrf_token { | |
89 | verify_csrf_prevention_token( | |
90 | csrf_secret(), | |
91 | &userid, | |
92 | &csrf_token, | |
93 | -300, | |
94 | ticket_lifetime, | |
95 | )?; | |
96 | } else { | |
97 | return Err(format_err!("missing CSRF prevention token").into()); | |
98 | } | |
99 | } | |
ba39ab20 | 100 | |
98b7d58b | 101 | Ok((auth_id.to_string(), Box::new(user_info))) |
ba39ab20 | 102 | } |
26858dba SR |
103 | Some(AuthData::ApiToken(api_token)) => { |
104 | let mut parts = api_token.splitn(2, ':'); | |
105 | let tokenid = parts | |
106 | .next() | |
107 | .ok_or_else(|| format_err!("failed to split API token header"))?; | |
108 | let tokenid: Authid = tokenid.parse()?; | |
ba39ab20 | 109 | |
26858dba SR |
110 | if !user_info.is_active_auth_id(&tokenid) { |
111 | return Err(format_err!("user account or token disabled or expired.").into()); | |
112 | } | |
113 | ||
114 | let tokensecret = parts | |
115 | .next() | |
116 | .ok_or_else(|| format_err!("failed to split API token header"))?; | |
117 | let tokensecret = percent_decode_str(tokensecret) | |
118 | .decode_utf8() | |
119 | .map_err(|_| format_err!("failed to decode API token header"))?; | |
ba39ab20 | 120 | |
1cb08a0a | 121 | token_shadow::verify_secret(&tokenid, &tokensecret)?; |
ba39ab20 | 122 | |
98b7d58b | 123 | Ok((tokenid.to_string(), Box::new(user_info))) |
26858dba SR |
124 | } |
125 | None => Err(AuthError::NoData), | |
ba39ab20 SR |
126 | } |
127 | } | |
128 | } | |
038f3850 DM |
129 | |
130 | impl ApiAuth for UserApiAuth { | |
131 | fn check_auth<'a>( | |
132 | &'a self, | |
133 | headers: &'a http::HeaderMap, | |
134 | method: &'a hyper::Method, | |
135 | ) -> Pin<Box<dyn Future<Output = Result<(String, Box<dyn UserInformation + Sync + Send>), AuthError>> + Send + 'a>> { | |
136 | Box::pin(self.check_auth_async(headers, method)) | |
137 | } | |
138 | } |