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.
- package/lib/client-side-encryption/providers/azure.js +21 -6
- package/lib/client-side-encryption/providers/azure.js.map +1 -1
- package/lib/cmap/auth/mongo_credentials.js +24 -16
- package/lib/cmap/auth/mongo_credentials.js.map +1 -1
- package/lib/cmap/auth/mongodb_oidc/automated_callback_workflow.js +78 -0
- package/lib/cmap/auth/mongodb_oidc/automated_callback_workflow.js.map +1 -0
- package/lib/cmap/auth/mongodb_oidc/azure_machine_workflow.js +74 -0
- package/lib/cmap/auth/mongodb_oidc/azure_machine_workflow.js.map +1 -0
- package/lib/cmap/auth/mongodb_oidc/callback_workflow.js +74 -135
- package/lib/cmap/auth/mongodb_oidc/callback_workflow.js.map +1 -1
- package/lib/cmap/auth/mongodb_oidc/command_builders.js +45 -0
- package/lib/cmap/auth/mongodb_oidc/command_builders.js.map +1 -0
- package/lib/cmap/auth/mongodb_oidc/gcp_machine_workflow.js +46 -0
- package/lib/cmap/auth/mongodb_oidc/gcp_machine_workflow.js.map +1 -0
- package/lib/cmap/auth/mongodb_oidc/human_callback_workflow.js +122 -0
- package/lib/cmap/auth/mongodb_oidc/human_callback_workflow.js.map +1 -0
- package/lib/cmap/auth/mongodb_oidc/machine_workflow.js +107 -0
- package/lib/cmap/auth/mongodb_oidc/machine_workflow.js.map +1 -0
- package/lib/cmap/auth/mongodb_oidc/token_cache.js +52 -0
- package/lib/cmap/auth/mongodb_oidc/token_cache.js.map +1 -0
- package/lib/cmap/auth/mongodb_oidc/token_machine_workflow.js +34 -0
- package/lib/cmap/auth/mongodb_oidc/token_machine_workflow.js.map +1 -0
- package/lib/cmap/auth/mongodb_oidc.js +26 -24
- package/lib/cmap/auth/mongodb_oidc.js.map +1 -1
- package/lib/cmap/auth/providers.js +0 -1
- package/lib/cmap/auth/providers.js.map +1 -1
- package/lib/cmap/connect.js +4 -4
- package/lib/cmap/connect.js.map +1 -1
- package/lib/cmap/connection.js.map +1 -1
- package/lib/cmap/connection_pool.js +1 -1
- package/lib/cmap/connection_pool.js.map +1 -1
- package/lib/connection_string.js +3 -0
- package/lib/connection_string.js.map +1 -1
- package/lib/error.js +57 -2
- package/lib/error.js.map +1 -1
- package/lib/index.js +5 -3
- package/lib/index.js.map +1 -1
- package/lib/mongo_client.js +1 -1
- package/lib/mongo_client.js.map +1 -1
- package/lib/mongo_client_auth_providers.js +34 -4
- package/lib/mongo_client_auth_providers.js.map +1 -1
- package/lib/sdam/server_description.js +10 -4
- package/lib/sdam/server_description.js.map +1 -1
- package/lib/sessions.js +10 -0
- package/lib/sessions.js.map +1 -1
- package/lib/utils.js +32 -2
- package/lib/utils.js.map +1 -1
- package/mongodb.d.ts +115 -25
- package/package.json +5 -4
- package/src/client-side-encryption/providers/azure.ts +21 -10
- package/src/cmap/auth/mongo_credentials.ts +41 -34
- package/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts +82 -0
- package/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts +85 -0
- package/src/cmap/auth/mongodb_oidc/callback_workflow.ts +96 -204
- package/src/cmap/auth/mongodb_oidc/command_builders.ts +54 -0
- package/src/cmap/auth/mongodb_oidc/gcp_machine_workflow.ts +53 -0
- package/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts +142 -0
- package/src/cmap/auth/mongodb_oidc/machine_workflow.ts +137 -0
- package/src/cmap/auth/mongodb_oidc/token_cache.ts +62 -0
- package/src/cmap/auth/mongodb_oidc/token_machine_workflow.ts +34 -0
- package/src/cmap/auth/mongodb_oidc.ts +79 -49
- package/src/cmap/auth/providers.ts +0 -1
- package/src/cmap/connect.ts +14 -4
- package/src/cmap/connection.ts +1 -0
- package/src/cmap/connection_pool.ts +2 -1
- package/src/connection_string.ts +3 -0
- package/src/error.ts +58 -1
- package/src/index.ts +8 -4
- package/src/mongo_client.ts +4 -1
- package/src/mongo_client_auth_providers.ts +44 -6
- package/src/sdam/server_description.ts +13 -4
- package/src/sessions.ts +10 -0
- package/src/utils.ts +33 -0
- package/lib/client-side-encryption/providers/utils.js +0 -35
- package/lib/client-side-encryption/providers/utils.js.map +0 -1
- package/lib/cmap/auth/mongodb_oidc/aws_service_workflow.js +0 -30
- package/lib/cmap/auth/mongodb_oidc/aws_service_workflow.js.map +0 -1
- package/lib/cmap/auth/mongodb_oidc/azure_service_workflow.js +0 -73
- package/lib/cmap/auth/mongodb_oidc/azure_service_workflow.js.map +0 -1
- package/lib/cmap/auth/mongodb_oidc/azure_token_cache.js +0 -49
- package/lib/cmap/auth/mongodb_oidc/azure_token_cache.js.map +0 -1
- package/lib/cmap/auth/mongodb_oidc/cache.js +0 -55
- package/lib/cmap/auth/mongodb_oidc/cache.js.map +0 -1
- package/lib/cmap/auth/mongodb_oidc/callback_lock_cache.js +0 -90
- package/lib/cmap/auth/mongodb_oidc/callback_lock_cache.js.map +0 -1
- package/lib/cmap/auth/mongodb_oidc/service_workflow.js +0 -43
- package/lib/cmap/auth/mongodb_oidc/service_workflow.js.map +0 -1
- package/lib/cmap/auth/mongodb_oidc/token_entry_cache.js +0 -62
- package/lib/cmap/auth/mongodb_oidc/token_entry_cache.js.map +0 -1
- package/src/client-side-encryption/providers/utils.ts +0 -37
- package/src/cmap/auth/mongodb_oidc/aws_service_workflow.ts +0 -29
- package/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts +0 -86
- package/src/cmap/auth/mongodb_oidc/azure_token_cache.ts +0 -51
- package/src/cmap/auth/mongodb_oidc/cache.ts +0 -63
- package/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts +0 -115
- package/src/cmap/auth/mongodb_oidc/service_workflow.ts +0 -49
- package/src/cmap/auth/mongodb_oidc/token_entry_cache.ts +0 -77
|
@@ -1,26 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type Document } from 'bson';
|
|
2
|
+
import { setTimeout } from 'timers/promises';
|
|
2
3
|
|
|
3
|
-
import {
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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 {
|
|
16
|
-
import {
|
|
17
|
-
import { TokenEntryCache } from './token_entry_cache';
|
|
14
|
+
import { finishCommandDocument, startCommandDocument } from './command_builders';
|
|
15
|
+
import { type TokenCache } from './token_cache';
|
|
18
16
|
|
|
19
|
-
/**
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
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:
|
|
38
|
-
|
|
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 =
|
|
45
|
-
this.
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
*
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
126
|
+
protected async finishAuthentication(
|
|
179
127
|
connection: Connection,
|
|
180
128
|
credentials: MongoCredentials,
|
|
181
|
-
|
|
129
|
+
token: string,
|
|
182
130
|
conversationId?: number
|
|
183
|
-
): Promise<
|
|
184
|
-
|
|
131
|
+
): Promise<void> {
|
|
132
|
+
await connection.command(
|
|
185
133
|
ns(credentials.source),
|
|
186
|
-
finishCommandDocument(
|
|
134
|
+
finishCommandDocument(token, conversationId),
|
|
187
135
|
undefined
|
|
188
136
|
);
|
|
189
|
-
return result;
|
|
190
137
|
}
|
|
191
138
|
|
|
192
139
|
/**
|
|
193
|
-
*
|
|
194
|
-
* puts it in the cache.
|
|
140
|
+
* Executes the callback and validates the output.
|
|
195
141
|
*/
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
return {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
+
}
|