Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unauthorized status code on CloudRecordings.DownloadFileAsync invocation. #348

Closed
pvgritsenko-ansible opened this issue Jul 2, 2024 · 19 comments
Assignees
Labels
Enhancement New feature or request
Milestone

Comments

@pvgritsenko-ansible
Copy link
Contributor

pvgritsenko-ansible commented Jul 2, 2024

Sometimes when we try to download Zoom recording file we encounter following Pathoschild.Http.Client.ApiException with status code 'Unauthorized'. The only idea is access token expired and for some reasone it was not successfuly updated by OAuthTokenHandler.

And may it be possible that the source of issue is ZoomRetryCoordinator.ExecuteAsync method where we check the responseContent.message contains "access token is expired" string. I've simulated the exception and noted that actually message is empty. Can it be empty just because we cannot read stream content?

Full stack-trace:

Exception: Pathoschild.Http.Client.ApiException: The API query failed with status code Unauthorized: Unauthorized
   at Pathoschild.Http.Client.Extensibility.DefaultErrorFilter.OnResponse(IResponse response, Boolean httpErrorAsException)
   at Pathoschild.Http.Client.Internal.Request.Execute()
   at Pathoschild.Http.Client.Internal.Request.AsStream()
   at ZoomNet.Resources.CloudRecordings.DownloadFileAsync(String downloadUrl, CancellationToken cancellationToken)
@Jericho
Copy link
Owner

Jericho commented Jul 3, 2024

Are you able to capture the response from Zoom (using a tool such as Fiddler for example)? This would allow us to validate your theory rather than just guessing.

@Jericho
Copy link
Owner

Jericho commented Jul 3, 2024

I wrote a unit test to simulate the scenario in your theory and I am not able to reproduce the problem, the unit test completes successfully. This seems to indicate that the problem you are experiencing is not related to an expired token (or that my unit test does not reflect your scenario accurately).

/// <summary>
/// This unit test simulates a scenario where we attempt to download a file but our oAuth token has expired.
/// In this situation, we expect the token to be refreshed and the download request to be reissued.
/// </summary>
/// <returns></returns>
[Fact]
public async Task DownloadFileAsync_with_expired_token()
{
	// Arrange
	var downloadUrl = "http://dummywebsite.com/dummyfile.txt";

	var mockTokenHttp = new MockHttpMessageHandler();
	mockTokenHttp // Issue a new token
		.When(HttpMethod.Post, "https://api.zoom.us/oauth/token")
		.Respond(HttpStatusCode.OK, "application/json", "{\"refresh_token\":\"new refresh token\",\"access_token\":\"new access token\"}");

	var mockHttp = new MockHttpMessageHandler();
	mockHttp // The first time the file is requested, we return "401 Unauthorized" to simulate an expired token.
		.Expect(HttpMethod.Get, downloadUrl)
		.Respond(HttpStatusCode.Unauthorized, new StringContent("{\"message\":\"access token is expired\"}"));
	mockHttp // The second time the file is requested, we return "200 OK" with the file content.
		.Expect(HttpMethod.Get, downloadUrl)
		.Respond(HttpStatusCode.OK, new StringContent("This is the content of the file"));

	var client = Utils.GetFluentClient(mockHttp, mockTokenHttp);
	var recordings = new CloudRecordings(client);

	// Act
	var result = await recordings.DownloadFileAsync(downloadUrl, CancellationToken.None).ConfigureAwait(true);

	// Assert
	mockHttp.VerifyNoOutstandingExpectation();
	mockHttp.VerifyNoOutstandingRequest();
	result.ShouldNotBeNull();
}

Is it possible that the Unauthorized response is legitimate and you are simply not authorized to download this file? I know that Zoom documentation mentions something about an access token specific to downloading file and different than your own access token:

If a user has authorized and installed your OAuth app that contains recording scopes, use the download_access_token or the the user's OAuth access token to download the file, and set the access_token as a Bearer token in the Authorization header.

ZoomNet currently does not support this alternate token and always uses the token that was obtained when initiating your OAuth session. Maybe this is the scenario you are facing? Maybe your access token is not sufficient to download this file? I'm just speculating.

@Jericho
Copy link
Owner

Jericho commented Jul 12, 2024

@pvgritsenko-ansible are you still interested in researching this problem? In case I wasn't clear, let me reiterate that I was not able to reproduce and therefore I need more information from you to continue researching. Capturing the payload you receive from Zoom would be a great first step. Also, I shared with you some hypothesis. Do they make sense? Do you have any feedback?

@pvgritsenko-ansible
Copy link
Contributor Author

@pvgritsenko-ansible are you still interested in researching this problem? In case I wasn't clear, let me reiterate that I was not able to reproduce and therefore I need more information from you to continue researching. Capturing the payload you receive from Zoom would be a great first step. Also, I shared with you some hypothesis. Do they make sense? Do you have any feedback?

