Skip to content

Commit dbdd899

Browse files
committed
feat(NODE-5036): reauthenticate OIDC and retry
1 parent 4a7b5ec commit dbdd899

17 files changed

+849
-15
lines changed

src/cmap/auth/auth_provider.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,20 @@ import type { HandshakeDocument } from '../connect';
55
import type { Connection, ConnectionOptions } from '../connection';
66
import type { MongoCredentials } from './mongo_credentials';
77

8+
/** @internal */
89
export type AuthContextOptions = ConnectionOptions & ClientMetadataOptions;
910

10-
/** Context used during authentication */
11+
/**
12+
* Context used during authentication
13+
* @internal
14+
*/
1115
export class AuthContext {
1216
/** The connection to authenticate */
1317
connection: Connection;
1418
/** The credentials to use for authentication */
1519
credentials?: MongoCredentials;
20+
/** If the context if for reauthentication. */
21+
reauthenticating = false;
1622
/** The options passed to the `connect` method */
1723
options: AuthContextOptions;
1824

src/cmap/auth/mongodb_oidc.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export class MongoDBOIDC extends AuthProvider {
6565
* Authenticate using OIDC
6666
*/
6767
override auth(authContext: AuthContext, callback: Callback): void {
68-
const { connection, credentials, response } = authContext;
68+
const { connection, credentials, response, reauthenticating } = authContext;
6969

7070
if (response?.speculativeAuthenticate) {
7171
return callback();
@@ -86,7 +86,7 @@ export class MongoDBOIDC extends AuthProvider {
8686
)
8787
);
8888
}
89-
workflow.execute(connection, credentials).then(
89+
workflow.execute(connection, credentials, reauthenticating).then(
9090
result => {
9191
return callback(undefined, result);
9292
},

src/cmap/auth/mongodb_oidc/callback_workflow.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,11 @@ export class CallbackWorkflow implements Workflow {
5858
* - put the new entry in the cache.
5959
* - execute step two.
6060
*/
61-
async execute(connection: Connection, credentials: MongoCredentials): Promise<Document> {
61+
async execute(
62+
connection: Connection,
63+
credentials: MongoCredentials,
64+
reauthenticate = false
65+
): Promise<Document> {
6266
const request = credentials.mechanismProperties.REQUEST_TOKEN_CALLBACK;
6367
const refresh = credentials.mechanismProperties.REFRESH_TOKEN_CALLBACK;
6468

@@ -69,8 +73,8 @@ export class CallbackWorkflow implements Workflow {
6973
refresh || null
7074
);
7175
if (entry) {
72-
// Check if the entry is not expired.
73-
if (entry.isValid()) {
76+
// Check if the entry is not expired and if we are reauthenticating.
77+
if (!reauthenticate && entry.isValid()) {
7478
// Skip step one and execute the step two saslContinue.
7579
try {
7680
const result = await finishAuth(entry.tokenResult, undefined, connection, credentials);

src/cmap/auth/mongodb_oidc/workflow.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ export interface Workflow {
88
* All device workflows must implement this method in order to get the access
99
* token and then call authenticate with it.
1010
*/
11-
execute(connection: Connection, credentials: MongoCredentials): Promise<Document>;
11+
execute(
12+
connection: Connection,
13+
credentials: MongoCredentials,
14+
reauthenticate?: boolean
15+
): Promise<Document>;
1216

1317
/**
1418
* Get the document to add for speculative authentication.

src/cmap/auth/scram.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ class ScramSHA extends AuthProvider {
5353
}
5454

5555
override auth(authContext: AuthContext, callback: Callback) {
56-
const response = authContext.response;
57-
if (response && response.speculativeAuthenticate) {
56+
const { reauthenticating, response } = authContext;
57+
if (response?.speculativeAuthenticate && !reauthenticating) {
5858
continueScramConversation(
5959
this.cryptoMethod,
6060
response.speculativeAuthenticate,

src/cmap/connect.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ import {
3636
MIN_SUPPORTED_WIRE_VERSION
3737
} from './wire_protocol/constants';
3838

39-
const AUTH_PROVIDERS = new Map<AuthMechanism | string, AuthProvider>([
39+
/** @internal */
40+
export const AUTH_PROVIDERS = new Map<AuthMechanism | string, AuthProvider>([
4041
[AuthMechanism.MONGODB_AWS, new MongoDBAWS()],
4142
[AuthMechanism.MONGODB_CR, new MongoCR()],
4243
[AuthMechanism.MONGODB_GSSAPI, new GSSAPI()],
@@ -117,6 +118,7 @@ function performInitialHandshake(
117118
}
118119

119120
const authContext = new AuthContext(conn, credentials, options);
121+
conn.authContext = authContext;
120122
prepareHandshakeDocument(authContext, (err, handshakeDoc) => {
121123
if (err || !handshakeDoc) {
122124
return callback(err);

src/cmap/connection.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
uuidV4
3838
} from '../utils';
3939
import type { WriteConcern } from '../write_concern';
40+
import type { AuthContext } from './auth/auth_provider';
4041
import type { MongoCredentials } from './auth/mongo_credentials';
4142
import {
4243
CommandFailedEvent,
@@ -127,7 +128,6 @@ export interface ConnectionOptions
127128
noDelay?: boolean;
128129
socketTimeoutMS?: number;
129130
cancellationToken?: CancellationToken;
130-
131131
metadata: ClientMetadata;
132132
}
133133

@@ -165,6 +165,8 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
165165
cmd: Document,
166166
options: CommandOptions | undefined
167167
) => Promise<Document>;
168+
/** @internal */
169+
authContext?: AuthContext;
168170

169171
/**@internal */
170172
[kDelayedTimeoutId]: NodeJS.Timeout | null;

src/cmap/connection_pool.ts

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,18 @@ import {
1616
CONNECTION_READY
1717
} from '../constants';
1818
import {
19+
MONGODB_ERROR_CODES,
1920
MongoError,
2021
MongoInvalidArgumentError,
22+
MongoMissingCredentialsError,
2123
MongoNetworkError,
2224
MongoRuntimeError,
2325
MongoServerError
2426
} from '../error';
2527
import { CancellationToken, TypedEventEmitter } from '../mongo_types';
2628
import type { Server } from '../sdam/server';
2729
import { Callback, eachAsync, List, makeCounter } from '../utils';
28-
import { connect } from './connect';
30+
import { AUTH_PROVIDERS, connect } from './connect';
2931
import { Connection, ConnectionEvents, ConnectionOptions } from './connection';
3032
import {
3133
ConnectionCheckedInEvent,
@@ -544,7 +546,17 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
544546
fn(undefined, conn, (fnErr, result) => {
545547
if (typeof callback === 'function') {
546548
if (fnErr) {
547-
callback(fnErr);
549+
if ((fnErr as MongoError).code === MONGODB_ERROR_CODES.Reauthenticate) {
550+
this.reauthenticate(conn, fn, (error, res) => {
551+
if (error) {
552+
callback(error);
553+
} else {
554+
callback(undefined, res);
555+
}
556+
});
557+
} else {
558+
callback(fnErr);
559+
}
548560
} else {
549561
callback(undefined, result);
550562
}
@@ -559,7 +571,17 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
559571
fn(err as MongoError, conn, (fnErr, result) => {
560572
if (typeof callback === 'function') {
561573
if (fnErr) {
562-
callback(fnErr);
574+
if (conn && (fnErr as MongoError).code === MONGODB_ERROR_CODES.Reauthenticate) {
575+
this.reauthenticate(conn, fn, (error, res) => {
576+
if (error) {
577+
callback(error);
578+
} else {
579+
callback(undefined, res);
580+
}
581+
});
582+
} else {
583+
callback(fnErr);
584+
}
563585
} else {
564586
callback(undefined, result);
565587
}
@@ -572,6 +594,50 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
572594
});
573595
}
574596

597+
/**
598+
* Reauthenticate on the same connection and then retry the operation.
599+
*/
600+
private reauthenticate(
601+
connection: Connection,
602+
fn: WithConnectionCallback,
603+
callback: Callback
604+
): void {
605+
const authContext = connection.authContext;
606+
if (!authContext) {
607+
return callback(new MongoRuntimeError('No auth context found on connection.'));
608+
}
609+
authContext.reauthenticating = true;
610+
const credentials = authContext.credentials;
611+
if (!credentials) {
612+
return callback(
613+
new MongoMissingCredentialsError(
614+
'Connection is missing credentials when asked to reauthenticate'
615+
)
616+
);
617+
}
618+
const resolvedCredentials = credentials.resolveAuthMechanism(connection.hello || undefined);
619+
const provider = AUTH_PROVIDERS.get(resolvedCredentials.mechanism);
620+
if (!provider) {
621+
return callback(
622+
new MongoMissingCredentialsError(
623+
`Reauthenticate failed due to no auth provider for ${credentials.mechanism}`
624+
)
625+
);
626+
}
627+
provider.auth(authContext, error => {
628+
authContext.reauthenticating = false;
629+
if (error) {
630+
return callback(error);
631+
}
632+
return fn(undefined, connection, (fnErr, fnResult) => {
633+
if (fnErr) {
634+
return callback(fnErr);
635+
}
636+
callback(undefined, fnResult);
637+
});
638+
});
639+
}
640+
575641
/** Clear the min pool size timer */
576642
private clearMinPoolSizeTimer(): void {
577643
const minPoolSizeTimer = this[kMinPoolSizeTimer];

src/error.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ export const MONGODB_ERROR_CODES = Object.freeze({
5858
IllegalOperation: 20,
5959
MaxTimeMSExpired: 50,
6060
UnknownReplWriteConcern: 79,
61-
UnsatisfiableWriteConcern: 100
61+
UnsatisfiableWriteConcern: 100,
62+
Reauthenticate: 391
6263
} as const);
6364

6465
// From spec@https://github.com/mongodb/specifications/blob/f93d78191f3db2898a59013a7ed5650352ef6da8/source/change-streams/change-streams.rst#resumable-error

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ export type {
197197
ResumeToken,
198198
UpdateDescription
199199
} from './change_stream';
200+
export { AuthContext, AuthContextOptions } from './cmap/auth/auth_provider';
200201
export type {
201202
AuthMechanismProperties,
202203
MongoCredentials,

0 commit comments

Comments
 (0)