Skip to main content

osproxy_server/
auth.rs

1//! The reference authenticator the binary uses.
2//!
3//! A minimal token authenticator: a configured `token -> principal` map. With
4//! no tokens configured it runs in **dev mode**, accepting any caller as an
5//! anonymous (or token-named) principal, convenient for local runs, never for
6//! production. Real consumers provide their own [`Authenticator`] (mTLS, JWT, an
7//! external identity provider, …).
8
9use std::collections::HashMap;
10
11use osproxy_core::PrincipalId;
12use osproxy_spi::{Action, AuthError, Authenticator, Authorizer, ClientCredentials, Principal};
13
14/// The default [`Authorizer`]: permits every authenticated principal every
15/// action. Authentication still applies; this only declines to add a second
16/// policy layer, so a deployment that wants none pays nothing. Swap in a real
17/// [`Authorizer`] via [`crate::handler::AppHandler::with_authorizer`].
18#[derive(Debug, Default, Clone, Copy)]
19pub struct AllowAllAuthorizer;
20
21impl Authorizer for AllowAllAuthorizer {
22    async fn authorize(&self, _principal: &Principal, _action: &Action) -> Result<(), AuthError> {
23        Ok(())
24    }
25}
26
27/// A bearer-token authenticator over a static `token -> principal id` map.
28///
29/// This is a **reference** implementation; a real deployment supplies its own
30/// [`Authenticator`] (OIDC, LDAP, an mTLS-subject mapping, …). Two deliberate
31/// properties follow from it being a reference, not a hardened identity provider:
32///
33/// - **Token lookup is a `HashMap::get`, not a constant-time compare.** The map's
34///   randomized `SipHash` makes a timing oracle impractical, and the privileged
35///   admin token (a single fixed secret) *does* use a constant-time compare
36///   (`crate::bearer`). A deployment that treats data-plane tokens as
37///   timing-sensitive secrets should plug in its own authenticator.
38/// - **In token mode the verified mTLS client identity is not the principal.**
39///   mTLS provides transport authentication (the cert chain is verified by the
40///   TLS layer); the principal id here comes from the token map. A deployment
41///   wanting *certificate-derived* identity supplies an authenticator that maps
42///   `client_cert_subject` to a principal.
43#[derive(Debug, Default)]
44pub struct ReferenceAuthenticator {
45    tokens: HashMap<String, String>,
46}
47
48impl ReferenceAuthenticator {
49    /// Builds an authenticator requiring one of `tokens` (token -> principal id).
50    #[must_use]
51    pub fn new(tokens: HashMap<String, String>) -> Self {
52        Self { tokens }
53    }
54
55    /// A dev-mode authenticator that accepts any caller (no tokens configured).
56    #[must_use]
57    pub fn dev() -> Self {
58        Self::default()
59    }
60
61    /// Whether the authenticator is in permissive dev mode.
62    fn is_dev(&self) -> bool {
63        self.tokens.is_empty()
64    }
65}
66
67impl Authenticator for ReferenceAuthenticator {
68    async fn authenticate(&self, creds: &ClientCredentials) -> Result<Principal, AuthError> {
69        if self.is_dev() {
70            // Dev mode: name the principal after a verified client certificate
71            // if mTLS was used, else the presented token, else "anonymous".
72            // Never rejects.
73            let id = creds
74                .client_cert_subject
75                .as_deref()
76                .or(creds.bearer_token.as_deref())
77                .unwrap_or("anonymous");
78            return Ok(Principal::new(PrincipalId::from(id)));
79        }
80        let token = creds
81            .bearer_token
82            .as_deref()
83            .ok_or(AuthError::MissingCredentials)?;
84        self.tokens
85            .get(token)
86            .map(|pid| Principal::new(PrincipalId::from(pid.as_str())))
87            .ok_or(AuthError::InvalidCredentials)
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[tokio::test]
96    async fn dev_mode_accepts_anyone() {
97        let auth = ReferenceAuthenticator::dev();
98        let p = auth
99            .authenticate(&ClientCredentials::default())
100            .await
101            .unwrap();
102        assert_eq!(p.id().as_str(), "anonymous");
103        let p = auth
104            .authenticate(&ClientCredentials::bearer("svc-x"))
105            .await
106            .unwrap();
107        assert_eq!(p.id().as_str(), "svc-x");
108    }
109
110    #[tokio::test]
111    async fn configured_tokens_are_enforced() {
112        let mut tokens = HashMap::new();
113        tokens.insert("s3cr3t".to_owned(), "svc-ingest".to_owned());
114        let auth = ReferenceAuthenticator::new(tokens);
115
116        let p = auth
117            .authenticate(&ClientCredentials::bearer("s3cr3t"))
118            .await
119            .unwrap();
120        assert_eq!(p.id().as_str(), "svc-ingest");
121
122        assert_eq!(
123            auth.authenticate(&ClientCredentials::bearer("wrong")).await,
124            Err(AuthError::InvalidCredentials)
125        );
126        assert_eq!(
127            auth.authenticate(&ClientCredentials::default()).await,
128            Err(AuthError::MissingCredentials)
129        );
130    }
131}