mongodb 5.2.0 → 5.4.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/admin.js +18 -0
- package/lib/admin.js.map +1 -1
- package/lib/bulk/common.js +28 -7
- package/lib/bulk/common.js.map +1 -1
- package/lib/cmap/auth/mongo_credentials.js +29 -2
- package/lib/cmap/auth/mongo_credentials.js.map +1 -1
- package/lib/cmap/auth/mongodb_oidc/aws_service_workflow.js +5 -3
- package/lib/cmap/auth/mongodb_oidc/aws_service_workflow.js.map +1 -1
- package/lib/cmap/auth/mongodb_oidc/cache.js +28 -0
- package/lib/cmap/auth/mongodb_oidc/cache.js.map +1 -0
- package/lib/cmap/auth/mongodb_oidc/callback_lock_cache.js +83 -0
- package/lib/cmap/auth/mongodb_oidc/callback_lock_cache.js.map +1 -0
- package/lib/cmap/auth/mongodb_oidc/callback_workflow.js +138 -112
- package/lib/cmap/auth/mongodb_oidc/callback_workflow.js.map +1 -1
- package/lib/cmap/auth/mongodb_oidc/service_workflow.js +4 -2
- package/lib/cmap/auth/mongodb_oidc/service_workflow.js.map +1 -1
- package/lib/cmap/auth/mongodb_oidc/token_entry_cache.js +12 -56
- package/lib/cmap/auth/mongodb_oidc/token_entry_cache.js.map +1 -1
- package/lib/cmap/auth/mongodb_oidc.js +17 -13
- package/lib/cmap/auth/mongodb_oidc.js.map +1 -1
- package/lib/cmap/command_monitoring_events.js +6 -0
- package/lib/cmap/command_monitoring_events.js.map +1 -1
- package/lib/cmap/connect.js +1 -0
- package/lib/cmap/connect.js.map +1 -1
- package/lib/cmap/connection.js +12 -12
- package/lib/cmap/connection.js.map +1 -1
- package/lib/cmap/connection_pool.js +7 -3
- package/lib/cmap/connection_pool.js.map +1 -1
- package/lib/cmap/connection_pool_events.js +28 -3
- package/lib/cmap/connection_pool_events.js.map +1 -1
- package/lib/cmap/handshake/client_metadata.js +173 -0
- package/lib/cmap/handshake/client_metadata.js.map +1 -0
- package/lib/cmap/wire_protocol/constants.js +2 -2
- package/lib/cmap/wire_protocol/shared.js +2 -2
- package/lib/cmap/wire_protocol/shared.js.map +1 -1
- package/lib/collection.js +3 -0
- package/lib/collection.js.map +1 -1
- package/lib/connection_string.js +48 -53
- package/lib/connection_string.js.map +1 -1
- package/lib/constants.js +11 -0
- package/lib/constants.js.map +1 -1
- package/lib/cursor/abstract_cursor.js +1 -0
- package/lib/cursor/abstract_cursor.js.map +1 -1
- package/lib/db.js +18 -0
- package/lib/db.js.map +1 -1
- package/lib/mongo_client.js +16 -0
- package/lib/mongo_client.js.map +1 -1
- package/lib/mongo_logger.js +258 -27
- package/lib/mongo_logger.js.map +1 -1
- package/lib/operations/add_user.js.map +1 -1
- package/lib/operations/find.js +0 -7
- package/lib/operations/find.js.map +1 -1
- package/lib/operations/run_command.js.map +1 -1
- package/lib/operations/stats.js.map +1 -1
- package/lib/operations/update.js.map +1 -1
- package/lib/sdam/srv_polling.js +1 -15
- package/lib/sdam/srv_polling.js.map +1 -1
- package/lib/sdam/topology.js.map +1 -1
- package/lib/utils.js +48 -35
- package/lib/utils.js.map +1 -1
- package/mongodb.d.ts +247 -47
- package/package.json +3 -3
- package/src/admin.ts +18 -0
- package/src/bulk/common.ts +28 -7
- package/src/change_stream.ts +1 -1
- package/src/cmap/auth/mongo_credentials.ts +35 -2
- package/src/cmap/auth/mongodb_oidc/aws_service_workflow.ts +6 -3
- package/src/cmap/auth/mongodb_oidc/cache.ts +27 -0
- package/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts +107 -0
- package/src/cmap/auth/mongodb_oidc/callback_workflow.ts +208 -171
- package/src/cmap/auth/mongodb_oidc/service_workflow.ts +5 -3
- package/src/cmap/auth/mongodb_oidc/token_entry_cache.ts +17 -96
- package/src/cmap/auth/mongodb_oidc.ts +61 -37
- package/src/cmap/command_monitoring_events.ts +13 -1
- package/src/cmap/connect.ts +3 -1
- package/src/cmap/connection.ts +16 -13
- package/src/cmap/connection_pool.ts +14 -4
- package/src/cmap/connection_pool_events.ts +68 -6
- package/src/cmap/handshake/client_metadata.ts +272 -0
- package/src/cmap/wire_protocol/constants.ts +2 -2
- package/src/cmap/wire_protocol/shared.ts +2 -3
- package/src/collection.ts +6 -3
- package/src/connection_string.ts +55 -55
- package/src/constants.ts +11 -0
- package/src/cursor/abstract_cursor.ts +1 -0
- package/src/db.ts +18 -0
- package/src/index.ts +24 -6
- package/src/mongo_client.ts +50 -6
- package/src/mongo_logger.ts +363 -44
- package/src/operations/add_user.ts +8 -2
- package/src/operations/find.ts +0 -10
- package/src/operations/run_command.ts +40 -3
- package/src/operations/stats.ts +11 -2
- package/src/operations/update.ts +8 -4
- package/src/sdam/srv_polling.ts +1 -16
- package/src/sdam/topology.ts +1 -3
- package/src/utils.ts +54 -73
- package/lib/cmap/auth/mongodb_oidc/workflow.js +0 -3
- package/lib/cmap/auth/mongodb_oidc/workflow.js.map +0 -1
- package/src/cmap/auth/mongodb_oidc/workflow.ts +0 -21
|
@@ -1,16 +1,33 @@
|
|
|
1
1
|
import { Binary, BSON, type Document } from 'bson';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { MONGODB_ERROR_CODES, MongoError, MongoMissingCredentialsError } from '../../../error';
|
|
4
4
|
import { ns } from '../../../utils';
|
|
5
5
|
import type { Connection } from '../../connection';
|
|
6
6
|
import type { MongoCredentials } from '../mongo_credentials';
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
IdPServerInfo,
|
|
9
|
+
IdPServerResponse,
|
|
10
|
+
OIDCCallbackContext,
|
|
11
|
+
OIDCRefreshFunction,
|
|
12
|
+
OIDCRequestFunction,
|
|
13
|
+
Workflow
|
|
14
|
+
} from '../mongodb_oidc';
|
|
8
15
|
import { AuthMechanism } from '../providers';
|
|
16
|
+
import { CallbackLockCache } from './callback_lock_cache';
|
|
9
17
|
import { TokenEntryCache } from './token_entry_cache';
|
|
10
|
-
import type { Workflow } from './workflow';
|
|
11
18
|
|
|
12
|
-
|
|
13
|
-
const
|
|
19
|
+
/** The current version of OIDC implementation. */
|
|
20
|
+
const OIDC_VERSION = 0;
|
|
21
|
+
|
|
22
|
+
/** 5 minutes in seconds */
|
|
23
|
+
const TIMEOUT_S = 300;
|
|
24
|
+
|
|
25
|
+
/** Properties allowed on results of callbacks. */
|
|
26
|
+
const RESULT_PROPERTIES = ['accessToken', 'expiresInSeconds', 'refreshToken'];
|
|
27
|
+
|
|
28
|
+
/** Error message when the callback result is invalid. */
|
|
29
|
+
const CALLBACK_RESULT_ERROR =
|
|
30
|
+
'User provided OIDC callbacks must return a valid object with an accessToken.';
|
|
14
31
|
|
|
15
32
|
/**
|
|
16
33
|
* OIDC implementation of a callback based workflow.
|
|
@@ -18,206 +35,248 @@ const TIMEOUT_MS = 300000;
|
|
|
18
35
|
*/
|
|
19
36
|
export class CallbackWorkflow implements Workflow {
|
|
20
37
|
cache: TokenEntryCache;
|
|
38
|
+
callbackCache: CallbackLockCache;
|
|
21
39
|
|
|
22
40
|
/**
|
|
23
41
|
* Instantiate the workflow
|
|
24
42
|
*/
|
|
25
43
|
constructor() {
|
|
26
44
|
this.cache = new TokenEntryCache();
|
|
45
|
+
this.callbackCache = new CallbackLockCache();
|
|
27
46
|
}
|
|
28
47
|
|
|
29
48
|
/**
|
|
30
|
-
* Get the document to add for speculative authentication.
|
|
31
|
-
*
|
|
49
|
+
* Get the document to add for speculative authentication. This also needs
|
|
50
|
+
* to add a db field from the credentials source.
|
|
32
51
|
*/
|
|
33
|
-
speculativeAuth(): Promise<Document> {
|
|
34
|
-
|
|
52
|
+
async speculativeAuth(credentials: MongoCredentials): Promise<Document> {
|
|
53
|
+
const document = startCommandDocument(credentials);
|
|
54
|
+
document.db = credentials.source;
|
|
55
|
+
return { speculativeAuthenticate: document };
|
|
35
56
|
}
|
|
36
57
|
|
|
37
58
|
/**
|
|
38
|
-
* Execute the workflow.
|
|
39
|
-
*
|
|
40
|
-
* Steps:
|
|
41
|
-
* - If an entry is in the cache
|
|
42
|
-
* - If it is not expired
|
|
43
|
-
* - Skip step one and use the entry to execute step two.
|
|
44
|
-
* - If it is expired
|
|
45
|
-
* - If the refresh callback exists
|
|
46
|
-
* - remove expired entry from cache
|
|
47
|
-
* - call the refresh callback.
|
|
48
|
-
* - put the new entry in the cache.
|
|
49
|
-
* - execute step two.
|
|
50
|
-
* - If the refresh callback does not exist.
|
|
51
|
-
* - remove expired entry from cache
|
|
52
|
-
* - call the request callback.
|
|
53
|
-
* - put the new entry in the cache.
|
|
54
|
-
* - execute step two.
|
|
55
|
-
* - If no entry is in the cache.
|
|
56
|
-
* - execute step one.
|
|
57
|
-
* - call the refresh callback.
|
|
58
|
-
* - put the new entry in the cache.
|
|
59
|
-
* - execute step two.
|
|
59
|
+
* Execute the OIDC callback workflow.
|
|
60
60
|
*/
|
|
61
61
|
async execute(
|
|
62
62
|
connection: Connection,
|
|
63
63
|
credentials: MongoCredentials,
|
|
64
|
-
|
|
64
|
+
reauthenticating: boolean,
|
|
65
|
+
response?: Document
|
|
65
66
|
): Promise<Document> {
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
connection.address,
|
|
71
|
-
credentials.username,
|
|
72
|
-
request || null,
|
|
73
|
-
refresh || null
|
|
67
|
+
// Get the callbacks with locks from the callback lock cache.
|
|
68
|
+
const { requestCallback, refreshCallback, callbackHash } = this.callbackCache.getCallbacks(
|
|
69
|
+
connection,
|
|
70
|
+
credentials
|
|
74
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
75
|
if (entry) {
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
this.cache.deleteEntry(
|
|
86
|
-
connection.address,
|
|
87
|
-
credentials.username || '',
|
|
88
|
-
request || null,
|
|
89
|
-
refresh || null
|
|
90
|
-
);
|
|
91
|
-
throw error;
|
|
92
|
-
}
|
|
93
|
-
} else {
|
|
94
|
-
// Remove the expired entry from the cache.
|
|
95
|
-
this.cache.deleteEntry(
|
|
96
|
-
connection.address,
|
|
97
|
-
credentials.username || '',
|
|
98
|
-
request || null,
|
|
99
|
-
refresh || null
|
|
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
|
|
100
85
|
);
|
|
101
|
-
|
|
102
|
-
|
|
86
|
+
} 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(
|
|
103
90
|
connection,
|
|
104
91
|
credentials,
|
|
105
|
-
entry.
|
|
106
|
-
|
|
92
|
+
entry.serverInfo,
|
|
93
|
+
reauthenticating,
|
|
94
|
+
callbackHash,
|
|
95
|
+
requestCallback,
|
|
96
|
+
refreshCallback
|
|
107
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
|
+
}
|
|
108
120
|
}
|
|
109
121
|
} else {
|
|
110
|
-
// No entry
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
115
146
|
);
|
|
116
|
-
const stepOne = BSON.deserialize(result.payload.buffer) as OIDCMechanismServerStep1;
|
|
117
|
-
// Call the request callback and finish auth.
|
|
118
|
-
return this.requestAndFinish(connection, credentials, stepOne, result.conversationId);
|
|
119
147
|
}
|
|
148
|
+
return result;
|
|
120
149
|
}
|
|
121
150
|
|
|
122
151
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
152
|
+
* Starts the callback authentication process. If there is a speculative
|
|
153
|
+
* authentication document from the initial handshake, then we will use that
|
|
154
|
+
* value to get the issuer, otherwise we will send the saslStart command.
|
|
125
155
|
*/
|
|
126
|
-
private async
|
|
156
|
+
private async startAuthentication(
|
|
127
157
|
connection: Connection,
|
|
128
158
|
credentials: MongoCredentials,
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
conversationId?: number
|
|
159
|
+
reauthenticating: boolean,
|
|
160
|
+
response?: Document
|
|
132
161
|
): Promise<Document> {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (refresh) {
|
|
137
|
-
const result: OIDCRequestTokenResult = await refresh(
|
|
138
|
-
credentials.username,
|
|
139
|
-
stepOneResult,
|
|
140
|
-
tokenResult,
|
|
141
|
-
TIMEOUT_MS
|
|
142
|
-
);
|
|
143
|
-
// Validate the result.
|
|
144
|
-
if (!result || !result.accessToken) {
|
|
145
|
-
throw new MongoMissingCredentialsError(
|
|
146
|
-
'REFRESH_TOKEN_CALLBACK must return a valid object with an accessToken'
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
// Cache a new entry and continue with the saslContinue.
|
|
150
|
-
this.cache.addEntry(
|
|
151
|
-
connection.address,
|
|
152
|
-
credentials.username || '',
|
|
153
|
-
request || null,
|
|
154
|
-
refresh,
|
|
155
|
-
result,
|
|
156
|
-
stepOneResult
|
|
157
|
-
);
|
|
158
|
-
return finishAuth(result, conversationId, connection, credentials);
|
|
162
|
+
let result;
|
|
163
|
+
if (!reauthenticating && response?.speculativeAuthenticate) {
|
|
164
|
+
result = response.speculativeAuthenticate;
|
|
159
165
|
} else {
|
|
160
|
-
|
|
161
|
-
|
|
166
|
+
result = await connection.commandAsync(
|
|
167
|
+
ns(credentials.source),
|
|
168
|
+
startCommandDocument(credentials),
|
|
169
|
+
undefined
|
|
170
|
+
);
|
|
162
171
|
}
|
|
172
|
+
return result;
|
|
163
173
|
}
|
|
164
174
|
|
|
165
175
|
/**
|
|
166
|
-
*
|
|
176
|
+
* Finishes the callback authentication process.
|
|
167
177
|
*/
|
|
168
|
-
private async
|
|
178
|
+
private async finishAuthentication(
|
|
169
179
|
connection: Connection,
|
|
170
180
|
credentials: MongoCredentials,
|
|
171
|
-
|
|
181
|
+
tokenResult: IdPServerResponse,
|
|
172
182
|
conversationId?: number
|
|
173
183
|
): Promise<Document> {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
+
const result = await connection.commandAsync(
|
|
185
|
+
ns(credentials.source),
|
|
186
|
+
finishCommandDocument(tokenResult.accessToken, conversationId),
|
|
187
|
+
undefined
|
|
188
|
+
);
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Fetches an access token using either the request or refresh callbacks and
|
|
194
|
+
* puts it in the cache.
|
|
195
|
+
*/
|
|
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);
|
|
184
227
|
}
|
|
185
|
-
|
|
186
|
-
//
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
);
|
|
228
|
+
// Validate that the result returned by the callback is acceptable. If it is not
|
|
229
|
+
// we must clear the token result from the cache.
|
|
230
|
+
if (isCallbackResultInvalid(result)) {
|
|
231
|
+
this.cache.deleteEntry(connection.address, credentials.username, callbackHash);
|
|
232
|
+
throw new MongoMissingCredentialsError(CALLBACK_RESULT_ERROR);
|
|
191
233
|
}
|
|
192
|
-
//
|
|
234
|
+
// Cleanup the cache.
|
|
235
|
+
this.cache.deleteExpiredEntries();
|
|
236
|
+
// Put the new entry into the cache.
|
|
193
237
|
this.cache.addEntry(
|
|
194
238
|
connection.address,
|
|
195
239
|
credentials.username || '',
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
stepOneResult
|
|
240
|
+
callbackHash,
|
|
241
|
+
result,
|
|
242
|
+
serverInfo
|
|
200
243
|
);
|
|
201
|
-
return
|
|
244
|
+
return result;
|
|
202
245
|
}
|
|
203
246
|
}
|
|
204
247
|
|
|
205
248
|
/**
|
|
206
|
-
*
|
|
207
|
-
*
|
|
249
|
+
* Generate the finishing command document for authentication. Will be a
|
|
250
|
+
* saslStart or saslContinue depending on the presence of a conversation id.
|
|
208
251
|
*/
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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 }))
|
|
258
|
+
};
|
|
259
|
+
}
|
|
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
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Determines if a result returned from a request or refresh callback
|
|
273
|
+
* function is invalid. This means the result is nullish, doesn't contain
|
|
274
|
+
* the accessToken required field, and does not contain extra fields.
|
|
275
|
+
*/
|
|
276
|
+
function isCallbackResultInvalid(tokenResult: unknown): boolean {
|
|
277
|
+
if (tokenResult == null || typeof tokenResult !== 'object') return true;
|
|
278
|
+
if (!('accessToken' in tokenResult)) return true;
|
|
279
|
+
return !Object.getOwnPropertyNames(tokenResult).every(prop => RESULT_PROPERTIES.includes(prop));
|
|
221
280
|
}
|
|
222
281
|
|
|
223
282
|
/**
|
|
@@ -235,25 +294,3 @@ function startCommandDocument(credentials: MongoCredentials): Document {
|
|
|
235
294
|
payload: new Binary(BSON.serialize(payload))
|
|
236
295
|
};
|
|
237
296
|
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Generate the saslContinue command document.
|
|
241
|
-
*/
|
|
242
|
-
function continueCommandDocument(token: string, conversationId?: number): Document {
|
|
243
|
-
if (conversationId) {
|
|
244
|
-
return {
|
|
245
|
-
saslContinue: 1,
|
|
246
|
-
conversationId: conversationId,
|
|
247
|
-
payload: new Binary(BSON.serialize({ jwt: token }))
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
// saslContinue requires a conversationId in the command to be valid so in this
|
|
251
|
-
// case the server allows "step two" to actually be a saslStart with the token
|
|
252
|
-
// as the jwt since the use of the cached value has no correlating conversating
|
|
253
|
-
// on the particular connection.
|
|
254
|
-
return {
|
|
255
|
-
saslStart: 1,
|
|
256
|
-
mechanism: AuthMechanism.MONGODB_OIDC,
|
|
257
|
-
payload: new Binary(BSON.serialize({ jwt: token }))
|
|
258
|
-
};
|
|
259
|
-
}
|
|
@@ -3,8 +3,8 @@ import { BSON, type Document } from 'bson';
|
|
|
3
3
|
import { ns } from '../../../utils';
|
|
4
4
|
import type { Connection } from '../../connection';
|
|
5
5
|
import type { MongoCredentials } from '../mongo_credentials';
|
|
6
|
+
import type { Workflow } from '../mongodb_oidc';
|
|
6
7
|
import { AuthMechanism } from '../providers';
|
|
7
|
-
import type { Workflow } from './workflow';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Common behaviour for OIDC device workflows.
|
|
@@ -24,9 +24,11 @@ export abstract class ServiceWorkflow implements Workflow {
|
|
|
24
24
|
/**
|
|
25
25
|
* Get the document to add for speculative authentication.
|
|
26
26
|
*/
|
|
27
|
-
async speculativeAuth(): Promise<Document> {
|
|
27
|
+
async speculativeAuth(credentials: MongoCredentials): Promise<Document> {
|
|
28
28
|
const token = await this.getToken();
|
|
29
|
-
|
|
29
|
+
const document = commandDocument(token);
|
|
30
|
+
document.db = credentials.source;
|
|
31
|
+
return { speculativeAuthenticate: document };
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
/**
|
|
@@ -1,41 +1,22 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
OIDCRefreshFunction,
|
|
4
|
-
OIDCRequestFunction,
|
|
5
|
-
OIDCRequestTokenResult
|
|
6
|
-
} from '../mongodb_oidc';
|
|
1
|
+
import type { IdPServerInfo, IdPServerResponse } from '../mongodb_oidc';
|
|
2
|
+
import { Cache } from './cache';
|
|
7
3
|
|
|
8
|
-
/* 5 minutes in
|
|
4
|
+
/* 5 minutes in milliseconds */
|
|
9
5
|
const EXPIRATION_BUFFER_MS = 300000;
|
|
10
6
|
/* Default expiration is now for when no expiration provided */
|
|
11
7
|
const DEFAULT_EXPIRATION_SECS = 0;
|
|
12
|
-
/* Counter for function "hashes".*/
|
|
13
|
-
let FN_HASH_COUNTER = 0;
|
|
14
|
-
/* No function present function */
|
|
15
|
-
const NO_FUNCTION: OIDCRequestFunction = () => {
|
|
16
|
-
return Promise.resolve({ accessToken: 'test' });
|
|
17
|
-
};
|
|
18
|
-
/* The map of function hashes */
|
|
19
|
-
const FN_HASHES = new WeakMap<OIDCRequestFunction | OIDCRefreshFunction, number>();
|
|
20
|
-
/* Put the no function hash in the map. */
|
|
21
|
-
FN_HASHES.set(NO_FUNCTION, FN_HASH_COUNTER);
|
|
22
|
-
|
|
23
8
|
/** @internal */
|
|
24
9
|
export class TokenEntry {
|
|
25
|
-
tokenResult:
|
|
26
|
-
|
|
10
|
+
tokenResult: IdPServerResponse;
|
|
11
|
+
serverInfo: IdPServerInfo;
|
|
27
12
|
expiration: number;
|
|
28
13
|
|
|
29
14
|
/**
|
|
30
15
|
* Instantiate the entry.
|
|
31
16
|
*/
|
|
32
|
-
constructor(
|
|
33
|
-
tokenResult: OIDCRequestTokenResult,
|
|
34
|
-
serverResult: OIDCMechanismServerStep1,
|
|
35
|
-
expiration: number
|
|
36
|
-
) {
|
|
17
|
+
constructor(tokenResult: IdPServerResponse, serverInfo: IdPServerInfo, expiration: number) {
|
|
37
18
|
this.tokenResult = tokenResult;
|
|
38
|
-
this.
|
|
19
|
+
this.serverInfo = serverInfo;
|
|
39
20
|
this.expiration = expiration;
|
|
40
21
|
}
|
|
41
22
|
|
|
@@ -52,62 +33,38 @@ export class TokenEntry {
|
|
|
52
33
|
* Cache of OIDC token entries.
|
|
53
34
|
* @internal
|
|
54
35
|
*/
|
|
55
|
-
export class TokenEntryCache {
|
|
56
|
-
entries: Map<string, TokenEntry>;
|
|
57
|
-
|
|
58
|
-
constructor() {
|
|
59
|
-
this.entries = new Map();
|
|
60
|
-
}
|
|
61
|
-
|
|
36
|
+
export class TokenEntryCache extends Cache<TokenEntry> {
|
|
62
37
|
/**
|
|
63
38
|
* Set an entry in the token cache.
|
|
64
39
|
*/
|
|
65
40
|
addEntry(
|
|
66
41
|
address: string,
|
|
67
42
|
username: string,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
serverResult: OIDCMechanismServerStep1
|
|
43
|
+
callbackHash: string,
|
|
44
|
+
tokenResult: IdPServerResponse,
|
|
45
|
+
serverInfo: IdPServerInfo
|
|
72
46
|
): TokenEntry {
|
|
73
47
|
const entry = new TokenEntry(
|
|
74
48
|
tokenResult,
|
|
75
|
-
|
|
49
|
+
serverInfo,
|
|
76
50
|
expirationTime(tokenResult.expiresInSeconds)
|
|
77
51
|
);
|
|
78
|
-
this.entries.set(cacheKey(address, username,
|
|
52
|
+
this.entries.set(this.cacheKey(address, username, callbackHash), entry);
|
|
79
53
|
return entry;
|
|
80
54
|
}
|
|
81
55
|
|
|
82
|
-
/**
|
|
83
|
-
* Clear the cache.
|
|
84
|
-
*/
|
|
85
|
-
clear(): void {
|
|
86
|
-
this.entries.clear();
|
|
87
|
-
}
|
|
88
|
-
|
|
89
56
|
/**
|
|
90
57
|
* Delete an entry from the cache.
|
|
91
58
|
*/
|
|
92
|
-
deleteEntry(
|
|
93
|
-
address
|
|
94
|
-
username: string,
|
|
95
|
-
requestFn: OIDCRequestFunction | null,
|
|
96
|
-
refreshFn: OIDCRefreshFunction | null
|
|
97
|
-
): void {
|
|
98
|
-
this.entries.delete(cacheKey(address, username, requestFn, refreshFn));
|
|
59
|
+
deleteEntry(address: string, username: string, callbackHash: string): void {
|
|
60
|
+
this.entries.delete(this.cacheKey(address, username, callbackHash));
|
|
99
61
|
}
|
|
100
62
|
|
|
101
63
|
/**
|
|
102
64
|
* Get an entry from the cache.
|
|
103
65
|
*/
|
|
104
|
-
getEntry(
|
|
105
|
-
address
|
|
106
|
-
username: string,
|
|
107
|
-
requestFn: OIDCRequestFunction | null,
|
|
108
|
-
refreshFn: OIDCRefreshFunction | null
|
|
109
|
-
): TokenEntry | undefined {
|
|
110
|
-
return this.entries.get(cacheKey(address, username, requestFn, refreshFn));
|
|
66
|
+
getEntry(address: string, username: string, callbackHash: string): TokenEntry | undefined {
|
|
67
|
+
return this.entries.get(this.cacheKey(address, username, callbackHash));
|
|
111
68
|
}
|
|
112
69
|
|
|
113
70
|
/**
|
|
@@ -128,39 +85,3 @@ export class TokenEntryCache {
|
|
|
128
85
|
function expirationTime(expiresInSeconds?: number): number {
|
|
129
86
|
return Date.now() + (expiresInSeconds ?? DEFAULT_EXPIRATION_SECS) * 1000;
|
|
130
87
|
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Create a cache key from the address and username.
|
|
134
|
-
*/
|
|
135
|
-
function cacheKey(
|
|
136
|
-
address: string,
|
|
137
|
-
username: string,
|
|
138
|
-
requestFn: OIDCRequestFunction | null,
|
|
139
|
-
refreshFn: OIDCRefreshFunction | null
|
|
140
|
-
): string {
|
|
141
|
-
return `${address}-${username}-${hashFunctions(requestFn, refreshFn)}`;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Get the hash string for the request and refresh functions.
|
|
146
|
-
*/
|
|
147
|
-
function hashFunctions(
|
|
148
|
-
requestFn: OIDCRequestFunction | null,
|
|
149
|
-
refreshFn: OIDCRefreshFunction | null
|
|
150
|
-
): string {
|
|
151
|
-
let requestHash = FN_HASHES.get(requestFn || NO_FUNCTION);
|
|
152
|
-
let refreshHash = FN_HASHES.get(refreshFn || NO_FUNCTION);
|
|
153
|
-
if (!requestHash && requestFn) {
|
|
154
|
-
// Create a new one for the function and put it in the map.
|
|
155
|
-
FN_HASH_COUNTER++;
|
|
156
|
-
requestHash = FN_HASH_COUNTER;
|
|
157
|
-
FN_HASHES.set(requestFn, FN_HASH_COUNTER);
|
|
158
|
-
}
|
|
159
|
-
if (!refreshHash && refreshFn) {
|
|
160
|
-
// Create a new one for the function and put it in the map.
|
|
161
|
-
FN_HASH_COUNTER++;
|
|
162
|
-
refreshHash = FN_HASH_COUNTER;
|
|
163
|
-
FN_HASHES.set(refreshFn, FN_HASH_COUNTER);
|
|
164
|
-
}
|
|
165
|
-
return `${requestHash}-${refreshHash}`;
|
|
166
|
-
}
|