Skip to content
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

feat(NODE-5034): support OIDC auth options #3557

Merged
merged 7 commits into from
Feb 8, 2023
Merged
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
59 changes: 57 additions & 2 deletions src/cmap/auth/mongo_credentials.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
// Resolves the default auth mechanism according to
import type { Document } from '../../bson';
import { MongoAPIError, MongoMissingCredentialsError } from '../../error';
import {
MongoAPIError,
MongoInvalidArgumentError,
MongoMissingCredentialsError
} from '../../error';
import { GSSAPICanonicalizationValue } from './gssapi';
import type { OIDCRefreshFunction, OIDCRequestFunction } from './mongodb_oidc';
import { AUTH_MECHS_AUTH_SRC_EXTERNAL, AuthMechanism } from './providers';

// https://github.com/mongodb/specifications/blob/master/source/auth/auth.rst
Expand All @@ -25,13 +30,25 @@ function getDefaultAuthMechanism(hello?: Document): AuthMechanism {
return AuthMechanism.MONGODB_CR;
}

/** @public */
/**
* TODO: NODE-5035: Make OIDC properties public.
*
* @public
* */
export interface AuthMechanismProperties extends Document {
SERVICE_HOST?: string;
SERVICE_NAME?: string;
SERVICE_REALM?: string;
CANONICALIZE_HOST_NAME?: GSSAPICanonicalizationValue;
AWS_SESSION_TOKEN?: string;
/** @internal Name for the OIDC device workflow */
DEVICE_NAME?: 'aws' | 'azure' | 'gcp';
/** @internal Similar to a username, is require by OIDC when more than one IDP is configured. */
PRINCIPAL_NAME?: string;
/** @internal User provided callback to get OIDC auth credentials */
REQUEST_TOKEN_CALLBACK?: OIDCRequestFunction;
/** @internal User provided callback to refresh OIDC auth credentials */
REFRESH_TOKEN_CALLBACK?: OIDCRefreshFunction;
}

/** @public */
Expand Down Expand Up @@ -137,6 +154,44 @@ export class MongoCredentials {
throw new MongoMissingCredentialsError(`Username required for mechanism '${this.mechanism}'`);
}

