Skip to content

Commit c52a4ed

Browse files
authored
feat(NODE-5191): OIDC Auth Updates (#3637)
1 parent ce8e69b commit c52a4ed

23 files changed

+1952
-803
lines changed

.evergreen/config.in.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ functions:
152152
${PREPARE_SHELL}
153153
154154
OIDC_TOKEN_DIR="/tmp/tokens" \
155-
AWS_WEB_IDENTITY_TOKEN_FILE="/tmp/tokens/test1" \
155+
AWS_WEB_IDENTITY_TOKEN_FILE="/tmp/tokens/test_user1" \
156156
PROJECT_DIRECTORY="${PROJECT_DIRECTORY}" \
157157
bash ${PROJECT_DIRECTORY}/.evergreen/run-oidc-tests.sh
158158

.evergreen/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ functions:
123123
${PREPARE_SHELL}
124124
125125
OIDC_TOKEN_DIR="/tmp/tokens" \
126-
AWS_WEB_IDENTITY_TOKEN_FILE="/tmp/tokens/test1" \
126+
AWS_WEB_IDENTITY_TOKEN_FILE="/tmp/tokens/test_user1" \
127127
PROJECT_DIRECTORY="${PROJECT_DIRECTORY}" \
128128
bash ${PROJECT_DIRECTORY}/.evergreen/run-oidc-tests.sh
129129
run deployed aws lambda tests:

.evergreen/run-oidc-tests.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ MONGODB_URI_SINGLE="${MONGODB_URI}/?authMechanism=MONGODB-OIDC&authMechanismProp
1010
echo $MONGODB_URI_SINGLE
1111

1212
export MONGODB_URI="$MONGODB_URI_SINGLE"
13+
export OIDC_TOKEN_DIR=${OIDC_TOKEN_DIR}
1314

1415
npm run check:oidc

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@
127127
"check:atlas": "mocha --config test/manual/mocharc.json test/manual/atlas_connectivity.test.js",
128128
"check:adl": "mocha --config test/mocha_mongodb.json test/manual/atlas-data-lake-testing",
129129
"check:aws": "nyc mocha --config test/mocha_mongodb.json test/integration/auth/mongodb_aws.test.ts",
130-
"check:oidc": "mocha --config test/manual/mocharc.json test/manual/mongodb_oidc.prose.test.ts",
130+
"check:oidc": "mocha --config test/mocha_mongodb.json test/manual/mongodb_oidc.prose.test.ts",
131131
"check:ocsp": "mocha --config test/manual/mocharc.json test/manual/ocsp_support.test.js",
132132
"check:kerberos": "nyc mocha --config test/manual/mocharc.json test/manual/kerberos.test.ts",
133133
"check:tls": "mocha --config test/manual/mocharc.json test/manual/tls_support.test.js",

src/cmap/auth/mongo_credentials.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@ function getDefaultAuthMechanism(hello?: Document): AuthMechanism {
3030
return AuthMechanism.MONGODB_CR;
3131
}
3232

33+
const ALLOWED_HOSTS_ERROR = 'Auth mechanism property ALLOWED_HOSTS must be an array of strings.';
34+
35+
/** @internal */
36+
export const DEFAULT_ALLOWED_HOSTS = [
37+
'*.mongodb.net',
38+
'*.mongodb-dev.net',
39+
'*.mongodbgov.net',
40+
'localhost',
41+
'127.0.0.1',
42+
'::1'
43+
];
44+
3345
/** @public */
3446
export interface AuthMechanismProperties extends Document {
3547
SERVICE_HOST?: string;
@@ -43,11 +55,13 @@ export interface AuthMechanismProperties extends Document {
4355
REFRESH_TOKEN_CALLBACK?: OIDCRefreshFunction;
4456
/** @experimental */
4557
PROVIDER_NAME?: 'aws';
58+
/** @experimental */
59+
ALLOWED_HOSTS?: string[];
4660
}
4761

4862
/** @public */
4963
export interface MongoCredentialsOptions {
50-
username: string;
64+
username?: string;
5165
password: string;
5266
source: string;
5367
db?: string;
@@ -72,7 +86,7 @@ export class MongoCredentials {
7286
readonly mechanismProperties: AuthMechanismProperties;
7387

7488
constructor(options: MongoCredentialsOptions) {
75-
this.username = options.username;
89+
this.username = options.username ?? '';
7690
this.password = options.password;
7791
this.source = options.source;
7892
if (!this.source && options.db) {
@@ -101,6 +115,13 @@ export class MongoCredentials {
101115
}
102116
}
103117

118+
if (this.mechanism === AuthMechanism.MONGODB_OIDC && !this.mechanismProperties.ALLOWED_HOSTS) {
119+
this.mechanismProperties = {
120+
...this.mechanismProperties,
121+
ALLOWED_HOSTS: DEFAULT_ALLOWED_HOSTS
122+
};
123+
}
124+
104125
Object.freeze(this.mechanismProperties);
105126
Object.freeze(this);
106127
}
@@ -181,6 +202,18 @@ export class MongoCredentials {
181202
`Either a PROVIDER_NAME or a REQUEST_TOKEN_CALLBACK must be specified for mechanism '${this.mechanism}'.`
182203
);
183204
}
205+
206+
if (this.mechanismProperties.ALLOWED_HOSTS) {
207+
const hosts = this.mechanismProperties.ALLOWED_HOSTS;
208+
if (!Array.isArray(hosts)) {
209+
throw new MongoInvalidArgumentError(ALLOWED_HOSTS_ERROR);
210+
}
211+
for (const host of hosts) {
212+
if (typeof host !== 'string') {
213+
throw new MongoInvalidArgumentError(ALLOWED_HOSTS_ERROR);
214+
}
215+
}
216+
}
184217
}
185218

186219
if (AUTH_MECHS_AUTH_SRC_EXTERNAL.has(this.mechanism)) {

src/cmap/auth/mongodb_oidc.ts

Lines changed: 61 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,85 @@
1+
import type { Document } from 'bson';
2+
13
import { MongoInvalidArgumentError, MongoMissingCredentialsError } from '../../error';
24
import type { HandshakeDocument } from '../connect';
3-
import { type AuthContext, AuthProvider } from './auth_provider';
5+
import type { Connection } from '../connection';
6+
import { AuthContext, AuthProvider } from './auth_provider';
47
import type { MongoCredentials } from './mongo_credentials';
58
import { AwsServiceWorkflow } from './mongodb_oidc/aws_service_workflow';
69
import { CallbackWorkflow } from './mongodb_oidc/callback_workflow';
7-
import type { Workflow } from './mongodb_oidc/workflow';
10+
11+
/** Error when credentials are missing. */
12+
const MISSING_CREDENTIALS_ERROR = 'AuthContext must provide credentials.';
813

914
/**
1015
* @public
1116
* @experimental
1217
*/
13-
export interface OIDCMechanismServerStep1 {
14-
authorizationEndpoint?: string;
15-
tokenEndpoint?: string;
16-
deviceAuthorizationEndpoint?: string;
18+
export interface IdPServerInfo {
19+
issuer: string;
1720
clientId: string;
18-
clientSecret?: string;
1921
requestScopes?: string[];
2022
}
2123

2224
/**
2325
* @public
2426
* @experimental
2527
*/
26-
export interface OIDCRequestTokenResult {
28+
export interface IdPServerResponse {
2729
accessToken: string;
2830
expiresInSeconds?: number;
2931
refreshToken?: string;
3032
}
3133

34+
/**
35+
* @public
36+
* @experimental
37+
*/
38+
export interface OIDCCallbackContext {
39+
refreshToken?: string;
40+
timeoutSeconds?: number;
41+
timeoutContext?: AbortSignal;
42+
version: number;
43+
}
44+
3245
/**
3346
* @public
3447
* @experimental
3548
*/
3649
export type OIDCRequestFunction = (
37-
principalName: string,
38-
serverResult: OIDCMechanismServerStep1,
39-
timeout: AbortSignal | number
40-
) => Promise<OIDCRequestTokenResult>;
50+
info: IdPServerInfo,
51+
context: OIDCCallbackContext
52+
) => Promise<IdPServerResponse>;
4153

4254
/**
4355
* @public
4456
* @experimental
4557
*/
4658
export type OIDCRefreshFunction = (
47-
principalName: string,
48-
serverResult: OIDCMechanismServerStep1,
49-
result: OIDCRequestTokenResult,
50-
timeout: AbortSignal | number
51-
) => Promise<OIDCRequestTokenResult>;
59+
info: IdPServerInfo,
60+
context: OIDCCallbackContext
61+
) => Promise<IdPServerResponse>;
5262

5363
type ProviderName = 'aws' | 'callback';
5464

65+
export interface Workflow {
66+
/**
67+
* All device workflows must implement this method in order to get the access
68+
* token and then call authenticate with it.
69+
*/
70+
execute(
71+
connection: Connection,
72+
credentials: MongoCredentials,
73+
reauthenticating: boolean,
74+
response?: Document
75+
): Promise<Document>;
76+
77+
/**
78+
* Get the document to add for speculative authentication.
79+
*/
80+
speculativeAuth(credentials: MongoCredentials): Promise<Document>;
81+
}
82+
5583
/** @internal */
5684
export const OIDC_WORKFLOWS: Map<ProviderName, Workflow> = new Map();
5785
OIDC_WORKFLOWS.set('callback', new CallbackWorkflow());
@@ -73,19 +101,10 @@ export class MongoDBOIDC extends AuthProvider {
73101
* Authenticate using OIDC
74102
*/
75103
override async auth(authContext: AuthContext): Promise<void> {
76-
const { connection, credentials, response, reauthenticating } = authContext;
77-
78-
if (response?.speculativeAuthenticate) {
79-
return;
80-
}
81-
82-
if (!credentials) {
83-
throw new MongoMissingCredentialsError('AuthContext must provide credentials.');
84-
}
85-
104+
const { connection, reauthenticating, response } = authContext;
105+
const credentials = getCredentials(authContext);
86106
const workflow = getWorkflow(credentials);
87-
88-
await workflow.execute(connection, credentials, reauthenticating);
107+
await workflow.execute(connection, credentials, reauthenticating, response);
89108
}
90109

91110
/**
@@ -95,19 +114,24 @@ export class MongoDBOIDC extends AuthProvider {
95114
handshakeDoc: HandshakeDocument,
96115
authContext: AuthContext
97116
): Promise<HandshakeDocument> {
98-
const { credentials } = authContext;
99-
100-
if (!credentials) {
101-
throw new MongoMissingCredentialsError('AuthContext must provide credentials.');
102-
}
103-
117+
const credentials = getCredentials(authContext);
104118
const workflow = getWorkflow(credentials);
105-
106-
const result = await workflow.speculativeAuth();
119+
const result = await workflow.speculativeAuth(credentials);
107120
return { ...handshakeDoc, ...result };
108121
}
109122
}
110123

124+
/**
125+
* Get credentials from the auth context, throwing if they do not exist.
126+
*/
127+
function getCredentials(authContext: AuthContext): MongoCredentials {
128+
const { credentials } = authContext;
129+
if (!credentials) {
130+
throw new MongoMissingCredentialsError(MISSING_CREDENTIALS_ERROR);
131+
}
132+
return credentials;
133+
}
134+
111135
/**
112136
* Gets either a device workflow or callback workflow.
113137
*/

src/cmap/auth/mongodb_oidc/aws_service_workflow.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import { readFile } from 'fs/promises';
1+
import * as fs from 'fs';
22

33
import { MongoAWSError } from '../../../error';
44
import { ServiceWorkflow } from './service_workflow';
55

6+
/** Error for when the token is missing in the environment. */
7+
const TOKEN_MISSING_ERROR = 'AWS_WEB_IDENTITY_TOKEN_FILE must be set in the environment.';
8+
69
/**
710
* Device workflow implementation for AWS.
811
*
@@ -19,8 +22,8 @@ export class AwsServiceWorkflow extends ServiceWorkflow {
1922
async getToken(): Promise<string> {
2023
const tokenFile = process.env.AWS_WEB_IDENTITY_TOKEN_FILE;
2124
if (!tokenFile) {
22-
throw new MongoAWSError('AWS_WEB_IDENTITY_TOKEN_FILE must be set in the environment.');
25+
throw new MongoAWSError(TOKEN_MISSING_ERROR);
2326
}
24-
return readFile(tokenFile, 'utf8');
27+
return fs.promises.readFile(tokenFile, 'utf8');
2528
}
2629
}

src/cmap/auth/mongodb_oidc/cache.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Base class for OIDC caches.
3+
*/
4+
export abstract class Cache<T> {
5+
entries: Map<string, T>;
6+
7+
/**
8+
* Create a new cache.
9+
*/
10+
constructor() {
11+
this.entries = new Map<string, T>();
12+
}
13+
14+
/**
15+
* Clear the cache.
16+
*/
17+
clear() {
18+
this.entries.clear();
19+
}
20+
21+
/**
22+
* Create a cache key from the address and username.
23+
*/
24+
cacheKey(address: string, username: string, callbackHash: string): string {
25+
return JSON.stringify([address, username, callbackHash]);
26+
}
27+
}

0 commit comments

Comments
 (0)