1#[cfg(feature = "aws-auth")]
5pub(crate) mod aws;
6#[cfg(feature = "gssapi-auth")]
7mod gssapi;
8pub mod oidc;
9mod plain;
10mod sasl;
11mod scram;
12#[cfg(test)]
13mod test;
14mod x509;
15
16use std::{borrow::Cow, fmt::Debug, str::FromStr};
17
18use crate::{base64, bson::RawDocumentBuf, bson_compat::cstr, options::ClientOptions};
19use derive_where::derive_where;
20use hmac::{digest::KeyInit, Mac};
21use rand::Rng;
22use serde::{Deserialize, Serialize};
23use typed_builder::TypedBuilder;
24
25use self::scram::ScramVersion;
26#[cfg(feature = "gssapi-auth")]
27use crate::options::ResolverConfig;
28use crate::{
29 bson::Document,
30 client::options::ServerApi,
31 cmap::{Command, Connection, StreamDescription},
32 error::{Error, ErrorKind, Result},
33};
34
35const SCRAM_SHA_1_STR: &str = "SCRAM-SHA-1";
36const SCRAM_SHA_256_STR: &str = "SCRAM-SHA-256";
37const MONGODB_CR_STR: &str = "MONGODB-CR";
38const GSSAPI_STR: &str = "GSSAPI";
39const MONGODB_AWS_STR: &str = "MONGODB-AWS";
40const MONGODB_X509_STR: &str = "MONGODB-X509";
41const PLAIN_STR: &str = "PLAIN";
42const MONGODB_OIDC_STR: &str = "MONGODB-OIDC";
43
44#[derive(Clone, Deserialize, Serialize, PartialEq, Debug)]
48#[non_exhaustive]
49pub enum AuthMechanism {
50 MongoDbCr,
53
54 ScramSha1,
58
59 ScramSha256,
63
64 MongoDbX509,
69
70 #[cfg(feature = "gssapi-auth")]
74 Gssapi,
75
76 Plain,
83
84 #[cfg(feature = "aws-auth")]
99 MongoDbAws,
100
101 #[serde(alias = "MONGODB-OIDC")]
103 MongoDbOidc,
104}
105
106impl AuthMechanism {
107 fn from_scram_version(scram: &ScramVersion) -> Self {
108 match scram {
109 ScramVersion::Sha1 => Self::ScramSha1,
110 ScramVersion::Sha256 => Self::ScramSha256,
111 }
112 }
113
114 pub(crate) fn from_stream_description(description: &StreamDescription) -> AuthMechanism {
115 let scram_sha_256_found = description
116 .sasl_supported_mechs
117 .as_ref()
118 .map(|ms| ms.iter().any(|m| m == AuthMechanism::ScramSha256.as_str()))
119 .unwrap_or(false);
120
121 if scram_sha_256_found {
122 AuthMechanism::ScramSha256
123 } else {
124 AuthMechanism::ScramSha1
125 }
126 }
127
128 pub fn validate_credential(&self, credential: &Credential) -> Result<()> {
131 match self {
132 AuthMechanism::ScramSha1 | AuthMechanism::ScramSha256 => {
133 if credential.username.is_none() {
134 return Err(ErrorKind::InvalidArgument {
135 message: "No username provided for SCRAM authentication".to_string(),
136 }
137 .into());
138 };
139 Ok(())
140 }
141 AuthMechanism::MongoDbX509 => {
142 if credential.password.is_some() {
143 return Err(ErrorKind::InvalidArgument {
144 message: "A password cannot be specified with MONGODB-X509".to_string(),
145 }
146 .into());
147 }
148
149 if credential.source.as_deref().unwrap_or("$external") != "$external" {
150 return Err(ErrorKind::InvalidArgument {
151 message: "only $external may be specified as an auth source for \
152 MONGODB-X509"
153 .to_string(),
154 }
155 .into());
156 }
157
158 Ok(())
159 }
160 #[cfg(feature = "gssapi-auth")]
161 AuthMechanism::Gssapi => {
162 if credential.username.is_none() {
163 return Err(ErrorKind::InvalidArgument {
164 message: "No username provided for GSSAPI authentication".to_string(),
165 }
166 .into());
167 }
168
169 if credential.source.as_deref().unwrap_or("$external") != "$external" {
170 return Err(ErrorKind::InvalidArgument {
171 message: "only $external may be specified as an auth source for GSSAPI"
172 .to_string(),
173 }
174 .into());
175 }
176
177 Ok(())
178 }
179 AuthMechanism::Plain => {
180 if credential.username.is_none() {
181 return Err(ErrorKind::InvalidArgument {
182 message: "No username provided for PLAIN authentication".to_string(),
183 }
184 .into());
185 }
186
187 if credential.username.as_deref() == Some("") {
188 return Err(ErrorKind::InvalidArgument {
189 message: "Username for PLAIN authentication must be non-empty".to_string(),
190 }
191 .into());
192 }
193
194 if credential.password.is_none() {
195 return Err(ErrorKind::InvalidArgument {
196 message: "No password provided for PLAIN authentication".to_string(),
197 }
198 .into());
199 }
200
201 Ok(())
202 }
203 #[cfg(feature = "aws-auth")]
204 AuthMechanism::MongoDbAws => {
205 if credential.username.is_some() && credential.password.is_none() {
206 return Err(ErrorKind::InvalidArgument {
207 message: "Username cannot be provided without password for MONGODB-AWS \
208 authentication"
209 .to_string(),
210 }
211 .into());
212 }
213
214 Ok(())
215 }
216 AuthMechanism::MongoDbOidc => oidc::validate_credential(credential),
217 _ => Ok(()),
218 }
219 }
220
221 pub fn as_str(&self) -> &'static str {
223 match self {
224 AuthMechanism::ScramSha1 => SCRAM_SHA_1_STR,
225 AuthMechanism::ScramSha256 => SCRAM_SHA_256_STR,
226 AuthMechanism::MongoDbCr => MONGODB_CR_STR,
227 AuthMechanism::MongoDbX509 => MONGODB_X509_STR,
228 #[cfg(feature = "gssapi-auth")]
229 AuthMechanism::Gssapi => GSSAPI_STR,
230 AuthMechanism::Plain => PLAIN_STR,
231 #[cfg(feature = "aws-auth")]
232 AuthMechanism::MongoDbAws => MONGODB_AWS_STR,
233 AuthMechanism::MongoDbOidc => MONGODB_OIDC_STR,
234 }
235 }
236
237 pub(crate) fn default_source<'a>(&'a self, uri_db: Option<&'a str>) -> &'a str {
240 match self {
241 AuthMechanism::ScramSha1 | AuthMechanism::ScramSha256 | AuthMechanism::MongoDbCr => {
242 uri_db.unwrap_or("admin")
243 }
244 AuthMechanism::MongoDbX509 => "$external",
245 AuthMechanism::Plain => uri_db.unwrap_or("$external"),
246 AuthMechanism::MongoDbOidc => "$external",
247 #[cfg(feature = "aws-auth")]
248 AuthMechanism::MongoDbAws => "$external",
249 #[cfg(feature = "gssapi-auth")]
250 AuthMechanism::Gssapi => "$external",
251 }
252 }
253
254 pub(crate) async fn build_speculative_client_first(
257 &self,
258 credential: &Credential,
259 ) -> Result<Option<ClientFirst>> {
260 match self {
261 Self::ScramSha1 => {
262 let client_first = ScramVersion::Sha1.build_speculative_client_first(credential)?;
263
264 Ok(Some(ClientFirst::Scram(ScramVersion::Sha1, client_first)))
265 }
266 Self::ScramSha256 => {
267 let client_first =
268 ScramVersion::Sha256.build_speculative_client_first(credential)?;
269
270 Ok(Some(ClientFirst::Scram(ScramVersion::Sha256, client_first)))
271 }
272 Self::MongoDbX509 => Ok(Some(ClientFirst::X509(Box::new(
273 x509::build_speculative_client_first(credential)?,
274 )))),
275 #[cfg(feature = "gssapi-auth")]
276 AuthMechanism::Gssapi => Ok(None),
277 Self::Plain => Ok(None),
278 Self::MongoDbOidc => Ok(oidc::build_speculative_client_first(credential)
279 .await
280 .map(|comm| ClientFirst::Oidc(Box::new(comm)))),
281 #[cfg(feature = "aws-auth")]
282 AuthMechanism::MongoDbAws => Ok(None),
283 AuthMechanism::MongoDbCr => Err(ErrorKind::Authentication {
284 message: "MONGODB-CR is deprecated and not supported by this driver. Use SCRAM \
285 for password-based authentication instead"
286 .into(),
287 }
288 .into()),
289 }
290 }
291
292 pub(crate) async fn authenticate_stream(
293 &self,
294 stream: &mut Connection,
295 credential: &Credential,
296 opts: &AuthOptions,
297 ) -> Result<()> {
298 self.validate_credential(credential)?;
299
300 let server_api = opts.server_api.as_ref();
301 match self {
302 AuthMechanism::ScramSha1 => {
303 ScramVersion::Sha1
304 .authenticate_stream(stream, credential, server_api, None)
305 .await
306 }
307 AuthMechanism::ScramSha256 => {
308 ScramVersion::Sha256
309 .authenticate_stream(stream, credential, server_api, None)
310 .await
311 }
312 AuthMechanism::MongoDbX509 => {
313 x509::authenticate_stream(stream, credential, server_api, None).await
314 }
315 #[cfg(feature = "gssapi-auth")]
316 AuthMechanism::Gssapi => {
317 gssapi::authenticate_stream(
318 stream,
319 credential,
320 server_api,
321 opts.resolver_config.as_ref(),
322 )
323 .await
324 }
325 AuthMechanism::Plain => {
326 plain::authenticate_stream(stream, credential, server_api).await
327 }
328 #[cfg(feature = "aws-auth")]
329 AuthMechanism::MongoDbAws => {
330 aws::authenticate_stream(stream, credential, server_api).await
331 }
332 AuthMechanism::MongoDbCr => Err(ErrorKind::Authentication {
333 message: "MONGODB-CR is deprecated and not supported by this driver. Use SCRAM \
334 for password-based authentication instead"
335 .into(),
336 }
337 .into()),
338 AuthMechanism::MongoDbOidc => {
339 oidc::authenticate_stream(stream, credential, server_api, None).await
340 }
341 }
342 }
343
344 pub(crate) async fn reauthenticate_stream(
345 &self,
346 stream: &mut Connection,
347 credential: &Credential,
348 server_api: Option<&ServerApi>,
349 ) -> Result<()> {
350 self.validate_credential(credential)?;
351
352 match self {
353 AuthMechanism::ScramSha1
354 | AuthMechanism::ScramSha256
355 | AuthMechanism::MongoDbX509
356 | AuthMechanism::Plain
357 | AuthMechanism::MongoDbCr => Err(ErrorKind::Authentication {
358 message: format!(
359 "Reauthentication for authentication mechanism {self:?} is not supported."
360 ),
361 }
362 .into()),
363 #[cfg(feature = "gssapi-auth")]
364 AuthMechanism::Gssapi => Err(ErrorKind::Authentication {
365 message: format!(
366 "Reauthentication for authentication mechanism {self:?} is not supported."
367 ),
368 }
369 .into()),
370 #[cfg(feature = "aws-auth")]
371 AuthMechanism::MongoDbAws => Err(ErrorKind::Authentication {
372 message: format!(
373 "Reauthentication for authentication mechanism {self:?} is not supported."
374 ),
375 }
376 .into()),
377 AuthMechanism::MongoDbOidc => {
378 oidc::reauthenticate_stream(stream, credential, server_api).await
379 }
380 }
381 }
382}
383
384impl FromStr for AuthMechanism {
385 type Err = Error;
386
387 fn from_str(str: &str) -> Result<Self> {
388 match str {
389 SCRAM_SHA_1_STR => Ok(AuthMechanism::ScramSha1),
390 SCRAM_SHA_256_STR => Ok(AuthMechanism::ScramSha256),
391 MONGODB_CR_STR => Ok(AuthMechanism::MongoDbCr),
392 MONGODB_X509_STR => Ok(AuthMechanism::MongoDbX509),
393 #[cfg(feature = "gssapi-auth")]
394 GSSAPI_STR => Ok(AuthMechanism::Gssapi),
395 #[cfg(not(feature = "gssapi-auth"))]
396 GSSAPI_STR => Err(ErrorKind::InvalidArgument {
397 message: "GSSAPI auth is only supported with the gssapi-auth feature flag".into(),
398 }
399 .into()),
400 PLAIN_STR => Ok(AuthMechanism::Plain),
401 MONGODB_OIDC_STR => Ok(AuthMechanism::MongoDbOidc),
402 #[cfg(feature = "aws-auth")]
403 MONGODB_AWS_STR => Ok(AuthMechanism::MongoDbAws),
404 #[cfg(not(feature = "aws-auth"))]
405 MONGODB_AWS_STR => Err(ErrorKind::InvalidArgument {
406 message: "MONGODB-AWS auth is only supported with the aws-auth feature flag and \
407 the tokio runtime"
408 .into(),
409 }
410 .into()),
411
412 _ => Err(ErrorKind::InvalidArgument {
413 message: format!("invalid mechanism string: {str}"),
414 }
415 .into()),
416 }
417 }
418}
419
420#[derive(Clone, Debug, Default)]
421pub(crate) struct AuthOptions {
423 server_api: Option<ServerApi>,
424 #[cfg(feature = "gssapi-auth")]
425 resolver_config: Option<ResolverConfig>,
426}
427
428impl From<&ClientOptions> for AuthOptions {
429 fn from(opts: &ClientOptions) -> Self {
430 Self {
431 server_api: opts.server_api.clone(),
432 #[cfg(feature = "gssapi-auth")]
433 resolver_config: opts.resolver_config.clone(),
434 }
435 }
436}
437
438#[derive(Clone, Default, Deserialize, TypedBuilder)]
443#[derive_where(PartialEq)]
444#[builder(field_defaults(default, setter(into)))]
445#[non_exhaustive]
446pub struct Credential {
447 pub username: Option<String>,
450
451 pub source: Option<String>,
455
456 pub password: Option<String>,
458
459 pub mechanism: Option<AuthMechanism>,
462
463 pub mechanism_properties: Option<Document>,
469
470 #[serde(skip)]
494 #[derive_where(skip)]
495 #[builder(default)]
496 pub oidc_callback: oidc::Callback,
497}
498
499impl Credential {
500 pub(crate) fn resolved_source(&self) -> &str {
501 self.mechanism
502 .as_ref()
503 .map(|m| m.default_source(None))
504 .unwrap_or("admin")
505 }
506
507 pub(crate) fn append_needed_mechanism_negotiation(&self, command: &mut RawDocumentBuf) {
510 if let (Some(username), None) = (self.username.as_ref(), self.mechanism.as_ref()) {
511 command.append(
512 cstr!("saslSupportedMechs"),
513 format!("{}.{}", self.resolved_source(), username),
514 );
515 }
516 }
517
518 pub(crate) async fn authenticate_stream(
522 &self,
523 conn: &mut Connection,
524 first_round: Option<FirstRound>,
525 opts: &AuthOptions,
526 ) -> Result<()> {
527 let stream_description = conn.stream_description()?;
528
529 if !stream_description.initial_server_type.can_auth() {
531 return Ok(());
532 };
533
534 if let Some(first_round) = first_round {
537 let server_api = opts.server_api.as_ref();
538 return match first_round {
539 FirstRound::Scram(version, first_round) => {
540 version
541 .authenticate_stream(conn, self, server_api, first_round)
542 .await
543 }
544 FirstRound::X509(server_first) => {
545 x509::authenticate_stream(conn, self, server_api, server_first).await
546 }
547 FirstRound::Oidc(server_first) => {
548 oidc::authenticate_stream(conn, self, server_api, server_first).await
549 }
550 };
551 }
552
553 let mechanism = match self.mechanism {
554 None => Cow::Owned(AuthMechanism::from_stream_description(stream_description)),
555 Some(ref m) => Cow::Borrowed(m),
556 };
557 mechanism.authenticate_stream(conn, self, opts).await
559 }
560
561 pub(crate) fn serialize<S>(
562 credential: &Option<Credential>,
563 serializer: S,
564 ) -> std::result::Result<S::Ok, S::Error>
565 where
566 S: serde::Serializer,
567 {
568 use serde::ser::Serialize;
569
570 #[derive(serde::Serialize)]
571 struct CredentialHelper<'a> {
572 authsource: Option<&'a String>,
573 authmechanism: Option<&'a str>,
574 authmechanismproperties: Option<&'a Document>,
575 }
576
577 let state = credential.as_ref().map(|c| CredentialHelper {
578 authsource: c.source.as_ref(),
579 authmechanism: c.mechanism.as_ref().map(|s| s.as_str()),
580 authmechanismproperties: c.mechanism_properties.as_ref(),
581 });
582 state.serialize(serializer)
583 }
584}
585
586impl Debug for Credential {
587 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
588 f.debug_tuple("Credential")
589 .field(&"REDACTED".to_string())
590 .finish()
591 }
592}
593
594#[derive(Debug)]
596pub(crate) enum ClientFirst {
597 Scram(ScramVersion, scram::ClientFirst),
598 X509(Box<Command>),
599 Oidc(Box<Command>),
600}
601
602impl ClientFirst {
603 pub(crate) fn to_document(&self) -> Result<RawDocumentBuf> {
604 Ok(match self {
605 Self::Scram(version, client_first) => client_first.to_command(version)?.body,
606 Self::X509(command) => command.body.clone(),
607 Self::Oidc(command) => command.body.clone(),
608 })
609 }
610
611 pub(crate) fn into_first_round(self, server_first: Document) -> FirstRound {
612 match self {
613 Self::Scram(version, client_first) => FirstRound::Scram(
614 version,
615 scram::FirstRound {
616 client_first,
617 server_first,
618 },
619 ),
620 Self::X509(..) => FirstRound::X509(server_first),
621 Self::Oidc(..) => FirstRound::Oidc(server_first),
622 }
623 }
624}
625
626#[derive(Debug)]
629pub(crate) enum FirstRound {
630 Scram(ScramVersion, scram::FirstRound),
631 X509(Document),
632 Oidc(Document),
633}
634
635pub(crate) fn generate_nonce_bytes() -> [u8; 32] {
636 rand::rng().random()
637}
638
639pub(crate) fn generate_nonce() -> String {
640 let result = generate_nonce_bytes();
641 base64::encode(result)
642}
643
644fn mac<M: Mac + KeyInit>(
645 key: &[u8],
646 input: &[u8],
647 auth_mechanism: &str,
648) -> Result<impl AsRef<[u8]>> {
649 let mut mac = <M as Mac>::new_from_slice(key)
650 .map_err(|_| Error::unknown_authentication_error(auth_mechanism))?;
651 mac.update(input);
652 Ok(mac.finalize().into_bytes())
653}