Skip to content

Commit 20a4fec

Browse files
authored
feat(NODE-5034): support OIDC auth options (#3557)
1 parent e8a30b1 commit 20a4fec

File tree

10 files changed

+456
-31
lines changed

10 files changed

+456
-31
lines changed

src/cmap/auth/mongo_credentials.ts

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
// Resolves the default auth mechanism according to
22
import type { Document } from '../../bson';
3-
import { MongoAPIError, MongoMissingCredentialsError } from '../../error';
3+
import {
4+
MongoAPIError,
5+
MongoInvalidArgumentError,
6+
MongoMissingCredentialsError
7+
} from '../../error';
48
import { GSSAPICanonicalizationValue } from './gssapi';
9+
import type { OIDCRefreshFunction, OIDCRequestFunction } from './mongodb_oidc';
510
import { AUTH_MECHS_AUTH_SRC_EXTERNAL, AuthMechanism } from './providers';
611

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

28-
/** @public */
33+
/**
34+
* TODO: NODE-5035: Make OIDC properties public.
35+
*
36+
* @public
37+
* */
2938
export interface AuthMechanismProperties extends Document {
3039
SERVICE_HOST?: string;
3140
SERVICE_NAME?: string;
3241
SERVICE_REALM?: string;
3342
CANONICALIZE_HOST_NAME?: GSSAPICanonicalizationValue;
3443
AWS_SESSION_TOKEN?: string;
44+
/** @internal Name for the OIDC device workflow */
45+
DEVICE_NAME?: 'aws' | 'azure' | 'gcp';
46+
/** @internal Similar to a username, is require by OIDC when more than one IDP is configured. */
47+
PRINCIPAL_NAME?: string;
48+
/** @internal User provided callback to get OIDC auth credentials */
49+
REQUEST_TOKEN_CALLBACK?: OIDCRequestFunction;
50+
/** @internal User provided callback to refresh OIDC auth credentials */
51+
REFRESH_TOKEN_CALLBACK?: OIDCRefreshFunction;
3552
}
3653

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

157+
if (this.mechanism === AuthMechanism.MONGODB_OIDC) {
158+
if (this.username) {
159+
throw new MongoInvalidArgumentError(
160+
`Username not permitted for mechanism '${this.mechanism}'. Use PRINCIPAL_NAME instead.`
161+
);
162+
}
163+
164+
if (this.mechanismProperties.PRINCIPAL_NAME && this.mechanismProperties.DEVICE_NAME) {
165+
throw new MongoInvalidArgumentError(
166+
`PRINCIPAL_NAME and DEVICE_NAME may not be used together for mechanism '${this.mechanism}'.`
167+
);
168+
}
169+
170+
if (this.mechanismProperties.DEVICE_NAME && this.mechanismProperties.DEVICE_NAME !== 'aws') {
171+
throw new MongoInvalidArgumentError(
172+
`Currently only a DEVICE_NAME of 'aws' is supported for mechanism '${this.mechanism}'.`
173+
);
174+
}
175+
176+
if (
177+
this.mechanismProperties.REFRESH_TOKEN_CALLBACK &&
178+
!this.mechanismProperties.REQUEST_TOKEN_CALLBACK
179+
) {
180+
throw new MongoInvalidArgumentError(
181+
`A REQUEST_TOKEN_CALLBACK must be provided when using a REFRESH_TOKEN_CALLBACK for mechanism '${this.mechanism}'`
182+
);
183+
}
184+
185+
if (
186+
!this.mechanismProperties.DEVICE_NAME &&
187+
!this.mechanismProperties.REQUEST_TOKEN_CALLBACK
188+
) {
189+
throw new MongoInvalidArgumentError(
190+
`Either a DEVICE_NAME or a REQUEST_TOKEN_CALLBACK must be specified for mechanism '${this.mechanism}'.`
191+
);
192+
}
193+
}
194+
140195
if (AUTH_MECHS_AUTH_SRC_EXTERNAL.has(this.mechanism)) {
141196
if (this.source != null && this.source !== '$external') {
142197
// TODO(NODE-3485): Replace this with a MongoAuthValidationError

src/cmap/auth/mongodb_oidc.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* TODO: NODE-5035: Make API public
3+
*
4+
* @internal */
5+
export interface OIDCMechanismServerStep1 {
6+
authorizeEndpoint?: string;
7+
tokenEndpoint?: string;
8+
deviceAuthorizeEndpoint?: string;
9+
clientId: string;
10+
clientSecret?: string;
11+
requestScopes?: string[];
12+
}
13+
14+
/**
15+
* TODO: NODE-5035: Make API public
16+
*
17+
* @internal */
18+
export interface OIDCRequestTokenResult {
19+
accessToken: string;
20+
expiresInSeconds?: number;
21+
refreshToken?: string;
22+
}
23+
24+
/**
25+
* TODO: NODE-5035: Make API public
26+
*
27+
* @internal */
28+
export type OIDCRequestFunction = (
29+
idl: OIDCMechanismServerStep1
30+
) => Promise<OIDCRequestTokenResult>;
31+
32+
/**
33+
* TODO: NODE-5035: Make API public
34+
*
35+
* @internal */
36+
export type OIDCRefreshFunction = (
37+
idl: OIDCMechanismServerStep1,
38+
result: OIDCRequestTokenResult
39+
) => Promise<OIDCRequestTokenResult>;

src/cmap/auth/providers.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ export const AuthMechanism = Object.freeze({
77
MONGODB_PLAIN: 'PLAIN',
88
MONGODB_SCRAM_SHA1: 'SCRAM-SHA-1',
99
MONGODB_SCRAM_SHA256: 'SCRAM-SHA-256',
10-
MONGODB_X509: 'MONGODB-X509'
10+
MONGODB_X509: 'MONGODB-X509',
11+
/** @internal TODO: NODE-5035: Make mechanism public. */
12+
MONGODB_OIDC: 'MONGODB-OIDC'
1113
} as const);
1214

1315
/** @public */
@@ -17,5 +19,6 @@ export type AuthMechanism = typeof AuthMechanism[keyof typeof AuthMechanism];
1719
export const AUTH_MECHS_AUTH_SRC_EXTERNAL = new Set<AuthMechanism>([
1820
AuthMechanism.MONGODB_GSSAPI,
1921
AuthMechanism.MONGODB_AWS,
22+
AuthMechanism.MONGODB_OIDC,
2023
AuthMechanism.MONGODB_X509
2124
]);

src/connection_string.ts

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,7 @@ export function parseOptions(
386386
const isGssapi = mongoOptions.credentials.mechanism === AuthMechanism.MONGODB_GSSAPI;
387387
const isX509 = mongoOptions.credentials.mechanism === AuthMechanism.MONGODB_X509;
388388
const isAws = mongoOptions.credentials.mechanism === AuthMechanism.MONGODB_AWS;
389+
const isOidc = mongoOptions.credentials.mechanism === AuthMechanism.MONGODB_OIDC;
389390
if (
390391
(isGssapi || isX509) &&
391392
allOptions.has('authSource') &&
@@ -397,7 +398,11 @@ export function parseOptions(
397398
);
398399
}
399400

400-
if (!(isGssapi || isX509 || isAws) && mongoOptions.dbName && !allOptions.has('authSource')) {
401+
if (
402+
!(isGssapi || isX509 || isAws || isOidc) &&
403+
mongoOptions.dbName &&
404+
!allOptions.has('authSource')
405+
) {
401406
// inherit the dbName unless GSSAPI or X509, then silently ignore dbName
402407
// and there was no specific authSource given
403408
mongoOptions.credentials = MongoCredentials.merge(mongoOptions.credentials, {
@@ -678,26 +683,31 @@ export const OPTIONS = {
678683
},
679684
authMechanismProperties: {
680685
target: 'credentials',
681-
transform({ options, values: [optionValue] }): MongoCredentials {
682-
if (typeof optionValue === 'string') {
683-
const mechanismProperties = Object.create(null);
684-
685-
for (const [key, value] of entriesFromString(optionValue)) {
686-
try {
687-
mechanismProperties[key] = getBoolean(key, value);
688-
} catch {
689-
mechanismProperties[key] = value;
686+
transform({ options, values }): MongoCredentials {
687+
// We can have a combination of options passed in the URI and options passed
688+
// as an object to the MongoClient. So we must transform the string options
689+
// as well as merge them together with a potentially provided object.
690+
let mechanismProperties = Object.create(null);
691+
692+
for (const optionValue of values) {
693+
if (typeof optionValue === 'string') {
694+
for (const [key, value] of entriesFromString(optionValue)) {
695+
try {
696+
mechanismProperties[key] = getBoolean(key, value);
697+
} catch {
698+
mechanismProperties[key] = value;
699+
}
690700
}
701+
} else {
702+
if (!isRecord(optionValue)) {
703+
throw new MongoParseError('AuthMechanismProperties must be an object');
704+
}
705+
mechanismProperties = { ...optionValue };
691706
}
692-
693-
return MongoCredentials.merge(options.credentials, {
694-
mechanismProperties
695-
});
696-
}
697-
if (!isRecord(optionValue)) {
698-
throw new MongoParseError('AuthMechanismProperties must be an object');
699707
}
700-
return MongoCredentials.merge(options.credentials, { mechanismProperties: optionValue });
708+
return MongoCredentials.merge(options.credentials, {
709+
mechanismProperties
710+
});
701711
}
702712
},
703713
authSource: {

src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,12 @@ export type {
202202
MongoCredentials,
203203
MongoCredentialsOptions
204204
} from './cmap/auth/mongo_credentials';
205+
export type {
206+
OIDCMechanismServerStep1,
207+
OIDCRefreshFunction,
208+
OIDCRequestFunction,
209+
OIDCRequestTokenResult
210+
} from './cmap/auth/mongodb_oidc';
205211
export type {
206212
BinMsg,
207213
MessageHeader,

test/spec/auth/connection-string.json renamed to test/spec/auth/legacy/connection-string.json

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,156 @@
480480
"AWS_SESSION_TOKEN": "token!@#$%^&*()_+"
481481
}
482482
}
483+
},
484+
{
485+
"description": "should recognise the mechanism and request callback (MONGODB-OIDC)",
486+
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC",
487+
"callback": [
488+
"oidcRequest"
489+
],
490+
"valid": true,
491+
"credential": {
492+
"username": null,
493+
"password": null,
494+
"source": "$external",
495+
"mechanism": "MONGODB-OIDC",
496+
"mechanism_properties": {
497+
"REQUEST_TOKEN_CALLBACK": true
498+
}
499+
}
500+
},
501+
{
502+
"description": "should recognise the mechanism when auth source is explicitly specified and with request callback (MONGODB-OIDC)",
503+
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authSource=$external",
504+
"callback": [
505+
"oidcRequest"
506+
],
507+
"valid": true,
508+
"credential": {
509+
"username": null,
510+
"password": null,
511+
"source": "$external",
512+
"mechanism": "MONGODB-OIDC",
513+
"mechanism_properties": {
514+
"REQUEST_TOKEN_CALLBACK": true
515+
}
516+
}
517+
},
518+
{
519+
"description": "should recognise the mechanism with request and refresh callback (MONGODB-OIDC)",
520+
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authSource=$external",
521+
"callback": [
522+
"oidcRequest",
523+
"oidcRefresh"
524+
],
525+
"valid": true,
526+
"credential": {
527+
"username": null,
528+
"password": null,
529+
"source": "$external",
530+
"mechanism": "MONGODB-OIDC",
531+
"mechanism_properties": {
532+
"REQUEST_TOKEN_CALLBACK": true,
533+
"REFRESH_TOKEN_CALLBACK": true
534+
}
535+
}
536+
},
537+
{
538+
"description": "should recognise the mechanism and principalName with request callback (MONGODB-OIDC)",
539+
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PRINCIPAL_NAME:principalName",
540+
"callback": [
541+
"oidcRequest"
542+
],
543+
"valid": true,
544+
"credential": {
545+
"username": null,
546+
"password": null,
547+
"source": "$external",
548+
"mechanism": "MONGODB-OIDC",
549+
"mechanism_properties": {
550+
"REQUEST_TOKEN_CALLBACK": true,
551+
"PRINCIPAL_NAME": "principalName"
552+
}
553+
}
554+
},
555+
{
556+
"description": "should recognise the mechanism with aws device (MONGODB-OIDC)",
557+
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=DEVICE_NAME:aws",
558+
"valid": true,
559+
"credential": {
560+
"username": null,
561+
"password": null,
562+
"source": "$external",
563+
"mechanism": "MONGODB-OIDC",
564+
"mechanism_properties": {
565+
"DEVICE_NAME": "aws"
566+
}
567+
}
568+
},
569+
{
570+
"description": "should recognise the mechanism when auth source is explicitly specified and with aws device (MONGODB-OIDC)",
571+
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authSource=$external&authMechanismProperties=DEVICE_NAME:aws",
572+
"valid": true,
573+
"credential": {
574+
"username": null,
575+
"password": null,
576+
"source": "$external",
577+
"mechanism": "MONGODB-OIDC",
578+
"mechanism_properties": {
579+
"DEVICE_NAME": "aws"
580+
}
581+
}
582+
},
583+
{
584+
"description": "should throw an exception if username is specified (MONGODB-OIDC)",
585+
"uri": "mongodb://user@localhost/?authMechanism=MONGODB-OIDC",
586+
"callback": [
587+
"oidcRequest"
588+
],
589+
"valid": false,
590+
"credential": null
591+
},
592+
{
593+
"description": "should throw an exception if username and password are specified (MONGODB-OIDC)",
594+
"uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC",
595+
"callback": [
596+
"oidcRequest"
597+
],
598+
"valid": false,
599+
"credential": null
600+
},
601+
{
602+
"description": "should throw an exception if principalName and deviceName are specified (MONGODB-OIDC)",
603+
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PRINCIPAL_NAME:principalName,DEVICE_NAME:aws",
604+
"valid": false,
605+
"credential": null
606+
},
607+
{
608+
"description": "should throw an exception if specified deviceName is not supported (MONGODB-OIDC)",
609+
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=DEVICE_NAME:unexisted",
610+
"valid": false,
611+
"credential": null
612+
},
613+
{
614+
"description": "should throw an exception if neither deviceName nor callbacks specified (MONGODB-OIDC)",
615+
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC",
616+
"valid": false,
617+
"credential": null
618+
},
619+
{
620+
"description": "should throw an exception when only refresh callback is specified (MONGODB-OIDC)",
621+
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC",
622+
"callback": [
623+
"oidcRefresh"
624+
],
625+
"valid": false,
626+
"credential": null
627+
},
628+
{
629+
"description": "should throw an exception when unsupported auth property is specified (MONGODB-OIDC)",
630+
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=UnsupportedProperty:unexisted",
631+
"valid": false,
632+
"credential": null
483633
}
484634
]
485-
}
635+
}

0 commit comments

Comments
 (0)