Intermittent 401 error when getting users with a server-to-server OAuth app credentials

Description
After migrating from JWT to the server-to-server OAuth app, we are facing random 401 errors when calling the Zoom API. For example, when using the endpoint “GET https://api.zoom.us/v2/users” it sometimes returns a 401 error and sometimes it succeeds. We are always generating the token right before we make a request to the endpoint “GET https://api.zoom.us/v2/users”, which means that the token is never expired. We need to make recurrent checks in Zoom to make sure the Licenses are assigned to the users who are going to have a meeting, which means we are making a lot of requests to Zoom. This feature is part of the core of our application, which means that this is a critical issue for us.
The following is the code we are using to request the endpoint “GET https://api.zoom.us/v2/users”:

  async getUsers(nextPageToken?: string): Promise<any> {
    let url = `${this.url}/users?status=active`;

    if (nextPageToken) {
      url = url + `&next_page_token=${nextPageToken}`;
    }

    try {
      const { data } = await axios.default.get(url, {
        headers: { Authorization: `Bearer ${await this.refreshToken()}` },
      });

      return data;
    } catch (err) {
      this.errorLogger(err.message);
      return {
        users: [],
      };
    }
  }

  async refreshToken(): Promise<string> {
    const authorizationHeader = Buffer.from(
      `${this.zoomClientId}:${this.zoomClientSecret}`,
    ).toString("base64");
    const { data } = await axios.default.post(
      `https://zoom.us/oauth/token?grant_type=account_credentials&account_id=${this.zoomAccountId}`,
      {},
      { headers: { Authorization: `Basic ${authorizationHeader}` } },
    );

    return data.access_token;
  }

Take into account that the following attributes are defined within the class that contains those functions and they have been checked many times, besides the fact that it sometimes succeeds, which means there is no problem with their values:

  • zoomAccountId
  • zoomClientId
  • zoomClientSecret
  • url

Remember that it was just one example, it is not the only endpoint failing randomly.

Besides, we added all the scope types to the server-to-server OAuth app even when not all of them were needed, just to make sure it was not a problem with missing scopes.

Error
This is the entire error received in the cases when it fails (omitting the token value and using instead):

