mongodb 6.6.2 → 6.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/lib/client-side-encryption/providers/azure.js +21 -6
  2. package/lib/client-side-encryption/providers/azure.js.map +1 -1
  3. package/lib/cmap/auth/mongo_credentials.js +24 -16
  4. package/lib/cmap/auth/mongo_credentials.js.map +1 -1
  5. package/lib/cmap/auth/mongodb_oidc/automated_callback_workflow.js +78 -0
  6. package/lib/cmap/auth/mongodb_oidc/automated_callback_workflow.js.map +1 -0
  7. package/lib/cmap/auth/mongodb_oidc/azure_machine_workflow.js +74 -0
  8. package/lib/cmap/auth/mongodb_oidc/azure_machine_workflow.js.map +1 -0
  9. package/lib/cmap/auth/mongodb_oidc/callback_workflow.js +74 -135
  10. package/lib/cmap/auth/mongodb_oidc/callback_workflow.js.map +1 -1
  11. package/lib/cmap/auth/mongodb_oidc/command_builders.js +45 -0
  12. package/lib/cmap/auth/mongodb_oidc/command_builders.js.map +1 -0
  13. package/lib/cmap/auth/mongodb_oidc/gcp_machine_workflow.js +46 -0
  14. package/lib/cmap/auth/mongodb_oidc/gcp_machine_workflow.js.map +1 -0
  15. package/lib/cmap/auth/mongodb_oidc/human_callback_workflow.js +122 -0
  16. package/lib/cmap/auth/mongodb_oidc/human_callback_workflow.js.map +1 -0
  17. package/lib/cmap/auth/mongodb_oidc/machine_workflow.js +107 -0
  18. package/lib/cmap/auth/mongodb_oidc/machine_workflow.js.map +1 -0
  19. package/lib/cmap/auth/mongodb_oidc/token_cache.js +52 -0
  20. package/lib/cmap/auth/mongodb_oidc/token_cache.js.map +1 -0
  21. package/lib/cmap/auth/mongodb_oidc/token_machine_workflow.js +34 -0
  22. package/lib/cmap/auth/mongodb_oidc/token_machine_workflow.js.map +1 -0
  23. package/lib/cmap/auth/mongodb_oidc.js +26 -24
  24. package/lib/cmap/auth/mongodb_oidc.js.map +1 -1
  25. package/lib/cmap/auth/providers.js +0 -1
  26. package/lib/cmap/auth/providers.js.map +1 -1
  27. package/lib/cmap/connect.js +4 -4
  28. package/lib/cmap/connect.js.map +1 -1
  29. package/lib/cmap/connection.js.map +1 -1
  30. package/lib/cmap/connection_pool.js +1 -1
  31. package/lib/cmap/connection_pool.js.map +1 -1
  32. package/lib/connection_string.js +3 -0
  33. package/lib/connection_string.js.map +1 -1
  34. package/lib/error.js +57 -2
  35. package/lib/error.js.map +1 -1
  36. package/lib/index.js +5 -3
  37. package/lib/index.js.map +1 -1
  38. package/lib/mongo_client.js +1 -1
  39. package/lib/mongo_client.js.map +1 -1
  40. package/lib/mongo_client_auth_providers.js +34 -4
  41. package/lib/mongo_client_auth_providers.js.map +1 -1
  42. package/lib/sdam/server_description.js +10 -4
  43. package/lib/sdam/server_description.js.map +1 -1
  44. package/lib/sessions.js +10 -0
  45. package/lib/sessions.js.map +1 -1
  46. package/lib/utils.js +32 -2
  47. package/lib/utils.js.map +1 -1
  48. package/mongodb.d.ts +115 -25
  49. package/package.json +5 -4
  50. package/src/client-side-encryption/providers/azure.ts +21 -10
  51. package/src/cmap/auth/mongo_credentials.ts +41 -34
  52. package/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts +82 -0
  53. package/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts +85 -0
  54. package/src/cmap/auth/mongodb_oidc/callback_workflow.ts +96 -204
  55. package/src/cmap/auth/mongodb_oidc/command_builders.ts +54 -0
  56. package/src/cmap/auth/mongodb_oidc/gcp_machine_workflow.ts +53 -0
  57. package/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts +142 -0
  58. package/src/cmap/auth/mongodb_oidc/machine_workflow.ts +137 -0
  59. package/src/cmap/auth/mongodb_oidc/token_cache.ts +62 -0
  60. package/src/cmap/auth/mongodb_oidc/token_machine_workflow.ts +34 -0
  61. package/src/cmap/auth/mongodb_oidc.ts +79 -49
  62. package/src/cmap/auth/providers.ts +0 -1
  63. package/src/cmap/connect.ts +14 -4
  64. package/src/cmap/connection.ts +1 -0
  65. package/src/cmap/connection_pool.ts +2 -1
  66. package/src/connection_string.ts +3 -0
  67. package/src/error.ts +58 -1
  68. package/src/index.ts +8 -4
  69. package/src/mongo_client.ts +4 -1
  70. package/src/mongo_client_auth_providers.ts +44 -6
  71. package/src/sdam/server_description.ts +13 -4
  72. package/src/sessions.ts +10 -0
  73. package/src/utils.ts +33 -0
  74. package/lib/client-side-encryption/providers/utils.js +0 -35
  75. package/lib/client-side-encryption/providers/utils.js.map +0 -1
  76. package/lib/cmap/auth/mongodb_oidc/aws_service_workflow.js +0 -30
  77. package/lib/cmap/auth/mongodb_oidc/aws_service_workflow.js.map +0 -1
  78. package/lib/cmap/auth/mongodb_oidc/azure_service_workflow.js +0 -73
  79. package/lib/cmap/auth/mongodb_oidc/azure_service_workflow.js.map +0 -1
  80. package/lib/cmap/auth/mongodb_oidc/azure_token_cache.js +0 -49
  81. package/lib/cmap/auth/mongodb_oidc/azure_token_cache.js.map +0 -1
  82. package/lib/cmap/auth/mongodb_oidc/cache.js +0 -55
  83. package/lib/cmap/auth/mongodb_oidc/cache.js.map +0 -1
  84. package/lib/cmap/auth/mongodb_oidc/callback_lock_cache.js +0 -90
  85. package/lib/cmap/auth/mongodb_oidc/callback_lock_cache.js.map +0 -1
  86. package/lib/cmap/auth/mongodb_oidc/service_workflow.js +0 -43
  87. package/lib/cmap/auth/mongodb_oidc/service_workflow.js.map +0 -1
  88. package/lib/cmap/auth/mongodb_oidc/token_entry_cache.js +0 -62
  89. package/lib/cmap/auth/mongodb_oidc/token_entry_cache.js.map +0 -1
  90. package/src/client-side-encryption/providers/utils.ts +0 -37
  91. package/src/cmap/auth/mongodb_oidc/aws_service_workflow.ts +0 -29
  92. package/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts +0 -86
  93. package/src/cmap/auth/mongodb_oidc/azure_token_cache.ts +0 -51
  94. package/src/cmap/auth/mongodb_oidc/cache.ts +0 -63
  95. package/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts +0 -115
  96. package/src/cmap/auth/mongodb_oidc/service_workflow.ts +0 -49
  97. package/src/cmap/auth/mongodb_oidc/token_entry_cache.ts +0 -77