if (this.mechanism === AuthMechanism.MONGODB_OIDC) {
nbbeeken marked this conversation as resolved.
Show resolved Hide resolved
if (this.username) {
throw new MongoInvalidArgumentError(
`Username not permitted for mechanism '${this.mechanism}'. Use PRINCIPAL_NAME instead.`
);
}

if (this.mechanismProperties.PRINCIPAL_NAME && this.mechanismProperties.DEVICE_NAME) {
throw new MongoInvalidArgumentError(
`PRINCIPAL_NAME and DEVICE_NAME may not be used together for mechanism '${this.mechanism}'.`
);
}

if (this.mechanismProperties.DEVICE_NAME && this.mechanismProperties.DEVICE_NAME !== 'aws') {
throw new MongoInvalidArgumentError(
`Currently only a DEVICE_NAME of 'aws' is supported for mechanism '${this.mechanism}'.`
);
}

if (
this.mechanismProperties.REFRESH_TOKEN_CALLBACK &&
!this.mechanismProperties.REQUEST_TOKEN_CALLBACK
) {
throw new MongoInvalidArgumentError(
`A REQUEST_TOKEN_CALLBACK must be provided when using a REFRESH_TOKEN_CALLBACK for mechanism '${this.mechanism}'`
);
}

if (
!this.mechanismProperties.DEVICE_NAME &&
!this.mechanismProperties.REQUEST_TOKEN_CALLBACK
) {
throw new MongoInvalidArgumentError(
`Either a DEVICE_NAME or a REQUEST_TOKEN_CALLBACK must be specified 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
Expand Down
39 changes: 39 additions & 0 deletions src/cmap/auth/mongodb_oidc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* TODO: NODE-5035: Make API public
*
* @internal */
export interface OIDCMechanismServerStep1 {
authorizeEndpoint?: string;
tokenEndpoint?: string;
deviceAuthorizeEndpoint?: string;
clientId: string;
clientSecret?: string;
requestScopes?: string[];
}

/**
* TODO: NODE-5035: Make API public
*
* @internal */
export interface OIDCRequestTokenResult {
accessToken: string;
expiresInSeconds?: number;
refreshToken?: string;
}

/**
* TODO: NODE-5035: Make API public
*
* @internal */
export type OIDCRequestFunction = (
idl: OIDCMechanismServerStep1
) => Promise<OIDCRequestTokenResult>;

/**
* TODO: NODE-5035: Make API public
*
* @internal */
export type OIDCRefreshFunction = (
idl: OIDCMechanismServerStep1,
result: OIDCRequestTokenResult
) => Promise<OIDCRequestTokenResult>;
5 changes: 4 additions & 1 deletion src/cmap/auth/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ export const AuthMechanism = Object.freeze({
MONGODB_PLAIN: 'PLAIN',
MONGODB_SCRAM_SHA1: 'SCRAM-SHA-1',
MONGODB_SCRAM_SHA256: 'SCRAM-SHA-256',
MONGODB_X509: 'MONGODB-X509'
MONGODB_X509: 'MONGODB-X509',
/** @internal TODO: NODE-5035: Make mechanism public. */
MONGODB_OIDC: 'MONGODB-OIDC'
} as const);

/** @public */
Expand All @@ -17,5 +19,6 @@ export type AuthMechanism = typeof AuthMechanism[keyof typeof AuthMechanism];
export const AUTH_MECHS_AUTH_SRC_EXTERNAL = new Set<AuthMechanism>([
AuthMechanism.MONGODB_GSSAPI,
AuthMechanism.MONGODB_AWS,
AuthMechanism.MONGODB_OIDC,
AuthMechanism.MONGODB_X509
]);
46 changes: 28 additions & 18 deletions src/connection_string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@ export function parseOptions(
const isGssapi = mongoOptions.credentials.mechanism === AuthMechanism.MONGODB_GSSAPI;
const isX509 = mongoOptions.credentials.mechanism === AuthMechanism.MONGODB_X509;
const isAws = mongoOptions.credentials.mechanism === AuthMechanism.MONGODB_AWS;
const isOidc = mongoOptions.credentials.mechanism === AuthMechanism.MONGODB_OIDC;
if (
(isGssapi || isX509) &&
allOptions.has('authSource') &&
Expand All @@ -397,7 +398,11 @@ export function parseOptions(
);
}

if (!(isGssapi || isX509 || isAws) && mongoOptions.dbName && !allOptions.has('authSource')) {
if (
!(isGssapi || isX509 || isAws || isOidc) &&
mongoOptions.dbName &&
!allOptions.has('authSource')
) {
// inherit the dbName unless GSSAPI or X509, then silently ignore dbName
// and there was no specific authSource given
mongoOptions.credentials = MongoCredentials.merge(mongoOptions.credentials, {
Expand Down Expand Up @@ -678,26 +683,31 @@ export const OPTIONS = {
},
authMechanismProperties: {
target: 'credentials',
transform({ options, values: [optionValue] }): MongoCredentials {
if (typeof optionValue === 'string') {
const mechanismProperties = Object.create(null);

for (const [key, value] of entriesFromString(optionValue)) {
try {
mechanismProperties[key] = getBoolean(key, value);
} catch {
mechanismProperties[key] = value;
transform({ options, values }): MongoCredentials {
// We can have a combination of options passed in the URI and options passed
// as an object to the MongoClient. So we must transform the string options
// as well as merge them together with a potentially provided object.
let mechanismProperties = Object.create(null);

for (const optionValue of values) {
if (typeof optionValue === 'string') {
for (const [key, value] of entriesFromString(optionValue)) {
try {
mechanismProperties[key] = getBoolean(key, value);
} catch {
mechanismProperties[key] = value;
}
}
} else {
if (!isRecord(optionValue)) {
throw new MongoParseError('AuthMechanismProperties must be an object');
}
mechanismProperties = { ...optionValue };
}

return MongoCredentials.merge(options.credentials, {
mechanismProperties
});
}
if (!isRecord(optionValue)) {
throw new MongoParseError('AuthMechanismProperties must be an object');
}
return MongoCredentials.merge(options.credentials, { mechanismProperties: optionValue });
return MongoCredentials.merge(options.credentials, {
mechanismProperties
});
}
},
authSource: {
Expand Down
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,12 @@ export type {
MongoCredentials,
MongoCredentialsOptions
} from './cmap/auth/mongo_credentials';
export type {
OIDCMechanismServerStep1,
OIDCRefreshFunction,
OIDCRequestFunction,
OIDCRequestTokenResult
} from './cmap/auth/mongodb_oidc';
export type {
BinMsg,
MessageHeader,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,156 @@
"AWS_SESSION_TOKEN": "token!@#$%^&*()_+"
}
}
},
{
"description": "should recognise the mechanism and request callback (MONGODB-OIDC)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC",
"callback": [
"oidcRequest"
],
"valid": true,
"credential": {
"username": null,
"password": null,
"source": "$external",
"mechanism": "MONGODB-OIDC",
"mechanism_properties": {
"REQUEST_TOKEN_CALLBACK": true
}
}
},
{
"description": "should recognise the mechanism when auth source is explicitly specified and with request callback (MONGODB-OIDC)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authSource=$external",
"callback": [
"oidcRequest"
],
"valid": true,
"credential": {
"username": null,
"password": null,
"source": "$external",
"mechanism": "MONGODB-OIDC",
"mechanism_properties": {
"REQUEST_TOKEN_CALLBACK": true
}
}
},
{
"description": "should recognise the mechanism with request and refresh callback (MONGODB-OIDC)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authSource=$external",
"callback": [
"oidcRequest",
"oidcRefresh"
],
"valid": true,
"credential": {
"username": null,
"password": null,
"source": "$external",
"mechanism": "MONGODB-OIDC",
"mechanism_properties": {
"REQUEST_TOKEN_CALLBACK": true,
"REFRESH_TOKEN_CALLBACK": true
}
}
},
{
"description": "should recognise the mechanism and principalName with request callback (MONGODB-OIDC)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PRINCIPAL_NAME:principalName",
"callback": [
"oidcRequest"
],
"valid": true,
"credential": {
"username": null,
"password": null,
"source": "$external",
"mechanism": "MONGODB-OIDC",
"mechanism_properties": {
"REQUEST_TOKEN_CALLBACK": true,
"PRINCIPAL_NAME": "principalName"
}
}
},
{
"description": "should recognise the mechanism with aws device (MONGODB-OIDC)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=DEVICE_NAME:aws",
"valid": true,
"credential": {
"username": null,
"password": null,
"source": "$external",
"mechanism": "MONGODB-OIDC",
"mechanism_properties": {
"DEVICE_NAME": "aws"
}
}
},
{
"description": "should recognise the mechanism when auth source is explicitly specified and with aws device (MONGODB-OIDC)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authSource=$external&authMechanismProperties=DEVICE_NAME:aws",
"valid": true,
"credential": {
"username": null,
"password": null,
"source": "$external",
"mechanism": "MONGODB-OIDC",
"mechanism_properties": {
"DEVICE_NAME": "aws"
}
}
},
{
"description": "should throw an exception if username is specified (MONGODB-OIDC)",
"uri": "mongodb://user@localhost/?authMechanism=MONGODB-OIDC",
"callback": [
"oidcRequest"
],
"valid": false,
"credential": null
},
{
"description": "should throw an exception if username and password are specified (MONGODB-OIDC)",
"uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC",
"callback": [
"oidcRequest"
],
"valid": false,
"credential": null
},
{
"description": "should throw an exception if principalName and deviceName are specified (MONGODB-OIDC)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PRINCIPAL_NAME:principalName,DEVICE_NAME:aws",
"valid": false,
"credential": null
},
{
"description": "should throw an exception if specified deviceName is not supported (MONGODB-OIDC)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=DEVICE_NAME:unexisted",
"valid": false,
"credential": null
},
{
"description": "should throw an exception if neither deviceName nor callbacks specified (MONGODB-OIDC)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC",
"valid": false,
"credential": null
},
{
"description": "should throw an exception when only refresh callback is specified (MONGODB-OIDC)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC",
"callback": [
"oidcRefresh"
],
"valid": false,
"credential": null
},
{
"description": "should throw an exception when unsupported auth property is specified (MONGODB-OIDC)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=UnsupportedProperty:unexisted",
"valid": false,
"credential": null
}
]
}
}
Loading