Skip to main content

arminius_lib_nats/
credentials.rs

1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4//! NATS credentials, that can be turned into async_nats::ConnectOptions
5
6mod tests;
7
8use std::{env, fmt};
9
10use arminius_lib_config::ArminiusConfigurationError;
11use nkeys::KeyPair;
12
13/// Information required to authenticate against NATS server
14#[derive(Clone)]
15#[expect(
16	clippy::large_enum_variant,
17	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
18)]
19pub enum NatsCredentials {
20	/// connect anonymously
21	None,
22	/// Authenticate via JWT and private key
23	Jwt(NatsJwtCredentials),
24	/// Authenticate via username and plaintext password.
25	UserAndPassword {
26		/// username
27		user: String,
28		/// plaintext password
29		password: String,
30	},
31}
32
33impl NatsCredentials {
34	/// name of the auth type environment variable
35	const AUTH_TYPE: &'static str = "NATS_AUTH_TYPE";
36	/// name of the auth token environment variable
37	const AUTH_TOKEN: &'static str = "NATS_AUTH_TOKEN";
38	/// name of the auth key environment variable.
39	const AUTH_KEY: &'static str = "NATS_AUTH_KEY";
40
41	/// Attempts to parse [`NatsCredentials`] from environment vars.
42	///
43	/// The following env vars are taken into account:
44	///
45	/// - NATS_AUTH_TYPE : the authentication mechanism to use.
46	///   - If not set, [`NatsCredentials::None`] is returned.
47	///   - if set to JWT, the following vars need to be given:
48	/// 	- NATS_AUTH_TOKEN a JWT token denoting a user
49	///     - NATS_AUTH_KEY a Signing Key that is used to sign server nonces
50	///
51	/// As of now, only `JWT` is supported.
52	/// If the var is not set, [`NatsCredentials::None`] is returned.
53	pub fn from_env() -> Result<Self, ArminiusConfigurationError> {
54		Self::parse_from_env(&env::vars())
55	}
56
57	/// Attempts to parse Credentials from the given environment.
58	/// Currently, this only supports credentials of the type
59	/// [`NatsCredentials::Jwt`].
60	fn parse_from_env(env: &impl Env) -> Result<Self, ArminiusConfigurationError> {
61		match Self::get_env_var(env, Self::AUTH_TYPE) {
62			Ok(value) if value.eq_ignore_ascii_case("JWT") => Self::parse_jwt_credentials(env),
63			Ok(other_value) => {
64				Err(ArminiusConfigurationError::malformed(Self::AUTH_TYPE, Some(other_value)))
65			}
66			Err(ArminiusConfigurationError::MissingConfigValue { .. }) => Ok(NatsCredentials::None),
67			Err(other) => Err(other),
68		}
69	}
70
71	/// Attempts to parse a [`NatsCredentials::Jwt`] from the given environment.
72	fn parse_jwt_credentials(env: &impl Env) -> Result<Self, ArminiusConfigurationError> {
73		let token = Self::get_env_var(env, Self::AUTH_TOKEN)?;
74		let key = Self::get_env_var(env, Self::AUTH_KEY)?;
75		let signing_key = KeyPair::from_seed(&key).map_err(|err| {
76			ArminiusConfigurationError::malformed(
77				Self::AUTH_KEY,
78				format!("error decoding key: {err:?}"),
79			)
80		})?;
81		Ok(NatsCredentials::Jwt(NatsJwtCredentials { key: signing_key, token }))
82	}
83
84	/// Retrieves an environment variable, translating the error to a
85	/// [`ArminiusConfigurationError`]
86	fn get_env_var(
87		env: &impl Env,
88		var: &'static str,
89	) -> Result<EnvVarValue, ArminiusConfigurationError> {
90		env.get(var).map_err(|err| match err {
91			env::VarError::NotPresent => ArminiusConfigurationError::missing(var),
92			env::VarError::NotUnicode(invalid_value) => ArminiusConfigurationError::malformed(
93				var,
94				Some(invalid_value.to_string_lossy().to_string()),
95			),
96		})
97	}
98}
99
100impl From<NatsCredentials> for async_nats::ConnectOptions {
101	fn from(val: NatsCredentials) -> Self {
102		use async_nats::ConnectOptions;
103		match val {
104			NatsCredentials::None => ConnectOptions::default(),
105			NatsCredentials::Jwt(NatsJwtCredentials { key, token }) => {
106				ConnectOptions::with_jwt(token, move |nonce| {
107					tracing::trace!("signing server nonce: {nonce:?}");
108					let signing_key = key.clone();
109					async move {
110						signing_key.sign(&nonce).map_err(|err| {
111							async_nats::AuthError::new(format!(
112								"signing server nonce failed: {err:?}"
113							))
114						})
115					}
116				})
117			}
118			NatsCredentials::UserAndPassword { user, password } => {
119				ConnectOptions::default().user_and_password(user, password)
120			}
121		}
122	}
123}
124
125impl fmt::Debug for NatsCredentials {
126	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127		let kind = match self {
128			NatsCredentials::None => "None",
129			NatsCredentials::Jwt { .. } => "Jwt",
130			NatsCredentials::UserAndPassword { .. } => "UserAndPassword",
131		};
132		write!(f, "NatsCredentials::{kind}")
133	}
134}
135
136/// Credentials to authenticate to a NATS server via JWT and Private key.
137///
138/// The key is required for signing nonces that
139/// a server may send send every now and then as a proof that we're actually
140/// in possession of the key used to sign the JWT.
141#[derive(Debug, Clone)]
142pub struct NatsJwtCredentials {
143	/// The Key Pair required to sign server nonces
144	pub key: KeyPair,
145	/// The JWT required for authentication
146	pub token: String,
147}
148
149impl From<NatsJwtCredentials> for NatsCredentials {
150	fn from(value: NatsJwtCredentials) -> Self {
151		NatsCredentials::Jwt(value)
152	}
153}
154
155/// A thin wrapper around [`env::Vars`] to allows dummy implementations for unit
156/// testing. Code that deals with [`env::Vars`] is often hard to test, since
157/// altering env vars in a test may affect other tests that are running
158/// concurrently. This trait can easily be implemented for
159/// a [`std::collections::HashMap`], thus keeping updates locally.
160trait Env {
161	/// Fetches the value of a given environment variable.
162	/// Returns an error if no such variable is present.
163	fn get(&self, name: &'static str) -> Result<EnvVarValue, env::VarError>;
164}
165
166impl Env for env::Vars {
167	fn get(&self, name: &'static str) -> Result<String, env::VarError> {
168		env::var(name)
169	}
170}
171
172/// A value taken from an Environment Variable
173type EnvVarValue = String;