Skip to content

feat(NODE-5191): OIDC Auth Updates #3637

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 93 commits into from
May 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
51532f2
feat(NODE-5191): update oidc objects
durran Apr 12, 2023
08823d1
feat(NODE-5191): add allowed hosts option
durran Apr 13, 2023
a943d7b
test(NODE-5191): add prose spec template
durran Apr 13, 2023
3383e87
test: adding first prose test
durran Apr 13, 2023
3608ea0
test: prose tests up to aws
durran Apr 14, 2023
ac802a2
fix: issuer is not optional
durran Apr 14, 2023
58aa17e
test: more callback tests
durran Apr 14, 2023
7bac7ea
fix: mechanism properties
durran Apr 14, 2023
1ab3331
test: fix path
durran Apr 14, 2023
c45f285
test: add tests to speculative auth
durran Apr 17, 2023
4fef6d4
test: update connection string regex
durran Apr 17, 2023
0a5453d
fix: normal string testing
durran Apr 17, 2023
23fd2c5
test: add debug
durran Apr 17, 2023
640f31e
fix: fix formatting
durran Apr 17, 2023
b020c7d
test: fix sinon chai
durran Apr 17, 2023
2402d3a
test: fix test env
durran Apr 17, 2023
1d9af2d
test: adding reauth tests
durran Apr 18, 2023
5b9f955
test: finish reauth tests
durran Apr 18, 2023
3c74e55
chore: adding console debug
durran Apr 18, 2023
d504784
chore: more debug
durran Apr 18, 2023
47d9b4d
fix: no assert, just run
durran Apr 18, 2023
3f65e9e
fix: await
durran Apr 18, 2023
ec1b874
test: fixing callback, aws
durran Apr 18, 2023
45a6220
test: more test fixes
durran Apr 18, 2023
61448a1
chore: more debug to callback workflow
durran Apr 18, 2023
99009e1
test: move cache clearing
durran Apr 18, 2023
e658e7a
test: use same auth mech props
durran Apr 18, 2023
185c895
test: more test fixes:
durran Apr 18, 2023
8a8e3c5
chore: more debug
durran Apr 18, 2023
cb19e9c
test: more debug
durran Apr 19, 2023
76e3e2d
fix: result check
durran Apr 19, 2023
392b6f6
test: fix callback instances in tests
durran Apr 19, 2023
e365c69
test: more test updates
durran Apr 19, 2023
541e803
test: more updates
durran Apr 19, 2023
3ffa95e
test: more test fixes
durran Apr 19, 2023
5a7207a
test: update prose test debug
durran Apr 19, 2023
0d14b64
test: only track find events
durran Apr 19, 2023
3617ce2
fix: speculative auth
durran Apr 20, 2023
0f139dc
refactor: refactoring callback workflow
durran Apr 21, 2023
c5e435b
fix: speculative auth and messages
durran Apr 21, 2023
c722b9f
fix: speculative auth
durran Apr 21, 2023
e3e2422
fix: change cleanup location
durran Apr 21, 2023
fbccca5
fix: change cleanup location
durran Apr 21, 2023
acde726
chore: more debug
durran Apr 21, 2023
aec6db2
fix: reauth cannot use cached token
durran Apr 21, 2023
22f85d2
test: add speculative auth tests
durran Apr 21, 2023
75d3aa3
test: fix spec auth test
durran Apr 21, 2023
ecab8d5
test: fix test cleanup
durran Apr 21, 2023
6a07a15
fix: add db to speculative auth doc
durran Apr 21, 2023
f5ebffe
test: use correct error code
durran Apr 21, 2023
556b99f
fix: speculative auth
durran Apr 21, 2023
434bc01
test: more debug
durran Apr 21, 2023
bf15cc0
fix: no spec auth when reauth
durran Apr 21, 2023
bec5d9e
fix: client leak
durran Apr 21, 2023
d9ef4d1
fix: properly close, remove debug
durran Apr 21, 2023
5ca0e6b
feat: oidc updates
durran Apr 27, 2023
46534f1
test: fixing tests
durran Apr 27, 2023
b5b53e4
test: update tests
durran Apr 27, 2023
f35aa16
test: fix readfile
durran Apr 27, 2023
393799f
chore: debug
durran Apr 27, 2023
0f305a2
chore: dont send conversation id during reauth
durran Apr 27, 2023
fcfd9b6
chore: lint
durran Apr 27, 2023
1163de6
chore: debug error
durran Apr 27, 2023
d1563ae
refactor: moving validation
durran Apr 27, 2023
95d9064
refactor: move validation into client
durran Apr 27, 2023
82f3e63
chore: remove console debug
durran Apr 27, 2023
08fc2ef
test: fail reauth twice
durran Apr 27, 2023
699e5a1
test: updating test assertions
durran Apr 27, 2023
e969628
fix: lint
durran Apr 27, 2023
90b0522
fix: invalidate token in cache on error
durran Apr 27, 2023
916aca2
chore: cleanup
durran Apr 27, 2023
9f3f17c
chore: debug
durran Apr 27, 2023
780e4df
test: create new client
durran Apr 27, 2023
2e6d7a3
chore: remove console logs
durran Apr 27, 2023
e0899f6
test: fix unit tests
durran Apr 27, 2023
87345ec
fix: ts import version
durran Apr 28, 2023
f193239
test: add case for unix sockets
durran Apr 28, 2023
ce5fcab
feat: prevent simultaneous callback execution
durran Apr 28, 2023
8bb8ae1
test: add lock prose test
durran Apr 28, 2023
dda9dcb
chore: debug
durran Apr 28, 2023
98b7225
chore: lock again
durran Apr 28, 2023
b4f2008
test: use lock cache
durran Apr 28, 2023
1abd3b0
test: clear callback cache
durran Apr 28, 2023
76897a4
chore: debug
durran Apr 28, 2023
8e5c113
refactor: move function hashing
durran Apr 28, 2023
9335966
test: throw if entered with lock
durran Apr 28, 2023
f28c29d
fix:lint
durran Apr 28, 2023
68c52ef
fix: change cache keys
durran Apr 28, 2023
fd5a712
fix: timers on node 14
durran Apr 28, 2023
76aa52b
fix: settimeout issues
durran Apr 28, 2023
5fecafc
fix: suggestions
durran May 3, 2023
b947b11
fix: credentials defaulting
durran May 3, 2023
bc167a2
fix: comment addressing
durran May 3, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .evergreen/config.in.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ functions:
${PREPARE_SHELL}