Hi, @Jericho yes, I still try to find the source of problem. Sorry to don't keep you up to date. It's hard to capture the Zoom response since the issue for some reasone does't reproduce on dev machines (only in PROD env where we can't setup any additional apps). But I'm going to test another way to simulate this problem. I'll write to you this week and report results.

About your hypothesis. It is possible, but we've downloaded files before using a general access token. It is also strange that in this case the issue occurs only with some files, and not with all.

@Jericho
Copy link
Owner

Jericho commented Jul 16, 2024

Is this problem consistent with a given file? What I mean is: do you get this error consistently when you attempt to download a certain file or does this problem go away after a certain amount of time? If the problem goes away, maybe it gives credence to your original hypothesis this it's related to an expired token.

However, if the problem is consistent, maybe it points to the fact that there's some additional security around this file and it prevents you from downloading it. And if this is the case, maybe we need to invest time and effort to support the download_access_token I mentioned previously.

@pvgritsenko-ansible
Copy link
Contributor Author

pvgritsenko-ansible commented Jul 17, 2024

We try do download some transcript file periodically. And usually we don't have such exception. But sometimes it occurs.
Our simplified workflow looks like:

  1. Complete some meeting with recordings.
  2. Wait for "recording.transcript_completed" messages on our webhook and extract meeting instance UUID from message payload
  3. Check the files really prepared to download (sometimes they are not prepared and we need to periodically poll file statuses until they are completed) using ZoomClient.CloudRecordings.GetRecordingInformationAsync
  4. When the files completed we try to download them using DownloadUrl that we have taken from ZoomClient.CloudRecordings.GetRecordingInformationAsync method response

And sometimes at the step 4 we receive Unauthorized exception.

Jericho added a commit that referenced this issue Jul 22, 2024
…348

Unfortunately, the unit test is successful which means that it is not reproducing the problem
@pvgritsenko-ansible
Copy link
Contributor Author

There is an update.
I found following issue thread on the Zoom dev forum that referenced to the same problem: https://devforum.zoom.us/t/finding-the-value-for-download-access-token-for-the-get-meeting-recordings-api-endpoint/109685/9

And unfortunately it looks like the source of issue on the Zoom side.
In our project we will try to use download_access_token instead of common access_token. But I'm not sure that it will help us.
I'll post some comment if I have any updates on this story...

@Jericho
Copy link
Owner

Jericho commented Aug 1, 2024

The download_access_token is something I mentioned to you a few weeks ago:

I know that Zoom documentation mentions something about an access token specific to downloading file and different than your own access token:

If a user has authorized and installed your OAuth app that contains recording scopes, use the download_access_token or the the user's OAuth access token to download the file, and set the access_token as a Bearer token in the Authorization header.

ZoomNet currently does not support this alternate token and always uses the token that was obtained when initiating your OAuth session. Maybe this is the scenario you are facing? Maybe your access token is not sufficient to download this file? I'm just speculating.

And in a subsequent comment I said:

if the problem is consistent, maybe it points to the fact that there's some additional security around this file and it prevents you from downloading it. And if this is the case, maybe we need to invest time and effort to support the download_access_token I mentioned previously.

Let me know if your testing with this download_access_token is conclusive. If so, we can look into enhancing ZoomNet to support it.

@pvgritsenko-ansible
Copy link
Contributor Author

Yes, I've read your comments before. That's one of the reason why we'll try to use download_access_token.
I'll prepare a little PR with Recording model and related cloud recordings endpoint update to include this token to response.

@Jericho Jericho self-assigned this Aug 1, 2024
@Jericho Jericho added the Enhancement New feature or request label Aug 1, 2024
@Jericho Jericho added this to the 0.80.0 milestone Aug 1, 2024
@Jericho
Copy link
Owner

Jericho commented Aug 1, 2024

I've prepared a beta NuGet package for you with two improvements:

  • CloudRecordings.GetRecordingInformationAsync has been enhanced to return the download_access_token. Also, when you invoke this method you can specify the "time to live" for the token (in seconds).
  • CloudRecordings.DownloadFileAsync has been enhanced to accept an optional token. If you omit this value, the token for your current OAuth session will be used which matches the current behavior.

This package is called 'ZoomNet 0.80.0-download-access-0018' and it's available on my personal NuGet feed (instructions).

Let me know if this helps.

@pvgritsenko-ansible
Copy link
Contributor Author

pvgritsenko-ansible commented Aug 2, 2024

Thanks a lot!
I'll check it

@pvgritsenko-ansible
Copy link
Contributor Author

I have tested 'ZoomNet 0.80.0-download-access-0018'.
The GetRecordingInformationAsync method works well.

But it seems that something is wrong with DownloadFileAsync. I created a ZoomClient with an expired token using the constructor. Then, using Postman, I got a valid access_token and passed it as an argument to the 'DownloadFileAsync' method. But the method returned me an 'Unauthorized' exception.
After that, through Postman, I got the 'download token' and tried to use it as an argument. The result was the same.

Maybe I missed something?

@Jericho
Copy link
Owner

Jericho commented Aug 5, 2024

