mongodb/client/
auth.rs

1//! Contains the types needed to specify the auth configuration for a
2//! [`Client`](struct.Client.html).
3
4#[cfg(feature = "aws-auth")]
5pub(crate) mod aws;
6/// Contains the functionality for [`OIDC`](https://openid.net/developers/how-connect-works/) authorization and authentication.
7pub 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/// The authentication mechanisms supported by MongoDB.
42///
43/// Note: not all of these mechanisms are currently supported by the driver.
44#[derive(Clone, Deserialize, PartialEq, Debug)]
45#[non_exhaustive]
46pub enum AuthMechanism {
47    /// MongoDB Challenge Response nonce and MD5 based authentication system. It is currently
48    /// deprecated and will never be supported by this driver.
49    MongoDbCr,
50
51    /// The SCRAM-SHA-1 mechanism as defined in [RFC 5802](http://tools.ietf.org/html/rfc5802).
52    ///
53    /// See the [MongoDB documentation](https://www.mongodb.com/docs/manual/core/security-scram/) for more information.
54    ScramSha1,
55
56    /// The SCRAM-SHA-256 mechanism which extends [RFC 5802](http://tools.ietf.org/html/rfc5802) and is formally defined in [RFC 7677](https://tools.ietf.org/html/rfc7677).
57    ///
58    /// See the [MongoDB documentation](https://www.mongodb.com/docs/manual/core/security-scram/) for more information.
59    ScramSha256,
60
61    /// The MONGODB-X509 mechanism based on the usage of X.509 certificates to validate a client
62    /// where the distinguished subject name of the client certificate acts as the username.
63    ///
64    /// See the [MongoDB documentation](https://www.mongodb.com/docs/manual/core/security-x.509/) for more information.
65    MongoDbX509,
66
67    /// Kerberos authentication mechanism as defined in [RFC 4752](http://tools.ietf.org/html/rfc4752).
68    ///
69    /// See the [MongoDB documentation](https://www.mongodb.com/docs/manual/core/kerberos/) for more information.
70    ///
71    /// Note: This mechanism is not currently supported by this driver but will be in the future.
72    Gssapi,
73
74    /// The SASL PLAIN mechanism, as defined in [RFC 4616](), is used in MongoDB to perform LDAP
75    /// authentication and cannot be used for any other type of authentication.
76    /// Since the credentials are stored outside of MongoDB, the "$external" database must be used
77    /// for authentication.
78    ///
79    /// See the [MongoDB documentation](https://www.mongodb.com/docs/manual/core/security-ldap/#ldap-proxy-authentication) for more information on LDAP authentication.
80    Plain,
81
82    /// MONGODB-AWS authenticates using AWS IAM credentials (an access key ID and a secret access
83    /// key), temporary AWS IAM credentials obtained from an AWS Security Token Service (STS)
84    /// Assume Role request, or temporary AWS IAM credentials assigned to an EC2 instance or ECS
85    /// task.
86    ///
87    /// Note: Only server versions 4.4+ support AWS authentication. Additionally, the driver only
88    /// supports AWS authentication with the tokio runtime.
89    #[cfg(feature = "aws-auth")]
90    MongoDbAws,
91
92    /// MONGODB-OIDC authenticates using [OpenID Connect](https://openid.net/developers/specs/) access tokens.
93    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    /// Determines if the provided credentials have the required information to perform
119    /// authentication.
120    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    /// Returns this `AuthMechanism` as a string.
193    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    /// Get the default authSource for a given mechanism depending on the database provided in the
208    /// connection string.
209    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    /// Constructs the first message to be sent to the server as part of the authentication
224    /// handshake, which can be used for speculative authentication.
225    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/// A struct containing authentication information.
379///
380/// Some fields (mechanism and source) may be omitted and will either be negotiated or assigned a
381/// default value, depending on the values of other fields in the credential.
382#[derive(Clone, Default, Deserialize, TypedBuilder)]
383#[derive_where(PartialEq)]
384#[builder(field_defaults(default, setter(into)))]
385#[non_exhaustive]
386pub struct Credential {
387    /// The username to authenticate with. This applies to all mechanisms but may be omitted when
388    /// authenticating via MONGODB-X509.
389    pub username: Option<String>,
390
391    /// The database used to authenticate. This applies to all mechanisms and defaults to "admin"
392    /// in SCRAM authentication mechanisms, "$external" for GSSAPI and MONGODB-X509, and the
393    /// database name or "$external" for PLAIN.
394    pub source: Option<String>,
395
396    /// The password to authenticate with. This does not apply to all mechanisms.
397    pub password: Option<String>,
398
399    /// Which authentication mechanism to use. If not provided, one will be negotiated with the
400    /// server.
401    pub mechanism: Option<AuthMechanism>,
402
403    /// Additional properties for the given mechanism.
404    pub mechanism_properties: Option<Document>,
405
406    /// The token callback for OIDC authentication.
407    /// ```
408    /// use mongodb::{error::Error, Client, options::{ClientOptions, oidc::{Callback, CallbackContext, IdpServerResponse}}};
409    /// use std::time::{Duration, Instant};
410    /// use futures::future::FutureExt;
411    /// async fn do_human_flow(c: CallbackContext) -> Result<(String, Option<Instant>, Option<String>), Error> {
412    ///   // Do the human flow here see: https://auth0.com/docs/authenticate/login/oidc-conformant-authentication/oidc-adoption-auth-code-flow
413    ///   Ok(("some_access_token".to_string(), Some(Instant::now() + Duration::from_secs(60 * 60 * 12)), Some("some_refresh_token".to_string())))
414    /// }
415    ///
416    /// async fn setup_client() -> Result<Client, Error> {
417    ///     let mut opts =
418    ///     ClientOptions::parse("mongodb://localhost:27017,localhost:27018/admin?authSource=admin&authMechanism=MONGODB-OIDC").await?;
419    ///     opts.credential.as_mut().unwrap().oidc_callback =
420    ///         Callback::human(move |c: CallbackContext| {
421    ///         async move {
422    ///             let (access_token, expires, refresh_token) = do_human_flow(c).await?;
423    ///             Ok(IdpServerResponse::builder().access_token(access_token).expires(expires).refresh_token(refresh_token).build())
424    ///         }.boxed()
425    ///     });
426    ///     Client::with_options(opts)
427    /// }
428    /// ```
429    #[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    /// If the mechanism is missing, append the appropriate mechanism negotiation key-value-pair to
444    /// the provided hello or legacy hello command document.
445    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    /// Attempts to authenticate a stream according to this credential, returning an error
455    /// result on failure. A mechanism may be negotiated if one is not provided as part of the
456    /// credential.
457    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        // Verify server can authenticate.
467        if !stream_description.initial_server_type.can_auth() {
468            return Ok(());
469        };
470
471        // If speculative authentication returned a response, then short-circuit the authentication
472        // logic and use the first round from the handshake.
473        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        // Authenticate according to the chosen mechanism.
495        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/// Contains the first client message sent as part of the authentication handshake.
541#[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/// Contains the complete first round of the authentication handshake, including the client message
573/// and the server response.
574#[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}