Skip to content

Commit a41846d

Browse files
authored
feat(NODE-5036): reauthenticate OIDC and retry (#3589)
1 parent 7649722 commit a41846d

21 files changed

+923
-62
lines changed

.evergreen/setup-oidc-roles.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ set -o xtrace # Write all commands first to stderr
55
cd ${DRIVERS_TOOLS}/.evergreen/auth_oidc
66
. ./activate-authoidcvenv.sh
77

8-
${DRIVERS_TOOLS}/mongodb/bin/mongosh setup_oidc.js
8+
${DRIVERS_TOOLS}/mongodb/bin/mongosh "mongodb://localhost:27017,localhost:27018/?replicaSet=oidc-repl0&readPreference=primary" setup_oidc.js

src/cmap/auth/auth_provider.ts

Lines changed: 25 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 is for reauthentication. */
21+
reauthenticating = false;
1622
/** The options passed to the `connect` method */
1723
options: AuthContextOptions;
1824

@@ -57,4 +63,22 @@ export class AuthProvider {
5763
// TODO(NODE-3483): Replace this with MongoMethodOverrideError
5864
callback(new MongoRuntimeError('`auth` method must be overridden by subclass'));
5965
}
66+
67+
/**
68+
* Reauthenticate.
69+
* @param context - The shared auth context.
70+
* @param callback - The callback.
71+
*/
72+
reauth(context: AuthContext, callback: Callback): void {
73+
// If we are already reauthenticating this is a no-op.
74+
if (context.reauthenticating) {
75+
return callback(new MongoRuntimeError('Reauthentication already in progress.'));
76+
}
77+
context.reauthenticating = true;
78+
const cb: Callback = (error, result) => {
79+
context.reauthenticating = false;
80+
callback(error, result);
81+
};
82+
this.auth(context, cb);
83+
}
6084
}

src/cmap/auth/mongo_credentials.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,11 @@ export interface AuthMechanismProperties extends Document {
3737
SERVICE_REALM?: string;
3838
CANONICALIZE_HOST_NAME?: GSSAPICanonicalizationValue;
3939
AWS_SESSION_TOKEN?: string;
40+
/** @experimental */
4041
REQUEST_TOKEN_CALLBACK?: OIDCRequestFunction;
42+
/** @experimental */
4143
REFRESH_TOKEN_CALLBACK?: OIDCRefreshFunction;
44+
/** @experimental */
4245
PROVIDER_NAME?: 'aws';
4346
}
4447

src/cmap/auth/mongodb_oidc.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import { AwsServiceWorkflow } from './mongodb_oidc/aws_service_workflow';
1111
import { CallbackWorkflow } from './mongodb_oidc/callback_workflow';
1212
import type { Workflow } from './mongodb_oidc/workflow';
1313

14-
/** @public */
14+
/**
15+
* @public
16+
* @experimental
17+
*/
1518
export interface OIDCMechanismServerStep1 {
1619
authorizationEndpoint?: string;
1720
tokenEndpoint?: string;
@@ -21,21 +24,30 @@ export interface OIDCMechanismServerStep1 {
2124
requestScopes?: string[];
2225
}
2326

24-
/** @public */
27+
/**
28+
* @public
29+
* @experimental
30+
*/
2531
export interface OIDCRequestTokenResult {
2632
accessToken: string;
2733
expiresInSeconds?: number;
2834
refreshToken?: string;
2935
}
3036

31-
/** @public */
37+
/**
38+
* @public
39+
* @experimental
40+
*/
3241
export type OIDCRequestFunction = (
3342
principalName: string,
3443
serverResult: OIDCMechanismServerStep1,
3544
timeout: AbortSignal | number
3645
) => Promise<OIDCRequestTokenResult>;
3746

38-
/** @public */
47+
/**
48+
* @public
49+
* @experimental
50+
*/
3951
export type OIDCRefreshFunction = (
4052
principalName: string,
4153
serverResult: OIDCMechanismServerStep1,
@@ -52,6 +64,7 @@ OIDC_WORKFLOWS.set('aws', new AwsServiceWorkflow());
5264

5365
/**
5466
* OIDC auth provider.
67+
* @experimental
5568
*/
5669
export class MongoDBOIDC extends AuthProvider {
5770
/**
@@ -65,7 +78,7 @@ export class MongoDBOIDC extends AuthProvider {
6578
* Authenticate using OIDC
6679
*/
6780
override auth(authContext: AuthContext, callback: Callback): void {
68-
const { connection, credentials, response } = authContext;
81+
const { connection, credentials, response, reauthenticating } = authContext;
6982

7083
if (response?.speculativeAuthenticate) {
7184
return callback();
@@ -86,7 +99,7 @@ export class MongoDBOIDC extends AuthProvider {
8699
)
87100
);
88101
}
89-
workflow.execute(connection, credentials).then(
102+
workflow.execute(connection, credentials, reauthenticating).then(
90103
result => {
91104
return callback(undefined, result);
92105
},

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/providers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const AuthMechanism = Object.freeze({
88
MONGODB_SCRAM_SHA1: 'SCRAM-SHA-1',
99
MONGODB_SCRAM_SHA256: 'SCRAM-SHA-256',
1010
MONGODB_X509: 'MONGODB-X509',
11+
/** @experimental */
1112
MONGODB_OIDC: 'MONGODB-OIDC'
1213
} as const);
1314

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,
@@ -126,7 +127,6 @@ export interface ConnectionOptions
126127
noDelay?: boolean;
127128
socketTimeoutMS?: number;
128129
cancellationToken?: CancellationToken;
129-
130130
metadata: ClientMetadata;
131131
}
132132

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

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

0 commit comments

Comments
 (0)