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}