Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
40 changes: 36 additions & 4 deletions src/cmap/auth/gssapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ import {
import { Callback, ns } from '../../utils';
import { AuthContext, AuthProvider } from './auth_provider';

type CanonicalizationOptions = boolean | 'none' | 'forward' | 'forwardAndReverse';

type MechanismProperties = {
gssapiCanonicalizeHostName?: boolean;
CANONICALIZE_HOST_NAME?: CanonicalizationOptions;
SERVICE_HOST?: string;
SERVICE_NAME?: string;
SERVICE_REALM?: string;
};
Expand Down Expand Up @@ -70,6 +73,7 @@ export class GSSAPI extends AuthProvider {
});
}
}

function makeKerberosClient(authContext: AuthContext, callback: Callback<KerberosClient>): void {
const { hostAddress } = authContext.options;
const { credentials } = authContext;
Expand Down Expand Up @@ -100,7 +104,8 @@ function makeKerberosClient(authContext: AuthContext, callback: Callback<Kerbero
Object.assign(initOptions, { user: username, password: password });
}

let spn = `${serviceName}${process.platform === 'win32' ? '/' : '@'}${host}`;
const spnHost = mechanismProperties.SERVICE_HOST ?? host;
let spn = `${serviceName}${process.platform === 'win32' ? '/' : '@'}${spnHost}`;
if ('SERVICE_REALM' in mechanismProperties) {
spn = `${spn}@${mechanismProperties.SERVICE_REALM}`;
}
Expand Down Expand Up @@ -174,14 +179,41 @@ function performGssapiCanonicalizeHostName(
mechanismProperties: MechanismProperties,
callback: Callback<string>
): void {
if (!mechanismProperties.gssapiCanonicalizeHostName) return callback(undefined, host);
const mode = mechanismProperties.CANONICALIZE_HOST_NAME;
if (!mode || mode === 'none') return callback(undefined, host);

// If forward and reverse or true
if (mode === true || mode === '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);
});
});
}
// The case for forward is just to resolve the cname as dns.lookup()
// will not return it.
resolveCname(host, callback);
}

