Skip to main content

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#[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/// The authentication mechanisms supported by MongoDB.
45///
46/// Note: not all of these mechanisms are currently supported by the driver.
47#[derive(Clone, Deserialize, Serialize, PartialEq, Debug)]
48#[non_exhaustive]
49pub enum AuthMechanism {
50    /// MongoDB Challenge Response nonce and MD5 based authentication system. It is currently
51    /// deprecated and will never be supported by this driver.
52    MongoDbCr,
53
54    /// The SCRAM-SHA-1 mechanism as defined in [RFC 5802](http://tools.ietf.org/html/rfc5802).
55    ///
56    /// See the [MongoDB documentation](https://www.mongodb.com/docs/manual/core/security-scram/) for more information.
57    ScramSha1,
58
59    /// 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).
60    ///
61    /// See the [MongoDB documentation](https://www.mongodb.com/docs/manual/core/security-scram/) for more information.
62    ScramSha256,
63
64    /// The MONGODB-X509 mechanism based on the usage of X.509 certificates to validate a client
65    /// where the distinguished subject name of the client certificate acts as the username.
66    ///
67    /// See the [MongoDB documentation](https://www.mongodb.com/docs/manual/core/security-x.509/) for more information.
68    MongoDbX509,
69
70    /// Kerberos authentication mechanism as defined in [RFC 4752](http://tools.ietf.org/html/rfc4752).
71    ///
72    /// See the [MongoDB documentation](https://www.mongodb.com/docs/manual/core/kerberos/) for more information.
73    #[cfg(feature = "gssapi-auth")]
74    Gssapi,
75
76    /// The SASL PLAIN mechanism, as defined in [RFC 4616](), is used in MongoDB to perform LDAP
77    /// authentication and cannot be used for any other type of authentication.
78    /// Since the credentials are stored outside of MongoDB, the "$external" database must be used
79    /// for authentication.
80    ///
81    /// See the [MongoDB documentation](https://www.mongodb.com/docs/manual/core/security-ldap/#ldap-proxy-authentication) for more information on LDAP authentication.
82    Plain,
83
84    /// MONGODB-AWS authenticates using AWS IAM credentials (an access key ID and a secret access
85    /// key), temporary AWS IAM credentials obtained from an AWS Security Token Service (STS)
86    /// Assume Role request, or temporary AWS IAM credentials assigned to an EC2 instance or ECS
87    /// task.
88    ///
89    /// The driver uses the [AWS SDK](https://github.com/awslabs/aws-sdk-rust) to retrieve AWS
90    /// credentials. If you have a shared AWS credentials or config file, then those credentials
91    /// will be used by default if AWS authentication environment variables are not set. To
92    /// override this behavior, set `AWS_SHARED_CREDENTIALS_FILE=""` in your shell or set the
93    /// equivalent environment variable value in your script or application. Alternatively, you
94    /// can create an AWS profile specifically for your MongoDB credentials and set the
95    /// `AWS_PROFILE` environment variable to that profile name.
96    ///
97    /// Note: Only server versions 4.4+ support AWS authentication.
98    #[cfg(feature = "aws-auth")]
99    MongoDbAws,
100
101    /// MONGODB-OIDC authenticates using [OpenID Connect](https://openid.net/developers/specs/) access tokens.
102    #[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    /// Determines if the provided credentials have the required information to perform
129    /// authentication.
130    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    /// Returns this `AuthMechanism` as a string.
222    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    /// Get the default authSource for a given mechanism depending on the database provided in the
238    /// connection string.
239    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    /// Constructs the first message to be sent to the server as part of the authentication
255    /// handshake, which can be used for speculative authentication.
256    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)]
421// Auxiliary information needed by authentication mechanisms.
422pub(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/// A struct containing authentication information.
439///
440/// Some fields (mechanism and source) may be omitted and will either be negotiated or assigned a
441/// default value, depending on the values of other fields in the credential.
442#[derive(Clone, Default, Deserialize, TypedBuilder)]
443#[derive_where(PartialEq)]
444#[builder(field_defaults(default, setter(into)))]
445#[non_exhaustive]
446pub struct Credential {
447    /// The username to authenticate with. This applies to all mechanisms but may be omitted when
448    /// authenticating via MONGODB-X509.
449    pub username: Option<String>,
450
451    /// The database used to authenticate. This applies to all mechanisms and defaults to "admin"
452    /// in SCRAM authentication mechanisms, "$external" for GSSAPI and MONGODB-X509, and the
453    /// database name or "$external" for PLAIN.
454    pub source: Option<String>,
455
456    /// The password to authenticate with. This does not apply to all mechanisms.
457    pub password: Option<String>,
458
459    /// Which authentication mechanism to use. If not provided, one will be negotiated with the
460    /// server.
461    pub mechanism: Option<AuthMechanism>,
462
463    /// Additional properties for the given mechanism.
464    ///
465    /// If any value in the properties contains a comma, this field must be set directly on
466    /// [`ClientOptions`](crate::options::ClientOptions) and cannot be parsed from a connection
467    /// string.
468    pub mechanism_properties: Option<Document>,
469
470    /// The token callback for OIDC authentication.
471    /// ```
472    /// use mongodb::{error::Error, Client, options::{ClientOptions, oidc::{Callback, CallbackContext, IdpServerResponse}}};
473    /// use std::time::{Duration, Instant};
474    /// use futures::future::FutureExt;
475    /// async fn do_human_flow(c: CallbackContext) -> Result<(String, Option<Instant>, Option<String>), Error> {
476    ///   // Do the human flow here see: https://auth0.com/docs/authenticate/login/oidc-conformant-authentication/oidc-adoption-auth-code-flow
477    ///   Ok(("some_access_token".to_string(), Some(Instant::now() + Duration::from_secs(60 * 60 * 12)), Some("some_refresh_token".to_string())))
478    /// }
479    ///
480    /// async fn setup_client() -> Result<Client, Error> {
481    ///     let mut opts =
482    ///     ClientOptions::parse("mongodb://localhost:27017,localhost:27018/admin?authSource=admin&authMechanism=MONGODB-OIDC").await?;
483    ///     opts.credential.as_mut().unwrap().oidc_callback =
484    ///         Callback::human(move |c: CallbackContext| {
485    ///         async move {
486    ///             let (access_token, expires, refresh_token) = do_human_flow(c).await?;
487    ///             Ok(IdpServerResponse::builder().access_token(access_token).expires(expires).refresh_token(refresh_token).build())
488    ///         }.boxed()
489    ///     });
490    ///     Client::with_options(opts)
491    /// }
492    /// ```
493    #[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    /// If the mechanism is missing, append the appropriate mechanism negotiation key-value-pair to
508    /// the provided hello or legacy hello command document.
509    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    /// Attempts to authenticate a stream according to this credential, returning an error
519    /// result on failure. A mechanism may be negotiated if one is not provided as part of the
520    /// credential.
521    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        // Verify server can authenticate.
530        if !stream_description.initial_server_type.can_auth() {
531            return Ok(());
532        };
533
534        // If speculative authentication returned a response, then short-circuit the authentication
535        // logic and use the first round from the handshake.
536        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        // Authenticate according to the chosen mechanism.
558        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/// Contains the first client message sent as part of the authentication handshake.
595#[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/// Contains the complete first round of the authentication handshake, including the client message
627/// and the server response.
628#[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}