BUG downloading cloud recordings with access token set results in an invalid response

Steps to reproduce:

  1. List recordings for a meeting
  2. Use recording meeting with query parameter access_token set with a valid access token.
  3. 200 response received with body {"status":false,"errorCode":401,"errorMessage":"java.lang.IllegalArgumentException: Scope cannot be null","result":null}

However not setting the access_token query parameter will successfully download the correct recording.

3 Likes

Just wanted to post an update, the header is being set correctly to a 401 in that situation, my mistake. However the behaviour seems inconsistent.

Is this by design blocking a request with an access token while letting one through without one? What am I supposed to do with password protected files?

Setting the recording to private and attempting to download without an access token does return a 200 response, even though it’s an html error page.

And finally tried one last time with both public and private recordings, setting the authorization header returns a 401 with this response.
{"status":false,"errorCode":401,"errorMessage":"java.lang.IllegalArgumentException: urn:zoom:connect:clientid:<my client id> not support","result":null}.

Hi @matt,

Are you using one of our SDK’s ? or are you using your custom implementation?

Can you please provide us with a sample request that you are trying to send?

Thanks!

Hey @ojus.zoom, I wouldn’t call sending requests to the rest api without an sdk custom, just regular usage of a web api.

Anyway here are results that can be duplicated when the recording is public or private. I have redacted all sensitive information.

Setting the access token in the url query

First is setting the access token query parameter, this is usually done with a webhook that includes a download token. But a download token is not available outside the context of the webhook so here it is being substituted by the user’s access token obtained from their refresh token.