Error: Request failed with status code 401
    at createError (/opt/nodejs/node_modules/axios/lib/core/createError.js:16:15)
    at settle (/opt/nodejs/node_modules/axios/lib/core/settle.js:17:12)
    at IncomingMessage.handleStreamEnd (/opt/nodejs/node_modules/axios/lib/adapters/http.js:244:11)
    at IncomingMessage.emit (node:events:525:35)
    at IncomingMessage.emit (node:domain:489:12)
    at endReadableNT (node:internal/streams/readable:1358:12)
    at processTicksAndRejections (node:internal/process/task_queues:83:21) {
  config: {
    url: 'https://api.zoom.us/v2/users?status=active',
    method: 'get',
    headers: {
      Accept: 'application/json, text/plain, */*',
      Authorization: 'Bearer <token>',
      'User-Agent': 'axios/0.20.0'
    },
    transformRequest: [ [Function: transformRequest] ],
    transformResponse: [ [Function: transformResponse] ],
    timeout: 0,
    adapter: [Function: httpAdapter],
    xsrfCookieName: 'XSRF-TOKEN',
    xsrfHeaderName: 'X-XSRF-TOKEN',
    maxContentLength: -1,
    maxBodyLength: -1,
    validateStatus: [Function: validateStatus],
    data: undefined
  },
  request: <ref *1> ClientRequest {
    _events: [Object: null prototype] {
      error: [Array],
      abort: [Function (anonymous)],
      aborted: [Function (anonymous)],
      connect: [Function (anonymous)],
      socket: [Function (anonymous)],
      timeout: [Function (anonymous)],
      finish: [Function: requestOnFinish]
    },
    _eventsCount: 7,
    _maxListeners: undefined,
    outputData: [],
    outputSize: 0,
    writable: true,
    destroyed: false,
    _last: true,
    chunkedEncoding: false,
    shouldKeepAlive: false,
    maxRequestsOnConnectionReached: false,
    _defaultKeepAlive: true,
    useChunkedEncodingByDefault: false,
    sendDate: false,
    _removedConnection: false,
    _removedContLen: false,
    _removedTE: false,
    strictContentLength: false,
    _contentLength: 0,
    _hasBody: true,
    _trailer: '',
    finished: true,
    _headerSent: true,
    _closed: false,
    socket: TLSSocket {
      _tlsOptions: [Object],
      _secureEstablished: true,
      _securePending: false,
      _newSessionPending: false,
      _controlReleased: true,
      secureConnecting: false,
      _SNICallback: null,
      servername: 'api.zoom.us',
      alpnProtocol: false,
      authorized: true,
      authorizationError: null,
      encrypted: true,
      _events: [Object: null prototype],
      _eventsCount: 10,
      connecting: false,
      _hadError: false,
      _parent: null,
      _host: 'api.zoom.us',
      _closeAfterHandlingError: false,
      _readableState: [ReadableState],
      _maxListeners: undefined,
      _writableState: [WritableState],
      allowHalfOpen: false,
      _sockname: null,
      _pendingData: null,
      _pendingEncoding: '',
      server: undefined,
      _server: null,
      ssl: [TLSWrap],
      _requestCert: true,
      _rejectUnauthorized: true,
      parser: null,
      _httpMessage: [Circular *1],
      [Symbol(res)]: [TLSWrap],
      [Symbol(verified)]: true,
      [Symbol(pendingSession)]: null,
      [Symbol(async_id_symbol)]: 432,
      [Symbol(kHandle)]: [TLSWrap],
      [Symbol(lastWriteQueueSize)]: 0,
      [Symbol(timeout)]: null,
      [Symbol(kBuffer)]: null,
      [Symbol(kBufferCb)]: null,
      [Symbol(kBufferGen)]: null,
      [Symbol(kCapture)]: false,
      [Symbol(kSetNoDelay)]: false,
      [Symbol(kSetKeepAlive)]: false,
      [Symbol(kSetKeepAliveInitialDelay)]: 0,
      [Symbol(kBytesRead)]: 0,
      [Symbol(kBytesWritten)]: 0,
      [Symbol(connect-options)]: [Object],
      [Symbol(RequestTimeout)]: undefined
    },
    _header: 'GET /v2/users?status=active HTTP/1.1\r\n' +
      'Accept: application/json, text/plain, */*\r\n' +
      'Authorization: Bearer <token>\r\n' +
      'User-Agent: axios/0.20.0\r\n' +
      'Host: api.zoom.us\r\n' +
      'Connection: close\r\n' +
      '\r\n',
    _keepAliveTimeout: 0,
    _onPendingData: [Function: nop],
    agent: Agent {
      _events: [Object: null prototype],
      _eventsCount: 2,
      _maxListeners: undefined,
      defaultPort: 443,
      protocol: 'https:',
      options: [Object: null prototype],
      requests: [Object: null prototype] {},
      sockets: [Object: null prototype],
      freeSockets: [Object: null prototype] {},
      keepAliveMsecs: 1000,
      keepAlive: false,
      maxSockets: Infinity,
      maxFreeSockets: 256,
      scheduling: 'lifo',
      maxTotalSockets: Infinity,
      totalSocketCount: 1,
      maxCachedSessions: 100,
      _sessionCache: [Object],
      [Symbol(kCapture)]: false
    },
    socketPath: undefined,
    method: 'GET',
    maxHeaderSize: undefined,
    insecureHTTPParser: undefined,
    path: '/v2/users?status=active',
    _ended: true,
    res: IncomingMessage {
      _readableState: [ReadableState],
      _events: [Object: null prototype],
      _eventsCount: 3,
      _maxListeners: undefined,
      socket: [TLSSocket],
      httpVersionMajor: 1,
      httpVersionMinor: 1,
      httpVersion: '1.1',
      complete: true,
      rawHeaders: [Array],
      rawTrailers: [],
      aborted: false,
      upgrade: false,
      url: '',
      method: null,
      statusCode: 401,
      statusMessage: 'Unauthorized',
      client: [TLSSocket],
      _consuming: false,
      _dumped: false,
      req: [Circular *1],
      responseUrl: 'https://api.zoom.us/v2/users?status=active',
      redirects: [],
      [Symbol(kCapture)]: false,
      [Symbol(kHeaders)]: [Object],
      [Symbol(kHeadersCount)]: 54,
      [Symbol(kTrailers)]: null,
      [Symbol(kTrailersCount)]: 0,
      [Symbol(RequestTimeout)]: undefined
    },
    aborted: false,
    timeoutCb: null,
    upgradeOrConnect: false,
    parser: null,
    maxHeadersCount: null,
    reusedSocket: false,
    host: 'api.zoom.us',
    protocol: 'https:',
    _redirectable: Writable {
      _writableState: [WritableState],
      _events: [Object: null prototype],
      _eventsCount: 2,
      _maxListeners: undefined,
      _options: [Object],
      _ended: true,
      _ending: true,
      _redirectCount: 0,
      _redirects: [],
      _requestBodyLength: 0,
      _requestBodyBuffers: [],
      _onNativeResponse: [Function (anonymous)],
      _currentRequest: [Circular *1],
      _currentUrl: 'https://api.zoom.us/v2/users?status=active',
      [Symbol(kCapture)]: false
    },
    [Symbol(kCapture)]: false,
    [Symbol(kBytesWritten)]: 0,
    [Symbol(kEndCalled)]: true,
    [Symbol(kNeedDrain)]: false,
    [Symbol(corked)]: 0,
    [Symbol(kOutHeaders)]: [Object: null prototype] {
      accept: [Array],
      authorization: [Array],
      'user-agent': [Array],
      host: [Array]
    },
    [Symbol(kUniqueHeaders)]: null
  },
  response: {
    status: 401,
    statusText: 'Unauthorized',
    headers: {
      date: 'Tue, 06 Jun 2023 15:46:50 GMT',
      'content-type': 'application/json;charset=UTF-8',
      'content-length': '46',
      connection: 'close',
      'x-zm-trackingid': 'v=2.0;clid=us06;rid=WEB_fc4672f24a921f1e8f9f304ddafc763b',
      'x-content-type-options': 'nosniff',
      'cache-control': 'no-cache, no-store, must-revalidate, no-transform',
      pragma: 'no-cache',
      expires: 'Thu, 01 Jan 1970 00:00:00 GMT',
      'set-cookie': [Array],
      'x-zm-zoneid': 'VA2',
      'cf-cache-status': 'DYNAMIC',
      'report-to': '{"endpoints":[{"url":"https:\\/\\/a.nel.cloudflare.com\\/report\\/v3?s=ZKz4pwclXvE%2Bhb2oeMls4VZBPeZg2NJkKhUzZn9AyTu%2FKEKhEYVQsPT2Ye%2Ful0GkDObOmm6gi6v8EmaMDk1nd1lvRIFDdDW42%2BxXRqlGXPorA6SWZW5ZnUKHEUiG"}],"group":"cf-nel","max_age":604800}',
      nel: '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}',
      server: 'cloudflare',
      'cf-ray': '7d31ca5acd4d1b84-DUB',
      'alt-svc': 'h3=":443"; ma=86400'
    },
    config: {
      url: 'https://api.zoom.us/v2/users?status=active',
      method: 'get',
      headers: [Object],
      transformRequest: [Array],
      transformResponse: [Array],
      timeout: 0,
      adapter: [Function: httpAdapter],
      xsrfCookieName: 'XSRF-TOKEN',
      xsrfHeaderName: 'X-XSRF-TOKEN',
      maxContentLength: -1,
      maxBodyLength: -1,
      validateStatus: [Function: validateStatus],
      data: undefined
    },
    request: <ref *1> ClientRequest {
      _events: [Object: null prototype],
      _eventsCount: 7,
      _maxListeners: undefined,
      outputData: [],
      outputSize: 0,
      writable: true,
      destroyed: false,
      _last: true,
      chunkedEncoding: false,
      shouldKeepAlive: false,
      maxRequestsOnConnectionReached: false,
      _defaultKeepAlive: true,
      useChunkedEncodingByDefault: false,
      sendDate: false,
      _removedConnection: false,
      _removedContLen: false,
      _removedTE: false,
      strictContentLength: false,
      _contentLength: 0,
      _hasBody: true,
      _trailer: '',
      finished: true,
      _headerSent: true,
      _closed: false,
      socket: [TLSSocket],
      _header: 'GET /v2/users?status=active HTTP/1.1\r\n' +
        'Accept: application/json, text/plain, */*\r\n' +
        'Authorization: Bearer <token>\r\n' +
        'User-Agent: axios/0.20.0\r\n' +
        'Host: api.zoom.us\r\n' +
        'Connection: close\r\n' +
        '\r\n',
      _keepAliveTimeout: 0,
      _onPendingData: [Function: nop],
      agent: [Agent],
      socketPath: undefined,
      method: 'GET',
      maxHeaderSize: undefined,
      insecureHTTPParser: undefined,
      path: '/v2/users?status=active',
      _ended: true,
      res: [IncomingMessage],
      aborted: false,
      timeoutCb: null,
      upgradeOrConnect: false,
      parser: null,
      maxHeadersCount: null,
      reusedSocket: false,
      host: 'api.zoom.us',
      protocol: 'https:',
      _redirectable: [Writable],
      [Symbol(kCapture)]: false,
      [Symbol(kBytesWritten)]: 0,
      [Symbol(kEndCalled)]: true,
      [Symbol(kNeedDrain)]: false,
      [Symbol(corked)]: 0,
      [Symbol(kOutHeaders)]: [Object: null prototype],
      [Symbol(kUniqueHeaders)]: null
    },
    data: { code: 124, message: 'Invalid access token.' }
  },
  isAxiosError: true,
  toJSON: [Function: toJSON]
}

How To Reproduce
We shared the code before, just use some server-to-server OAuth credentials to run it.

2 Likes

@roxana.soca , we just had/are having this issue. Zoom Server-to-Server apps limit the number of access tokens that are valid at any given time, I think by default it is one. In our case, we had a webhook running and a cron job running on ec2 that were both generating access tokens. Due to the limit of one (never seen an API limit access tokens like this before), they were constantly stepping on each other’s toes, and we were getting intermittent 401 errors like you are describing. We were able to increase the number of access token by submitting an S2S token tolerance increase request They ask for account id, application id and client id, and the number of tokens you would like.

This was not ideal because we had to add logic to manage tokens for lambda vs server. And there is additional complexity managing them for lambda because we don’t know how many lambdas will be running at once. Hope this helps.

3 Likes

Actually, your comment is really helpful. We actually have exactly the same situation, the API and a Lambda using the same server-to-server OAuth Zoom app. Really thanks for your input :+1: :+1:

2 Likes