winauth/http/
mod.rs

1//! Components to perform HTTP-based winauth authentication with a generic http library
2
3/// The response, that the user of the API is expected to transform into a HTTP response to the client
4pub struct Response {
5    pub headers: Vec<(&'static str, String)>,
6    pub status_code: u16,
7}
8
9/// The current authentication state that incoming data transitioned into
10pub enum AuthState {
11    /// Authentication was performed.
12    Success,
13    /// An authentication HTTP response that should be sent to the other party (client/server)
14    Response(Response),
15    /// Client only. The server does not support or wish authentication.
16    NotRequested,
17}
18
19/// An authenticator that can authenticate incoming requests for servers
20pub trait Authenticator: crate::NextBytes {
21    /// HTTP auth schemes, as defined in RFC7235
22    fn auth_scheme(&self) -> &'static str;
23
24    /// Performs authentication against a received request from the client. 
25    /// If authentication is incomplete, the caller is instructed through AuthState::Response 
26    /// to send the http response contained in AuthState::Response (401) to the client.
27    /// After a full authentication attempt, do not call this method on the same Authenticator instance again.
28    fn http_incoming_auth<'a, R>(&'a mut self, get_header: R) -> Result<AuthState, Box<dyn std::error::Error>>
29    where
30        R: Fn(&'static str) -> Result<Option<&'a str>, Box<dyn std::error::Error>> 
31    {
32        let auth_scheme = self.auth_scheme();
33
34        let auth_bytes = match get_header("Authorization")? {
35            None => {
36                // Initially prompt the client that we require authentication
37                return Ok(AuthState::Response(Response {
38                    headers: vec![("WWW-Authenticate", auth_scheme.to_owned())],
39                    status_code: 401,
40                }))
41            }
42            Some(header) => {
43                // Extract received challenge
44                if !header.starts_with(auth_scheme) {
45                    return Err(format!("unsupported auth scheme: {}", header))?;
46                }
47                let challenge = header.trim_start_matches(auth_scheme).trim_start();
48                base64::decode(challenge).map_err(|err| format!("Malformed Base64 in Authorization header: {:?}", err))?
49            }
50        };
51        // Get response, if we're not done yet
52        if let Some(next_bytes) = self.next_bytes(Some(&auth_bytes))? {
53            return Ok(AuthState::Response(Response {
54                headers: vec![("WWW-Authenticate", format!("{} {}", auth_scheme, base64::encode(&next_bytes)))],
55                status_code: 401,
56            }));
57        }
58
59        Ok(AuthState::Success)
60    }
61
62    /// Provide an authentication state so the caller can retry an outgoing request until the server
63    /// has all needed authentication information.
64    /// If authentication is incomplete, the caller is instructed through AuthState::Response 
65    /// to retry the http request to the server with the headers contained in AuthState::Response.
66    /// After a full authentication attempt, do not call this method on the same Authenticator instance again.
67    fn http_outgoing_auth<'a, F>(&'a mut self, get_header: F) -> Result<AuthState, Box<dyn std::error::Error>>
68    where
69        F: Fn(&'static str) -> Result<Vec<&'a str>, Box<dyn std::error::Error>>
70    {
71        let methods = get_header("WWW-Authenticate")?;
72        let auth_scheme = self.auth_scheme();
73
74        // Start a new authentication process
75        if methods.contains(&auth_scheme) {
76            if let Some(next_bytes) = self.next_bytes(None)? {
77                return Ok(AuthState::Response(Response {
78                    headers: vec![("Authorization", format!("{} {}", auth_scheme, base64::encode(&next_bytes)))],
79                    status_code: 0,
80                }));
81            }
82        }
83        // Continue authentication, if already started
84        else if methods.len() == 1 && methods[0].starts_with(auth_scheme) {
85            let challenge = methods[0].trim_start_matches(auth_scheme).trim_start();
86            let in_bytes = base64::decode(challenge).map_err(|err| format!("Malformed Base64 in WWW-Authenticate header: {:?}", err))?;
87            if let Some(next_bytes) = self.next_bytes(Some(&in_bytes))? {
88                return Ok(AuthState::Response(Response { 
89                    headers: vec![("Authorization", format!("{} {}", auth_scheme, base64::encode(&next_bytes)))],
90                    status_code: 0,
91                }));
92            }
93            return Ok(AuthState::Success);
94        }
95
96        // No authentication is possible / required (requested by the server)
97        Ok(AuthState::NotRequested)
98    }
99}