All files / src/cmap/auth mongodb_oidc.ts

39.47% Statements 15/38
0% Branches 0/10
0% Functions 0/8
44.11% Lines 15/34

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181  147x     147x   147x 147x 147x 147x 147x     147x                                                                                                                                                       147x                                                       147x 147x 147x 147x 147x         147x                                                                                                            
import type { Document } from '../../bson';
import { MongoInvalidArgumentError, MongoMissingCredentialsError } from '../../error';
import type { HandshakeDocument } from '../connect';
import type { Connection } from '../connection';
import { type AuthContext, AuthProvider } from './auth_provider';
import type { MongoCredentials } from './mongo_credentials';
import { AzureMachineWorkflow } from './mongodb_oidc/azure_machine_workflow';
import { GCPMachineWorkflow } from './mongodb_oidc/gcp_machine_workflow';
import { K8SMachineWorkflow } from './mongodb_oidc/k8s_machine_workflow';
import { TokenCache } from './mongodb_oidc/token_cache';
import { TokenMachineWorkflow } from './mongodb_oidc/token_machine_workflow';
 
/** Error when credentials are missing. */
const MISSING_CREDENTIALS_ERROR = 'AuthContext must provide credentials.';
 
/**
 * The information returned by the server on the IDP server.
 * @public
 */
export interface IdPInfo {
  /**
   * A URL which describes the Authentication Server. This identifier should
   * be the iss of provided access tokens, and be viable for RFC8414 metadata
   * discovery and RFC9207 identification.
   */
  issuer: string;
  /** A unique client ID for this OIDC client. */
  clientId: string;
  /** A list of additional scopes to request from IdP. */
  requestScopes?: string[];
}
 
/**
 * The response from the IdP server with the access token and
 * optional expiration time and refresh token.
 * @public
 */
export interface IdPServerResponse {
  /** The OIDC access token. */
  accessToken: string;
  /** The time when the access token expires. For future use. */
  expiresInSeconds?: number;
  /** The refresh token, if applicable, to be used by the callback to request a new token from the issuer. */
  refreshToken?: string;
}
 
/**
 * The response required to be returned from the machine or
 * human callback workflows' callback.
 * @public
 */
export interface OIDCResponse {
  /** The OIDC access token. */
  accessToken: string;
  /** The time when the access token expires. For future use. */
  expiresInSeconds?: number;
  /** The refresh token, if applicable, to be used by the callback to request a new token from the issuer. */
  refreshToken?: string;
}
 
/**
 * The parameters that the driver provides to the user supplied
 * human or machine callback.
 *
 * The version number is used to communicate callback API changes that are not breaking but that
 * users may want to know about and review their implementation. Users may wish to check the version
 * number and throw an error if their expected version number and the one provided do not match.
 * @public
 */
export interface OIDCCallbackParams {
  /** Optional username. */
  username?: string;
  /** The context in which to timeout the OIDC callback. */
  timeoutContext: AbortSignal;
  /** The current OIDC API version. */
  version: 1;
  /** The IdP information returned from the server. */
  idpInfo?: IdPInfo;
  /** The refresh token, if applicable, to be used by the callback to request a new token from the issuer. */
  refreshToken?: string;
}
 
/**
 * The signature of the human or machine callback functions.
 * @public
 */
export type OIDCCallbackFunction = (params: OIDCCallbackParams) => Promise<OIDCResponse>;
 
/** The current version of OIDC implementation. */
export const OIDC_VERSION = 1;
 
type EnvironmentName = 'test' | 'azure' | 'gcp' | 'k8s' | undefined;
 
/** @internal */
export interface Workflow {
  /**
   * All device workflows must implement this method in order to get the access
   * token and then call authenticate with it.
   */
  execute(
    connection: Connection,
    credentials: MongoCredentials,
    response?: Document
  ): Promise<void>;
 
  /**
   * Each workflow should specify the correct custom behaviour for reauthentication.
   */
  reauthenticate(connection: Connection, credentials: MongoCredentials): Promise<void>;
 
  /**
   * Get the document to add for speculative authentication.
   */
  speculativeAuth(connection: Connection, credentials: MongoCredentials): Promise<Document>;
}
 
/** @internal */
export const OIDC_WORKFLOWS: Map<EnvironmentName, () => Workflow> = new Map();
OIDC_WORKFLOWS.set('test', () => new TokenMachineWorkflow(new TokenCache()));
OIDC_WORKFLOWS.set('azure', () => new AzureMachineWorkflow(new TokenCache()));
OIDC_WORKFLOWS.set('gcp', () => new GCPMachineWorkflow(new TokenCache()));
OIDC_WORKFLOWS.set('k8s', () => new K8SMachineWorkflow(new TokenCache()));
 
/**
 * OIDC auth provider.
 */
export class MongoDBOIDC extends AuthProvider {
  workflow: Workflow;
 
  /**
   * Instantiate the auth provider.
   */
  constructor(workflow?: Workflow) {
    super();
    if (!workflow) {
      throw new MongoInvalidArgumentError('No workflow provided to the OIDC auth provider.');
    }
    this.workflow = workflow;
  }
 
  /**
   * Authenticate using OIDC
   */
  override async auth(authContext: AuthContext): Promise<void> {
    const { connection, reauthenticating, response } = authContext;
    if (response?.speculativeAuthenticate?.done && !reauthenticating) {
      return;
    }
    const credentials = getCredentials(authContext);
    if (reauthenticating) {
      await this.workflow.reauthenticate(connection, credentials);
    } else {
      await this.workflow.execute(connection, credentials, response);
    }
  }
 
  /**
   * Add the speculative auth for the initial handshake.
   */
  override async prepare(
    handshakeDoc: HandshakeDocument,
    authContext: AuthContext
  ): Promise<HandshakeDocument> {
    const { connection } = authContext;
    const credentials = getCredentials(authContext);
    const result = await this.workflow.speculativeAuth(connection, credentials);
    return { ...handshakeDoc, ...result };
  }
}
 
/**
 * Get credentials from the auth context, throwing if they do not exist.
 */
function getCredentials(authContext: AuthContext): MongoCredentials {
  const { credentials } = authContext;
  if (!credentials) {
    throw new MongoMissingCredentialsError(MISSING_CREDENTIALS_ERROR);
  }
  return credentials;
}