Skip to content

Commit 05361ce

Browse files
authored
RUST-1663: Implement and enable reauthentication for OIDC (#1044)
1 parent 5fdda29 commit 05361ce

File tree

5 files changed

+110
-16
lines changed

5 files changed

+110
-16
lines changed

src/client/auth.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,44 @@ impl AuthMechanism {
341341
.into()),
342342
}
343343
}
344+
345+
pub(crate) async fn reauthenticate_stream(
346+
&self,
347+
stream: &mut Connection,
348+
credential: &Credential,
349+
server_api: Option<&ServerApi>,
350+
) -> Result<()> {
351+
self.validate_credential(credential)?;
352+
353+
match self {
354+
AuthMechanism::ScramSha1
355+
| AuthMechanism::ScramSha256
356+
| AuthMechanism::MongoDbX509
357+
| AuthMechanism::Plain
358+
| AuthMechanism::MongoDbCr => Err(ErrorKind::Authentication {
359+
message: format!(
360+
"Reauthentication for authentication mechanism {:?} is not supported.",
361+
self
362+
),
363+
}
364+
.into()),
365+
#[cfg(feature = "aws-auth")]
366+
AuthMechanism::MongoDbAws => Err(ErrorKind::Authentication {
367+
message: format!(
368+
"Reauthentication for authentication mechanism {:?} is not supported.",
369+
self
370+
),
371+
}
372+
.into()),
373+
AuthMechanism::MongoDbOidc => {
374+
oidc::reauthenticate_stream(stream, credential, server_api).await
375+
}
376+
_ => Err(ErrorKind::Authentication {
377+
message: format!("Authentication mechanism {:?} not yet implemented.", self),
378+
}
379+
.into()),
380+
}
381+
}
344382
}
345383

