something is working
This commit is contained in:
+60
@@ -0,0 +1,60 @@
|
||||
import type { Document } from '../../bson';
|
||||
import { MongoRuntimeError } from '../../error';
|
||||
import type { Callback, ClientMetadataOptions } from '../../utils';
|
||||
import type { HandshakeDocument } from '../connect';
|
||||
import type { Connection, ConnectionOptions } from '../connection';
|
||||
import type { MongoCredentials } from './mongo_credentials';
|
||||
|
||||
export type AuthContextOptions = ConnectionOptions & ClientMetadataOptions;
|
||||
|
||||
/** Context used during authentication */
|
||||
export class AuthContext {
|
||||
/** The connection to authenticate */
|
||||
connection: Connection;
|
||||
/** The credentials to use for authentication */
|
||||
credentials?: MongoCredentials;
|
||||
/** The options passed to the `connect` method */
|
||||
options: AuthContextOptions;
|
||||
|
||||
/** A response from an initial auth attempt, only some mechanisms use this (e.g, SCRAM) */
|
||||
response?: Document;
|
||||
/** A random nonce generated for use in an authentication conversation */
|
||||
nonce?: Buffer;
|
||||
|
||||
constructor(
|
||||
connection: Connection,
|
||||
credentials: MongoCredentials | undefined,
|
||||
options: AuthContextOptions
|
||||
) {
|
||||
this.connection = connection;
|
||||
this.credentials = credentials;
|
||||
this.options = options;
|
||||
}
|
||||
}
|
||||
|
||||
export class AuthProvider {
|
||||
/**
|
||||
* Prepare the handshake document before the initial handshake.
|
||||
*
|
||||
* @param handshakeDoc - The document used for the initial handshake on a connection
|
||||
* @param authContext - Context for authentication flow
|
||||
*/
|
||||
prepare(
|
||||
handshakeDoc: HandshakeDocument,
|
||||
authContext: AuthContext,
|
||||
callback: Callback<HandshakeDocument>
|
||||
): void {
|
||||
callback(undefined, handshakeDoc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
*
|
||||
* @param context - A shared context for authentication flow
|
||||
* @param callback - The callback to return the result from the authentication
|
||||
*/
|
||||
auth(context: AuthContext, callback: Callback): void {
|
||||
// TODO(NODE-3483): Replace this with MongoMethodOverrideError
|
||||
callback(new MongoRuntimeError('`auth` method must be overridden by subclass'));
|
||||
}
|
||||
}
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
import * as dns from 'dns';
|
||||
|
||||
import type { Document } from '../../bson';
|
||||
import { Kerberos, KerberosClient } from '../../deps';
|
||||
import {
|
||||
MongoError,
|
||||
MongoInvalidArgumentError,
|
||||
MongoMissingCredentialsError,
|
||||
MongoMissingDependencyError,
|
||||
MongoRuntimeError
|
||||
} from '../../error';
|
||||
import { Callback, ns } from '../../utils';
|
||||
import { AuthContext, AuthProvider } from './auth_provider';
|
||||
|
||||
/** @public */
|
||||
export const GSSAPICanonicalizationValue = Object.freeze({
|
||||
on: true,
|
||||
off: false,
|
||||
none: 'none',
|
||||
forward: 'forward',
|
||||
forwardAndReverse: 'forwardAndReverse'
|
||||
} as const);
|
||||
|
||||
/** @public */
|
||||
export type GSSAPICanonicalizationValue =
|
||||
typeof GSSAPICanonicalizationValue[keyof typeof GSSAPICanonicalizationValue];
|
||||
|
||||
type MechanismProperties = {
|
||||
/** @deprecated use `CANONICALIZE_HOST_NAME` instead */
|
||||
gssapiCanonicalizeHostName?: boolean;
|
||||
CANONICALIZE_HOST_NAME?: GSSAPICanonicalizationValue;
|
||||
SERVICE_HOST?: string;
|
||||
SERVICE_NAME?: string;
|
||||
SERVICE_REALM?: string;
|
||||
};
|
||||
|
||||
export class GSSAPI extends AuthProvider {
|
||||
override auth(authContext: AuthContext, callback: Callback): void {
|
||||
const { connection, credentials } = authContext;
|
||||
if (credentials == null)
|
||||
return callback(
|
||||
new MongoMissingCredentialsError('Credentials required for GSSAPI authentication')
|
||||
);
|
||||
const { username } = credentials;
|
||||
function externalCommand(
|
||||
command: Document,
|
||||
cb: Callback<{ payload: string; conversationId: any }>
|
||||
) {
|
||||
return connection.command(ns('$external.$cmd'), command, undefined, cb);
|
||||
}
|
||||
makeKerberosClient(authContext, (err, client) => {
|
||||
if (err) return callback(err);
|
||||
if (client == null) return callback(new MongoMissingDependencyError('GSSAPI client missing'));
|
||||
client.step('', (err, payload) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
externalCommand(saslStart(payload), (err, result) => {
|
||||
if (err) return callback(err);
|
||||
if (result == null) return callback();
|
||||
negotiate(client, 10, result.payload, (err, payload) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
externalCommand(saslContinue(payload, result.conversationId), (err, result) => {
|
||||
if (err) return callback(err);
|
||||
if (result == null) return callback();
|
||||
finalize(client, username, result.payload, (err, payload) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
externalCommand(
|
||||
{
|
||||
saslContinue: 1,
|
||||
conversationId: result.conversationId,
|
||||
payload
|
||||
},
|
||||
(err, result) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
callback(undefined, result);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function makeKerberosClient(authContext: AuthContext, callback: Callback<KerberosClient>): void {
|
||||
const { hostAddress } = authContext.options;
|
||||
const { credentials } = authContext;
|
||||
if (!hostAddress || typeof hostAddress.host !== 'string' || !credentials) {
|
||||
return callback(
|
||||
new MongoInvalidArgumentError('Connection must have host and port and credentials defined.')
|
||||
);
|
||||
}
|
||||
|
||||
if ('kModuleError' in Kerberos) {
|
||||
return callback(Kerberos['kModuleError']);
|
||||
}
|
||||
const { initializeClient } = Kerberos;
|
||||
|
||||
const { username, password } = credentials;
|
||||
const mechanismProperties = credentials.mechanismProperties as MechanismProperties;
|
||||
|
||||
const serviceName = mechanismProperties.SERVICE_NAME ?? 'mongodb';
|
||||
|
||||
performGSSAPICanonicalizeHostName(
|
||||
hostAddress.host,
|
||||
mechanismProperties,
|
||||
(err?: Error | MongoError, host?: string) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
const initOptions = {};
|
||||
if (password != null) {
|
||||
Object.assign(initOptions, { user: username, password: password });
|
||||
}
|
||||
|
||||
const spnHost = mechanismProperties.SERVICE_HOST ?? host;
|
||||
let spn = `${serviceName}${process.platform === 'win32' ? '/' : '@'}${spnHost}`;
|
||||
if ('SERVICE_REALM' in mechanismProperties) {
|
||||
spn = `${spn}@${mechanismProperties.SERVICE_REALM}`;
|
||||
}
|
||||
|
||||
initializeClient(spn, initOptions, (err: string, client: KerberosClient): void => {
|
||||
// TODO(NODE-3483)
|
||||
if (err) return callback(new MongoRuntimeError(err));
|
||||
callback(undefined, client);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function saslStart(payload?: string): Document {
|
||||
return {
|
||||
saslStart: 1,
|
||||
mechanism: 'GSSAPI',
|
||||
payload,
|
||||
autoAuthorize: 1
|
||||
};
|
||||
}
|
||||
|
||||
function saslContinue(payload?: string, conversationId?: number): Document {
|
||||
return {
|
||||
saslContinue: 1,
|
||||
conversationId,
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
||||
function negotiate(
|
||||
client: KerberosClient,
|
||||
retries: number,
|
||||
payload: string,
|
||||
callback: Callback<string>
|
||||
): void {
|
||||
client.step(payload, (err, response) => {
|
||||
// Retries exhausted, raise error
|
||||
if (err && retries === 0) return callback(err);
|
||||
|
||||
// Adjust number of retries and call step again
|
||||
if (err) return negotiate(client, retries - 1, payload, callback);
|
||||
|
||||
// Return the payload
|
||||
callback(undefined, response || '');
|
||||
});
|
||||
}
|
||||
|
||||
function finalize(
|
||||
client: KerberosClient,
|
||||
user: string,
|
||||
payload: string,
|
||||
callback: Callback<string>
|
||||
): void {
|
||||
// GSS Client Unwrap
|
||||
client.unwrap(payload, (err, response) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
// Wrap the response
|
||||
client.wrap(response || '', { user }, (err, wrapped) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
// Return the payload
|
||||
callback(undefined, wrapped);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function performGSSAPICanonicalizeHostName(
|
||||
host: string,
|
||||
mechanismProperties: MechanismProperties,
|
||||
callback: Callback<string>
|
||||
): void {
|
||||
const mode = mechanismProperties.CANONICALIZE_HOST_NAME;
|
||||
if (!mode || mode === GSSAPICanonicalizationValue.none) {
|
||||
return callback(undefined, host);
|
||||
}
|
||||
|
||||
// If forward and reverse or true
|
||||
if (
|
||||
mode === GSSAPICanonicalizationValue.on ||
|
||||
mode === GSSAPICanonicalizationValue.forwardAndReverse
|
||||
) {
|
||||
// Perform the lookup of the ip address.
|
||||
dns.lookup(host, (error, address) => {
|
||||
// No ip found, return the error.
|
||||
if (error) return callback(error);
|
||||
|
||||
// Perform a reverse ptr lookup on the ip address.
|
||||
dns.resolvePtr(address, (err, results) => {
|
||||
// This can error as ptr records may not exist for all ips. In this case
|
||||
// fallback to a cname lookup as dns.lookup() does not return the
|
||||
// cname.
|
||||
if (err) {
|
||||
return resolveCname(host, callback);
|
||||
}
|
||||
// If the ptr did not error but had no results, return the host.
|
||||
callback(undefined, results.length > 0 ? results[0] : host);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// The case for forward is just to resolve the cname as dns.lookup()
|
||||
// will not return it.
|
||||
resolveCname(host, callback);
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveCname(host: string, callback: Callback<string>): void {
|
||||
// Attempt to resolve the host name
|
||||
dns.resolveCname(host, (err, r) => {
|
||||
if (err) return callback(undefined, host);
|
||||
|
||||
// Get the first resolve host id
|
||||
if (r.length > 0) {
|
||||
return callback(undefined, r[0]);
|
||||
}
|
||||
|
||||
callback(undefined, host);
|
||||
});
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
// Resolves the default auth mechanism according to
|
||||
import type { Document } from '../../bson';
|
||||
import { MongoAPIError, MongoMissingCredentialsError } from '../../error';
|
||||
import { emitWarningOnce } from '../../utils';
|
||||
import { GSSAPICanonicalizationValue } from './gssapi';
|
||||
import { AUTH_MECHS_AUTH_SRC_EXTERNAL, AuthMechanism } from './providers';
|
||||
|
||||
// https://github.com/mongodb/specifications/blob/master/source/auth/auth.rst
|
||||
function getDefaultAuthMechanism(hello?: Document): AuthMechanism {
|
||||
if (hello) {
|
||||
// If hello contains saslSupportedMechs, use scram-sha-256
|
||||
// if it is available, else scram-sha-1
|
||||
if (Array.isArray(hello.saslSupportedMechs)) {
|
||||
return hello.saslSupportedMechs.includes(AuthMechanism.MONGODB_SCRAM_SHA256)
|
||||
? AuthMechanism.MONGODB_SCRAM_SHA256
|
||||
: AuthMechanism.MONGODB_SCRAM_SHA1;
|
||||
}
|
||||
|
||||
// Fallback to legacy selection method. If wire version >= 3, use scram-sha-1
|
||||
if (hello.maxWireVersion >= 3) {
|
||||
return AuthMechanism.MONGODB_SCRAM_SHA1;
|
||||
}
|
||||
}
|
||||
|
||||
// Default for wireprotocol < 3
|
||||
return AuthMechanism.MONGODB_CR;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface AuthMechanismProperties extends Document {
|
||||
SERVICE_HOST?: string;
|
||||
SERVICE_NAME?: string;
|
||||
SERVICE_REALM?: string;
|
||||
CANONICALIZE_HOST_NAME?: GSSAPICanonicalizationValue;
|
||||
AWS_SESSION_TOKEN?: string;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface MongoCredentialsOptions {
|
||||
username: string;
|
||||
password: string;
|
||||
source: string;
|
||||
db?: string;
|
||||
mechanism?: AuthMechanism;
|
||||
mechanismProperties: AuthMechanismProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* A representation of the credentials used by MongoDB
|
||||
* @public
|
||||
*/
|
||||
export class MongoCredentials {
|
||||
/** The username used for authentication */
|
||||
readonly username: string;
|
||||
/** The password used for authentication */
|
||||
readonly password: string;
|
||||
/** The database that the user should authenticate against */
|
||||
readonly source: string;
|
||||
/** The method used to authenticate */
|
||||
readonly mechanism: AuthMechanism;
|
||||
/** Special properties used by some types of auth mechanisms */
|
||||
readonly mechanismProperties: AuthMechanismProperties;
|
||||
|
||||
constructor(options: MongoCredentialsOptions) {
|
||||
this.username = options.username;
|
||||
this.password = options.password;
|
||||
this.source = options.source;
|
||||
if (!this.source && options.db) {
|
||||
this.source = options.db;
|
||||
}
|
||||
this.mechanism = options.mechanism || AuthMechanism.MONGODB_DEFAULT;
|
||||
this.mechanismProperties = options.mechanismProperties || {};
|
||||
|
||||
if (this.mechanism.match(/MONGODB-AWS/i)) {
|
||||
if (!this.username && process.env.AWS_ACCESS_KEY_ID) {
|
||||
this.username = process.env.AWS_ACCESS_KEY_ID;
|
||||
}
|
||||
|
||||
if (!this.password && process.env.AWS_SECRET_ACCESS_KEY) {
|
||||
this.password = process.env.AWS_SECRET_ACCESS_KEY;
|
||||
}
|
||||
|
||||
if (
|
||||
this.mechanismProperties.AWS_SESSION_TOKEN == null &&
|
||||
process.env.AWS_SESSION_TOKEN != null
|
||||
) {
|
||||
this.mechanismProperties = {
|
||||
...this.mechanismProperties,
|
||||
AWS_SESSION_TOKEN: process.env.AWS_SESSION_TOKEN
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if ('gssapiCanonicalizeHostName' in this.mechanismProperties) {
|
||||
emitWarningOnce(
|
||||
'gssapiCanonicalizeHostName is deprecated. Please use CANONICALIZE_HOST_NAME instead.'
|
||||
);
|
||||
this.mechanismProperties.CANONICALIZE_HOST_NAME =
|
||||
this.mechanismProperties.gssapiCanonicalizeHostName;
|
||||
}
|
||||
|
||||
Object.freeze(this.mechanismProperties);
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
/** Determines if two MongoCredentials objects are equivalent */
|
||||
equals(other: MongoCredentials): boolean {
|
||||
return (
|
||||
this.mechanism === other.mechanism &&
|
||||
this.username === other.username &&
|
||||
this.password === other.password &&
|
||||
this.source === other.source
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the authentication mechanism is set to "default", resolves the authMechanism
|
||||
* based on the server version and server supported sasl mechanisms.
|
||||
*
|
||||
* @param hello - A hello response from the server
|
||||
*/
|
||||
resolveAuthMechanism(hello?: Document): MongoCredentials {
|
||||
// If the mechanism is not "default", then it does not need to be resolved
|
||||
if (this.mechanism.match(/DEFAULT/i)) {
|
||||
return new MongoCredentials({
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
source: this.source,
|
||||
mechanism: getDefaultAuthMechanism(hello),
|
||||
mechanismProperties: this.mechanismProperties
|
||||
});
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
validate(): void {
|
||||
if (
|
||||
(this.mechanism === AuthMechanism.MONGODB_GSSAPI ||
|
||||
this.mechanism === AuthMechanism.MONGODB_CR ||
|
||||
this.mechanism === AuthMechanism.MONGODB_PLAIN ||
|
||||
this.mechanism === AuthMechanism.MONGODB_SCRAM_SHA1 ||
|
||||
this.mechanism === AuthMechanism.MONGODB_SCRAM_SHA256) &&
|
||||
!this.username
|
||||
) {
|
||||
throw new MongoMissingCredentialsError(`Username required for mechanism '${this.mechanism}'`);
|
||||
}
|
||||
|
||||
if (AUTH_MECHS_AUTH_SRC_EXTERNAL.has(this.mechanism)) {
|
||||
if (this.source != null && this.source !== '$external') {
|
||||
// TODO(NODE-3485): Replace this with a MongoAuthValidationError
|
||||
throw new MongoAPIError(
|
||||
`Invalid source '${this.source}' for mechanism '${this.mechanism}' specified.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.mechanism === AuthMechanism.MONGODB_PLAIN && this.source == null) {
|
||||
// TODO(NODE-3485): Replace this with a MongoAuthValidationError
|
||||
throw new MongoAPIError('PLAIN Authentication Mechanism needs an auth source');
|
||||
}
|
||||
|
||||
if (this.mechanism === AuthMechanism.MONGODB_X509 && this.password != null) {
|
||||
if (this.password === '') {
|
||||
Reflect.set(this, 'password', undefined);
|
||||
return;
|
||||
}
|
||||
// TODO(NODE-3485): Replace this with a MongoAuthValidationError
|
||||
throw new MongoAPIError(`Password not allowed for mechanism MONGODB-X509`);
|
||||
}
|
||||
|
||||
const canonicalization = this.mechanismProperties.CANONICALIZE_HOST_NAME ?? false;
|
||||
if (!Object.values(GSSAPICanonicalizationValue).includes(canonicalization)) {
|
||||
throw new MongoAPIError(`Invalid CANONICALIZE_HOST_NAME value: ${canonicalization}`);
|
||||
}
|
||||
}
|
||||
|
||||
static merge(
|
||||
creds: MongoCredentials | undefined,
|
||||
options: Partial<MongoCredentialsOptions>
|
||||
): MongoCredentials {
|
||||
return new MongoCredentials({
|
||||
username: options.username ?? creds?.username ?? '',
|
||||
password: options.password ?? creds?.password ?? '',
|
||||
mechanism: options.mechanism ?? creds?.mechanism ?? AuthMechanism.MONGODB_DEFAULT,
|
||||
mechanismProperties: options.mechanismProperties ?? creds?.mechanismProperties ?? {},
|
||||
source: options.source ?? options.db ?? creds?.source ?? 'admin'
|
||||
});
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
import { MongoMissingCredentialsError } from '../../error';
|
||||
import { Callback, ns } from '../../utils';
|
||||
import { AuthContext, AuthProvider } from './auth_provider';
|
||||
|
||||
export class MongoCR extends AuthProvider {
|
||||
override auth(authContext: AuthContext, callback: Callback): void {
|
||||
const { connection, credentials } = authContext;
|
||||
if (!credentials) {
|
||||
return callback(new MongoMissingCredentialsError('AuthContext must provide credentials.'));
|
||||
}
|
||||
const username = credentials.username;
|
||||
const password = credentials.password;
|
||||
const source = credentials.source;
|
||||
connection.command(ns(`${source}.$cmd`), { getnonce: 1 }, undefined, (err, r) => {
|
||||
let nonce = null;
|
||||
let key = null;
|
||||
|
||||
// Get nonce
|
||||
if (err == null) {
|
||||
nonce = r.nonce;
|
||||
|
||||
// Use node md5 generator
|
||||
let md5 = crypto.createHash('md5');
|
||||
|
||||
// Generate keys used for authentication
|
||||
md5.update(`${username}:mongo:${password}`, 'utf8');
|
||||
const hash_password = md5.digest('hex');
|
||||
|
||||
// Final key
|
||||
md5 = crypto.createHash('md5');
|
||||
md5.update(nonce + username + hash_password, 'utf8');
|
||||
key = md5.digest('hex');
|
||||
}
|
||||
|
||||
const authenticateCommand = {
|
||||
authenticate: 1,
|
||||
user: username,
|
||||
nonce,
|
||||
key
|
||||
};
|
||||
|
||||
connection.command(ns(`${source}.$cmd`), authenticateCommand, undefined, callback);
|
||||
});
|
||||
}
|
||||
}
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
import * as crypto from 'crypto';
|
||||
import * as http from 'http';
|
||||
import * as url from 'url';
|
||||
|
||||
import type { Binary, BSONSerializeOptions } from '../../bson';
|
||||
import * as BSON from '../../bson';
|
||||
import { aws4 } from '../../deps';
|
||||
import {
|
||||
MongoAWSError,
|
||||
MongoCompatibilityError,
|
||||
MongoMissingCredentialsError,
|
||||
MongoRuntimeError
|
||||
} from '../../error';
|
||||
import { Callback, maxWireVersion, ns } from '../../utils';
|
||||
import { AuthContext, AuthProvider } from './auth_provider';
|
||||
import { MongoCredentials } from './mongo_credentials';
|
||||
import { AuthMechanism } from './providers';
|
||||
|
||||
const ASCII_N = 110;
|
||||
const AWS_RELATIVE_URI = 'http://169.254.170.2';
|
||||
const AWS_EC2_URI = 'http://169.254.169.254';
|
||||
const AWS_EC2_PATH = '/latest/meta-data/iam/security-credentials';
|
||||
const bsonOptions: BSONSerializeOptions = {
|
||||
promoteLongs: true,
|
||||
promoteValues: true,
|
||||
promoteBuffers: false,
|
||||
bsonRegExp: false
|
||||
};
|
||||
|
||||
interface AWSSaslContinuePayload {
|
||||
a: string;
|
||||
d: string;
|
||||
t?: string;
|
||||
}
|
||||
|
||||
export class MongoDBAWS extends AuthProvider {
|
||||
override auth(authContext: AuthContext, callback: Callback): void {
|
||||
const { connection, credentials } = authContext;
|
||||
if (!credentials) {
|
||||
return callback(new MongoMissingCredentialsError('AuthContext must provide credentials.'));
|
||||
}
|
||||
|
||||
if ('kModuleError' in aws4) {
|
||||
return callback(aws4['kModuleError']);
|
||||
}
|
||||
const { sign } = aws4;
|
||||
|
||||
if (maxWireVersion(connection) < 9) {
|
||||
callback(
|
||||
new MongoCompatibilityError(
|
||||
'MONGODB-AWS authentication requires MongoDB version 4.4 or later'
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!credentials.username) {
|
||||
makeTempCredentials(credentials, (err, tempCredentials) => {
|
||||
if (err || !tempCredentials) return callback(err);
|
||||
|
||||
authContext.credentials = tempCredentials;
|
||||
this.auth(authContext, callback);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const accessKeyId = credentials.username;
|
||||
const secretAccessKey = credentials.password;
|
||||
const sessionToken = credentials.mechanismProperties.AWS_SESSION_TOKEN;
|
||||
|
||||
// If all three defined, include sessionToken, else include username and pass, else no credentials
|
||||
const awsCredentials =
|
||||
accessKeyId && secretAccessKey && sessionToken
|
||||
? { accessKeyId, secretAccessKey, sessionToken }
|
||||
: accessKeyId && secretAccessKey
|
||||
? { accessKeyId, secretAccessKey }
|
||||
: undefined;
|
||||
|
||||
const db = credentials.source;
|
||||
crypto.randomBytes(32, (err, nonce) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
const saslStart = {
|
||||
saslStart: 1,
|
||||
mechanism: 'MONGODB-AWS',
|
||||
payload: BSON.serialize({ r: nonce, p: ASCII_N }, bsonOptions)
|
||||
};
|
||||
|
||||
connection.command(ns(`${db}.$cmd`), saslStart, undefined, (err, res) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
const serverResponse = BSON.deserialize(res.payload.buffer, bsonOptions) as {
|
||||
s: Binary;
|
||||
h: string;
|
||||
};
|
||||
const host = serverResponse.h;
|
||||
const serverNonce = serverResponse.s.buffer;
|
||||
if (serverNonce.length !== 64) {
|
||||
callback(
|
||||
// TODO(NODE-3483)
|
||||
new MongoRuntimeError(`Invalid server nonce length ${serverNonce.length}, expected 64`)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (serverNonce.compare(nonce, 0, nonce.length, 0, nonce.length) !== 0) {
|
||||
// TODO(NODE-3483)
|
||||
callback(new MongoRuntimeError('Server nonce does not begin with client nonce'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (host.length < 1 || host.length > 255 || host.indexOf('..') !== -1) {
|
||||
// TODO(NODE-3483)
|
||||
callback(new MongoRuntimeError(`Server returned an invalid host: "${host}"`));
|
||||
return;
|
||||
}
|
||||
|
||||
const body = 'Action=GetCallerIdentity&Version=2011-06-15';
|
||||
const options = sign(
|
||||
{
|
||||
method: 'POST',
|
||||
host,
|
||||
region: deriveRegion(serverResponse.h),
|
||||
service: 'sts',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Content-Length': body.length,
|
||||
'X-MongoDB-Server-Nonce': serverNonce.toString('base64'),
|
||||
'X-MongoDB-GS2-CB-Flag': 'n'
|
||||
},
|
||||
path: '/',
|
||||
body
|
||||
},
|
||||
awsCredentials
|
||||
);
|
||||
|
||||
const payload: AWSSaslContinuePayload = {
|
||||
a: options.headers.Authorization,
|
||||
d: options.headers['X-Amz-Date']
|
||||
};
|
||||
if (sessionToken) {
|
||||
payload.t = sessionToken;
|
||||
}
|
||||
|
||||
const saslContinue = {
|
||||
saslContinue: 1,
|
||||
conversationId: 1,
|
||||
payload: BSON.serialize(payload, bsonOptions)
|
||||
};
|
||||
|
||||
connection.command(ns(`${db}.$cmd`), saslContinue, undefined, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface AWSTempCredentials {
|
||||
AccessKeyId?: string;
|
||||
SecretAccessKey?: string;
|
||||
Token?: string;
|
||||
RoleArn?: string;
|
||||
Expiration?: Date;
|
||||
}
|
||||
|
||||
function makeTempCredentials(credentials: MongoCredentials, callback: Callback<MongoCredentials>) {
|
||||
function done(creds: AWSTempCredentials) {
|
||||
if (!creds.AccessKeyId || !creds.SecretAccessKey || !creds.Token) {
|
||||
callback(
|
||||
new MongoMissingCredentialsError('Could not obtain temporary MONGODB-AWS credentials')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
callback(
|
||||
undefined,
|
||||
new MongoCredentials({
|
||||
username: creds.AccessKeyId,
|
||||
password: creds.SecretAccessKey,
|
||||
source: credentials.source,
|
||||
mechanism: AuthMechanism.MONGODB_AWS,
|
||||
mechanismProperties: {
|
||||
AWS_SESSION_TOKEN: creds.Token
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// If the environment variable AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
|
||||
// is set then drivers MUST assume that it was set by an AWS ECS agent
|
||||
if (process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI) {
|
||||
request(
|
||||
`${AWS_RELATIVE_URI}${process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI}`,
|
||||
undefined,
|
||||
(err, res) => {
|
||||
if (err) return callback(err);
|
||||
done(res);
|
||||
}
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise assume we are on an EC2 instance
|
||||
|
||||
// get a token
|
||||
request(
|
||||
`${AWS_EC2_URI}/latest/api/token`,
|
||||
{ method: 'PUT', json: false, headers: { 'X-aws-ec2-metadata-token-ttl-seconds': 30 } },
|
||||
(err, token) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
// get role name
|
||||
request(
|
||||
`${AWS_EC2_URI}/${AWS_EC2_PATH}`,
|
||||
{ json: false, headers: { 'X-aws-ec2-metadata-token': token } },
|
||||
(err, roleName) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
// get temp credentials
|
||||
request(
|
||||
`${AWS_EC2_URI}/${AWS_EC2_PATH}/${roleName}`,
|
||||
{ headers: { 'X-aws-ec2-metadata-token': token } },
|
||||
(err, creds) => {
|
||||
if (err) return callback(err);
|
||||
done(creds);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function deriveRegion(host: string) {
|
||||
const parts = host.split('.');
|
||||
if (parts.length === 1 || parts[1] === 'amazonaws') {
|
||||
return 'us-east-1';
|
||||
}
|
||||
|
||||
return parts[1];
|
||||
}
|
||||
|
||||
interface RequestOptions {
|
||||
json?: boolean;
|
||||
method?: string;
|
||||
timeout?: number;
|
||||
headers?: http.OutgoingHttpHeaders;
|
||||
}
|
||||
|
||||
function request(uri: string, _options: RequestOptions | undefined, callback: Callback) {
|
||||
const options = Object.assign(
|
||||
{
|
||||
method: 'GET',
|
||||
timeout: 10000,
|
||||
json: true
|
||||
},
|
||||
url.parse(uri),
|
||||
_options
|
||||
);
|
||||
|
||||
const req = http.request(options, res => {
|
||||
res.setEncoding('utf8');
|
||||
|
||||
let data = '';
|
||||
res.on('data', d => (data += d));
|
||||
res.on('end', () => {
|
||||
if (options.json === false) {
|
||||
callback(undefined, data);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
callback(undefined, parsed);
|
||||
} catch (err) {
|
||||
// TODO(NODE-3483)
|
||||
callback(new MongoRuntimeError(`Invalid JSON response: "${data}"`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy(new MongoAWSError(`AWS request to ${uri} timed out after ${options.timeout} ms`));
|
||||
});
|
||||
|
||||
req.on('error', err => callback(err));
|
||||
req.end();
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { Binary } from '../../bson';
|
||||
import { MongoMissingCredentialsError } from '../../error';
|
||||
import { Callback, ns } from '../../utils';
|
||||
import { AuthContext, AuthProvider } from './auth_provider';
|
||||
|
||||
export class Plain extends AuthProvider {
|
||||
override auth(authContext: AuthContext, callback: Callback): void {
|
||||
const { connection, credentials } = authContext;
|
||||
if (!credentials) {
|
||||
return callback(new MongoMissingCredentialsError('AuthContext must provide credentials.'));
|
||||
}
|
||||
const username = credentials.username;
|
||||
const password = credentials.password;
|
||||
|
||||
const payload = new Binary(Buffer.from(`\x00${username}\x00${password}`));
|
||||
const command = {
|
||||
saslStart: 1,
|
||||
mechanism: 'PLAIN',
|
||||
payload: payload,
|
||||
autoAuthorize: 1
|
||||
};
|
||||
|
||||
connection.command(ns('$external.$cmd'), command, undefined, callback);
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/** @public */
|
||||
export const AuthMechanism = Object.freeze({
|
||||
MONGODB_AWS: 'MONGODB-AWS',
|
||||
MONGODB_CR: 'MONGODB-CR',
|
||||
MONGODB_DEFAULT: 'DEFAULT',
|
||||
MONGODB_GSSAPI: 'GSSAPI',
|
||||
MONGODB_PLAIN: 'PLAIN',
|
||||
MONGODB_SCRAM_SHA1: 'SCRAM-SHA-1',
|
||||
MONGODB_SCRAM_SHA256: 'SCRAM-SHA-256',
|
||||
MONGODB_X509: 'MONGODB-X509'
|
||||
} as const);
|
||||
|
||||
/** @public */
|
||||
export type AuthMechanism = typeof AuthMechanism[keyof typeof AuthMechanism];
|
||||
|
||||
/** @internal */
|
||||
export const AUTH_MECHS_AUTH_SRC_EXTERNAL = new Set<AuthMechanism>([
|
||||
AuthMechanism.MONGODB_GSSAPI,
|
||||
AuthMechanism.MONGODB_AWS,
|
||||
AuthMechanism.MONGODB_X509
|
||||
]);
|
||||
+384
@@ -0,0 +1,384 @@
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
import { Binary, Document } from '../../bson';
|
||||
import { saslprep } from '../../deps';
|
||||
import {
|
||||
AnyError,
|
||||
MongoInvalidArgumentError,
|
||||
MongoMissingCredentialsError,
|
||||
MongoRuntimeError,
|
||||
MongoServerError
|
||||
} from '../../error';
|
||||
import { Callback, emitWarning, ns } from '../../utils';
|
||||
import type { HandshakeDocument } from '../connect';
|
||||
import { AuthContext, AuthProvider } from './auth_provider';
|
||||
import type { MongoCredentials } from './mongo_credentials';
|
||||
import { AuthMechanism } from './providers';
|
||||
|
||||
type CryptoMethod = 'sha1' | 'sha256';
|
||||
|
||||
class ScramSHA extends AuthProvider {
|
||||
cryptoMethod: CryptoMethod;
|
||||
constructor(cryptoMethod: CryptoMethod) {
|
||||
super();
|
||||
this.cryptoMethod = cryptoMethod || 'sha1';
|
||||
}
|
||||
|
||||
override prepare(handshakeDoc: HandshakeDocument, authContext: AuthContext, callback: Callback) {
|
||||
const cryptoMethod = this.cryptoMethod;
|
||||
const credentials = authContext.credentials;
|
||||
if (!credentials) {
|
||||
return callback(new MongoMissingCredentialsError('AuthContext must provide credentials.'));
|
||||
}
|
||||
if (cryptoMethod === 'sha256' && saslprep == null) {
|
||||
emitWarning('Warning: no saslprep library specified. Passwords will not be sanitized');
|
||||
}
|
||||
|
||||
crypto.randomBytes(24, (err, nonce) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// store the nonce for later use
|
||||
Object.assign(authContext, { nonce });
|
||||
|
||||
const request = Object.assign({}, handshakeDoc, {
|
||||
speculativeAuthenticate: Object.assign(makeFirstMessage(cryptoMethod, credentials, nonce), {
|
||||
db: credentials.source
|
||||
})
|
||||
});
|
||||
|
||||
callback(undefined, request);
|
||||
});
|
||||
}
|
||||
|
||||
override auth(authContext: AuthContext, callback: Callback) {
|
||||
const response = authContext.response;
|
||||
if (response && response.speculativeAuthenticate) {
|
||||
continueScramConversation(
|
||||
this.cryptoMethod,
|
||||
response.speculativeAuthenticate,
|
||||
authContext,
|
||||
callback
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
executeScram(this.cryptoMethod, authContext, callback);
|
||||
}
|
||||
}
|
||||
|
||||
function cleanUsername(username: string) {
|
||||
return username.replace('=', '=3D').replace(',', '=2C');
|
||||
}
|
||||
|
||||
function clientFirstMessageBare(username: string, nonce: Buffer) {
|
||||
// NOTE: This is done b/c Javascript uses UTF-16, but the server is hashing in UTF-8.
|
||||
// Since the username is not sasl-prep-d, we need to do this here.
|
||||
return Buffer.concat([
|
||||
Buffer.from('n=', 'utf8'),
|
||||
Buffer.from(username, 'utf8'),
|
||||
Buffer.from(',r=', 'utf8'),
|
||||
Buffer.from(nonce.toString('base64'), 'utf8')
|
||||
]);
|
||||
}
|
||||
|
||||
function makeFirstMessage(
|
||||
cryptoMethod: CryptoMethod,
|
||||
credentials: MongoCredentials,
|
||||
nonce: Buffer
|
||||
) {
|
||||
const username = cleanUsername(credentials.username);
|
||||
const mechanism =
|
||||
cryptoMethod === 'sha1' ? AuthMechanism.MONGODB_SCRAM_SHA1 : AuthMechanism.MONGODB_SCRAM_SHA256;
|
||||
|
||||
// NOTE: This is done b/c Javascript uses UTF-16, but the server is hashing in UTF-8.
|
||||
// Since the username is not sasl-prep-d, we need to do this here.
|
||||
return {
|
||||
saslStart: 1,
|
||||
mechanism,
|
||||
payload: new Binary(
|
||||
Buffer.concat([Buffer.from('n,,', 'utf8'), clientFirstMessageBare(username, nonce)])
|
||||
),
|
||||
autoAuthorize: 1,
|
||||
options: { skipEmptyExchange: true }
|
||||
};
|
||||
}
|
||||
|
||||
function executeScram(cryptoMethod: CryptoMethod, authContext: AuthContext, callback: Callback) {
|
||||
const { connection, credentials } = authContext;
|
||||
if (!credentials) {
|
||||
return callback(new MongoMissingCredentialsError('AuthContext must provide credentials.'));
|
||||
}
|
||||
if (!authContext.nonce) {
|
||||
return callback(
|
||||
new MongoInvalidArgumentError('AuthContext must contain a valid nonce property')
|
||||
);
|
||||
}
|
||||
const nonce = authContext.nonce;
|
||||
const db = credentials.source;
|
||||
|
||||
const saslStartCmd = makeFirstMessage(cryptoMethod, credentials, nonce);
|
||||
connection.command(ns(`${db}.$cmd`), saslStartCmd, undefined, (_err, result) => {
|
||||
const err = resolveError(_err, result);
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
continueScramConversation(cryptoMethod, result, authContext, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function continueScramConversation(
|
||||
cryptoMethod: CryptoMethod,
|
||||
response: Document,
|
||||
authContext: AuthContext,
|
||||
callback: Callback
|
||||
) {
|
||||
const connection = authContext.connection;
|
||||
const credentials = authContext.credentials;
|
||||
if (!credentials) {
|
||||
return callback(new MongoMissingCredentialsError('AuthContext must provide credentials.'));
|
||||
}
|
||||
if (!authContext.nonce) {
|
||||
return callback(new MongoInvalidArgumentError('Unable to continue SCRAM without valid nonce'));
|
||||
}
|
||||
const nonce = authContext.nonce;
|
||||
|
||||
const db = credentials.source;
|
||||
const username = cleanUsername(credentials.username);
|
||||
const password = credentials.password;
|
||||
|
||||
let processedPassword;
|
||||
if (cryptoMethod === 'sha256') {
|
||||
processedPassword = 'kModuleError' in saslprep ? password : saslprep(password);
|
||||
} else {
|
||||
try {
|
||||
processedPassword = passwordDigest(username, password);
|
||||
} catch (e) {
|
||||
return callback(e);
|
||||
}
|
||||
}
|
||||
|
||||
const payload = Buffer.isBuffer(response.payload)
|
||||
? new Binary(response.payload)
|
||||
: response.payload;
|
||||
const dict = parsePayload(payload.value());
|
||||
|
||||
const iterations = parseInt(dict.i, 10);
|
||||
if (iterations && iterations < 4096) {
|
||||
callback(
|
||||
// TODO(NODE-3483)
|
||||
new MongoRuntimeError(`Server returned an invalid iteration count ${iterations}`),
|
||||
false
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const salt = dict.s;
|
||||
const rnonce = dict.r;
|
||||
if (rnonce.startsWith('nonce')) {
|
||||
// TODO(NODE-3483)
|
||||
callback(new MongoRuntimeError(`Server returned an invalid nonce: ${rnonce}`), false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set up start of proof
|
||||
const withoutProof = `c=biws,r=${rnonce}`;
|
||||
const saltedPassword = HI(
|
||||
processedPassword,
|
||||
Buffer.from(salt, 'base64'),
|
||||
iterations,
|
||||
cryptoMethod
|
||||
);
|
||||
|
||||
const clientKey = HMAC(cryptoMethod, saltedPassword, 'Client Key');
|
||||
const serverKey = HMAC(cryptoMethod, saltedPassword, 'Server Key');
|
||||
const storedKey = H(cryptoMethod, clientKey);
|
||||
const authMessage = [clientFirstMessageBare(username, nonce), payload.value(), withoutProof].join(
|
||||
','
|
||||
);
|
||||
|
||||
const clientSignature = HMAC(cryptoMethod, storedKey, authMessage);
|
||||
const clientProof = `p=${xor(clientKey, clientSignature)}`;
|
||||
const clientFinal = [withoutProof, clientProof].join(',');
|
||||
|
||||
const serverSignature = HMAC(cryptoMethod, serverKey, authMessage);
|
||||
const saslContinueCmd = {
|
||||
saslContinue: 1,
|
||||
conversationId: response.conversationId,
|
||||
payload: new Binary(Buffer.from(clientFinal))
|
||||
};
|
||||
|
||||
connection.command(ns(`${db}.$cmd`), saslContinueCmd, undefined, (_err, r) => {
|
||||
const err = resolveError(_err, r);
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const parsedResponse = parsePayload(r.payload.value());
|
||||
if (!compareDigest(Buffer.from(parsedResponse.v, 'base64'), serverSignature)) {
|
||||
callback(new MongoRuntimeError('Server returned an invalid signature'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!r || r.done !== false) {
|
||||
return callback(err, r);
|
||||
}
|
||||
|
||||
const retrySaslContinueCmd = {
|
||||
saslContinue: 1,
|
||||
conversationId: r.conversationId,
|
||||
payload: Buffer.alloc(0)
|
||||
};
|
||||
|
||||
connection.command(ns(`${db}.$cmd`), retrySaslContinueCmd, undefined, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function parsePayload(payload: string) {
|
||||
const dict: Document = {};
|
||||
const parts = payload.split(',');
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const valueParts = parts[i].split('=');
|
||||
dict[valueParts[0]] = valueParts[1];
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
function passwordDigest(username: string, password: string) {
|
||||
if (typeof username !== 'string') {
|
||||
throw new MongoInvalidArgumentError('Username must be a string');
|
||||
}
|
||||
|
||||
if (typeof password !== 'string') {
|
||||
throw new MongoInvalidArgumentError('Password must be a string');
|
||||
}
|
||||
|
||||
if (password.length === 0) {
|
||||
throw new MongoInvalidArgumentError('Password cannot be empty');
|
||||
}
|
||||
|
||||
let md5: crypto.Hash;
|
||||
try {
|
||||
md5 = crypto.createHash('md5');
|
||||
} catch (err) {
|
||||
if (crypto.getFips()) {
|
||||
// This error is (slightly) more helpful than what comes from OpenSSL directly, e.g.
|
||||
// 'Error: error:060800C8:digital envelope routines:EVP_DigestInit_ex:disabled for FIPS'
|
||||
throw new Error('Auth mechanism SCRAM-SHA-1 is not supported in FIPS mode');
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
md5.update(`${username}:mongo:${password}`, 'utf8');
|
||||
return md5.digest('hex');
|
||||
}
|
||||
|
||||
// XOR two buffers
|
||||
function xor(a: Buffer, b: Buffer) {
|
||||
if (!Buffer.isBuffer(a)) {
|
||||
a = Buffer.from(a);
|
||||
}
|
||||
|
||||
if (!Buffer.isBuffer(b)) {
|
||||
b = Buffer.from(b);
|
||||
}
|
||||
|
||||
const length = Math.max(a.length, b.length);
|
||||
const res = [];
|
||||
|
||||
for (let i = 0; i < length; i += 1) {
|
||||
res.push(a[i] ^ b[i]);
|
||||
}
|
||||
|
||||
return Buffer.from(res).toString('base64');
|
||||
}
|
||||
|
||||
function H(method: CryptoMethod, text: Buffer) {
|
||||
return crypto.createHash(method).update(text).digest();
|
||||
}
|
||||
|
||||
function HMAC(method: CryptoMethod, key: Buffer, text: Buffer | string) {
|
||||
return crypto.createHmac(method, key).update(text).digest();
|
||||
}
|
||||
|
||||
interface HICache {
|
||||
[key: string]: Buffer;
|
||||
}
|
||||
|
||||
let _hiCache: HICache = {};
|
||||
let _hiCacheCount = 0;
|
||||
function _hiCachePurge() {
|
||||
_hiCache = {};
|
||||
_hiCacheCount = 0;
|
||||
}
|
||||
|
||||
const hiLengthMap = {
|
||||
sha256: 32,
|
||||
sha1: 20
|
||||
};
|
||||
|
||||
function HI(data: string, salt: Buffer, iterations: number, cryptoMethod: CryptoMethod) {
|
||||
// omit the work if already generated
|
||||
const key = [data, salt.toString('base64'), iterations].join('_');
|
||||
if (_hiCache[key] != null) {
|
||||
return _hiCache[key];
|
||||
}
|
||||
|
||||
// generate the salt
|
||||
const saltedData = crypto.pbkdf2Sync(
|
||||
data,
|
||||
salt,
|
||||
iterations,
|
||||
hiLengthMap[cryptoMethod],
|
||||
cryptoMethod
|
||||
);
|
||||
|
||||
// cache a copy to speed up the next lookup, but prevent unbounded cache growth
|
||||
if (_hiCacheCount >= 200) {
|
||||
_hiCachePurge();
|
||||
}
|
||||
|
||||
_hiCache[key] = saltedData;
|
||||
_hiCacheCount += 1;
|
||||
return saltedData;
|
||||
}
|
||||
|
||||
function compareDigest(lhs: Buffer, rhs: Uint8Array) {
|
||||
if (lhs.length !== rhs.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof crypto.timingSafeEqual === 'function') {
|
||||
return crypto.timingSafeEqual(lhs, rhs);
|
||||
}
|
||||
|
||||
let result = 0;
|
||||
for (let i = 0; i < lhs.length; i++) {
|
||||
result |= lhs[i] ^ rhs[i];
|
||||
}
|
||||
|
||||
return result === 0;
|
||||
}
|
||||
|
||||
function resolveError(err?: AnyError, result?: Document) {
|
||||
if (err) return err;
|
||||
if (result) {
|
||||
if (result.$err || result.errmsg) return new MongoServerError(result);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
export class ScramSHA1 extends ScramSHA {
|
||||
constructor() {
|
||||
super('sha1');
|
||||
}
|
||||
}
|
||||
|
||||
export class ScramSHA256 extends ScramSHA {
|
||||
constructor() {
|
||||
super('sha256');
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import type { Document } from '../../bson';
|
||||
import { MongoMissingCredentialsError } from '../../error';
|
||||
import { Callback, ns } from '../../utils';
|
||||
import type { HandshakeDocument } from '../connect';
|
||||
import { AuthContext, AuthProvider } from './auth_provider';
|
||||
import type { MongoCredentials } from './mongo_credentials';
|
||||
|
||||
export class X509 extends AuthProvider {
|
||||
override prepare(
|
||||
handshakeDoc: HandshakeDocument,
|
||||
authContext: AuthContext,
|
||||
callback: Callback
|
||||
): void {
|
||||
const { credentials } = authContext;
|
||||
if (!credentials) {
|
||||
return callback(new MongoMissingCredentialsError('AuthContext must provide credentials.'));
|
||||
}
|
||||
Object.assign(handshakeDoc, {
|
||||
speculativeAuthenticate: x509AuthenticateCommand(credentials)
|
||||
});
|
||||
|
||||
callback(undefined, handshakeDoc);
|
||||
}
|
||||
|
||||
override auth(authContext: AuthContext, callback: Callback): void {
|
||||
const connection = authContext.connection;
|
||||
const credentials = authContext.credentials;
|
||||
if (!credentials) {
|
||||
return callback(new MongoMissingCredentialsError('AuthContext must provide credentials.'));
|
||||
}
|
||||
const response = authContext.response;
|
||||
|
||||
if (response && response.speculativeAuthenticate) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
connection.command(
|
||||
ns('$external.$cmd'),
|
||||
x509AuthenticateCommand(credentials),
|
||||
undefined,
|
||||
callback
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function x509AuthenticateCommand(credentials: MongoCredentials) {
|
||||
const command: Document = { authenticate: 1, mechanism: 'MONGODB-X509' };
|
||||
if (credentials.username) {
|
||||
command.user = credentials.username;
|
||||
}
|
||||
|
||||
return command;
|
||||
}
|
||||
Reference in New Issue
Block a user