{"id":"c0bfef8a63962e41","repo":"mongodb/node-mongodb-native","slug":"user-provided-oidc-callbacks-must-return-a-valid-o","errorCode":null,"errorMessage":"User provided OIDC callbacks must return a valid object with an accessToken.","messagePattern":"User provided OIDC callbacks must return a valid object with an accessToken\\.","errorType":"exception","errorClass":"MongoMissingCredentialsError","httpStatus":null,"severity":"error","filePath":"src/cmap/auth/mongodb_oidc/callback_workflow.ts","lineNumber":147,"sourceCode":"    token: string,\n    conversationId?: number\n  ): Promise<void> {\n    await connection.command(\n      ns(credentials.source),\n      finishCommandDocument(token, conversationId),\n      undefined\n    );\n  }\n\n  /**\n   * Executes the callback and validates the output.\n   */\n  protected async executeAndValidateCallback(params: OIDCCallbackParams): Promise<OIDCResponse> {\n    const result = await this.callback(params);\n    // Validate that the result returned by the callback is acceptable. If it is not\n    // we must clear the token result from the cache.\n    if (isCallbackResultInvalid(result)) {\n      throw new MongoMissingCredentialsError(CALLBACK_RESULT_ERROR);\n    }\n    return result;\n  }\n\n  /**\n   * Ensure the callback is only executed one at a time and throttles the calls\n   * to every 100ms.\n   */\n  protected withLock(callback: OIDCCallbackFunction): OIDCCallbackFunction {\n    let lock: Promise<any> = Promise.resolve();\n    return async (params: OIDCCallbackParams): Promise<OIDCResponse> => {\n      // We do this to ensure that we would never return the result of the\n      // previous lock, only the current callback's value would get returned.\n      await lock;\n      lock = lock\n\n        .catch(() => null)\n","sourceCodeStart":129,"sourceCodeEnd":165,"githubUrl":"https://github.com/mongodb/node-mongodb-native/blob/3366c21a6311e02f1be91da982f9b93d3cce99a0/src/cmap/auth/mongodb_oidc/callback_workflow.ts#L129-L165","documentation":"Thrown by the shared OIDC callback workflow when a user-provided OIDC callback returns a value that fails validation: it must be a non-null object containing accessToken, and may only contain the allowed properties accessToken, expiresInSeconds, refreshToken (src/cmap/auth/mongodb_oidc/callback_workflow.ts:146 and isCallbackResultInvalid at line 184). Extra unknown properties also invalidate the result. Surfaced as a MongoMissingCredentialsError.","triggerScenarios":"A custom OIDC callback (configured via MongoClient auth mechanism properties 'OIDC_CALLBACK' or through a human workflow) returns undefined/null, returns an object without accessToken, returns accessToken of non-string type, or returns an object containing properties outside the allow-list.","commonSituations":"Callback returns the raw IdP response object verbatim (which may have extra fields like 'scope', 'token_type'), returns a Promise that resolves to undefined on a code path, or returns a nested token object like { token: {...} } instead of the flat shape.","solutions":["Ensure the callback resolves to an object with at least { accessToken: '<jwt string>' }.","Strip any fields other than accessToken, expiresInSeconds (number), and refreshToken (string) from the returned object.","Handle errors inside the callback and re-throw a meaningful Error rather than returning undefined.","Double-check the return path: e.g. return { accessToken } not return { token: jwt }."],"exampleFix":"// before\nconst callback = async (params) => {\n  const r = await fetch(idp);\n  return await r.json(); // returns {access_token, token_type, scope, ...}\n};\n\n// after\nconst callback = async (params) => {\n  const r = await fetch(idp);\n  const body = await r.json();\n  return {\n    accessToken: body.access_token,\n    expiresInSeconds: body.expires_in\n  };\n};","handlingStrategy":"validation","validationCode":"function validateOidcCallbackResult(r: unknown): void {\n  if (r == null || typeof r !== 'object') throw new Error('OIDC callback must return an object');\n  const allowed = new Set(['accessToken', 'expiresInSeconds', 'refreshToken']);\n  const obj = r as Record<string, unknown>;\n  if (typeof obj.accessToken !== 'string') throw new Error('OIDC callback result missing string accessToken');\n  for (const k of Object.keys(obj)) {\n    if (!allowed.has(k)) throw new Error(`OIDC callback returned disallowed property: ${k}`);\n  }\n}","typeGuard":"function isOidcResponse(r: unknown): r is { accessToken: string; expiresInSeconds?: number; refreshToken?: string } {\n  if (r == null || typeof r !== 'object') return false;\n  const o = r as Record<string, unknown>;\n  if (typeof o.accessToken !== 'string') return false;\n  return Object.getOwnPropertyNames(o).every(k => ['accessToken','expiresInSeconds','refreshToken'].includes(k));\n}","tryCatchPattern":"try {\n  await client.connect();\n} catch (e) {\n  if (e instanceof MongoMissingCredentialsError && /OIDC callbacks must return/.test(e.message)) {\n    throw new Error('OIDC callback returned an invalid shape - ensure { accessToken: string } and no extra fields.');\n  }\n  throw e;\n}","preventionTips":["Wrap custom OIDC callbacks to map the IdP response to exactly { accessToken, expiresInSeconds?, refreshToken? }.","Never return undefined from a callback - throw an Error instead.","Unit-test the callback against isOidcResponse before deploying."],"tags":["auth","oidc","callback","validation","configuration"],"analyzedSha":"3366c21a6311e02f1be91da982f9b93d3cce99a0","analyzedAt":"2026-08-04T13:40:15.335Z","schemaVersion":2}