Skip to main content

corium_protocol/
auth.rs

1//! Authentication and TLS configuration shared by Corium servers and clients.
2//!
3//! v1 ships bearer-token auth behind a pluggable [`Authenticator`] trait and
4//! TLS via tonic/rustls (see `docs/design/protocol.md`).
5
6use std::path::Path;
7use std::sync::Arc;
8
9use tonic::metadata::MetadataMap;
10use tonic::service::Interceptor;
11use tonic::{Request, Status};
12
13/// Pluggable per-request authenticator.
14pub trait Authenticator: Send + Sync + 'static {
15    /// Accepts or rejects a request given its bearer token (if any).
16    fn authenticate(&self, bearer: Option<&str>) -> bool;
17}
18
19/// Static-token authenticator; `None` accepts every request.
20pub struct StaticToken(Option<String>);
21
22impl StaticToken {
23    /// Requires `token` on every request, or accepts all when `None`.
24    #[must_use]
25    pub const fn new(token: Option<String>) -> Self {
26        Self(token)
27    }
28}
29
30impl Authenticator for StaticToken {
31    fn authenticate(&self, bearer: Option<&str>) -> bool {
32        match &self.0 {
33            None => true,
34            Some(expected) => bearer == Some(expected.as_str()),
35        }
36    }
37}
38
39fn bearer(metadata: &MetadataMap) -> Option<&str> {
40    metadata
41        .get("authorization")
42        .and_then(|value| value.to_str().ok())
43        .and_then(|value| value.strip_prefix("Bearer "))
44}
45
46/// Server interceptor enforcing an [`Authenticator`].
47#[derive(Clone)]
48pub struct AuthInterceptor(Arc<dyn Authenticator>);
49
50impl AuthInterceptor {
51    /// Wraps an authenticator for use with `InterceptedService`.
52    #[must_use]
53    pub fn new(authenticator: Arc<dyn Authenticator>) -> Self {
54        Self(authenticator)
55    }
56}
57
58impl Interceptor for AuthInterceptor {
59    fn call(&mut self, request: Request<()>) -> Result<Request<()>, Status> {
60        if self.0.authenticate(bearer(request.metadata())) {
61            Ok(request)
62        } else {
63            Err(Status::unauthenticated("invalid or missing bearer token"))
64        }
65    }
66}
67
68/// Client interceptor attaching an optional bearer token to every request.
69#[derive(Clone, Default)]
70pub struct TokenInterceptor(Option<String>);
71
72impl TokenInterceptor {
73    /// Attaches `token` when present.
74    #[must_use]
75    pub const fn new(token: Option<String>) -> Self {
76        Self(token)
77    }
78}
79
80impl Interceptor for TokenInterceptor {
81    fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, Status> {
82        if let Some(token) = &self.0 {
83            let value = format!("Bearer {token}")
84                .parse()
85                .map_err(|_| Status::internal("token is not valid metadata"))?;
86            request.metadata_mut().insert("authorization", value);
87        }
88        Ok(request)
89    }
90}
91
92/// Builds a server TLS config from PEM cert/key files.
93///
94/// # Errors
95/// Returns an error when either file cannot be read.
96pub fn server_tls(
97    cert_pem: &Path,
98    key_pem: &Path,
99) -> Result<tonic::transport::ServerTlsConfig, std::io::Error> {
100    let cert = std::fs::read(cert_pem)?;
101    let key = std::fs::read(key_pem)?;
102    Ok(tonic::transport::ServerTlsConfig::new()
103        .identity(tonic::transport::Identity::from_pem(cert, key)))
104}
105
106/// Builds a client TLS config, optionally trusting a custom CA and
107/// overriding the domain name expected on the server certificate.
108///
109/// # Errors
110/// Returns an error when the CA file cannot be read.
111pub fn client_tls(
112    ca_pem: Option<&Path>,
113    domain: Option<&str>,
114) -> Result<tonic::transport::ClientTlsConfig, std::io::Error> {
115    let mut config = tonic::transport::ClientTlsConfig::new().with_enabled_roots();
116    if let Some(path) = ca_pem {
117        let ca = std::fs::read(path)?;
118        config = config.ca_certificate(tonic::transport::Certificate::from_pem(ca));
119    }
120    if let Some(domain) = domain {
121        config = config.domain_name(domain);
122    }
123    Ok(config)
124}