@@ -1,26 +1,23 @@
1
- import { Binary, BSON, type Document } from 'bson';
1
+ import { type Document } from 'bson';
2
+ import { setTimeout } from 'timers/promises';
2
3
 
3
- import { MONGODB_ERROR_CODES, MongoError, MongoMissingCredentialsError } from '../../../error';
4
+ import { MongoMissingCredentialsError } from '../../../error';
4
5
  import { ns } from '../../../utils';
5
6
  import type { Connection } from '../../connection';
6
7
  import type { MongoCredentials } from '../mongo_credentials';
7
- import type {
8
- IdPServerInfo,
9
- IdPServerResponse,
10
- OIDCCallbackContext,
11
- OIDCRefreshFunction,
12
- OIDCRequestFunction,
13
- Workflow
8
+ import {
9
+ type OIDCCallbackFunction,
10
+ type OIDCCallbackParams,
11
+ type OIDCResponse,
12
+ type Workflow
14
13
  } from '../mongodb_oidc';
15
- import { AuthMechanism } from '../providers';
16
- import { CallbackLockCache } from './callback_lock_cache';
17
- import { TokenEntryCache } from './token_entry_cache';
14
+ import { finishCommandDocument, startCommandDocument } from './command_builders';
15
+ import { type TokenCache } from './token_cache';
18
16
 
19
- /** The current version of OIDC implementation. */
20
- const OIDC_VERSION = 0;
21
-
22
- /** 5 minutes in seconds */
23
- const TIMEOUT_S = 300;
17
+ /** 5 minutes in milliseconds */
18
+ export const HUMAN_TIMEOUT_MS = 300000;
19
+ /** 1 minute in milliseconds */
20
+ export const AUTOMATED_TIMEOUT_MS = 60000;
24
21
 
25
22
  /** Properties allowed on results of callbacks. */
26
23
  const RESULT_PROPERTIES = ['accessToken', 'expiresInSeconds', 'refreshToken'];
@@ -29,138 +26,89 @@ const RESULT_PROPERTIES = ['accessToken', 'expiresInSeconds', 'refreshToken'];
29
26
  const CALLBACK_RESULT_ERROR =
30
27
  'User provided OIDC callbacks must return a valid object with an accessToken.';
31
28
 
29
+ /** The time to throttle callback calls. */
30
+ const THROTTLE_MS = 100;
31
+
32
32
  /**
33
33
  * OIDC implementation of a callback based workflow.
34
34
  * @internal
35
35
  */
36
- export class CallbackWorkflow implements Workflow {
37
- cache: TokenEntryCache;
38
- callbackCache: CallbackLockCache;
36
+ export abstract class CallbackWorkflow implements Workflow {
37
+ cache: TokenCache;
38
+ callback: OIDCCallbackFunction;
39
+ lastExecutionTime: number;
39
40
 
40
41
  /**
41
- * Instantiate the workflow
42
+ * Instantiate the callback workflow.
42
43
  */
43
- constructor() {
44
- this.cache = new TokenEntryCache();
45
- this.callbackCache = new CallbackLockCache();
44
+ constructor(cache: TokenCache, callback: OIDCCallbackFunction) {
45
+ this.cache = cache;
46
+ this.callback = this.withLock(callback);
47
+ this.lastExecutionTime = Date.now() - THROTTLE_MS;
46
48
  }
47
49
 
48
50
  /**
49
51
  * Get the document to add for speculative authentication. This also needs
50
52
  * to add a db field from the credentials source.
51
53
  */
52
- async speculativeAuth(credentials: MongoCredentials): Promise<Document> {
53
- const document = startCommandDocument(credentials);
54
- document.db = credentials.source;
55
- return { speculativeAuthenticate: document };
54
+ async speculativeAuth(connection: Connection, credentials: MongoCredentials): Promise<Document> {
55
+ // Check if the Client Cache has an access token.
56
+ // If it does, cache the access token in the Connection Cache and send a JwtStepRequest
57
+ // with the cached access token in the speculative authentication SASL payload.
58
+ if (this.cache.hasAccessToken) {
59
+ const accessToken = this.cache.getAccessToken();
60
+ connection.accessToken = accessToken;
61
+ const document = finishCommandDocument(accessToken);
62
+ document.db = credentials.source;
63
+ return { speculativeAuthenticate: document };
64
+ }
65
+ return {};
56
66
  }
57
67
 
58
68
  /**
59
- * Execute the OIDC callback workflow.
69
+ * Reauthenticate the callback workflow. For this we invalidated the access token
70
+ * in the cache and run the authentication steps again. No initial handshake needs
71
+ * to be sent.
60
72
  */
61
- async execute(
62
- connection: Connection,
63
- credentials: MongoCredentials,
64
- reauthenticating: boolean,
65
- response?: Document
66
- ): Promise<Document> {
67
- // Get the callbacks with locks from the callback lock cache.
68
- const { requestCallback, refreshCallback, callbackHash } = this.callbackCache.getEntry(
69
- connection,
70
- credentials
71
- );
72
- // Look for an existing entry in the cache.
73
- const entry = this.cache.getEntry(connection.address, credentials.username, callbackHash);
74
- let result;
75
- if (entry) {
76
- // Reauthentication cannot use a token from the cache since the server has
77
- // stated it is invalid by the request for reauthentication.
78
- if (entry.isValid() && !reauthenticating) {
79
- // Presence of a valid cache entry means we can skip to the finishing step.
80
- result = await this.finishAuthentication(
81
- connection,
82
- credentials,
83
- entry.tokenResult,
84
- response?.speculativeAuthenticate?.conversationId
85
- );
73
+ async reauthenticate(connection: Connection, credentials: MongoCredentials): Promise<void> {
74
+ if (this.cache.hasAccessToken) {
75
+ // Reauthentication implies the token has expired.
76
+ if (connection.accessToken === this.cache.getAccessToken()) {
77
+ // If connection's access token is the same as the cache's, remove
78
+ // the token from the cache and connection.
79
+ this.cache.removeAccessToken();
80
+ delete connection.accessToken;
86
81
  } else {
87
- // Presence of an expired cache entry means we must fetch a new one and
88
- // then execute the final step.
89
- const tokenResult = await this.fetchAccessToken(
90
- connection,
91
- credentials,
92
- entry.serverInfo,
93
- reauthenticating,
94
- callbackHash,
95
- requestCallback,
96
- refreshCallback
97
- );
98
- try {
99
- result = await this.finishAuthentication(
100
- connection,
101
- credentials,
102
- tokenResult,
103
- reauthenticating ? undefined : response?.speculativeAuthenticate?.conversationId
104
- );
105
- } catch (error) {
106
- // If we are reauthenticating and this errors with reauthentication
107
- // required, we need to do the entire process over again and clear
108
- // the cache entry.
109
- if (
110
- reauthenticating &&
111
- error instanceof MongoError &&
112
- error.code === MONGODB_ERROR_CODES.Reauthenticate
113
- ) {
114
- this.cache.deleteEntry(connection.address, credentials.username, callbackHash);
115
- result = await this.execute(connection, credentials, reauthenticating);
116
- } else {
117
- throw error;
118
- }
119
- }
82
+ // If the connection's access token is different from the cache's, set
83
+ // the cache's token on the connection and do not remove from the
84
+ // cache.
85
+ connection.accessToken = this.cache.getAccessToken();
120
86
  }
121
- } else {
122
- // No entry in the cache requires us to do all authentication steps
123
- // from start to finish, including getting a fresh token for the cache.
124
- const startDocument = await this.startAuthentication(
125
- connection,
126
- credentials,
127
- reauthenticating,
128
- response
129
- );
130
- const conversationId = startDocument.conversationId;
131
- const serverResult = BSON.deserialize(startDocument.payload.buffer) as IdPServerInfo;
132
- const tokenResult = await this.fetchAccessToken(
133
- connection,
134
- credentials,
135
- serverResult,
136
- reauthenticating,
137
- callbackHash,
138
- requestCallback,
139
- refreshCallback
140
- );
141
- result = await this.finishAuthentication(
142
- connection,
143
- credentials,
144
- tokenResult,
145
- conversationId
146
- );
147
87
  }
148
- return result;
88
+ await this.execute(connection, credentials);
149
89
  }
150
90
 
91
+ /**
92
+ * Execute the OIDC callback workflow.
93
+ */
94
+ abstract execute(
95
+ connection: Connection,
96
+ credentials: MongoCredentials,
97
+ response?: Document
98
+ ): Promise<void>;
99
+
151
100
  /**
152
101
  * Starts the callback authentication process. If there is a speculative
153
102
  * authentication document from the initial handshake, then we will use that
154
103
  * value to get the issuer, otherwise we will send the saslStart command.
155
104
  */
156
- private async startAuthentication(
105
+ protected async startAuthentication(
157
106
  connection: Connection,
158
107
  credentials: MongoCredentials,
159
- reauthenticating: boolean,
160
108
  response?: Document
161
109
  ): Promise<Document> {
162
110
  let result;
163
- if (!reauthenticating && response?.speculativeAuthenticate) {
111
+ if (response?.speculativeAuthenticate) {
164
112
  result = response.speculativeAuthenticate;
165
113
  } else {
166
114
  result = await connection.command(
@@ -175,97 +123,57 @@ export class CallbackWorkflow implements Workflow {
175
123
  /**
176
124
  * Finishes the callback authentication process.
177
125
  */
178
- private async finishAuthentication(
126
+ protected async finishAuthentication(
179
127
  connection: Connection,
180
128
  credentials: MongoCredentials,
181
- tokenResult: IdPServerResponse,
129
+ token: string,
182
130
  conversationId?: number
183
- ): Promise<Document> {
184
- const result = await connection.command(
131
+ ): Promise<void> {
132
+ await connection.command(
185
133
  ns(credentials.source),
186
- finishCommandDocument(tokenResult.accessToken, conversationId),
134
+ finishCommandDocument(token, conversationId),
187
135
  undefined
188
136
  );
189
- return result;
190
137
  }
191
138
 
192
139
  /**
193
- * Fetches an access token using either the request or refresh callbacks and
194
- * puts it in the cache.
140
+ * Executes the callback and validates the output.
195
141
  */
196
- private async fetchAccessToken(
197
- connection: Connection,
198
- credentials: MongoCredentials,
199
- serverInfo: IdPServerInfo,
200
- reauthenticating: boolean,
201
- callbackHash: string,
202
- requestCallback: OIDCRequestFunction,
203
- refreshCallback?: OIDCRefreshFunction
204
- ): Promise<IdPServerResponse> {
205
- // Get the token from the cache.
206
- const entry = this.cache.getEntry(connection.address, credentials.username, callbackHash);
207
- let result;
208
- const context: OIDCCallbackContext = { timeoutSeconds: TIMEOUT_S, version: OIDC_VERSION };
209
- // Check if there's a token in the cache.
210
- if (entry) {
211
- // If the cache entry is valid, return the token result.
212
- if (entry.isValid() && !reauthenticating) {
213
- return entry.tokenResult;
214
- }
215
- // If the cache entry is not valid, remove it from the cache and first attempt
216
- // to use the refresh callback to get a new token. If no refresh callback
217
- // exists, then fallback to the request callback.
218
- if (refreshCallback) {
219
- context.refreshToken = entry.tokenResult.refreshToken;
220
- result = await refreshCallback(serverInfo, context);
221
- } else {
222
- result = await requestCallback(serverInfo, context);
223
- }
224
- } else {
225
- // With no token in the cache we use the request callback.
226
- result = await requestCallback(serverInfo, context);
227
- }
142
+ protected async executeAndValidateCallback(params: OIDCCallbackParams): Promise<OIDCResponse> {
143
+ const result = await this.callback(params);
228
144
  // Validate that the result returned by the callback is acceptable. If it is not
229
145
  // we must clear the token result from the cache.
230
146
  if (isCallbackResultInvalid(result)) {
231
- this.cache.deleteEntry(connection.address, credentials.username, callbackHash);
232
147
  throw new MongoMissingCredentialsError(CALLBACK_RESULT_ERROR);
233
148
  }
234
- // Cleanup the cache.
235
- this.cache.deleteExpiredEntries();
236
- // Put the new entry into the cache.
237
- this.cache.addEntry(
238
- connection.address,
239
- credentials.username || '',
240
- callbackHash,
241
- result,
242
- serverInfo
243
- );
244
149
  return result;
245
150
  }
246
- }
247
151
 
248
- /**
249
- * Generate the finishing command document for authentication. Will be a
250
- * saslStart or saslContinue depending on the presence of a conversation id.
251
- */
252
- function finishCommandDocument(token: string, conversationId?: number): Document {
253
- if (conversationId != null && typeof conversationId === 'number') {
254
- return {
255
- saslContinue: 1,
256
- conversationId: conversationId,
257
- payload: new Binary(BSON.serialize({ jwt: token }))
152
+ /**
153
+ * Ensure the callback is only executed one at a time and throttles the calls
154
+ * to every 100ms.
155
+ */
156
+ protected withLock(callback: OIDCCallbackFunction): OIDCCallbackFunction {
157
+ let lock: Promise<any> = Promise.resolve();
158
+ return async (params: OIDCCallbackParams): Promise<OIDCResponse> => {
159
+ // We do this to ensure that we would never return the result of the
160
+ // previous lock, only the current callback's value would get returned.
161
+ await lock;
162
+ lock = lock
163
+ // eslint-disable-next-line github/no-then
164
+ .catch(() => null)
165
+ // eslint-disable-next-line github/no-then
166
+ .then(async () => {
167
+ const difference = Date.now() - this.lastExecutionTime;
168
+ if (difference <= THROTTLE_MS) {
169
+ await setTimeout(THROTTLE_MS - difference, { signal: params.timeoutContext });
170
+ }
171
+ this.lastExecutionTime = Date.now();
172
+ return await callback(params);
173
+ });
174
+ return await lock;
258
175
  };
259
176
  }
260
- // saslContinue requires a conversationId in the command to be valid so in this
261
- // case the server allows "step two" to actually be a saslStart with the token
262
- // as the jwt since the use of the cached value has no correlating conversating
263
- // on the particular connection.
264
- return {
265
- saslStart: 1,
266
- mechanism: AuthMechanism.MONGODB_OIDC,
267
- payload: new Binary(BSON.serialize({ jwt: token }))
268
- };
269
177
  }
270
178
 
271
179
  /**
@@ -278,19 +186,3 @@ function isCallbackResultInvalid(tokenResult: unknown): boolean {
278
186
  if (!('accessToken' in tokenResult)) return true;
279
187
  return !Object.getOwnPropertyNames(tokenResult).every(prop => RESULT_PROPERTIES.includes(prop));
280
188
  }
281
-
282
- /**
283
- * Generate the saslStart command document.
284
- */
285
- function startCommandDocument(credentials: MongoCredentials): Document {
286
- const payload: Document = {};
287
- if (credentials.username) {
288
- payload.n = credentials.username;
289
- }
290
- return {
291
- saslStart: 1,
292
- autoAuthorize: 1,
293
- mechanism: AuthMechanism.MONGODB_OIDC,
294
- payload: new Binary(BSON.serialize(payload))
295
- };
296
- }
@@ -0,0 +1,54 @@
1
+ import { Binary, BSON, type Document } from 'bson';
2
+
3
+ import { type MongoCredentials } from '../mongo_credentials';
4
+ import { AuthMechanism } from '../providers';
5
+
6
+ /** @internal */
7
+ export interface OIDCCommand {
8
+ saslStart?: number;
9
+ saslContinue?: number;
10
+ conversationId?: number;
11
+ mechanism?: string;
12
+ autoAuthorize?: number;
13
+ db?: string;
14
+ payload: Binary;
15
+ }
16
+
17
+ /**
18
+ * Generate the finishing command document for authentication. Will be a
19
+ * saslStart or saslContinue depending on the presence of a conversation id.
20
+ */
21
+ export function finishCommandDocument(token: string, conversationId?: number): OIDCCommand {
22
+ if (conversationId != null) {
23
+ return {
24
+ saslContinue: 1,
25
+ conversationId: conversationId,
26
+ payload: new Binary(BSON.serialize({ jwt: token }))
27
+ };
28
+ }
29
+ // saslContinue requires a conversationId in the command to be valid so in this
30
+ // case the server allows "step two" to actually be a saslStart with the token
31
+ // as the jwt since the use of the cached value has no correlating conversating
32
+ // on the particular connection.
33
+ return {
34
+ saslStart: 1,
35
+ mechanism: AuthMechanism.MONGODB_OIDC,
36
+ payload: new Binary(BSON.serialize({ jwt: token }))
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Generate the saslStart command document.
42
+ */
43
+ export function startCommandDocument(credentials: MongoCredentials): OIDCCommand {
44
+ const payload: Document = {};
45
+ if (credentials.username) {
46
+ payload.n = credentials.username;
47
+ }
48
+ return {
49
+ saslStart: 1,
50
+ autoAuthorize: 1,
51
+ mechanism: AuthMechanism.MONGODB_OIDC,
52
+ payload: new Binary(BSON.serialize(payload))
53
+ };
54
+ }
@@ -0,0 +1,53 @@
1
+ import { MongoGCPError } from '../../../error';
2
+ import { get } from '../../../utils';
3
+ import { type MongoCredentials } from '../mongo_credentials';
4
+ import { type AccessToken, MachineWorkflow } from './machine_workflow';
5
+ import { type TokenCache } from './token_cache';
6
+
7
+ /** GCP base URL. */
8
+ const GCP_BASE_URL =
9
+ 'http://metadata/computeMetadata/v1/instance/service-accounts/default/identity';
10
+
11
+ /** GCP request headers. */
12
+ const GCP_HEADERS = Object.freeze({ 'Metadata-Flavor': 'Google' });
13
+
14
+ /** Error for when the token audience is missing in the environment. */
15
+ const TOKEN_RESOURCE_MISSING_ERROR =
16
+ 'TOKEN_RESOURCE must be set in the auth mechanism properties when ENVIRONMENT is gcp.';
17
+
18
+ export class GCPMachineWorkflow extends MachineWorkflow {
19
+ /**
20
+ * Instantiate the machine workflow.
21
+ */
22
+ constructor(cache: TokenCache) {
23
+ super(cache);
24
+ }
25
+
26
+ /**
27
+ * Get the token from the environment.
28
+ */
29
+ async getToken(credentials?: MongoCredentials): Promise<AccessToken> {
30
+ const tokenAudience = credentials?.mechanismProperties.TOKEN_RESOURCE;
31
+ if (!tokenAudience) {
32
+ throw new MongoGCPError(TOKEN_RESOURCE_MISSING_ERROR);
33
+ }
34
+ return await getGcpTokenData(tokenAudience);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Hit the GCP endpoint to get the token data.
40
+ */
41
+ async function getGcpTokenData(tokenAudience: string): Promise<AccessToken> {
42
+ const url = new URL(GCP_BASE_URL);
43
+ url.searchParams.append('audience', tokenAudience);
44
+ const response = await get(url, {
45
+ headers: GCP_HEADERS
46
+ });
47
+ if (response.status !== 200) {
48
+ throw new MongoGCPError(
49
+ `Status code ${response.status} returned from the GCP endpoint. Response body: ${response.body}`
50
+ );
51
+ }
52
+ return { access_token: response.body };
53
+ }
@@ -0,0 +1,142 @@
1
+ import { BSON } from 'bson';
2
+
3
+ import { MONGODB_ERROR_CODES, MongoError, MongoOIDCError } from '../../../error';
4
+ import { Timeout, TimeoutError } from '../../../timeout';
5
+ import { type Connection } from '../../connection';
6
+ import { type MongoCredentials } from '../mongo_credentials';
7
+ import {
8
+ type IdPInfo,
9
+ OIDC_VERSION,
10
+ type OIDCCallbackFunction,
11
+ type OIDCCallbackParams,
12
+ type OIDCResponse
13
+ } from '../mongodb_oidc';
14
+ import { CallbackWorkflow, HUMAN_TIMEOUT_MS } from './callback_workflow';
15
+ import { type TokenCache } from './token_cache';
16
+
17
+ /**
18
+ * Class implementing behaviour for the non human callback workflow.
19
+ * @internal
20
+ */
21
+ export class HumanCallbackWorkflow extends CallbackWorkflow {
22
+ /**
23
+ * Instantiate the human callback workflow.
24
+ */
25
+ constructor(cache: TokenCache, callback: OIDCCallbackFunction) {
26
+ super(cache, callback);
27
+ }
28
+
29
+ /**
30
+ * Execute the OIDC human callback workflow.
31
+ */
32
+ async execute(connection: Connection, credentials: MongoCredentials): Promise<void> {
33
+ // Check if the Client Cache has an access token.
34
+ // If it does, cache the access token in the Connection Cache and perform a One-Step SASL conversation
35
+ // using the access token. If the server returns an Authentication error (18),
36
+ // invalidate the access token token from the Client Cache, clear the Connection Cache,
37
+ // and restart the authentication flow. Raise any other errors to the user. On success, exit the algorithm.
38
+ if (this.cache.hasAccessToken) {
39
+ const token = this.cache.getAccessToken();
40
+ connection.accessToken = token;
41
+ try {
42
+ return await this.finishAuthentication(connection, credentials, token);
43
+ } catch (error) {
44
+ if (
45
+ error instanceof MongoError &&
46
+ error.code === MONGODB_ERROR_CODES.AuthenticationFailed
47
+ ) {
48
+ this.cache.removeAccessToken();
49
+ delete connection.accessToken;
50
+ return await this.execute(connection, credentials);
51
+ } else {
52
+ throw error;
53
+ }
54
+ }
55
+ }
56
+ // Check if the Client Cache has a refresh token.
57
+ // If it does, call the OIDC Human Callback with the cached refresh token and IdpInfo to get a
58
+ // new access token. Cache the new access token in the Client Cache and Connection Cache.
59
+ // Perform a One-Step SASL conversation using the new access token. If the the server returns
60
+ // an Authentication error (18), clear the refresh token, invalidate the access token from the
61
+ // Client Cache, clear the Connection Cache, and restart the authentication flow. Raise any other
62
+ // errors to the user. On success, exit the algorithm.
63
+ if (this.cache.hasRefreshToken) {
64
+ const refreshToken = this.cache.getRefreshToken();
65
+ const result = await this.fetchAccessToken(
66
+ this.cache.getIdpInfo(),
67
+ credentials,
68
+ refreshToken
69
+ );
70
+ this.cache.put(result);
71
+ connection.accessToken = result.accessToken;
72
+ try {
73
+ return await this.finishAuthentication(connection, credentials, result.accessToken);
74
+ } catch (error) {
75
+ if (
76
+ error instanceof MongoError &&
77
+ error.code === MONGODB_ERROR_CODES.AuthenticationFailed
78
+ ) {
79
+ this.cache.removeRefreshToken();
80
+ delete connection.accessToken;
81
+ return await this.execute(connection, credentials);
82
+ } else {
83
+ throw error;
84
+ }
85
+ }
86
+ }
87
+
88
+ // Start a new Two-Step SASL conversation.
89
+ // Run a PrincipalStepRequest to get the IdpInfo.
90
+ // Call the OIDC Human Callback with the new IdpInfo to get a new access token and optional refresh
91
+ // token. Drivers MUST NOT pass a cached refresh token to the callback when performing
92
+ // a new Two-Step conversation. Cache the new IdpInfo and refresh token in the Client Cache and the
93
+ // new access token in the Client Cache and Connection Cache.
94
+ // Attempt to authenticate using a JwtStepRequest with the new access token. Raise any errors to the user.
95
+ const startResponse = await this.startAuthentication(connection, credentials);
96
+ const conversationId = startResponse.conversationId;
97
+ const idpInfo = BSON.deserialize(startResponse.payload.buffer) as IdPInfo;
98
+ const callbackResponse = await this.fetchAccessToken(idpInfo, credentials);
99
+ this.cache.put(callbackResponse, idpInfo);
100
+ connection.accessToken = callbackResponse.accessToken;
101
+ return await this.finishAuthentication(
102
+ connection,
103
+ credentials,
104
+ callbackResponse.accessToken,
105
+ conversationId
106
+ );
107
+ }
108
+
109
+ /**
110
+ * Fetches an access token using the callback.
111
+ */
112
+ private async fetchAccessToken(
113
+ idpInfo: IdPInfo,
114
+ credentials: MongoCredentials,
115
+ refreshToken?: string
116
+ ): Promise<OIDCResponse> {
117
+ const controller = new AbortController();
118
+ const params: OIDCCallbackParams = {
119
+ timeoutContext: controller.signal,
120
+ version: OIDC_VERSION,
121
+ idpInfo: idpInfo
122
+ };
123
+ if (credentials.username) {
124
+ params.username = credentials.username;
125
+ }
126
+ if (refreshToken) {
127
+ params.refreshToken = refreshToken;
128
+ }
129
+ const timeout = Timeout.expires(HUMAN_TIMEOUT_MS);
130
+ try {
131
+ return await Promise.race([this.executeAndValidateCallback(params), timeout]);
132
+ } catch (error) {
133
+ if (TimeoutError.is(error)) {
134
+ controller.abort();
135
+ throw new MongoOIDCError(`OIDC callback timed out after ${HUMAN_TIMEOUT_MS}ms.`);
136
+ }
137
+ throw error;
138
+ } finally {
139
+ timeout.clear();
140
+ }
141
+ }
142
+ }