1#[cfg(feature = "aws-auth")]
5pub(crate) mod aws;
6pub mod oidc;
8mod plain;
9mod sasl;
10mod scram;
11#[cfg(test)]
12mod test;
13mod x509;
14
15use std::{borrow::Cow, fmt::Debug, str::FromStr};
16
17use bson::RawDocumentBuf;
18use derive_where::derive_where;
19use hmac::{digest::KeyInit, Mac};
20use rand::Rng;
21use serde::Deserialize;
22use typed_builder::TypedBuilder;
23
24use self::scram::ScramVersion;
25use crate::{
26 bson::Document,
27 client::options::ServerApi,
28 cmap::{Command, Connection, StreamDescription},
29 error::{Error, ErrorKind, Result},
30};
31
32const SCRAM_SHA_1_STR: &str = "SCRAM-SHA-1";
33const SCRAM_SHA_256_STR: &str = "SCRAM-SHA-256";
34const MONGODB_CR_STR: &str = "MONGODB-CR";
35const GSSAPI_STR: &str = "GSSAPI";
36const MONGODB_AWS_STR: &str = "MONGODB-AWS";
37const MONGODB_X509_STR: &str = "MONGODB-X509";
38const PLAIN_STR: &str = "PLAIN";
39const MONGODB_OIDC_STR: &str = "MONGODB-OIDC";
40
41#[derive(Clone, Deserialize, PartialEq, Debug)]
45#[non_exhaustive]
46pub enum AuthMechanism {
47 MongoDbCr,
50
51 ScramSha1,
55
56 ScramSha256,
60
61 MongoDbX509,
66
67 Gssapi,
73
74 Plain,
81
82 #[cfg(feature = "aws-auth")]
90 MongoDbAws,
91
92 MongoDbOidc,
94}
95
96impl AuthMechanism {
97 fn from_scram_version(scram: &ScramVersion) -> Self {
98 match scram {
99 ScramVersion::Sha1 => Self::ScramSha1,
100 ScramVersion::Sha256 => Self::ScramSha256,
101 }
102 }
103
104 pub(crate) fn from_stream_description(description: &StreamDescription) -> AuthMechanism {
105 let scram_sha_256_found = description
106 .sasl_supported_mechs
107 .as_ref()
108 .map(|ms| ms.iter().any(|m| m == AuthMechanism::ScramSha256.as_str()))
109 .unwrap_or(false);
110
111 if scram_sha_256_found {
112 AuthMechanism::ScramSha256
113 } else {
114 AuthMechanism::ScramSha1
115 }
116 }
117
118 pub fn validate_credential(&self, credential: &Credential) -> Result<()> {
121 match self {
122 AuthMechanism::ScramSha1 | AuthMechanism::ScramSha256 => {
123 if credential.username.is_none() {
124 return Err(ErrorKind::InvalidArgument {
125 message: "No username provided for SCRAM authentication".to_string(),
126 }
127 .into());
128 };
129 Ok(())
130 }
131 AuthMechanism::MongoDbX509 => {
132 if credential.password.is_some() {
133 return Err(ErrorKind::InvalidArgument {
134 message: "A password cannot be specified with MONGODB-X509".to_string(),
135 }
136 .into());
137 }
138
139 if credential.source.as_deref().unwrap_or("$external") != "$external" {
140 return Err(ErrorKind::InvalidArgument {
141 message: "only $external may be specified as an auth source for \
142 MONGODB-X509"
143 .to_string(),
144 }
145 .into());
146 }
147
148 Ok(())
149 }
150 AuthMechanism::Plain => {
151 if credential.username.is_none() {
152 return Err(ErrorKind::InvalidArgument {
153 message: "No username provided for PLAIN authentication".to_string(),
154 }
155 .into());
156 }
157
158 if credential.username.as_deref() == Some("") {
159 return Err(ErrorKind::InvalidArgument {
160 message: "Username for PLAIN authentication must be non-empty".to_string(),
161 }
162 .into());
163 }
164
165 if credential.password.is_none() {
166 return Err(ErrorKind::InvalidArgument {
167 message: "No password provided for PLAIN authentication".to_string(),
168 }
169 .into());
170 }
171
172 Ok(())
173 }
174 #[cfg(feature = "aws-auth")]
175 AuthMechanism::MongoDbAws => {
176 if credential.username.is_some() && credential.password.is_none() {
177 return Err(ErrorKind::InvalidArgument {
178 message: "Username cannot be provided without password for MONGODB-AWS \
179 authentication"
180 .to_string(),
181 }
182 .into());
183 }
184
185 Ok(())
186 }
187 AuthMechanism::MongoDbOidc => oidc::validate_credential(credential),
188 _ => Ok(()),
189 }
190 }
191
192 pub fn as_str(&self) -> &'static str {
194 match self {
195 AuthMechanism::ScramSha1 => SCRAM_SHA_1_STR,
196 AuthMechanism::ScramSha256 => SCRAM_SHA_256_STR,
197 AuthMechanism::MongoDbCr => MONGODB_CR_STR,
198 AuthMechanism::MongoDbX509 => MONGODB_X509_STR,
199 AuthMechanism::Gssapi => GSSAPI_STR,
200 AuthMechanism::Plain => PLAIN_STR,
201 #[cfg(feature = "aws-auth")]
202 AuthMechanism::MongoDbAws => MONGODB_AWS_STR,
203 AuthMechanism::MongoDbOidc => MONGODB_OIDC_STR,
204 }
205 }
206
207 pub(crate) fn default_source<'a>(&'a self, uri_db: Option<&'a str>) -> &'a str {
210 match self {
211 AuthMechanism::ScramSha1 | AuthMechanism::ScramSha256 | AuthMechanism::MongoDbCr => {
212 uri_db.unwrap_or("admin")
213 }
214 AuthMechanism::MongoDbX509 => "$external",
215 AuthMechanism::Plain => uri_db.unwrap_or("$external"),
216 AuthMechanism::MongoDbOidc => "$external",
217 #[cfg(feature = "aws-auth")]
218 AuthMechanism::MongoDbAws => "$external",
219 AuthMechanism::Gssapi => "",
220 }
221 }
222
223 pub(crate) async fn build_speculative_client_first(
226 &self,
227 credential: &Credential,
228 ) -> Result<Option<ClientFirst>> {
229 match self {
230 Self::ScramSha1 => {
231 let client_first = ScramVersion::Sha1.build_speculative_client_first(credential)?;
232
233 Ok(Some(ClientFirst::Scram(ScramVersion::Sha1, client_first)))
234 }
235 Self::ScramSha256 => {
236 let client_first =
237 ScramVersion::Sha256.build_speculative_client_first(credential)?;
238
239 Ok(Some(ClientFirst::Scram(ScramVersion::Sha256, client_first)))
240 }
241 Self::MongoDbX509 => Ok(Some(ClientFirst::X509(Box::new(
242 x509::build_speculative_client_first(credential),
243 )))),
244 Self::Plain => Ok(None),
245 Self::MongoDbOidc => Ok(oidc::build_speculative_client_first(credential)
246 .await
247 .map(|comm| ClientFirst::Oidc(Box::new(comm)))),
248 #[cfg(feature = "aws-auth")]
249 AuthMechanism::MongoDbAws => Ok(None),
250 AuthMechanism::MongoDbCr => Err(ErrorKind::Authentication {
251 message: "MONGODB-CR is deprecated and not supported by this driver. Use SCRAM \
252 for password-based authentication instead"
253 .into(),
254 }
255 .into()),
256 _ => Err(ErrorKind::Authentication {
257 message: format!("Authentication mechanism {:?} not yet implemented.", self),
258 }
259 .into()),
260 }
261 }
262
263 pub(crate) async fn authenticate_stream(
264 &self,
265 stream: &mut Connection,
266 credential: &Credential,
267 server_api: Option<&ServerApi>,
268 #[cfg(feature = "aws-auth")] http_client: &crate::runtime::HttpClient,
269 ) -> Result<()> {
270 self.validate_credential(credential)?;
271
272 match self {
273 AuthMechanism::ScramSha1 => {
274 ScramVersion::Sha1
275 .authenticate_stream(stream, credential, server_api, None)
276 .await
277 }
278 AuthMechanism::ScramSha256 => {
279 ScramVersion::Sha256
280 .authenticate_stream(stream, credential, server_api, None)
281 .await
282 }
283 AuthMechanism::MongoDbX509 => {
284 x509::authenticate_stream(stream, credential, server_api, None).await
285 }
286 AuthMechanism::Plain => {
287 plain::authenticate_stream(stream, credential, server_api).await
288 }
289 #[cfg(feature = "aws-auth")]
290 AuthMechanism::MongoDbAws => {
291 aws::authenticate_stream(stream, credential, server_api, http_client).await
292 }
293 AuthMechanism::MongoDbCr => Err(ErrorKind::Authentication {
294 message: "MONGODB-CR is deprecated and not supported by this driver. Use SCRAM \
295 for password-based authentication instead"
296 .into(),
297 }
298 .into()),
299 AuthMechanism::MongoDbOidc => {
300 oidc::authenticate_stream(stream, credential, server_api, None).await
301 }
302 _ => Err(ErrorKind::Authentication {
303 message: format!("Authentication mechanism {:?} not yet implemented.", self),
304 }
305 .into()),
306 }
307 }
308
309 pub(crate) async fn reauthenticate_stream(
310 &self,
311 stream: &mut Connection,
312 credential: &Credential,
313 server_api: Option<&ServerApi>,
314 ) -> Result<()> {
315 self.validate_credential(credential)?;
316
317 match self {
318 AuthMechanism::ScramSha1
319 | AuthMechanism::ScramSha256
320 | AuthMechanism::MongoDbX509
321 | AuthMechanism::Plain
322 | AuthMechanism::MongoDbCr => Err(ErrorKind::Authentication {
323 message: format!(
324 "Reauthentication for authentication mechanism {:?} is not supported.",
325 self
326 ),
327 }
328 .into()),
329 #[cfg(feature = "aws-auth")]
330 AuthMechanism::MongoDbAws => Err(ErrorKind::Authentication {
331 message: format!(
332 "Reauthentication for authentication mechanism {:?} is not supported.",
333 self
334 ),
335 }
336 .into()),
337 AuthMechanism::MongoDbOidc => {
338 oidc::reauthenticate_stream(stream, credential, server_api).await
339 }
340 _ => Err(ErrorKind::Authentication {
341 message: format!("Authentication mechanism {:?} not yet implemented.", self),
342 }
343 .into()),
344 }
345 }
346}
347
348impl FromStr for AuthMechanism {
349 type Err = Error;
350
351 fn from_str(str: &str) -> Result<Self> {
352 match str {
353 SCRAM_SHA_1_STR => Ok(AuthMechanism::ScramSha1),
354 SCRAM_SHA_256_STR => Ok(AuthMechanism::ScramSha256),
355 MONGODB_CR_STR => Ok(AuthMechanism::MongoDbCr),
356 MONGODB_X509_STR => Ok(AuthMechanism::MongoDbX509),
357 GSSAPI_STR => Ok(AuthMechanism::Gssapi),
358 PLAIN_STR => Ok(AuthMechanism::Plain),
359 MONGODB_OIDC_STR => Ok(AuthMechanism::MongoDbOidc),
360 #[cfg(feature = "aws-auth")]
361 MONGODB_AWS_STR => Ok(AuthMechanism::MongoDbAws),
362 #[cfg(not(feature = "aws-auth"))]
363 MONGODB_AWS_STR => Err(ErrorKind::InvalidArgument {
364 message: "MONGODB-AWS auth is only supported with the aws-auth feature flag and \
365 the tokio runtime"
366 .into(),
367 }
368 .into()),
369
370 _ => Err(ErrorKind::InvalidArgument {
371 message: format!("invalid mechanism string: {}", str),
372 }
373 .into()),
374 }
375 }
376}
377
378#[derive(Clone, Default, Deserialize, TypedBuilder)]
383#[derive_where(PartialEq)]
384#[builder(field_defaults(default, setter(into)))]
385#[non_exhaustive]
386pub struct Credential {
387 pub username: Option<String>,
390
391 pub source: Option<String>,
395
396 pub password: Option<String>,
398
399 pub mechanism: Option<AuthMechanism>,
402
403 pub mechanism_properties: Option<Document>,
405
406 #[serde(skip)]
430 #[derive_where(skip)]
431 #[builder(default)]
432 pub oidc_callback: oidc::Callback,
433}
434
435impl Credential {
436 pub(crate) fn resolved_source(&self) -> &str {
437 self.mechanism
438 .as_ref()
439 .map(|m| m.default_source(None))
440 .unwrap_or("admin")
441 }
442
443 pub(crate) fn append_needed_mechanism_negotiation(&self, command: &mut RawDocumentBuf) {
446 if let (Some(username), None) = (self.username.as_ref(), self.mechanism.as_ref()) {
447 command.append(
448 "saslSupportedMechs",
449 format!("{}.{}", self.resolved_source(), username),
450 );
451 }
452 }
453
454 pub(crate) async fn authenticate_stream(
458 &self,
459 conn: &mut Connection,
460 server_api: Option<&ServerApi>,
461 first_round: Option<FirstRound>,
462 #[cfg(feature = "aws-auth")] http_client: &crate::runtime::HttpClient,
463 ) -> Result<()> {
464 let stream_description = conn.stream_description()?;
465
466 if !stream_description.initial_server_type.can_auth() {
468 return Ok(());
469 };
470
471 if let Some(first_round) = first_round {
474 return match first_round {
475 FirstRound::Scram(version, first_round) => {
476 version
477 .authenticate_stream(conn, self, server_api, first_round)
478 .await
479 }
480 FirstRound::X509(server_first) => {
481 x509::authenticate_stream(conn, self, server_api, server_first).await
482 }
483 FirstRound::Oidc(server_first) => {
484 oidc::authenticate_stream(conn, self, server_api, server_first).await
485 }
486 };
487 }
488
489 let mechanism = match self.mechanism {
490 None => Cow::Owned(AuthMechanism::from_stream_description(stream_description)),
491 Some(ref m) => Cow::Borrowed(m),
492 };
493
494 mechanism
496 .authenticate_stream(
497 conn,
498 self,
499 server_api,
500 #[cfg(feature = "aws-auth")]
501 http_client,
502 )
503 .await
504 }
505
506 #[cfg(test)]
507 pub(crate) fn serialize_for_client_options<S>(
508 credential: &Option<Credential>,
509 serializer: S,
510 ) -> std::result::Result<S::Ok, S::Error>
511 where
512 S: serde::Serializer,
513 {
514 use serde::ser::Serialize;
515
516 #[derive(serde::Serialize)]
517 struct CredentialHelper<'a> {
518 authsource: Option<&'a String>,
519 authmechanism: Option<&'a str>,
520 authmechanismproperties: Option<&'a Document>,
521 }
522
523 let state = credential.as_ref().map(|c| CredentialHelper {
524 authsource: c.source.as_ref(),
525 authmechanism: c.mechanism.as_ref().map(|s| s.as_str()),
526 authmechanismproperties: c.mechanism_properties.as_ref(),
527 });
528 state.serialize(serializer)
529 }
530}
531
532impl Debug for Credential {
533 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
534 f.debug_tuple("Credential")
535 .field(&"REDACTED".to_string())
536 .finish()
537 }
538}
539
540#[derive(Debug)]
542pub(crate) enum ClientFirst {
543 Scram(ScramVersion, scram::ClientFirst),
544 X509(Box<Command>),
545 Oidc(Box<Command>),
546}
547
548impl ClientFirst {
549 pub(crate) fn to_document(&self) -> RawDocumentBuf {
550 match self {
551 Self::Scram(version, client_first) => client_first.to_command(version).body,
552 Self::X509(command) => command.body.clone(),
553 Self::Oidc(command) => command.body.clone(),
554 }
555 }
556
557 pub(crate) fn into_first_round(self, server_first: Document) -> FirstRound {
558 match self {
559 Self::Scram(version, client_first) => FirstRound::Scram(
560 version,
561 scram::FirstRound {
562 client_first,
563 server_first,
564 },
565 ),
566 Self::X509(..) => FirstRound::X509(server_first),
567 Self::Oidc(..) => FirstRound::Oidc(server_first),
568 }
569 }
570}
571
572#[derive(Debug)]
575pub(crate) enum FirstRound {
576 Scram(ScramVersion, scram::FirstRound),
577 X509(Document),
578 Oidc(Document),
579}
580
581pub(crate) fn generate_nonce_bytes() -> [u8; 32] {
582 rand::thread_rng().gen()
583}
584
585pub(crate) fn generate_nonce() -> String {
586 let result = generate_nonce_bytes();
587 base64::encode(result)
588}
589
590fn mac<M: Mac + KeyInit>(
591 key: &[u8],
592 input: &[u8],
593 auth_mechanism: &str,
594) -> Result<impl AsRef<[u8]>> {
595 let mut mac = <M as Mac>::new_from_slice(key)
596 .map_err(|_| Error::unknown_authentication_error(auth_mechanism))?;
597 mac.update(input);
598 Ok(mac.finalize().into_bytes())
599}