Can you please provide code snippet to help me reproduce

@Jericho
Copy link
Owner

Jericho commented Aug 5, 2024

I created a ZoomClient with an expired token using the constructor.

I'm guessing you're using an expired token because you want to see if ZoomNet will refresh this expired token.

Then, using Postman, I got a valid access_token and passed it as an argument to the 'DownloadFileAsync' method.

You have previously established that Zoom is rejecting your OAuth token when you attempt to download a file. If your OAuth token is not valid to download a file, I highly doubt that the tool you use to get a refreshed token will change anything. I don't think Zoom cares that use used Postman or any other tool to renew your token. You token is not valid, period. Therefore, I'm not surprised that Zoom is rejecting this OAuth token.

After that, through Postman, I got the 'download token' and tried to use it as an argument. The result was the same.

The improvement I made to GetRecordingInformationAsync includes the download token that you should use to download the file, therefore you shouldn't need to use Postman for this.

your code should look something like this:

// The Id of the meeting
var meetingId = 123;

// Use a ttl that seems reasonable to you. In this example, I'm using 5 minutes.
const int ttl = 60 * 5;

// Get recording information for the meeting. This includes the download_access_token
var recordingInfo = await client.CloudRecordings.GetRecordingInformationAsync(meetingId, ttl, cancellationToken).ConfigureAwait(false);

// This is the new property added in the beta NuGet package
var downloadToken = recordingInfo.DownloadAccessToken;

// Download all files for the meeting. Don't forget to specify the download token when invoking the DownloadFileAsync method
foreach (var recordingFile in recordingInfo.RecordingFiles)
{
    var stream = await client.CloudRecordings.DownloadFileAsync(recordingFile.DownloadUrl, downloadToken, cancellationToken).ConfigureAwait(false);
}

Having said that, if you are still getting the 'Unauthorized' exception, I think it demonstrate that the token is probably not the source of this problem. I think it's time for you to escalate this problem to Zoom support and get guidance from them.

@pvgritsenko-ansible
Copy link
Contributor Author

pvgritsenko-ansible commented Aug 5, 2024

I just wanted to check that when I use the 'DownloadFileAsync' method with a download token, this token will be used even if the access token has expired. My workflow:

  1. Get OAuth token from Zoom using Postman.
  2. Wait for token expiration
  3. Create ZoomNet.ZoomClient with expired token parameter.
  4. Generate new access token using Postman
  5. Get Zoom Recordings info using Postman. Retrieve download_token and download_url from given info
  6. Call ZoomClient.CloudRecordings.DownloadFileAsync(download_url, download_token). Here I get an Unauthorized exception, which indicates that access_token is used to download file, with which ZoomClient was created.

If I create new ZoomClient without token argument and call ZoomClient.CloudRecordings.DownloadFileAsync(download_url, download_token), it will work as expected and files will be downloaded. No metter how I retrieved download_url and download_token, by Postman or ZoomClient.CloudRecordings.GetRecordingInformationAsynce.
I'm not sure if this will help, but here is the code with mockup data that describes what I did:

        private ZoomClient CreateZoomClient(string token = null)
        {
            var connectionInfo = OAuthConnectionInfo.ForServerToServer(
                "MyClientId",
                "MyClientSecret",
                "MyAccountId",
                accessToken: token);

            return new ZoomClient(connectionInfo);
        }
        
        public async Task<Stream> TestMethod()
        {
		ZoomClient _zoomClient = CreateZoomClient("Expired_token");
		try
		{
			return await _zoomClient.CloudRecordings.DownloadFileAsync("download_url_given_from_postman", "download_token_given_from_postman");
		}
		catch (Pathoschild.Http.Client.ApiException apiException)
		{
                        if (apiException.Status != System.Net.HttpStatusCode.Unauthorized)
                        {
                               // Here I get an exception.
                        }
		}
        }

And actually we already on the way to start the Zoom support thread.

@Jericho
Copy link
Owner

Jericho commented Aug 5, 2024

  1. Call ZoomClient.CloudRecordings.DownloadFileAsync(download_url, download_token). Here I get an Unauthorized exception, which indicates that access_token is used to download file, with which ZoomClient was created.

I didn't think about the fact that the OAuth handler in our library (here) overrides the custom token. Let me fix that and I'll publish a new beta package.

I'm not sure if this will help, but here is the code with mockup data that describes what I did

Yes, it's always helpful. Thank you for providing this.

@Jericho
Copy link
Owner

Jericho commented Aug 6, 2024

Beta package 0.80.0-download-access-token.1-20 published

@pvgritsenko-ansible
Copy link
Contributor Author

Thanks, it works as expected. I will integrate this version of ZoomNet into my project and monitor for "unauthorized" exceptions.

@Jericho Jericho closed this as completed in e8bed59 Aug 7, 2024
@Jericho
Copy link
Owner

Jericho commented Aug 7, 2024

🎉 This issue has been resolved in version 0.80.0 🎉

The release is available on:

Your GitReleaseManager bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants