Skip to content

Commit 0b7be80

Browse files
committed
feat: oidc updates
1 parent 99bc224 commit 0b7be80

File tree

7 files changed

+356
-66
lines changed

7 files changed

+356
-66
lines changed

src/cmap/auth/mongodb_oidc.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
import { MongoInvalidArgumentError, MongoMissingCredentialsError } from '../../error';
2+
import { hostMatchesWildcards } from '../../utils';
23
import type { HandshakeDocument } from '../connect';
34
import { type AuthContext, AuthProvider } from './auth_provider';
4-
import type { MongoCredentials } from './mongo_credentials';
5+
import { DEFAULT_ALLOWED_HOSTS, MongoCredentials } from './mongo_credentials';
56
import { AwsServiceWorkflow } from './mongodb_oidc/aws_service_workflow';
67
import { CallbackWorkflow } from './mongodb_oidc/callback_workflow';
78
import type { Workflow } from './mongodb_oidc/workflow';
89

10+
/**
11+
* @internal
12+
* The current version of OIDC implementation.
13+
*/
14+
export const OIDC_VERSION = 0;
15+
916
/**
1017
* @public
1118
* @experimental
1219
*/
13-
export interface OIDCMechanismServerStep1 {
20+
export interface IdPServerInfo {
1421
issuer: string;
1522
clientId: string;
1623
requestScopes?: string[];
@@ -20,7 +27,7 @@ export interface OIDCMechanismServerStep1 {
2027
* @public
2128
* @experimental
2229
*/
23-
export interface OIDCRequestTokenResult {
30+
export interface IdPServerResponse {
2431
accessToken: string;
2532
expiresInSeconds?: number;
2633
refreshToken?: string;
@@ -30,30 +37,30 @@ export interface OIDCRequestTokenResult {
3037
* @public
3138
* @experimental
3239
*/
33-
export interface OIDCClientInfo {
34-
principalName: string;
40+
export interface OIDCCallbackContext {
41+
refreshToken?: string;
3542
timeoutSeconds?: number;
3643
timeoutContext?: AbortSignal;
44+
version: number;
3745
}
3846

3947
/**
4048
* @public
4149
* @experimental
4250
*/
4351
export type OIDCRequestFunction = (
44-
clientInfo: OIDCClientInfo,
45-
serverInfo: OIDCMechanismServerStep1
46-
) => Promise<OIDCRequestTokenResult>;
52+
info: IdPServerInfo,
53+
context: OIDCCallbackContext
54+
) => Promise<IdPServerResponse>;
4755

4856
/**
4957
* @public
5058
* @experimental
5159
*/
5260
export type OIDCRefreshFunction = (
53-
clientInfo: OIDCClientInfo,
54-
serverInfo: OIDCMechanismServerStep1,
55-
tokenResult: OIDCRequestTokenResult
56-
) => Promise<OIDCRequestTokenResult>;
61+
info: IdPServerInfo,
62+
context: OIDCCallbackContext
63+
) => Promise<IdPServerResponse>;
5764

5865
type ProviderName = 'aws' | 'callback';
5966

@@ -92,6 +99,11 @@ export class MongoDBOIDC extends AuthProvider {
9299
authContext: AuthContext
93100
): Promise<HandshakeDocument> {
94101
const credentials = getCredentials(authContext);
102+
const { connection } = authContext;
103+
const allowedHosts = credentials.mechanismProperties.ALLOWED_HOSTS || DEFAULT_ALLOWED_HOSTS;
104+
if (!hostMatchesWildcards(connection.address, allowedHosts)) {
105+
throw new MongoInvalidArgumentError('Host does not match provided ALLOWED_HOSTS values');
106+
}
95107
const workflow = getWorkflow(credentials);
96108
const result = await workflow.speculativeAuth(credentials);
97109
return { ...handshakeDoc, ...result };

src/cmap/auth/mongodb_oidc/callback_workflow.ts

Lines changed: 51 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
import { Binary, BSON, type Document } from 'bson';
22

3-
import { MongoInvalidArgumentError, MongoMissingCredentialsError } from '../../../error';
3+
import {
4+
MONGODB_ERROR_CODES,
5+
MongoError,
6+
MongoInvalidArgumentError,
7+
MongoMissingCredentialsError
8+
} from '../../../error';
49
import { ns } from '../../../utils';
510
import type { Connection } from '../../connection';
611
import type { MongoCredentials } from '../mongo_credentials';
7-
import type {
8-
OIDCMechanismServerStep1,
12+
import {
13+
IdPServerInfo,
14+
IdPServerResponse,
15+
OIDC_VERSION,
16+
OIDCCallbackContext,
917
OIDCRefreshFunction,
10-
OIDCRequestFunction,
11-
OIDCRequestTokenResult
18+
OIDCRequestFunction
1219
} from '../mongodb_oidc';
1320
import { AuthMechanism } from '../providers';
1421
import { TokenEntryCache } from './token_entry_cache';
@@ -71,8 +78,8 @@ export class CallbackWorkflow implements Workflow {
7178
let result;
7279
// Reauthentication must go through all the steps again regards of a cache entry
7380
// being present.
74-
if (entry && !reauthenticating) {
75-
if (entry.isValid()) {
81+
if (entry) {
82+
if (entry.isValid() && !reauthenticating) {
7683
// Presence of a valid cache entry means we can skip to the finishing step.
7784
result = await this.finishAuthentication(
7885
connection,
@@ -91,12 +98,33 @@ export class CallbackWorkflow implements Workflow {
9198
requestCallback,
9299
refreshCallback
93100
);
94-
result = await this.finishAuthentication(
95-
connection,
96-
credentials,
97-
tokenResult,
98-
response?.speculativeAuthenticate?.conversationId
99-
);
101+
try {
102+
result = await this.finishAuthentication(
103+
connection,
104+
credentials,
105+
tokenResult,
106+
response?.speculativeAuthenticate?.conversationId
107+
);
108+
} catch (error) {
109+
// If we are reauthenticating and this errors with reauthentication
110+
// required, we need to do the entire process over again and clear
111+
// the cache entry.
112+
if (
113+
reauthenticating &&
114+
error instanceof MongoError &&
115+
error.code === MONGODB_ERROR_CODES.Reauthenticate
116+
) {
117+
this.cache.deleteEntry(
118+
connection.address,
119+
credentials.username || '',
120+
requestCallback,
121+
refreshCallback || null
122+
);
123+
result = await this.execute(connection, credentials, reauthenticating);
124+
} else {
125+
throw error;
126+
}
127+
}
100128
}
101129
} else {
102130
// No entry in the cache requires us to do all authentication steps
@@ -108,9 +136,7 @@ export class CallbackWorkflow implements Workflow {
108136
response
109137
);
110138
const conversationId = startDocument.conversationId;
111-
const serverResult = BSON.deserialize(
112-
startDocument.payload.buffer
113-
) as OIDCMechanismServerStep1;
139+
const serverResult = BSON.deserialize(startDocument.payload.buffer) as IdPServerInfo;
114140
const tokenResult = await this.fetchAccessToken(
115141
connection,
116142
credentials,
@@ -159,7 +185,7 @@ export class CallbackWorkflow implements Workflow {
159185
private async finishAuthentication(
160186
connection: Connection,
161187
credentials: MongoCredentials,
162-
tokenResult: OIDCRequestTokenResult,
188+
tokenResult: IdPServerResponse,
163189
conversationId?: number
164190
): Promise<Document> {
165191
const result = await connection.commandAsync(
@@ -177,11 +203,11 @@ export class CallbackWorkflow implements Workflow {
177203
private async fetchAccessToken(
178204
connection: Connection,
179205
credentials: MongoCredentials,
180-
startResult: OIDCMechanismServerStep1,
206+
serverInfo: IdPServerInfo,
181207
reauthenticating: boolean,
182208
requestCallback: OIDCRequestFunction,
183209
refreshCallback?: OIDCRefreshFunction
184-
): Promise<OIDCRequestTokenResult> {
210+
): Promise<IdPServerResponse> {
185211
// Get the token from the cache.
186212
const entry = this.cache.getEntry(
187213
connection.address,
@@ -190,7 +216,7 @@ export class CallbackWorkflow implements Workflow {
190216
refreshCallback || null
191217
);
192218
let result;
193-
const clientInfo = { principalName: credentials.username, timeoutSeconds: TIMEOUT_S };
219+
const context: OIDCCallbackContext = { timeoutSeconds: TIMEOUT_S, version: OIDC_VERSION };
194220
// Check if there's a token in the cache.
195221
if (entry) {
196222
// If the cache entry is valid, return the token result.
@@ -201,13 +227,14 @@ export class CallbackWorkflow implements Workflow {
201227
// to use the refresh callback to get a new token. If no refresh callback
202228
// exists, then fallback to the request callback.
203229
if (refreshCallback) {
204-
result = await refreshCallback(clientInfo, startResult, entry.tokenResult);
230+
context.refreshToken = entry.tokenResult.refreshToken;
231+
result = await refreshCallback(serverInfo, context);
205232
} else {
206-
result = await requestCallback(clientInfo, startResult);
233+
result = await requestCallback(serverInfo, context);
207234
}
208235
} else {
209236
// With no token in the cache we use the request callback.
210-
result = await requestCallback(clientInfo, startResult);
237+
result = await requestCallback(serverInfo, context);
211238
}
212239
// Validate that the result returned by the callback is acceptable.
213240
if (isCallbackResultInvalid(result)) {
@@ -224,7 +251,7 @@ export class CallbackWorkflow implements Workflow {
224251
requestCallback,
225252
refreshCallback || null,
226253
result,
227-
startResult
254+
serverInfo
228255
);
229256
return result;
230257
}

src/cmap/auth/mongodb_oidc/token_entry_cache.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type {
2-
OIDCMechanismServerStep1,
2+
PrincipalStepRequest,
33
OIDCRefreshFunction,
44
OIDCRequestFunction,
5-
OIDCRequestTokenResult
5+
IdPServerResponse
66
} from '../mongodb_oidc';
77

88
/* 5 minutes in milliseonds */
@@ -22,16 +22,16 @@ FN_HASHES.set(NO_FUNCTION, FN_HASH_COUNTER);
2222

2323
/** @internal */
2424
export class TokenEntry {
25-
tokenResult: OIDCRequestTokenResult;
26-
serverResult: OIDCMechanismServerStep1;
25+
tokenResult: IdPServerResponse;
26+
serverResult: PrincipalStepRequest;
2727
expiration: number;
2828

2929
/**
3030
* Instantiate the entry.
3131
*/
3232
constructor(
33-
tokenResult: OIDCRequestTokenResult,
34-
serverResult: OIDCMechanismServerStep1,
33+
tokenResult: IdPServerResponse,
34+
serverResult: PrincipalStepRequest,
3535
expiration: number
3636
) {
3737
this.tokenResult = tokenResult;
@@ -67,8 +67,8 @@ export class TokenEntryCache {
6767
username: string,
6868
requestFn: OIDCRequestFunction | null,
6969
refreshFn: OIDCRefreshFunction | null,
70-
tokenResult: OIDCRequestTokenResult,
71-
serverResult: OIDCMechanismServerStep1
70+
tokenResult: IdPServerResponse,
71+
serverResult: PrincipalStepRequest
7272
): TokenEntry {
7373
const entry = new TokenEntry(
7474
tokenResult,

src/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,11 +204,11 @@ export type {
204204
MongoCredentialsOptions
205205
} from './cmap/auth/mongo_credentials';
206206
export type {
207-
OIDCClientInfo,
208-
OIDCMechanismServerStep1,
207+
OIDCCallbackContext as OIDCClientInfo,
208+
PrincipalStepRequest as OIDCMechanismServerStep1,
209209
OIDCRefreshFunction,
210210
OIDCRequestFunction,
211-
OIDCRequestTokenResult
211+
IdPServerResponse as OIDCRequestTokenResult
212212
} from './cmap/auth/mongodb_oidc';
213213
export type {
214214
BinMsg,

src/utils.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,23 @@ export const ByteUtils = {
5858
}
5959
};
6060

61+
/**
62+
* Determines if a connection's address matches a user provided list
63+
* of domain wildcards.
64+
*/
65+
export function hostMatchesWildcards(address: string, wildcards: string[]): boolean {
66+
const host = HostAddress.fromString(address).host;
67+
for (const wildcard of wildcards) {
68+
if (
69+
host === wildcard ||
70+
(wildcard.startsWith('*.') && host?.endsWith(wildcard.substring(2, wildcard.length)))
71+
) {
72+
return true;
73+
}
74+
}
75+
return false;
76+
}
77+
6178
/**
6279
* Throws if collectionName is not a valid mongodb collection namespace.
6380
* @internal

0 commit comments

Comments
 (0)