function resolveCname(host: string, callback: Callback<string>): void {
// Attempt to resolve the host name
dns.resolveCname(host, (err, r) => {
if (err) return callback(err);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this should also be

Suggested change
if (err) return callback(err);
if (err) return callback(undefined, host);

because Node.js gives you ENODATA errors for domains which exist but have no CNAME records attached


// Get the first resolve host id
if (Array.isArray(r) && r.length > 0) {
if (r.length > 0) {
return callback(undefined, r[0]);
}

Expand Down
13 changes: 12 additions & 1 deletion src/cmap/auth/mongo_credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,17 @@ function getDefaultAuthMechanism(hello?: Document): AuthMechanism {
return AuthMechanism.MONGODB_CR;
}

const CANONICALIZATION_VALUES = [true, false, 'none', 'forward', 'forwardAndReverse'];

/** @public */
export type CanonicalizationProperties = boolean | 'none' | 'forward' | 'forwardAndReverse';

/** @public */
export interface AuthMechanismProperties extends Document {
SERVICE_HOST?: string;
SERVICE_NAME?: string;
SERVICE_REALM?: string;
CANONICALIZE_HOST_NAME?: boolean;
CANONICALIZE_HOST_NAME?: CanonicalizationProperties;
AWS_SESSION_TOKEN?: string;
}

Expand Down Expand Up @@ -158,6 +164,11 @@ export class MongoCredentials {
// 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 (!CANONICALIZATION_VALUES.includes(canonicalization)) {
throw new MongoAPIError(`Invalid CANONICALIZE_HOST_NAME value: ${canonicalization}`);
}
}

static merge(
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ export type {
} from './change_stream';
export type {
AuthMechanismProperties,
CanonicalizationProperties,
MongoCredentials,
MongoCredentialsOptions
} from './cmap/auth/mongo_credentials';
Expand Down
42 changes: 41 additions & 1 deletion test/manual/kerberos.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ describe('Kerberos', function () {
return;
}
let krb5Uri = process.env.MONGODB_URI;
const parts = krb5Uri.split('@', 2);

if (!process.env.KRB5_PRINCIPAL) {
console.error('skipping Kerberos tests, KRB5_PRINCIPAL environment variable is not defined');
Expand All @@ -39,7 +40,6 @@ describe('Kerberos', function () {
if (process.env.LDAPTEST_PASSWORD == null) {
throw new Error('The env parameter LDAPTEST_PASSWORD must be set');
}
const parts = krb5Uri.split('@', 2);
krb5Uri = `${parts[0]}:${process.env.LDAPTEST_PASSWORD}@${parts[1]}`;
}

Expand All @@ -51,6 +51,15 @@ describe('Kerberos', function () {
});
});

it('validate that CANONICALIZE_HOST_NAME can be passed in', async function () {
const client = new MongoClient(
`${krb5Uri}&authMechanismProperties=SERVICE_NAME:mongodb,CANONICALIZE_HOST_NAME:true&maxPoolSize=1`
);
await client.connect();
verifyKerberosAuthentication(client, done);
});
});

// Unskip this test when a proper setup is available - see NODE-3060
it.skip('validate that SERVICE_REALM and CANONICALIZE_HOST_NAME can be passed in', function (done) {
const client = new MongoClient(
Expand All @@ -62,6 +71,37 @@ describe('Kerberos', function () {
});
});

context('when passsing SERVICE_HOST', function () {
context('when the SERVICE_HOST is invalid', function () {
const client = new MongoClient(`${krb5Uri}&maxPoolSize=1`, {
authMechanismProperties: {
SERVICE_HOST: 'example.com'
}
});

it('fails to authenticate', function () {
return client.connect().catch(e => {
expect(e).to.exist;
});
});
});

context('when the SERVICE_HOST is valid', function () {
const client = new MongoClient(`${krb5Uri}&maxPoolSize=1`, {
authMechanismProperties: {
SERVICE_HOST: 'ldaptest.10gen.cc'
}
});

it('authenticates', function (done) {
client.connect(function (err, client) {
expect(err).to.not.exist;
verifyKerberosAuthentication(client, done);
});
});
});
});

describe('should use the SERVICE_NAME property', function () {
it('as an option handed to the MongoClient', function (done) {
const client = new MongoClient(`${krb5Uri}&maxPoolSize=1`, {
Expand Down
121 changes: 117 additions & 4 deletions test/spec/auth/connection-string.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
},
{
"description": "should accept generic mechanism property (GSSAPI)",
"uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:true",
"uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:forward",
"valid": true,
"credential": {
"username": "user@DOMAIN.COM",
Expand All @@ -89,10 +89,45 @@
"mechanism": "GSSAPI",
"mechanism_properties": {
"SERVICE_NAME": "other",
"CANONICALIZE_HOST_NAME": true
"CANONICALIZE_HOST_NAME": "forward"
}
}
},
{
"description": "should accept forwardAndReverse hostname canonicalization (GSSAPI)",
"uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:forwardAndReverse",
"valid": true,
"credential": {
"username": "user@DOMAIN.COM",
"password": null,
"source": "$external",
"mechanism": "GSSAPI",
"mechanism_properties": {
"SERVICE_NAME": "other",
"CANONICALIZE_HOST_NAME": "forwardAndReverse"
}
}
},
{
"description": "should accept no hostname canonicalization (GSSAPI)",
"uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:none",
"valid": true,
"credential": {
"username": "user@DOMAIN.COM",
"password": null,
"source": "$external",
"mechanism": "GSSAPI",
"mechanism_properties": {
"SERVICE_NAME": "other",
"CANONICALIZE_HOST_NAME": "none"
}
}
},
{
"description": "must raise an error when the hostname canonicalization is invalid",
"uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:invalid",
"valid": false
},
{
"description": "should accept the password (GSSAPI)",
"uri": "mongodb://user%40DOMAIN.COM:password@localhost/?authMechanism=GSSAPI&authSource=$external",
Expand All @@ -107,6 +142,16 @@
}
}
},
{
"description": "must raise an error when the authSource is empty",
"uri": "mongodb://user:password@localhost/foo?authSource=",
"valid": false
},
{
"description": "must raise an error when the authSource is empty without credentials",
"uri": "mongodb://localhost/admin?authSource=",
"valid": false
},
{
"description": "should throw an exception if authSource is invalid (GSSAPI)",
"uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authSource=foo",
Expand Down Expand Up @@ -206,6 +251,18 @@
"mechanism_properties": null
}
},
{
"description": "should recognize the mechanism with no username when auth source is explicitly specified (MONGODB-X509)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-X509&authSource=$external",
"valid": true,
"credential": {
"username": null,
"password": null,
"source": "$external",
"mechanism": "MONGODB-X509",
"mechanism_properties": null
}
},
{
"description": "should throw an exception if supplied a password (MONGODB-X509)",
"uri": "mongodb://user:password@localhost/?authMechanism=MONGODB-X509",
Expand Down Expand Up @@ -352,7 +409,7 @@
"credential": null
},
{
"description": "authSource without username doesn't create credential",
"description": "authSource without username doesn't create credential (default mechanism)",
"uri": "mongodb://localhost/?authSource=foo",
"valid": true,
"credential": null
Expand All @@ -366,6 +423,62 @@
"description": "should throw an exception if no username/password provided (userinfo implies default mechanism)",
"uri": "mongodb://:@localhost.com/",
"valid": false
},
{
"description": "should recognise the mechanism (MONGODB-AWS)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-AWS",
"valid": true,
"credential": {
"username": null,
"password": null,
"source": "$external",
"mechanism": "MONGODB-AWS",
"mechanism_properties": null
}
},
{
"description": "should recognise the mechanism when auth source is explicitly specified (MONGODB-AWS)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-AWS&authSource=$external",
"valid": true,
"credential": {
"username": null,
"password": null,
"source": "$external",
"mechanism": "MONGODB-AWS",
"mechanism_properties": null
}
},
{
"description": "should throw an exception if username and no password (MONGODB-AWS)",
"uri": "mongodb://user@localhost/?authMechanism=MONGODB-AWS",
"valid": false,
"credential": null
},
{
"description": "should use username and password if specified (MONGODB-AWS)",
"uri": "mongodb://user%21%40%23%24%25%5E%26%2A%28%29_%2B:pass%21%40%23%24%25%5E%26%2A%28%29_%2B@localhost/?authMechanism=MONGODB-AWS",
"valid": true,
"credential": {
"username": "user!@#$%^&*()_+",
"password": "pass!@#$%^&*()_+",
"source": "$external",
"mechanism": "MONGODB-AWS",
"mechanism_properties": null
}
},
{
"description": "should use username, password and session token if specified (MONGODB-AWS)",
"uri": "mongodb://user:password@localhost/?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN:token%21%40%23%24%25%5E%26%2A%28%29_%2B",
"valid": true,
"credential": {
"username": "user",
"password": "password",
"source": "$external",
"mechanism": "MONGODB-AWS",
"mechanism_properties": {
"AWS_SESSION_TOKEN": "token!@#$%^&*()_+"
}
}
}
]
}
}
Loading