mongodb 6.6.2 → 6.7.0-dev.20240607.sha.aa429f8c
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/README.md +19 -0
- 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 +140 -36
- package/package.json +7 -6
- package/src/client-side-encryption/client_encryption.ts +27 -13
- 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 +9 -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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { type Document } from 'bson';
|
|
2
|
+
import { setTimeout } from 'timers/promises';
|
|
3
|
+
|
|
4
|
+
import { ns } from '../../../utils';
|
|
5
|
+
import type { Connection } from '../../connection';
|
|
6
|
+
import type { MongoCredentials } from '../mongo_credentials';
|
|
7
|
+
import type { Workflow } from '../mongodb_oidc';
|
|
8
|
+
import { finishCommandDocument } from './command_builders';
|
|
9
|
+
import { type TokenCache } from './token_cache';
|
|
10
|
+
|
|
11
|
+
/** The time to throttle callback calls. */
|
|
12
|
+
const THROTTLE_MS = 100;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The access token format.
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export interface AccessToken {
|
|
19
|
+
access_token: string;
|
|
20
|
+
expires_in?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** @internal */
|
|
24
|
+
export type OIDCTokenFunction = (credentials: MongoCredentials) => Promise<AccessToken>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Common behaviour for OIDC machine workflows.
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
export abstract class MachineWorkflow implements Workflow {
|
|
31
|
+
cache: TokenCache;
|
|
32
|
+
callback: OIDCTokenFunction;
|
|
33
|
+
lastExecutionTime: number;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Instantiate the machine workflow.
|
|
37
|
+
*/
|
|
38
|
+
constructor(cache: TokenCache) {
|
|
39
|
+
this.cache = cache;
|
|
40
|
+
this.callback = this.withLock(this.getToken.bind(this));
|
|
41
|
+
this.lastExecutionTime = Date.now() - THROTTLE_MS;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Execute the workflow. Gets the token from the subclass implementation.
|
|
46
|
+
*/
|
|
47
|
+
async execute(connection: Connection, credentials: MongoCredentials): Promise<void> {
|
|
48
|
+
const token = await this.getTokenFromCacheOrEnv(connection, credentials);
|
|
49
|
+
const command = finishCommandDocument(token);
|
|
50
|
+
await connection.command(ns(credentials.source), command, undefined);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Reauthenticate on a machine workflow just grabs the token again since the server
|
|
55
|
+
* has said the current access token is invalid or expired.
|
|
56
|
+
*/
|
|
57
|
+
async reauthenticate(connection: Connection, credentials: MongoCredentials): Promise<void> {
|
|
58
|
+
if (this.cache.hasAccessToken) {
|
|
59
|
+
// Reauthentication implies the token has expired.
|
|
60
|
+
if (connection.accessToken === this.cache.getAccessToken()) {
|
|
61
|
+
// If connection's access token is the same as the cache's, remove
|
|
62
|
+
// the token from the cache and connection.
|
|
63
|
+
this.cache.removeAccessToken();
|
|
64
|
+
delete connection.accessToken;
|
|
65
|
+
} else {
|
|
66
|
+
// If the connection's access token is different from the cache's, set
|
|
67
|
+
// the cache's token on the connection and do not remove from the
|
|
68
|
+
// cache.
|
|
69
|
+
connection.accessToken = this.cache.getAccessToken();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
await this.execute(connection, credentials);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get the document to add for speculative authentication.
|
|
77
|
+
*/
|
|
78
|
+
async speculativeAuth(connection: Connection, credentials: MongoCredentials): Promise<Document> {
|
|
79
|
+
// The spec states only cached access tokens can use speculative auth.
|
|
80
|
+
if (!this.cache.hasAccessToken) {
|
|
81
|
+
return {};
|
|
82
|
+
}
|
|
83
|
+
const token = await this.getTokenFromCacheOrEnv(connection, credentials);
|
|
84
|
+
const document = finishCommandDocument(token);
|
|
85
|
+
document.db = credentials.source;
|
|
86
|
+
return { speculativeAuthenticate: document };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get the token from the cache or environment.
|
|
91
|
+
*/
|
|
92
|
+
private async getTokenFromCacheOrEnv(
|
|
93
|
+
connection: Connection,
|
|
94
|
+
credentials: MongoCredentials
|
|
95
|
+
): Promise<string> {
|
|
96
|
+
if (this.cache.hasAccessToken) {
|
|
97
|
+
return this.cache.getAccessToken();
|
|
98
|
+
} else {
|
|
99
|
+
const token = await this.callback(credentials);
|
|
100
|
+
this.cache.put({ accessToken: token.access_token, expiresInSeconds: token.expires_in });
|
|
101
|
+
// Put the access token on the connection as well.
|
|
102
|
+
connection.accessToken = token.access_token;
|
|
103
|
+
return token.access_token;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Ensure the callback is only executed one at a time, and throttled to
|
|
109
|
+
* only once per 100ms.
|
|
110
|
+
*/
|
|
111
|
+
private withLock(callback: OIDCTokenFunction): OIDCTokenFunction {
|
|
112
|
+
let lock: Promise<any> = Promise.resolve();
|
|
113
|
+
return async (credentials: MongoCredentials): Promise<AccessToken> => {
|
|
114
|
+
// We do this to ensure that we would never return the result of the
|
|
115
|
+
// previous lock, only the current callback's value would get returned.
|
|
116
|
+
await lock;
|
|
117
|
+
lock = lock
|
|
118
|
+
// eslint-disable-next-line github/no-then
|
|
119
|
+
.catch(() => null)
|
|
120
|
+
// eslint-disable-next-line github/no-then
|
|
121
|
+
.then(async () => {
|
|
122
|
+
const difference = Date.now() - this.lastExecutionTime;
|
|
123
|
+
if (difference <= THROTTLE_MS) {
|
|
124
|
+
await setTimeout(THROTTLE_MS - difference);
|
|
125
|
+
}
|
|
126
|
+
this.lastExecutionTime = Date.now();
|
|
127
|
+
return await callback(credentials);
|
|
128
|
+
});
|
|
129
|
+
return await lock;
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get the token from the environment or endpoint.
|
|
135
|
+
*/
|
|
136
|
+
abstract getToken(credentials: MongoCredentials): Promise<AccessToken>;
|
|
137
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { MongoDriverError } from '../../../error';
|
|
2
|
+
import type { IdPInfo, OIDCResponse } from '../mongodb_oidc';
|
|
3
|
+
|
|
4
|
+
class MongoOIDCError extends MongoDriverError {}
|
|
5
|
+
|
|
6
|
+
/** @internal */
|
|
7
|
+
export class TokenCache {
|
|
8
|
+
private accessToken?: string;
|
|
9
|
+
private refreshToken?: string;
|
|
10
|
+
private idpInfo?: IdPInfo;
|
|
11
|
+
private expiresInSeconds?: number;
|
|
12
|
+
|
|
13
|
+
get hasAccessToken(): boolean {
|
|
14
|
+
return !!this.accessToken;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get hasRefreshToken(): boolean {
|
|
18
|
+
return !!this.refreshToken;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get hasIdpInfo(): boolean {
|
|
22
|
+
return !!this.idpInfo;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getAccessToken(): string {
|
|
26
|
+
if (!this.accessToken) {
|
|
27
|
+
throw new MongoOIDCError('Attempted to get an access token when none exists.');
|
|
28
|
+
}
|
|
29
|
+
return this.accessToken;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getRefreshToken(): string {
|
|
33
|
+
if (!this.refreshToken) {
|
|
34
|
+
throw new MongoOIDCError('Attempted to get a refresh token when none exists.');
|
|
35
|
+
}
|
|
36
|
+
return this.refreshToken;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getIdpInfo(): IdPInfo {
|
|
40
|
+
if (!this.idpInfo) {
|
|
41
|
+
throw new MongoOIDCError('Attempted to get IDP information when none exists.');
|
|
42
|
+
}
|
|
43
|
+
return this.idpInfo;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
put(response: OIDCResponse, idpInfo?: IdPInfo) {
|
|
47
|
+
this.accessToken = response.accessToken;
|
|
48
|
+
this.refreshToken = response.refreshToken;
|
|
49
|
+
this.expiresInSeconds = response.expiresInSeconds;
|
|
50
|
+
if (idpInfo) {
|
|
51
|
+
this.idpInfo = idpInfo;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
removeAccessToken() {
|
|
56
|
+
this.accessToken = undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
removeRefreshToken() {
|
|
60
|
+
this.refreshToken = undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
|
|
3
|
+
import { MongoAWSError } from '../../../error';
|
|
4
|
+
import { type AccessToken, MachineWorkflow } from './machine_workflow';
|
|
5
|
+
import { type TokenCache } from './token_cache';
|
|
6
|
+
|
|
7
|
+
/** Error for when the token is missing in the environment. */
|
|
8
|
+
const TOKEN_MISSING_ERROR = 'OIDC_TOKEN_FILE must be set in the environment.';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Device workflow implementation for AWS.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export class TokenMachineWorkflow extends MachineWorkflow {
|
|
16
|
+
/**
|
|
17
|
+
* Instantiate the machine workflow.
|
|
18
|
+
*/
|
|
19
|
+
constructor(cache: TokenCache) {
|
|
20
|
+
super(cache);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get the token from the environment.
|
|
25
|
+
*/
|
|
26
|
+
async getToken(): Promise<AccessToken> {
|
|
27
|
+
const tokenFile = process.env.OIDC_TOKEN_FILE;
|
|
28
|
+
if (!tokenFile) {
|
|
29
|
+
throw new MongoAWSError(TOKEN_MISSING_ERROR);
|
|
30
|
+
}
|
|
31
|
+
const token = await fs.promises.readFile(tokenFile, 'utf8');
|
|
32
|
+
return { access_token: token };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -5,64 +5,93 @@ import type { HandshakeDocument } from '../connect';
|
|
|
5
5
|
import type { Connection } from '../connection';
|
|
6
6
|
import { type AuthContext, AuthProvider } from './auth_provider';
|
|
7
7
|
import type { MongoCredentials } from './mongo_credentials';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import { AzureMachineWorkflow } from './mongodb_oidc/azure_machine_workflow';
|
|
9
|
+
import { GCPMachineWorkflow } from './mongodb_oidc/gcp_machine_workflow';
|
|
10
|
+
import { TokenCache } from './mongodb_oidc/token_cache';
|
|
11
|
+
import { TokenMachineWorkflow } from './mongodb_oidc/token_machine_workflow';
|
|
11
12
|
|
|
12
13
|
/** Error when credentials are missing. */
|
|
13
14
|
const MISSING_CREDENTIALS_ERROR = 'AuthContext must provide credentials.';
|
|
14
15
|
|
|
15
16
|
/**
|
|
17
|
+
* The information returned by the server on the IDP server.
|
|
16
18
|
* @public
|
|
17
|
-
* @experimental
|
|
18
19
|
*/
|
|
19
|
-
export interface
|
|
20
|
+
export interface IdPInfo {
|
|
21
|
+
/**
|
|
22
|
+
* A URL which describes the Authentication Server. This identifier should
|
|
23
|
+
* be the iss of provided access tokens, and be viable for RFC8414 metadata
|
|
24
|
+
* discovery and RFC9207 identification.
|
|
25
|
+
*/
|
|
20
26
|
issuer: string;
|
|
27
|
+
/** A unique client ID for this OIDC client. */
|
|
21
28
|
clientId: string;
|
|
29
|
+
/** A list of additional scopes to request from IdP. */
|
|
22
30
|
requestScopes?: string[];
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
/**
|
|
34
|
+
* The response from the IdP server with the access token and
|
|
35
|
+
* optional expiration time and refresh token.
|
|
26
36
|
* @public
|
|
27
|
-
* @experimental
|
|
28
37
|
*/
|
|
29
38
|
export interface IdPServerResponse {
|
|
39
|
+
/** The OIDC access token. */
|
|
30
40
|
accessToken: string;
|
|
41
|
+
/** The time when the access token expires. For future use. */
|
|
31
42
|
expiresInSeconds?: number;
|
|
43
|
+
/** The refresh token, if applicable, to be used by the callback to request a new token from the issuer. */
|
|
32
44
|
refreshToken?: string;
|
|
33
45
|
}
|
|
34
46
|
|
|
35
47
|
/**
|
|
48
|
+
* The response required to be returned from the machine or
|
|
49
|
+
* human callback workflows' callback.
|
|
36
50
|
* @public
|
|
37
|
-
* @experimental
|
|
38
51
|
*/
|
|
39
|
-
export interface
|
|
52
|
+
export interface OIDCResponse {
|
|
53
|
+
/** The OIDC access token. */
|
|
54
|
+
accessToken: string;
|
|
55
|
+
/** The time when the access token expires. For future use. */
|
|
56
|
+
expiresInSeconds?: number;
|
|
57
|
+
/** The refresh token, if applicable, to be used by the callback to request a new token from the issuer. */
|
|
40
58
|
refreshToken?: string;
|
|
41
|
-
timeoutSeconds?: number;
|
|
42
|
-
timeoutContext?: AbortSignal;
|
|
43
|
-
version: number;
|
|
44
59
|
}
|
|
45
60
|
|
|
46
61
|
/**
|
|
62
|
+
* The parameters that the driver provides to the user supplied
|
|
63
|
+
* human or machine callback.
|
|
64
|
+
*
|
|
65
|
+
* The version number is used to communicate callback API changes that are not breaking but that
|
|
66
|
+
* users may want to know about and review their implementation. Users may wish to check the version
|
|
67
|
+
* number and throw an error if their expected version number and the one provided do not match.
|
|
47
68
|
* @public
|
|
48
|
-
* @experimental
|
|
49
69
|
*/
|
|
50
|
-
export
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
70
|
+
export interface OIDCCallbackParams {
|
|
71
|
+
/** Optional username. */
|
|
72
|
+
username?: string;
|
|
73
|
+
/** The context in which to timeout the OIDC callback. */
|
|
74
|
+
timeoutContext: AbortSignal;
|
|
75
|
+
/** The current OIDC API version. */
|
|
76
|
+
version: 1;
|
|
77
|
+
/** The IdP information returned from the server. */
|
|
78
|
+
idpInfo?: IdPInfo;
|
|
79
|
+
/** The refresh token, if applicable, to be used by the callback to request a new token from the issuer. */
|
|
80
|
+
refreshToken?: string;
|
|
81
|
+
}
|
|
54
82
|
|
|
55
83
|
/**
|
|
84
|
+
* The signature of the human or machine callback functions.
|
|
56
85
|
* @public
|
|
57
|
-
* @experimental
|
|
58
86
|
*/
|
|
59
|
-
export type
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
87
|
+
export type OIDCCallbackFunction = (params: OIDCCallbackParams) => Promise<OIDCResponse>;
|
|
88
|
+
|
|
89
|
+
/** The current version of OIDC implementation. */
|
|
90
|
+
export const OIDC_VERSION = 1;
|
|
63
91
|
|
|
64
|
-
type
|
|
92
|
+
type EnvironmentName = 'test' | 'azure' | 'gcp' | undefined;
|
|
65
93
|
|
|
94
|
+
/** @internal */
|
|
66
95
|
export interface Workflow {
|
|
67
96
|
/**
|
|
68
97
|
* All device workflows must implement this method in order to get the access
|
|
@@ -71,32 +100,41 @@ export interface Workflow {
|
|
|
71
100
|
execute(
|
|
72
101
|
connection: Connection,
|
|
73
102
|
credentials: MongoCredentials,
|
|
74
|
-
reauthenticating: boolean,
|
|
75
103
|
response?: Document
|
|
76
|
-
): Promise<
|
|
104
|
+
): Promise<void>;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Each workflow should specify the correct custom behaviour for reauthentication.
|
|
108
|
+
*/
|
|
109
|
+
reauthenticate(connection: Connection, credentials: MongoCredentials): Promise<void>;
|
|
77
110
|
|
|
78
111
|
/**
|
|
79
112
|
* Get the document to add for speculative authentication.
|
|
80
113
|
*/
|
|
81
|
-
speculativeAuth(credentials: MongoCredentials): Promise<Document>;
|
|
114
|
+
speculativeAuth(connection: Connection, credentials: MongoCredentials): Promise<Document>;
|
|
82
115
|
}
|
|
83
116
|
|
|
84
117
|
/** @internal */
|
|
85
|
-
export const OIDC_WORKFLOWS: Map<
|
|
86
|
-
OIDC_WORKFLOWS.set('
|
|
87
|
-
OIDC_WORKFLOWS.set('
|
|
88
|
-
OIDC_WORKFLOWS.set('
|
|
118
|
+
export const OIDC_WORKFLOWS: Map<EnvironmentName, () => Workflow> = new Map();
|
|
119
|
+
OIDC_WORKFLOWS.set('test', () => new TokenMachineWorkflow(new TokenCache()));
|
|
120
|
+
OIDC_WORKFLOWS.set('azure', () => new AzureMachineWorkflow(new TokenCache()));
|
|
121
|
+
OIDC_WORKFLOWS.set('gcp', () => new GCPMachineWorkflow(new TokenCache()));
|
|
89
122
|
|
|
90
123
|
/**
|
|
91
124
|
* OIDC auth provider.
|
|
92
|
-
* @experimental
|
|
93
125
|
*/
|
|
94
126
|
export class MongoDBOIDC extends AuthProvider {
|
|
127
|
+
workflow: Workflow;
|
|
128
|
+
|
|
95
129
|
/**
|
|
96
130
|
* Instantiate the auth provider.
|
|
97
131
|
*/
|
|
98
|
-
constructor() {
|
|
132
|
+
constructor(workflow?: Workflow) {
|
|
99
133
|
super();
|
|
134
|
+
if (!workflow) {
|
|
135
|
+
throw new MongoInvalidArgumentError('No workflow provided to the OIDC auth provider.');
|
|
136
|
+
}
|
|
137
|
+
this.workflow = workflow;
|
|
100
138
|
}
|
|
101
139
|
|
|
102
140
|
/**
|
|
@@ -104,9 +142,15 @@ export class MongoDBOIDC extends AuthProvider {
|
|
|
104
142
|
*/
|
|
105
143
|
override async auth(authContext: AuthContext): Promise<void> {
|
|
106
144
|
const { connection, reauthenticating, response } = authContext;
|
|
145
|
+
if (response?.speculativeAuthenticate?.done) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
107
148
|
const credentials = getCredentials(authContext);
|
|
108
|
-
|
|
109
|
-
|
|
149
|
+
if (reauthenticating) {
|
|
150
|
+
await this.workflow.reauthenticate(connection, credentials);
|
|
151
|
+
} else {
|
|
152
|
+
await this.workflow.execute(connection, credentials, response);
|
|
153
|
+
}
|
|
110
154
|
}
|
|
111
155
|
|
|
112
156
|
/**
|
|
@@ -116,9 +160,9 @@ export class MongoDBOIDC extends AuthProvider {
|
|
|
116
160
|
handshakeDoc: HandshakeDocument,
|
|
117
161
|
authContext: AuthContext
|
|
118
162
|
): Promise<HandshakeDocument> {
|
|
163
|
+
const { connection } = authContext;
|
|
119
164
|
const credentials = getCredentials(authContext);
|
|
120
|
-
const
|
|
121
|
-
const result = await workflow.speculativeAuth(credentials);
|
|
165
|
+
const result = await this.workflow.speculativeAuth(connection, credentials);
|
|
122
166
|
return { ...handshakeDoc, ...result };
|
|
123
167
|
}
|
|
124
168
|
}
|
|
@@ -133,17 +177,3 @@ function getCredentials(authContext: AuthContext): MongoCredentials {
|
|
|
133
177
|
}
|
|
134
178
|
return credentials;
|
|
135
179
|
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Gets either a device workflow or callback workflow.
|
|
139
|
-
*/
|
|
140
|
-
function getWorkflow(credentials: MongoCredentials): Workflow {
|
|
141
|
-
const providerName = credentials.mechanismProperties.PROVIDER_NAME;
|
|
142
|
-
const workflow = OIDC_WORKFLOWS.get(providerName || 'callback');
|
|
143
|
-
if (!workflow) {
|
|
144
|
-
throw new MongoInvalidArgumentError(
|
|
145
|
-
`Could not load workflow for provider ${credentials.mechanismProperties.PROVIDER_NAME}`
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
return workflow;
|
|
149
|
-
}
|
package/src/cmap/connect.ts
CHANGED
|
@@ -91,7 +91,10 @@ export async function performInitialHandshake(
|
|
|
91
91
|
if (credentials) {
|
|
92
92
|
if (
|
|
93
93
|
!(credentials.mechanism === AuthMechanism.MONGODB_DEFAULT) &&
|
|
94
|
-
!options.authProviders.getOrCreateProvider(
|
|
94
|
+
!options.authProviders.getOrCreateProvider(
|
|
95
|
+
credentials.mechanism,
|
|
96
|
+
credentials.mechanismProperties
|
|
97
|
+
)
|
|
95
98
|
) {
|
|
96
99
|
throw new MongoInvalidArgumentError(`AuthMechanism '${credentials.mechanism}' not supported`);
|
|
97
100
|
}
|
|
@@ -146,7 +149,10 @@ export async function performInitialHandshake(
|
|
|
146
149
|
authContext.response = response;
|
|
147
150
|
|
|
148
151
|
const resolvedCredentials = credentials.resolveAuthMechanism(response);
|
|
149
|
-
const provider = options.authProviders.getOrCreateProvider(
|
|
152
|
+
const provider = options.authProviders.getOrCreateProvider(
|
|
153
|
+
resolvedCredentials.mechanism,
|
|
154
|
+
resolvedCredentials.mechanismProperties
|
|
155
|
+
);
|
|
150
156
|
if (!provider) {
|
|
151
157
|
throw new MongoInvalidArgumentError(
|
|
152
158
|
`No AuthProvider for ${resolvedCredentials.mechanism} defined.`
|
|
@@ -218,7 +224,8 @@ export async function prepareHandshakeDocument(
|
|
|
218
224
|
handshakeDoc.saslSupportedMechs = `${credentials.source}.${credentials.username}`;
|
|
219
225
|
|
|
220
226
|
const provider = authContext.options.authProviders.getOrCreateProvider(
|
|
221
|
-
AuthMechanism.MONGODB_SCRAM_SHA256
|
|
227
|
+
AuthMechanism.MONGODB_SCRAM_SHA256,
|
|
228
|
+
credentials.mechanismProperties
|
|
222
229
|
);
|
|
223
230
|
if (!provider) {
|
|
224
231
|
// This auth mechanism is always present.
|
|
@@ -228,7 +235,10 @@ export async function prepareHandshakeDocument(
|
|
|
228
235
|
}
|
|
229
236
|
return await provider.prepare(handshakeDoc, authContext);
|
|
230
237
|
}
|
|
231
|
-
const provider = authContext.options.authProviders.getOrCreateProvider(
|
|
238
|
+
const provider = authContext.options.authProviders.getOrCreateProvider(
|
|
239
|
+
credentials.mechanism,
|
|
240
|
+
credentials.mechanismProperties
|
|
241
|
+
);
|
|
232
242
|
if (!provider) {
|
|
233
243
|
throw new MongoInvalidArgumentError(`No AuthProvider for ${credentials.mechanism} defined.`);
|
|
234
244
|
}
|
package/src/cmap/connection.ts
CHANGED
|
@@ -174,6 +174,7 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
|
|
|
174
174
|
public authContext?: AuthContext;
|
|
175
175
|
public delayedTimeoutId: NodeJS.Timeout | null = null;
|
|
176
176
|
public generation: number;
|
|
177
|
+
public accessToken?: string;
|
|
177
178
|
public readonly description: Readonly<StreamDescription>;
|
|
178
179
|
/**
|
|
179
180
|
* Represents if the connection has been established:
|
|
@@ -551,7 +551,8 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
|
|
|
551
551
|
|
|
552
552
|
const resolvedCredentials = credentials.resolveAuthMechanism(connection.hello);
|
|
553
553
|
const provider = this[kServer].topology.client.s.authProviders.getOrCreateProvider(
|
|
554
|
-
resolvedCredentials.mechanism
|
|
554
|
+
resolvedCredentials.mechanism,
|
|
555
|
+
resolvedCredentials.mechanismProperties
|
|
555
556
|
);
|
|
556
557
|
|
|
557
558
|
if (!provider) {
|
package/src/connection_string.ts
CHANGED
|
@@ -698,6 +698,9 @@ export const OPTIONS = {
|
|
|
698
698
|
});
|
|
699
699
|
}
|
|
700
700
|
},
|
|
701
|
+
// Note that if the authMechanismProperties contain a TOKEN_RESOURCE that has a
|
|
702
|
+
// comma in it, it MUST be supplied as a MongoClient option instead of in the
|
|
703
|
+
// connection string.
|
|
701
704
|
authMechanismProperties: {
|
|
702
705
|
target: 'credentials',
|
|
703
706
|
transform({ options, values }): MongoCredentials {
|