Skip to content

Commit ad4ada2

Browse files
committed
refactor(NODE-5464): refactor reauth signature
1 parent f782829 commit ad4ada2

File tree

6 files changed

+101
-98
lines changed

6 files changed

+101
-98
lines changed

src/cmap/auth/mongodb_oidc.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,14 @@ export interface Workflow {
6262
execute(
6363
connection: Connection,
6464
credentials: MongoCredentials,
65-
reauthenticating: boolean,
6665
response?: Document
6766
): Promise<Document>;
6867

68+
/**
69+
* Each workflow should specify the correct custom behaviour for reauthentication.
70+
*/
71+
reauthenticate(connection: Connection, credentials: MongoCredentials): Promise<Document>;
72+
6973
/**
7074
* Get the document to add for speculative authentication.
7175
*/
@@ -97,7 +101,11 @@ export class MongoDBOIDC extends AuthProvider {
97101
const { connection, reauthenticating, response } = authContext;
98102
const credentials = getCredentials(authContext);
99103
const workflow = getWorkflow(credentials);
100-
await workflow.execute(connection, credentials, reauthenticating, response);
104+
if (reauthenticating) {
105+
await workflow.reauthenticate(connection, credentials);
106+
} else {
107+
await workflow.execute(connection, credentials, response);
108+
}
101109
}
102110

103111
/**

src/cmap/auth/mongodb_oidc/callback_workflow.ts

Lines changed: 20 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Binary, BSON, type Document } from 'bson';
1+
import { BSON, type Document } from 'bson';
22

33
import { MongoMissingCredentialsError } from '../../../error';
44
import { ns } from '../../../utils';
@@ -11,7 +11,7 @@ import type {
1111
OIDCRequestFunction,
1212
Workflow
1313
} from '../mongodb_oidc';
14-
import { AuthMechanism } from '../providers';
14+
import { finishCommandDocument, startCommandDocument } from './command_builders';
1515

1616
/** The current version of OIDC implementation. */
1717
const OIDC_VERSION = 0;
@@ -43,27 +43,35 @@ export class CallbackWorkflow implements Workflow {
4343
return { speculativeAuthenticate: document };
4444
}
4545

46+
/**
47+
* Reauthenticate the callback workflow.
48+
* For reauthentication:
49+
* - Check if the connection's accessToken is not equal to the token manager's.
50+
* - If they are different, use the token from the manager and set it on the connection and finish auth.
51+
* - On success return, on error continue.
52+
* - start auth to update the IDP information
53+
* - If the idp info has changed, clear access token and refresh token.
54+
* - If the idp info has not changed, attempt to use the refresh token.
55+
* - if there's still a refresh token at this point, attempt to finish auth with that.
56+
* - Attempt the full auth run, on error, raise to user.
57+
*/
58+
async reauthenticate(connection: Connection, credentials: MongoCredentials): Promise<Document> {
59+
return this.execute(connection, credentials);
60+
}
61+
4662
/**
4763
* Execute the OIDC callback workflow.
4864
*/
4965
async execute(
5066
connection: Connection,
5167
credentials: MongoCredentials,
52-
reauthenticating: boolean,
5368
response?: Document
5469
): Promise<Document> {
5570
const requestCallback = credentials.mechanismProperties.REQUEST_TOKEN_CALLBACK;
5671
if (!requestCallback) {
5772
throw new MongoMissingCredentialsError(NO_REQUEST_CALLBACK);
5873
}
59-
// No entry in the cache requires us to do all authentication steps
60-
// from start to finish, including getting a fresh token for the cache.
61-
const startDocument = await this.startAuthentication(
62-
connection,
63-
credentials,
64-
reauthenticating,
65-
response
66-
);
74+
const startDocument = await this.startAuthentication(connection, credentials, response);
6775
const conversationId = startDocument.conversationId;
6876
const serverResult = BSON.deserialize(startDocument.payload.buffer) as IdPServerInfo;
6977
const tokenResult = await this.fetchAccessToken(
@@ -89,11 +97,10 @@ export class CallbackWorkflow implements Workflow {
8997
private async startAuthentication(
9098
connection: Connection,
9199
credentials: MongoCredentials,
92-
reauthenticating: boolean,
93100
response?: Document
94101
): Promise<Document> {
95102
let result;
96-
if (!reauthenticating && response?.speculativeAuthenticate) {
103+
if (response?.speculativeAuthenticate) {
97104
result = response.speculativeAuthenticate;
98105
} else {
99106
result = await connection.commandAsync(
@@ -144,29 +151,6 @@ export class CallbackWorkflow implements Workflow {
144151
}
145152
}
146153

147-
/**
148-
* Generate the finishing command document for authentication. Will be a
149-
* saslStart or saslContinue depending on the presence of a conversation id.
150-
*/
151-
function finishCommandDocument(token: string, conversationId?: number): Document {
152-
if (conversationId != null && typeof conversationId === 'number') {
153-
return {
154-
saslContinue: 1,
155-
conversationId: conversationId,
156-
payload: new Binary(BSON.serialize({ jwt: token }))
157-
};
158-
}
159-
// saslContinue requires a conversationId in the command to be valid so in this
160-
// case the server allows "step two" to actually be a saslStart with the token
161-
// as the jwt since the use of the cached value has no correlating conversating
162-
// on the particular connection.
163-
return {
164-
saslStart: 1,
165-
mechanism: AuthMechanism.MONGODB_OIDC,
166-
payload: new Binary(BSON.serialize({ jwt: token }))
167-
};
168-
}
169-
170154
/**
171155
* Determines if a result returned from a request or refresh callback
172156
* function is invalid. This means the result is nullish, doesn't contain
@@ -177,19 +161,3 @@ function isCallbackResultInvalid(tokenResult: unknown): boolean {
177161
if (!('accessToken' in tokenResult)) return true;
178162
return !Object.getOwnPropertyNames(tokenResult).every(prop => RESULT_PROPERTIES.includes(prop));
179163
}
180-
181-
/**
182-
* Generate the saslStart command document.
183-
*/
184-
function startCommandDocument(credentials: MongoCredentials): Document {
185-
const payload: Document = {};
186-
if (credentials.username) {
187-
payload.n = credentials.username;
188-
}
189-
return {
190-
saslStart: 1,
191-
autoAuthorize: 1,
192-
mechanism: AuthMechanism.MONGODB_OIDC,
193-
payload: new Binary(BSON.serialize(payload))
194-
};
195-
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Binary, BSON, type Document } from 'bson';
2+
3+
import { type MongoCredentials } from '../mongo_credentials';
4+
import { AuthMechanism } from '../providers';
5+
6+
/**
7+
* Generate the finishing command document for authentication. Will be a
8+
* saslStart or saslContinue depending on the presence of a conversation id.
9+
*/
10+
export function finishCommandDocument(token: string, conversationId?: number): Document {
11+
if (conversationId != null && typeof conversationId === 'number') {
12+
return {
13+
saslContinue: 1,
14+
conversationId: conversationId,
15+
payload: new Binary(BSON.serialize({ jwt: token }))
16+
};
17+
}
18+
// saslContinue requires a conversationId in the command to be valid so in this
19+
// case the server allows "step two" to actually be a saslStart with the token
20+
// as the jwt since the use of the cached value has no correlating conversating
21+
// on the particular connection.
22+
return {
23+
saslStart: 1,
24+
mechanism: AuthMechanism.MONGODB_OIDC,
25+
payload: new Binary(BSON.serialize({ jwt: token }))
26+
};
27+
}
28+
29+
/**
30+
* Generate the saslStart command document.
31+
*/
32+
export function startCommandDocument(credentials: MongoCredentials): Document {
33+
const payload: Document = {};
34+
if (credentials.username) {
35+
payload.n = credentials.username;
36+
}
37+
return {
38+
saslStart: 1,
39+
autoAuthorize: 1,
40+
mechanism: AuthMechanism.MONGODB_OIDC,
41+
payload: new Binary(BSON.serialize(payload))
42+
};
43+
}
Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,39 @@
1-
import { BSON, type Document } from 'bson';
1+
import { type Document } from 'bson';
22

33
import { ns } from '../../../utils';
44
import type { Connection } from '../../connection';
55
import type { MongoCredentials } from '../mongo_credentials';
66
import type { Workflow } from '../mongodb_oidc';
7-
import { AuthMechanism } from '../providers';
7+
import { finishCommandDocument } from './command_builders';
88

99
/**
10-
* Common behaviour for OIDC device workflows.
10+
* Common behaviour for OIDC machine workflows.
1111
* @internal
1212
*/
1313
export abstract class MachineWorkflow implements Workflow {
1414
/**
15-
* Execute the workflow. Looks for AWS_WEB_IDENTITY_TOKEN_FILE in the environment
16-
* and then attempts to read the token from that path.
15+
* Execute the workflow. Gets the token from the subclass implementation.
1716
*/
1817
async execute(connection: Connection, credentials: MongoCredentials): Promise<Document> {
1918
const token = await this.getToken(credentials);
20-
const command = commandDocument(token);
19+
const command = finishCommandDocument(token);
2120
return connection.commandAsync(ns(credentials.source), command, undefined);
2221
}
2322

23+
/**
24+
* Reauthenticate on a machine workflow just grabs the token again since the server
25+
* has said the current access token is invalid or expired.
26+
*/
27+
async reauthenticate(connection: Connection, credentials: MongoCredentials): Promise<Document> {
28+
return this.execute(connection, credentials);
29+
}
30+
2431
/**
2532
* Get the document to add for speculative authentication.
2633
*/
2734
async speculativeAuth(credentials: MongoCredentials): Promise<Document> {
2835
const token = await this.getToken(credentials);
29-
const document = commandDocument(token);
36+
const document = finishCommandDocument(token);
3037
document.db = credentials.source;
3138
return { speculativeAuthenticate: document };
3239
}
@@ -36,14 +43,3 @@ export abstract class MachineWorkflow implements Workflow {
3643
*/
3744
abstract getToken(credentials: MongoCredentials): Promise<string>;
3845
}
39-
40-
/**
41-
* Create the saslStart command document.
42-
*/
43-
export function commandDocument(token: string): Document {
44-
return {
45-
saslStart: 1,
46-
mechanism: AuthMechanism.MONGODB_OIDC,
47-
payload: BSON.serialize({ jwt: token })
48-
};
49-
}

src/cmap/auth/mongodb_oidc/token_manager.ts

Lines changed: 0 additions & 26 deletions
This file was deleted.

src/cmap/connection.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ const kHello = Symbol('hello');
8282
const kAutoEncrypter = Symbol('autoEncrypter');
8383
/** @internal */
8484
const kDelayedTimeoutId = Symbol('delayedTimeoutId');
85+
/** @internal */
86+
const kAccessToken = Symbol('accessToken');
8587

8688
const INVALID_QUEUE_SIZE = 'Connection internal queue contains more than 1 operation description';
8789

@@ -138,6 +140,7 @@ export interface ConnectionOptions
138140
socketTimeoutMS?: number;
139141
cancellationToken?: CancellationToken;
140142
metadata: ClientMetadata;
143+
accessToken?: string;
141144
}
142145

143146
/** @internal */
@@ -195,6 +198,8 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
195198
[kHello]: Document | null;
196199
/** @internal */
197200
[kClusterTime]: Document | null;
201+
/** @internal */
202+
[kAccessToken]?: string;
198203

199204
/** @event */
200205
static readonly COMMAND_STARTED = COMMAND_STARTED;
@@ -237,6 +242,7 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
237242
this[kDescription] = new StreamDescription(this.address, options);
238243
this[kGeneration] = options.generation;
239244
this[kLastUseTime] = now();
245+
this[kAccessToken] = options.accessToken;
240246

241247
// setup parser stream and message handling
242248
this[kQueue] = new Map();
@@ -278,6 +284,14 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
278284
this[kHello] = response;
279285
}
280286

287+
get accessToken(): string | undefined {
288+
return this[kAccessToken];
289+
}
290+
291+
set accessToken(value: string | undefined) {
292+
this[kAccessToken] = value;
293+
}
294+
281295
// Set the whether the message stream is for a monitoring connection.
282296
set isMonitoringConnection(value: boolean) {
283297
this[kMessageStream].isMonitoringConnection = value;

0 commit comments

Comments
 (0)