OIDC_TOKEN_DIR="/tmp/tokens" \
AWS_WEB_IDENTITY_TOKEN_FILE="/tmp/tokens/test1" \
AWS_WEB_IDENTITY_TOKEN_FILE="/tmp/tokens/test_user1" \
PROJECT_DIRECTORY="${PROJECT_DIRECTORY}" \
bash ${PROJECT_DIRECTORY}/.evergreen/run-oidc-tests.sh

Expand Down
2 changes: 1 addition & 1 deletion .evergreen/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ functions:
${PREPARE_SHELL}

OIDC_TOKEN_DIR="/tmp/tokens" \
AWS_WEB_IDENTITY_TOKEN_FILE="/tmp/tokens/test1" \
AWS_WEB_IDENTITY_TOKEN_FILE="/tmp/tokens/test_user1" \
PROJECT_DIRECTORY="${PROJECT_DIRECTORY}" \
bash ${PROJECT_DIRECTORY}/.evergreen/run-oidc-tests.sh
run deployed aws lambda tests:
Expand Down
1 change: 1 addition & 0 deletions .evergreen/run-oidc-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ MONGODB_URI_SINGLE="${MONGODB_URI}/?authMechanism=MONGODB-OIDC&authMechanismProp
echo $MONGODB_URI_SINGLE

export MONGODB_URI="$MONGODB_URI_SINGLE"
export OIDC_TOKEN_DIR=${OIDC_TOKEN_DIR}

npm run check:oidc
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@
"check:atlas": "mocha --config test/manual/mocharc.json test/manual/atlas_connectivity.test.js",
"check:adl": "mocha --config test/mocha_mongodb.json test/manual/atlas-data-lake-testing",
"check:aws": "nyc mocha --config test/mocha_mongodb.json test/integration/auth/mongodb_aws.test.ts",
"check:oidc": "mocha --config test/manual/mocharc.json test/manual/mongodb_oidc.prose.test.ts",
"check:oidc": "mocha --config test/mocha_mongodb.json test/manual/mongodb_oidc.prose.test.ts",
"check:ocsp": "mocha --config test/manual/mocharc.json test/manual/ocsp_support.test.js",
"check:kerberos": "nyc mocha --config test/manual/mocharc.json test/manual/kerberos.test.ts",
"check:tls": "mocha --config test/manual/mocharc.json test/manual/tls_support.test.js",
Expand Down
37 changes: 35 additions & 2 deletions src/cmap/auth/mongo_credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ function getDefaultAuthMechanism(hello?: Document): AuthMechanism {
return AuthMechanism.MONGODB_CR;
}

