UNPKG

mongodb

Version:
143 lines (128 loc) 4.78 kB
import { setTimeout } from 'timers/promises'; import { type Document } from '../../../bson'; import { ns } from '../../../utils'; import type { Connection } from '../../connection'; import type { MongoCredentials } from '../mongo_credentials'; import type { Workflow } from '../mongodb_oidc'; import { finishCommandDocument } from './command_builders'; import { type TokenCache } from './token_cache'; /** The time to throttle callback calls. */ const THROTTLE_MS = 100; /** * The access token format. * @internal */ export interface AccessToken { access_token: string; expires_in?: number; } /** @internal */ export type OIDCTokenFunction = (credentials: MongoCredentials) => Promise<AccessToken>; /** * Common behaviour for OIDC machine workflows. * @internal */ export abstract class MachineWorkflow implements Workflow { cache: TokenCache; callback: OIDCTokenFunction; lastExecutionTime: number; /** * Instantiate the machine workflow. */ constructor(cache: TokenCache) { this.cache = cache; this.callback = this.withLock(this.getToken.bind(this)); this.lastExecutionTime = Date.now() - THROTTLE_MS; } /** * Execute the workflow. Gets the token from the subclass implementation. */ async execute(connection: Connection, credentials: MongoCredentials): Promise<void> { const token = await this.getTokenFromCacheOrEnv(connection, credentials); const command = finishCommandDocument(token); await connection.command(ns(credentials.source), command, undefined); } /** * Reauthenticate on a machine workflow just grabs the token again since the server * has said the current access token is invalid or expired. */ async reauthenticate(connection: Connection, credentials: MongoCredentials): Promise<void> { if (this.cache.hasAccessToken) { // Reauthentication implies the token has expired. if (connection.accessToken === this.cache.getAccessToken()) { // If connection's access token is the same as the cache's, remove // the token from the cache and connection. this.cache.removeAccessToken(); delete connection.accessToken; } else { // If the connection's access token is different from the cache's, set // the cache's token on the connection and do not remove from the // cache. connection.accessToken = this.cache.getAccessToken(); } } await this.execute(connection, credentials); } /** * Get the document to add for speculative authentication. */ async speculativeAuth(connection: Connection, credentials: MongoCredentials): Promise<Document> { // The spec states only cached access tokens can use speculative auth. if (!this.cache.hasAccessToken) { return {}; } const token = await this.getTokenFromCacheOrEnv(connection, credentials); const document = finishCommandDocument(token); document.db = credentials.source; return { speculativeAuthenticate: document }; } /** * Get the token from the cache or environment. */ private async getTokenFromCacheOrEnv( connection: Connection, credentials: MongoCredentials ): Promise<string> { if (this.cache.hasAccessToken) { const token = this.cache.getAccessToken(); // New connections won't have an access token so ensure we set here. if (!connection.accessToken) { connection.accessToken = token; } return token; } else { const token = await this.callback(credentials); this.cache.put({ accessToken: token.access_token, expiresInSeconds: token.expires_in }); // Put the access token on the connection as well. connection.accessToken = token.access_token; return token.access_token; } } /** * Ensure the callback is only executed one at a time, and throttled to * only once per 100ms. */ private withLock(callback: OIDCTokenFunction): OIDCTokenFunction { let lock: Promise<any> = Promise.resolve(); return async (credentials: MongoCredentials): Promise<AccessToken> => { // We do this to ensure that we would never return the result of the // previous lock, only the current callback's value would get returned. await lock; lock = lock .catch(() => null) .then(async () => { const difference = Date.now() - this.lastExecutionTime; if (difference <= THROTTLE_MS) { await setTimeout(THROTTLE_MS - difference); } this.lastExecutionTime = Date.now(); return await callback(credentials); }); return await lock; }; } /** * Get the token from the environment or endpoint. */ abstract getToken(credentials: MongoCredentials): Promise<AccessToken>; }