curl -v "https://api.zoom.us/recording/download/recording/download/<< download id>>?access_token=<< access token obtained from refresh token>>"
*   Trying 52.202.62.237...
* TCP_NODELAY set
* Connected to api.zoom.us (52.202.62.237) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/cert.pem
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Client hello (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN, server did not agree to a protocol
* Server certificate:
*  subject: OU=Domain Control Validated; CN=*.zoom.us
*  start date: Mar 25 19:38:42 2019 GMT
*  expire date: Mar 25 19:38:42 2021 GMT
*  subjectAltName: host "api.zoom.us" matched cert's "*.zoom.us"
*  issuer: C=US; ST=Arizona; L=Scottsdale; O=GoDaddy.com, Inc.; OU=http://certs.godaddy.com/repository/; CN=Go Daddy Secure Certificate Authority - G2
*  SSL certificate verify ok.
> GET /recording/download/<< download id>>?access_token=<< access token obtained from refresh token>> HTTP/1.1
> Host: api.zoom.us
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< Date: Thu, 16 May 2019 20:27:34 GMT
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< Server: ZOOM
< x-zm-trackingid: WEB_39d8a5536bcac863bc669f7ae0edbe9c
< Set-Cookie: _zm_mtk_guid=<<cookie stuff>>; Domain=.zoom.us; Expires=Tue, 03-Jun-2087 23:41:41 GMT; Path=/; Secure
<
* Connection #0 to host api.zoom.us left intact
{"status":false,"errorCode":401,"errorMessage":"java.lang.IllegalArgumentException: Scope cannot be null","result":null}%

What’s funky is we have all the correct scopes being set, and the access token is being validated because I messed up and forgot a letter in the access token which resulted in a different error stating that it was an invalid access token.

Setting the Authorization Header

Next is setting the Authorization header, which is how the rest of the zoom api performs, this results in a different error seen here.

curl -v -H "Authorization: Bearer <<access token obtained from client refresh token>>" "https://api.zoom.us/recording/download/<<download id>>"
*   Trying 52.202.62.238...
* TCP_NODELAY set
* Connected to api.zoom.us (52.202.62.238) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/cert.pem
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Client hello (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN, server did not agree to a protocol
* Server certificate:
*  subject: OU=Domain Control Validated; CN=*.zoom.us
*  start date: Mar 25 19:38:42 2019 GMT
*  expire date: Mar 25 19:38:42 2021 GMT
*  subjectAltName: host "api.zoom.us" matched cert's "*.zoom.us"
*  issuer: C=US; ST=Arizona; L=Scottsdale; O=GoDaddy.com, Inc.; OU=http://certs.godaddy.com/repository/; CN=Go Daddy Secure Certificate Authority - G2
*  SSL certificate verify ok.
> GET /recording/download/<<download id>> HTTP/1.1
> Host: api.zoom.us
> User-Agent: curl/7.54.0
> Accept: */*
> Authorization: Bearer <<access token obtained from client refresh token>>
>
< HTTP/1.1 401 Unauthorized
< Date: Thu, 16 May 2019 20:31:17 GMT
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< Server: ZOOM
< x-zm-trackingid: WEB_bfeee8f55376baf1272152960b86a627
< Set-Cookie: _zm_mtk_guid=<<cookie stuff>>; Domain=.zoom.us; Expires=Tue, 03-Jun-2087 23:45:24 GMT; Path=/; Secure
<
* Connection #0 to host api.zoom.us left intact
{"status":false,"errorCode":401,"errorMessage":"java.lang.IllegalArgumentException: urn:zoom:connect:clientid:<<my client id>> not support","result":null}%

What’s crazy here is it even returned my client id.

Success

Here is a correct one for a public recording, this is nothing special as I have no auth provided.

curl -v "https://api.zoom.us/recording/download/<<download id>>"
*   Trying 52.202.62.238...
* TCP_NODELAY set
* Connected to api.zoom.us (52.202.62.238) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/cert.pem
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Client hello (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN, server did not agree to a protocol
* Server certificate:
*  subject: OU=Domain Control Validated; CN=*.zoom.us
*  start date: Mar 25 19:38:42 2019 GMT
*  expire date: Mar 25 19:38:42 2021 GMT
*  subjectAltName: host "api.zoom.us" matched cert's "*.zoom.us"
*  issuer: C=US; ST=Arizona; L=Scottsdale; O=GoDaddy.com, Inc.; OU=http://certs.godaddy.com/repository/; CN=Go Daddy Secure Certificate Authority - G2
*  SSL certificate verify ok.
> GET /recording/download/<<download id>> HTTP/1.1
> Host: api.zoom.us
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Thu, 16 May 2019 20:23:18 GMT
< Content-Type: txt;charset=UTF-8
< Content-Length: 495
< Connection: keep-alive
< Server: ZOOM
< x-zm-trackingid: WEB_0c253a7085e655c599d8bbfdcfc85bc6
< Set-Cookie: _zm_mtk_guid=<<more cookie stuff>>; Domain=.zoom.us; Expires=Tue, 03-Jun-2087 23:37:24 GMT; Path=/; Secure
< X-Robots-Tag: noindex, nofollow
< X-Content-Type-Options: nosniff
< Set-Cookie: cred=<<cookie stuff>>; Path=/; Secure; HttpOnly
< Set-Cookie: _zm_page_auth=<<hide this>>; Domain=.zoom.us; Path=/; Secure; HttpOnly
< p3p: CP="NOI ADM DEV PSAi COM NAV OUR OTR STP IND DEM"
< Set-Cookie: _zm_ssid=<<more cookie stuff>>; Domain=.zoom.us; Path=/; Secure; HttpOnly
< Content-Disposition: attachment;filename=GMT20190513-223817_-No-title-.transcript.vtt
< Strict-Transport-Security: max-age=31536000
< X-XSS-Protection: 1; mode=block
<
WEBVTT

<< web vtt file stuff>>

* Connection #0 to host api.zoom.us left intact

So far the only time I can download a private recording on behalf of a user is when the webhook provides a download token, and a public one can only be downloaded by that token or without authentication.

Hi @matt,

Thank you for sending a detailed response.

It is expected behavior that a download token is required to download a recording.
If the recording is a private recording, it is only available internally (i.e. for account members only). The download token is nothing but an OAuth Token, which allows you to access the user’s recording without exposing his password. The other way that you can access a private recording is by logging in as the user.

Since public recording is available to everyone (who has the download link), you do not need to authenticate. You still need the token to access the recording.

If you need to keep a recording public, but want to share it only with a limited number of people, I would recommend you enable the password protect option.

I hope that answers your question and resolves the issue.

Thanks,

Hey @ojus.zoom, not particularly. How would I get a download token if this is not coming from a webhook? I cannot seem to find any documentation for getting a download token.

The webhook includes the download link. Are you saying that the download link in the weebhook is not working?

This is what the payload for a recording complete looks like:

{
  "event": "string",
  "payload": {
    "account_id": "string",
    "object": {
      "id": "string",
      "uuid": "string",
      "host_id": "string",
      "topic": "string",
      "type": "integer",
      "start_time": "string",
      "timezone": "string",
      "duration": "integer",
      "share_url": "string",
      "total_size": "string",
      "recording_count": "integer",
      "recording_files": [
        {
          "id": "string",
          "meeting_id": "string",
          "recording_start": "string",
          "recording_end": "string",
          "file_type": "string",
          "file_size": "number",
          "play_url": "string",
          "download_url": "string",
          "status": "string",
          "recording_type": "string"
        }
      ]
    }
  }
}

You can find the recording webhook events here: https://marketplace.zoom.us/docs/api-reference/webhook-reference/recording-events

Hey Tim,

I think we are different pages here.

My issue is downloading recordings not coming from the web hook. I am using the access token I obtained from the refresh token as the download token, but that does not work and raises errors on the server side.

Is there a way to fetch a token that has access to download recordings without going the webhook route.

We already have a webhook set up that works fine, I am currently writing a service that backfills recordings in case of a service disruption, and to handle transcript webhooks not firing.

Got ya,

You can totally get the download link by doing a search for individual meetings.

This endpoint will return all available recording files in an object that you can then go download/view. The important bit is the download link in the recording_files array that will come with a download link for each object in the array.

Yep, did that too! Unfortunately this does not bypass password protected files and private ones, it only works on public files.

The webhook with a download token bypasses this.

Ah, interesting. Ok, can you tell me which scopes your tokens have on it? Also, is this a JWT or OAuth implementation? Account Level or user Level?

OAuth on the user level.

Scopes are

  • meeting:read
  • meeting:write
  • recording:read
  • recording:write
  • user:read

Looks like it’s a bug as you suspected. Thank you for working with me to give me additional details. I raised this internally with our eng team for a resolution.

In case you work with someone else in the future you can reference DEVELOPERS-286 as the ticket for tracking.

1 Like

In the meantime, I have tested and you can use JWT instead of OAuth tokens to get this done. :slight_smile:

Hi, I have the same issue using JWT token for non-public recordings. I’m getting 401 HTTP. Is there any way to download non-public recordings with JWT token?

@patricio.giacomino,

can you please provide some more details on your error?

It’s been a month since reporting, any updates on this?

@matt,

Thank you for your patience. We are still waiting on resolving this error.

Thanks!