Skip to content

Commit

Permalink
feat(NODE-5036): reauthenticate OIDC and retry
Browse files Browse the repository at this point in the history
  • Loading branch information
durran committed Mar 3, 2023
1 parent 4a7b5ec commit dbdd899
Show file tree
Hide file tree
Showing 17 changed files with 849 additions and 15 deletions.
8 changes: 7 additions & 1 deletion src/cmap/auth/auth_provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,20 @@ import type { HandshakeDocument } from '../connect';
import type { Connection, ConnectionOptions } from '../connection';
import type { MongoCredentials } from './mongo_credentials';

/** @internal */
export type AuthContextOptions = ConnectionOptions & ClientMetadataOptions;

/** Context used during authentication */
/**
* Context used during authentication
* @internal
*/
export class AuthContext {
/** The connection to authenticate */
connection: Connection;
/** The credentials to use for authentication */
credentials?: MongoCredentials;
/** If the context if for reauthentication. */
reauthenticating = false;
/** The options passed to the `connect` method */
options: AuthContextOptions;

Expand Down
4 changes: 2 additions & 2 deletions src/cmap/auth/mongodb_oidc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export class MongoDBOIDC extends AuthProvider {
* Authenticate using OIDC
*/
override auth(authContext: AuthContext, callback: Callback): void {
const { connection, credentials, response } = authContext;
const { connection, credentials, response, reauthenticating } = authContext;

if (response?.speculativeAuthenticate) {
return callback();
Expand All @@ -86,7 +86,7 @@ export class MongoDBOIDC extends AuthProvider {
)
);
}
workflow.execute(connection, credentials).then(
workflow.execute(connection, credentials, reauthenticating).then(
result => {
return callback(undefined, result);
},
Expand Down
10 changes: 7 additions & 3 deletions src/cmap/auth/mongodb_oidc/callback_workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ export class CallbackWorkflow implements Workflow {
* - put the new entry in the cache.
* - execute step two.
*/
async execute(connection: Connection, credentials: MongoCredentials): Promise<Document> {
async execute(
connection: Connection,
credentials: MongoCredentials,
reauthenticate = false
): Promise<Document> {
const request = credentials.mechanismProperties.REQUEST_TOKEN_CALLBACK;
const refresh = credentials.mechanismProperties.REFRESH_TOKEN_CALLBACK;

Expand All @@ -69,8 +73,8 @@ export class CallbackWorkflow implements Workflow {
refresh || null
);
if (entry) {
// Check if the entry is not expired.
if (entry.isValid()) {
// Check if the entry is not expired and if we are reauthenticating.
if (!reauthenticate && entry.isValid()) {
// Skip step one and execute the step two saslContinue.
try {
const result = await finishAuth(entry.tokenResult, undefined, connection, credentials);
Expand Down
6 changes: 5 additions & 1 deletion src/cmap/auth/mongodb_oidc/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ 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): Promise<Document>;
execute(
connection: Connection,
credentials: MongoCredentials,
reauthenticate?: boolean
): Promise<Document>;

/**
* Get the document to add for speculative authentication.
Expand Down
4 changes: 2 additions & 2 deletions src/cmap/auth/scram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ class ScramSHA extends AuthProvider {
}

override auth(authContext: AuthContext, callback: Callback) {
const response = authContext.response;
if (response && response.speculativeAuthenticate) {
const { reauthenticating, response } = authContext;
if (response?.speculativeAuthenticate && !reauthenticating) {
continueScramConversation(
this.cryptoMethod,
response.speculativeAuthenticate,
Expand Down
4 changes: 3 additions & 1 deletion src/cmap/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ import {
MIN_SUPPORTED_WIRE_VERSION
} from './wire_protocol/constants';

const AUTH_PROVIDERS = new Map<AuthMechanism | string, AuthProvider>([
/** @internal */
export const AUTH_PROVIDERS = new Map<AuthMechanism | string, AuthProvider>([
[AuthMechanism.MONGODB_AWS, new MongoDBAWS()],
[AuthMechanism.MONGODB_CR, new MongoCR()],
[AuthMechanism.MONGODB_GSSAPI, new GSSAPI()],
Expand Down Expand Up @@ -117,6 +118,7 @@ function performInitialHandshake(
}

const authContext = new AuthContext(conn, credentials, options);
conn.authContext = authContext;
prepareHandshakeDocument(authContext, (err, handshakeDoc) => {
if (err || !handshakeDoc) {
return callback(err);
Expand Down
4 changes: 3 additions & 1 deletion src/cmap/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
uuidV4
} from '../utils';
import type { WriteConcern } from '../write_concern';
import type { AuthContext } from './auth/auth_provider';
import type { MongoCredentials } from './auth/mongo_credentials';
import {
CommandFailedEvent,
Expand Down Expand Up @@ -127,7 +128,6 @@ export interface ConnectionOptions
noDelay?: boolean;
socketTimeoutMS?: number;
cancellationToken?: CancellationToken;

metadata: ClientMetadata;
}

Expand Down Expand Up @@ -165,6 +165,8 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
cmd: Document,
options: CommandOptions | undefined
) => Promise<Document>;
/** @internal */
authContext?: AuthContext;

/**@internal */
[kDelayedTimeoutId]: NodeJS.Timeout | null;
Expand Down
72 changes: 69 additions & 3 deletions src/cmap/connection_pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,18 @@ import {
CONNECTION_READY
} from '../constants';
import {
MONGODB_ERROR_CODES,
MongoError,
MongoInvalidArgumentError,
MongoMissingCredentialsError,
MongoNetworkError,
MongoRuntimeError,
MongoServerError
} from '../error';
import { CancellationToken, TypedEventEmitter } from '../mongo_types';
import type { Server } from '../sdam/server';
import { Callback, eachAsync, List, makeCounter } from '../utils';
import { connect } from './connect';
import { AUTH_PROVIDERS, connect } from './connect';
import { Connection, ConnectionEvents, ConnectionOptions } from './connection';
import {
ConnectionCheckedInEvent,
Expand Down Expand Up @@ -544,7 +546,17 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
fn(undefined, conn, (fnErr, result) => {
if (typeof callback === 'function') {
if (fnErr) {
callback(fnErr);
if ((fnErr as MongoError).code === MONGODB_ERROR_CODES.Reauthenticate) {
this.reauthenticate(conn, fn, (error, res) => {
if (error) {
callback(error);
} else {
callback(undefined, res);
}
});
} else {
callback(fnErr);
}
} else {
callback(undefined, result);
}
Expand All @@ -559,7 +571,17 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
fn(err as MongoError, conn, (fnErr, result) => {
if (typeof callback === 'function') {
if (fnErr) {
callback(fnErr);
if (conn && (fnErr as MongoError).code === MONGODB_ERROR_CODES.Reauthenticate) {
this.reauthenticate(conn, fn, (error, res) => {
if (error) {
callback(error);
} else {
callback(undefined, res);
}
});
} else {
callback(fnErr);
}
} else {
callback(undefined, result);
}
Expand All @@ -572,6 +594,50 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
});
}

/**
* Reauthenticate on the same connection and then retry the operation.
*/
private reauthenticate(
connection: Connection,
fn: WithConnectionCallback,
callback: Callback
): void {
const authContext = connection.authContext;
if (!authContext) {
return callback(new MongoRuntimeError('No auth context found on connection.'));
}
authContext.reauthenticating = true;
const credentials = authContext.credentials;
if (!credentials) {
return callback(
new MongoMissingCredentialsError(
'Connection is missing credentials when asked to reauthenticate'
)
);
}
const resolvedCredentials = credentials.resolveAuthMechanism(connection.hello || undefined);
const provider = AUTH_PROVIDERS.get(resolvedCredentials.mechanism);
if (!provider) {
return callback(
new MongoMissingCredentialsError(
`Reauthenticate failed due to no auth provider for ${credentials.mechanism}`
)
);
}
provider.auth(authContext, error => {
authContext.reauthenticating = false;
if (error) {
return callback(error);
}
return fn(undefined, connection, (fnErr, fnResult) => {
if (fnErr) {
return callback(fnErr);
}
callback(undefined, fnResult);
});
});
}

/** Clear the min pool size timer */
private clearMinPoolSizeTimer(): void {
const minPoolSizeTimer = this[kMinPoolSizeTimer];
Expand Down
3 changes: 2 additions & 1 deletion src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ export const MONGODB_ERROR_CODES = Object.freeze({
IllegalOperation: 20,
MaxTimeMSExpired: 50,
UnknownReplWriteConcern: 79,
UnsatisfiableWriteConcern: 100
UnsatisfiableWriteConcern: 100,
Reauthenticate: 391
} as const);

// From spec@https://github.com/mongodb/specifications/blob/f93d78191f3db2898a59013a7ed5650352ef6da8/source/change-streams/change-streams.rst#resumable-error
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ export type {
ResumeToken,
UpdateDescription
} from './change_stream';
export { AuthContext, AuthContextOptions } from './cmap/auth/auth_provider';
export type {
AuthMechanismProperties,
MongoCredentials,
Expand Down
8 changes: 8 additions & 0 deletions test/integration/auth/auth.spec.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as path from 'path';

import { loadSpecTests } from '../../spec';
import { runUnifiedSuite } from '../../tools/unified-spec-runner/runner';

describe('Auth (unified)', function () {
runUnifiedSuite(loadSpecTests(path.join('auth', 'unified')));
});

0 comments on commit dbdd899

Please sign in to comment.