1pub mod bearer;
23pub mod introspect;
24pub mod mtls;
25pub mod oauth;
26
27#[cfg(feature = "http")]
28pub mod proof;
29
30#[cfg(feature = "jwt")]
31pub mod jwt;
32
33#[cfg(feature = "oauth-pkce")]
34pub mod pkce;
35
36#[cfg(feature = "oauth-pkce-server")]
37pub mod pkce_server;
38
39use std::collections::BTreeMap;
40use std::sync::Arc;
41
42use crate::errors::RpcError;
43
44#[derive(Clone, Debug, Default)]
50pub struct AuthContext {
51 pub domain: String,
54 pub authenticated: bool,
56 pub principal: String,
58 pub claims: BTreeMap<String, String>,
60}
61
62impl AuthContext {
63 pub fn anonymous() -> Self {
65 Self::default()
66 }
67
68 pub fn for_principal(domain: impl Into<String>, principal: impl Into<String>) -> Self {
70 Self {
71 domain: domain.into(),
72 authenticated: true,
73 principal: principal.into(),
74 claims: BTreeMap::new(),
75 }
76 }
77
78 pub fn require_authenticated(&self) -> crate::errors::Result<()> {
80 if self.authenticated {
81 Ok(())
82 } else {
83 Err(RpcError::permission_error("authentication required"))
84 }
85 }
86
87 pub fn with_claim(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
89 self.claims.insert(key.into(), value.into());
90 self
91 }
92}
93
94#[derive(Debug)]
99pub struct AuthRequest<'a> {
100 pub method: &'a str,
101 pub headers: &'a [(String, String)],
102 pub peer_addr: Option<&'a str>,
103}
104
105impl<'a> AuthRequest<'a> {
106 pub fn anonymous_pipe(method: &'a str) -> Self {
108 Self {
109 method,
110 headers: &[],
111 peer_addr: None,
112 }
113 }
114
115 pub fn header(&self, name: &str) -> Option<&str> {
117 self.headers
118 .iter()
119 .find(|(k, _)| k.eq_ignore_ascii_case(name))
120 .map(|(_, v)| v.as_str())
121 }
122}
123
124pub type AuthResult = std::result::Result<AuthContext, RpcError>;
129
130pub type Authenticate = Arc<dyn Fn(&AuthRequest<'_>) -> AuthResult + Send + Sync>;
135
136pub fn chain_authenticate(a: Authenticate, b: Authenticate) -> Authenticate {
150 Arc::new(move |req| {
151 let first = (a)(req)?;
152 if first.authenticated {
153 return Ok(first);
154 }
155 (b)(req)
156 })
157}
158
159pub(crate) fn extract_bearer<'a>(req: &'a AuthRequest<'a>) -> Option<&'a str> {
164 let h = req.header("authorization")?;
165 let prefix = "Bearer ";
166 if h.len() > prefix.len() && h[..prefix.len()].eq_ignore_ascii_case(prefix) {
167 let tok = h[prefix.len()..].trim();
168 (!tok.is_empty()).then_some(tok)
169 } else {
170 None
171 }
172}
173
174pub fn chain_all<I: IntoIterator<Item = Authenticate>>(cbs: I) -> Option<Authenticate> {
176 let mut it = cbs.into_iter();
177 let mut acc = it.next()?;
178 for next in it {
179 acc = chain_authenticate(acc, next);
180 }
181 Some(acc)
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn require_authenticated_rejects_anonymous() {
190 let anon = AuthContext::anonymous();
191 assert!(anon.require_authenticated().is_err());
192 let authd = AuthContext::for_principal("bearer", "alice");
193 assert!(authd.require_authenticated().is_ok());
194 }
195
196 #[test]
197 fn chain_tries_second_when_first_anonymous() {
198 let a: Authenticate = Arc::new(|_| Ok(AuthContext::anonymous()));
199 let b: Authenticate = Arc::new(|_| Ok(AuthContext::for_principal("bearer", "alice")));
200 let chain = chain_authenticate(a, b);
201 let req = AuthRequest::anonymous_pipe("echo");
202 let ctx = chain(&req).unwrap();
203 assert_eq!(ctx.principal, "alice");
204 }
205
206 #[test]
207 fn chain_propagates_a_transient_failure_rather_than_advancing() {
208 let down: Authenticate = Arc::new(|_| Err(RpcError::auth_unavailable("jwks fetch failed")));
213 let reached = Arc::new(std::sync::atomic::AtomicBool::new(false));
214 let flag = reached.clone();
215 let next: Authenticate = Arc::new(move |_| {
216 flag.store(true, std::sync::atomic::Ordering::SeqCst);
217 Ok(AuthContext::for_principal("bearer", "alice"))
218 });
219 let req = AuthRequest::anonymous_pipe("echo");
220
221 for chain in [
222 chain_authenticate(down.clone(), next.clone()),
223 chain_all([down.clone(), next.clone()]).expect("non-empty"),
224 ] {
225 let err = chain(&req).expect_err("an outage must not resolve to an identity");
226 assert!(err.is_auth_unavailable(), "reclassified as {err}");
227 assert_eq!(err.retry_after_seconds, Some(5));
228 }
229 assert!(
230 !reached.load(std::sync::atomic::Ordering::SeqCst),
231 "the chain advanced past an outage"
232 );
233 }
234
235 #[test]
236 fn chain_uses_first_when_authenticated() {
237 let a: Authenticate = Arc::new(|_| Ok(AuthContext::for_principal("mtls", "bob")));
238 let b: Authenticate = Arc::new(|_| Ok(AuthContext::for_principal("bearer", "alice")));
239 let chain = chain_authenticate(a, b);
240 let req = AuthRequest::anonymous_pipe("echo");
241 assert_eq!(chain(&req).unwrap().principal, "bob");
242 }
243}