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/// The built-in development bearer token.
14///
15/// Every `corium` CLI program defaults to this token — servers accept it and
16/// clients present it — so a local database works with no auth configuration:
17/// start a transactor, create a database, and open a console without passing a
18/// single credential flag. Override it with `--token` / `--serve-token` or the
19/// `CORIUM_TOKEN` environment variable.
20///
21/// It is deliberately a fixed, well-known string: it exists to make local
22/// experimentation frictionless, **not** to secure anything. Never expose a
23/// surface that accepts it to a network anyone else can reach; set a real
24/// secret (or an OIDC issuer) there instead.
25pub const DEFAULT_DEV_TOKEN: &str = "corium-dev-insecure-token";
26
27/// Pluggable per-request authenticator.
28pub trait Authenticator: Send + Sync + 'static {
29    /// Accepts or rejects a request given its bearer token (if any).
30    fn authenticate(&self, bearer: Option<&str>) -> bool;
31}
32
33/// Static-token authenticator; `None` accepts every request.
34pub struct StaticToken(Option<String>);
35
36impl StaticToken {
37    /// Requires `token` on every request, or accepts all when `None`.
38    #[must_use]
39    pub const fn new(token: Option<String>) -> Self {
40        Self(token)
41    }
42}
43
44impl Authenticator for StaticToken {
45    fn authenticate(&self, bearer: Option<&str>) -> bool {
46        match &self.0 {
47            None => true,
48            Some(expected) => bearer == Some(expected.as_str()),
49        }
50    }
51}
52
53fn bearer(metadata: &MetadataMap) -> Option<&str> {
54    metadata
55        .get("authorization")
56        .and_then(|value| value.to_str().ok())
57        .and_then(|value| value.strip_prefix("Bearer "))
58}
59
60/// Server interceptor enforcing an [`Authenticator`].
61#[derive(Clone)]
62pub struct AuthInterceptor(Arc<dyn Authenticator>);
63
64impl AuthInterceptor {
65    /// Wraps an authenticator for use with `InterceptedService`.
66    #[must_use]
67    pub fn new(authenticator: Arc<dyn Authenticator>) -> Self {
68        Self(authenticator)
69    }
70}
71
72impl Interceptor for AuthInterceptor {
73    fn call(&mut self, request: Request<()>) -> Result<Request<()>, Status> {
74        if self.0.authenticate(bearer(request.metadata())) {
75            Ok(request)
76        } else {
77            Err(Status::unauthenticated("invalid or missing bearer token"))
78        }
79    }
80}
81
82/// Client interceptor attaching an optional bearer token to every request.
83#[derive(Clone, Default)]
84pub struct TokenInterceptor(Option<String>);
85
86impl TokenInterceptor {
87    /// Attaches `token` when present.
88    #[must_use]
89    pub const fn new(token: Option<String>) -> Self {
90        Self(token)
91    }
92}
93
94impl Interceptor for TokenInterceptor {
95    fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, Status> {
96        if let Some(token) = &self.0 {
97            let value = format!("Bearer {token}")
98                .parse()
99                .map_err(|_| Status::internal("token is not valid metadata"))?;
100            request.metadata_mut().insert("authorization", value);
101        }
102        Ok(request)
103    }
104}
105
106/// Builds a server TLS config from PEM cert/key files.
107///
108/// # Errors
109/// Returns an error when either file cannot be read.
110pub fn server_tls(
111    cert_pem: &Path,
112    key_pem: &Path,
113) -> Result<tonic::transport::ServerTlsConfig, std::io::Error> {
114    let cert = std::fs::read(cert_pem)?;
115    let key = std::fs::read(key_pem)?;
116    Ok(tonic::transport::ServerTlsConfig::new()
117        .identity(tonic::transport::Identity::from_pem(cert, key)))
118}
119
120/// Builds a client TLS config, optionally trusting a custom CA and
121/// overriding the domain name expected on the server certificate.
122///
123/// # Errors
124/// Returns an error when the CA file cannot be read.
125pub fn client_tls(
126    ca_pem: Option<&Path>,
127    domain: Option<&str>,
128) -> Result<tonic::transport::ClientTlsConfig, std::io::Error> {
129    let mut config = tonic::transport::ClientTlsConfig::new().with_enabled_roots();
130    if let Some(path) = ca_pem {
131        let ca = std::fs::read(path)?;
132        config = config.ca_certificate(tonic::transport::Certificate::from_pem(ca));
133    }
134    if let Some(domain) = domain {
135        config = config.domain_name(domain);
136    }
137    Ok(config)
138}