346384
impl FromStr for AuthMechanism {

src/client/auth/oidc.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,15 @@ fn get_refresh_token_and_idp_info(
224224
(refresh_token, idp_info)
225225
}
226226

227+
pub(crate) async fn reauthenticate_stream(
228+
conn: &mut Connection,
229+
credential: &Credential,
230+
server_api: Option<&ServerApi>,
231+
) -> Result<()> {
232+
invalidate_caches(conn, credential);
233+
authenticate_stream(conn, credential, server_api, None).await
234+
}
235+
227236
pub(crate) async fn authenticate_stream(
228237
conn: &mut Connection,
229238
credential: &Credential,

src/client/executor.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,8 @@ impl Client {
280280
}
281281

282282
/// Selects a server and executes the given operation on it, optionally using a provided
283-
/// session. Retries the operation upon failure if retryability is supported.
283+
/// session. Retries the operation upon failure if retryability is supported or after
284+
/// reauthenticating if reauthentication is required.
284285
async fn execute_operation_with_retry<T: Operation>(
285286
&self,
286287
mut op: T,
@@ -397,6 +398,30 @@ impl Client {
397398
implicit_session,
398399
},
399400
Err(mut err) => {
401+
// If the error is a reauthentication required error, we reauthenticate and
402+
// retry the operation.
403+
if err.is_reauthentication_required() {
404+
let credential = self.inner.options.credential.as_ref().ok_or(
405+
ErrorKind::Authentication {
406+
message: "No Credential when reauthentication required error \
407+
occured"
408+
.to_string(),
409+
},
410+
)?;
411+
let server_api = self.inner.options.server_api.as_ref();
412+
413+
credential
414+
.mechanism
415+
.as_ref()
416+
.ok_or(ErrorKind::Authentication {
417+
message: "No AuthMechanism when reauthentication required error \
418+
occured"
419+
.to_string(),
420+
})?
421+
.reauthenticate_stream(&mut conn, credential, server_api)
422+
.await?;
423+
continue;
424+
}
400425
err.wire_version = conn.stream_description()?.max_wire_version;
401426

402427
// Retryable writes are only supported by storage engines with document-level

src/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const RETRYABLE_WRITE_CODES: [i32; 12] = [
2626
11600, 11602, 10107, 13435, 13436, 189, 91, 7, 6, 89, 9001, 262,
2727
];
2828
const UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL_CODES: [i32; 3] = [50, 64, 91];
29+
const REAUTHENTICATION_REQUIRED_CODE: i32 = 391;
2930

3031
/// Retryable write error label. This label will be added to an error when the error is
3132
/// write-retryable.
@@ -378,6 +379,11 @@ impl Error {
378379
.unwrap_or(false)
379380
}
380381

382+
/// If this error corresponds to a "reauthentication required" error.
383+
pub(crate) fn is_reauthentication_required(&self) -> bool {
384+
self.sdam_code() == Some(REAUTHENTICATION_REQUIRED_CODE)
385+
}
386+
381387
/// If this error corresponds to a "node is recovering" error as per the SDAM spec.
382388
pub(crate) fn is_recovering(&self) -> bool {
383389
self.sdam_code()

src/test/spec/oidc.rs

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1+
use crate::{
2+
client::{
3+
auth::{oidc, AuthMechanism, Credential},
4+
options::ClientOptions,
5+
},
6+
test::log_uncaptured,
7+
Client,
8+
};
9+
use std::sync::{Arc, Mutex};
10+
11+
// Machine Callback tests
112
// Prose test 1.1 Single Principal Implicit Username
213
#[tokio::test]
3-
async fn single_principal_implicit_username() -> anyhow::Result<()> {
4-
use crate::{
5-
client::{
6-
auth::{oidc, AuthMechanism, Credential},
7-
options::ClientOptions,
8-
},
9-
test::log_uncaptured,
10-
Client,
11-
};
14+
async fn machine_single_principal_implicit_username() -> anyhow::Result<()> {
1215
use bson::Document;
1316
use futures_util::FutureExt;
1417

@@ -17,10 +20,16 @@ async fn single_principal_implicit_username() -> anyhow::Result<()> {
1720
return Ok(());
1821
}
1922

23+
// we need to assert that the callback is only called once
24+
let call_count = Arc::new(Mutex::new(0));
25+
let cb_call_count = call_count.clone();
26+
2027
let mut opts = ClientOptions::parse("mongodb://localhost/?authMechanism=MONGODB-OIDC").await?;
2128
opts.credential = Credential::builder()
2229
.mechanism(AuthMechanism::MongoDbOidc)
23-
.oidc_callback(oidc::Callback::machine(|_| {
30+
.oidc_callback(oidc::Callback::machine(move |_| {
31+
let call_count = cb_call_count.clone();
32+
*call_count.lock().unwrap() += 1;
2433
async move {
2534
Ok(oidc::IdpServerResponse {
2635
access_token: tokio::fs::read_to_string("/tmp/tokens/test_user1").await?,
@@ -38,14 +47,14 @@ async fn single_principal_implicit_username() -> anyhow::Result<()> {
3847
.collection::<Document>("test")
3948
.find_one(None, None)
4049
.await?;
50+
assert_eq!(1, *(*call_count).lock().unwrap());
4151
Ok(())
4252
}
4353

44-
// TODO RUST-1497: The following test will be removed because it is not an actual test in the spec,
45-
// but just showing that the human flow is still working for two_step (nothing in caching is
46-
// correctly exercised here)
54+
// Human Callback tests
55+
// Prose test 1.1 Single Principal Implicit Username
4756
#[tokio::test]
48-
async fn human_flow() -> anyhow::Result<()> {
57+
async fn human_single_principal_implicit_username() -> anyhow::Result<()> {
4958
use crate::{
5059
client::{
5160
auth::{oidc, AuthMechanism, Credential},
@@ -62,10 +71,16 @@ async fn human_flow() -> anyhow::Result<()> {
6271
return Ok(());
6372
}
6473

74+
// we need to assert that the callback is only called once
75+
let call_count = Arc::new(Mutex::new(0));
76+
let cb_call_count = call_count.clone();
77+
6578
let mut opts = ClientOptions::parse("mongodb://localhost/?authMechanism=MONGODB-OIDC").await?;
6679
opts.credential = Credential::builder()
6780
.mechanism(AuthMechanism::MongoDbOidc)
68-
.oidc_callback(oidc::Callback::human(|_| {
81+
.oidc_callback(oidc::Callback::human(move |_| {
82+
let call_count = cb_call_count.clone();
83+
*call_count.lock().unwrap() += 1;
6984
async move {
7085
Ok(oidc::IdpServerResponse {
7186
access_token: tokio::fs::read_to_string("/tmp/tokens/test_user1").await?,
@@ -83,5 +98,6 @@ async fn human_flow() -> anyhow::Result<()> {
8398
.collection::<Document>("test")
8499
.find_one(None, None)
85100
.await?;
101+
assert_eq!(1, *(*call_count).lock().unwrap());
86102
Ok(())
87103
}

0 commit comments

Comments
 (0)