const ALLOWED_HOSTS_ERROR = 'Auth mechanism property ALLOWED_HOSTS must be an array of strings.';

/** @internal */
export const DEFAULT_ALLOWED_HOSTS = [
'*.mongodb.net',
'*.mongodb-dev.net',
'*.mongodbgov.net',
'localhost',
'127.0.0.1',
'::1'
];

/** @public */
export interface AuthMechanismProperties extends Document {
SERVICE_HOST?: string;
Expand All @@ -43,11 +55,13 @@ export interface AuthMechanismProperties extends Document {
REFRESH_TOKEN_CALLBACK?: OIDCRefreshFunction;
/** @experimental */
PROVIDER_NAME?: 'aws';
/** @experimental */
ALLOWED_HOSTS?: string[];
}

/** @public */
export interface MongoCredentialsOptions {
username: string;
username?: string;
password: string;
source: string;
db?: string;
Expand All @@ -72,7 +86,7 @@ export class MongoCredentials {
readonly mechanismProperties: AuthMechanismProperties;

constructor(options: MongoCredentialsOptions) {
this.username = options.username;
this.username = options.username ?? '';
this.password = options.password;
this.source = options.source;
if (!this.source && options.db) {
Expand Down Expand Up @@ -101,6 +115,13 @@ export class MongoCredentials {
}
}

if (this.mechanism === AuthMechanism.MONGODB_OIDC && !this.mechanismProperties.ALLOWED_HOSTS) {
this.mechanismProperties = {
...this.mechanismProperties,
ALLOWED_HOSTS: DEFAULT_ALLOWED_HOSTS
};
}

Object.freeze(this.mechanismProperties);
Object.freeze(this);
}
Expand Down Expand Up @@ -181,6 +202,18 @@ export class MongoCredentials {
`Either a PROVIDER_NAME or a REQUEST_TOKEN_CALLBACK must be specified for mechanism '${this.mechanism}'.`
);
}

if (this.mechanismProperties.ALLOWED_HOSTS) {
const hosts = this.mechanismProperties.ALLOWED_HOSTS;
if (!Array.isArray(hosts)) {
throw new MongoInvalidArgumentError(ALLOWED_HOSTS_ERROR);
}
for (const host of hosts) {
if (typeof host !== 'string') {
throw new MongoInvalidArgumentError(ALLOWED_HOSTS_ERROR);
}
}
}
}

if (AUTH_MECHS_AUTH_SRC_EXTERNAL.has(this.mechanism)) {
Expand Down
98 changes: 61 additions & 37 deletions src/cmap/auth/mongodb_oidc.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,85 @@
import type { Document } from 'bson';

import { MongoInvalidArgumentError, MongoMissingCredentialsError } from '../../error';
import type { HandshakeDocument } from '../connect';
import { type AuthContext, AuthProvider } from './auth_provider';
import type { Connection } from '../connection';
import { AuthContext, AuthProvider } from './auth_provider';
import type { MongoCredentials } from './mongo_credentials';
import { AwsServiceWorkflow } from './mongodb_oidc/aws_service_workflow';
import { CallbackWorkflow } from './mongodb_oidc/callback_workflow';
import type { Workflow } from './mongodb_oidc/workflow';

/** Error when credentials are missing. */
const MISSING_CREDENTIALS_ERROR = 'AuthContext must provide credentials.';

/**
* @public
* @experimental
*/
export interface OIDCMechanismServerStep1 {
authorizationEndpoint?: string;
tokenEndpoint?: string;
deviceAuthorizationEndpoint?: string;
export interface IdPServerInfo {
issuer: string;
clientId: string;
clientSecret?: string;
requestScopes?: string[];
}

/**
* @public
* @experimental
*/
export interface OIDCRequestTokenResult {
export interface IdPServerResponse {
accessToken: string;
expiresInSeconds?: number;
refreshToken?: string;
}

/**
* @public
* @experimental
*/
export interface OIDCCallbackContext {
refreshToken?: string;
timeoutSeconds?: number;
timeoutContext?: AbortSignal;
version: number;
}

/**
* @public
* @experimental
*/
export type OIDCRequestFunction = (
principalName: string,
serverResult: OIDCMechanismServerStep1,
timeout: AbortSignal | number
) => Promise<OIDCRequestTokenResult>;
info: IdPServerInfo,
context: OIDCCallbackContext
) => Promise<IdPServerResponse>;

/**
* @public
* @experimental
*/
export type OIDCRefreshFunction = (
principalName: string,
serverResult: OIDCMechanismServerStep1,
result: OIDCRequestTokenResult,
timeout: AbortSignal | number
) => Promise<OIDCRequestTokenResult>;
info: IdPServerInfo,
context: OIDCCallbackContext
) => Promise<IdPServerResponse>;

type ProviderName = 'aws' | 'callback';

export interface Workflow {
/**
* All device workflows must implement this method in order to get the access
* token and then call authenticate with it.
*/
execute(
connection: Connection,
credentials: MongoCredentials,
reauthenticating: boolean,
response?: Document
): Promise<Document>;

/**
* Get the document to add for speculative authentication.
*/
speculativeAuth(credentials: MongoCredentials): Promise<Document>;
}

/** @internal */
export const OIDC_WORKFLOWS: Map<ProviderName, Workflow> = new Map();
OIDC_WORKFLOWS.set('callback', new CallbackWorkflow());
Expand All @@ -73,19 +101,10 @@ export class MongoDBOIDC extends AuthProvider {
* Authenticate using OIDC
*/
override async auth(authContext: AuthContext): Promise<void> {
const { connection, credentials, response, reauthenticating } = authContext;

if (response?.speculativeAuthenticate) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Speculative auth is implemented in the the workflows.

return;
}

if (!credentials) {
throw new MongoMissingCredentialsError('AuthContext must provide credentials.');
}

const { connection, reauthenticating, response } = authContext;
const credentials = getCredentials(authContext);
const workflow = getWorkflow(credentials);

await workflow.execute(connection, credentials, reauthenticating);
await workflow.execute(connection, credentials, reauthenticating, response);
}

/**
Expand All @@ -95,19 +114,24 @@ export class MongoDBOIDC extends AuthProvider {
handshakeDoc: HandshakeDocument,
authContext: AuthContext
): Promise<HandshakeDocument> {
const { credentials } = authContext;

if (!credentials) {
throw new MongoMissingCredentialsError('AuthContext must provide credentials.');
}

const credentials = getCredentials(authContext);
const workflow = getWorkflow(credentials);

const result = await workflow.speculativeAuth();
const result = await workflow.speculativeAuth(credentials);
return { ...handshakeDoc, ...result };
}
}

/**
* Get credentials from the auth context, throwing if they do not exist.
*/
function getCredentials(authContext: AuthContext): MongoCredentials {
const { credentials } = authContext;
if (!credentials) {
throw new MongoMissingCredentialsError(MISSING_CREDENTIALS_ERROR);
}
return credentials;
}

/**
* Gets either a device workflow or callback workflow.
*/
Expand Down
9 changes: 6 additions & 3 deletions src/cmap/auth/mongodb_oidc/aws_service_workflow.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { readFile } from 'fs/promises';
import * as fs from 'fs';

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

/** Error for when the token is missing in the environment. */
const TOKEN_MISSING_ERROR = 'AWS_WEB_IDENTITY_TOKEN_FILE must be set in the environment.';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and other string constants just a simple refactoring of any non-interpolated string being a constant.


/**
* Device workflow implementation for AWS.
*
Expand All @@ -19,8 +22,8 @@ export class AwsServiceWorkflow extends ServiceWorkflow {
async getToken(): Promise<string> {
const tokenFile = process.env.AWS_WEB_IDENTITY_TOKEN_FILE;
if (!tokenFile) {
throw new MongoAWSError('AWS_WEB_IDENTITY_TOKEN_FILE must be set in the environment.');
throw new MongoAWSError(TOKEN_MISSING_ERROR);
}
return readFile(tokenFile, 'utf8');
return fs.promises.readFile(tokenFile, 'utf8');
}
}
27 changes: 27 additions & 0 deletions src/cmap/auth/mongodb_oidc/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Base class for OIDC caches.
*/
export abstract class Cache<T> {
entries: Map<string, T>;

/**
* Create a new cache.
*/
constructor() {
this.entries = new Map<string, T>();
}

/**
* Clear the cache.
*/
clear() {
this.entries.clear();
}

/**
* Create a cache key from the address and username.
*/
cacheKey(address: string, username: string, callbackHash: string): string {
return JSON.stringify([address, username, callbackHash]);
}
}
Loading