1
votes

I am uploading files from Cordova to Azure Blob Storage through a C# API that I created. Recently, the upload stopped working. I had been making changes, so I reverted my changes but the issue is still occurring. I've debugged the process as best I can but I seem to be stuck.

The Process

When I want to upload a file, these steps are taken:

  1. Data about the file is uploaded to Azure SQL. During the insert, a SAS key is generated and returned.
  2. The file is uploaded to the C# Web API via JavaScript and the SAS key is sent along with it.
  3. The Web API stores the file locally and then calls the WebClient.UploadFile method using the SAS key for authentication.
  4. The Web API should return a 200.

The Error

The first part of the process works well. I generate a sas key properly (with a five minute window - I've expanded it to 15 without success) and the JavaScript upload of the file to C# Web API goes well (I've put a breakpoint in the code and verified everything). The issue arises when I call the WebClient.UploadFile method. It used to work without issue. Now I get a 403 error. The status property of the error says ProtocolError. When I run it in Fiddler, The response I get back is:

Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.

Signature fields not well formed.

The Request

Here is the raw view of the PUT call to my Azure storage account (names changed to protect the innocent):

PUT

https://myappname.blob.core.windows.net/myapp/792b6b3c-d8df-4100-f9be-bbd69be7f307.jpg?se=2014-11-11T19%253A32%253A33Z&sr=b&sp=w&sig=Lk5SiEEaGUwyOjxjUGkIjink26Gwe7VpXMBPw975ZrM%253D

HTTP/1.1 x-ms-blob-type: BlockBlob x-ms-date: Tue, 11 Nov 2014

19:17:24 GMT x-ms-version: 2012-02-12 Content-Type:

application/octet-stream Host: myappname.blob.core.windows.net

Content-Length: 74784 Expect: 100-continue

And here is the SAS key that I generated:

se=2014-11-11T19%3A32%3A33Z&sr=b&sp=w&sig=Lk5SiEEaGUwyOjxjUGkIjink26Gwe7VpXMBPw975ZrM%3D

The Code

Here is my C# Code:

var client = new WebClient();
client.Headers.Add("x-ms-blob-type", "BlockBlob");
client.Headers.Add("x-ms-date", DateTime.UtcNow.ToString("R",
    System.Globalization.CultureInfo.InvariantCulture));
client.Headers.Add("x-ms-version", "2012-02-12");
sas = Request.RequestUri.Query.Substring(Request.RequestUri.Query.IndexOf("sas=") + 4);

startIndex = Request.RequestUri.Query.IndexOf("name=") + 5;
endIndex = Request.RequestUri.Query.IndexOf("&", startIndex);
fileName = Request.RequestUri.Query.Substring(startIndex, endIndex - startIndex);

client.UploadFile(string.Format(@"{0}/{1}?{2}", endpoint, fileName, sas), "PUT", file.LocalFileName);

And here is my JavaScript code that runs on the Insert call of to the SQL Azure table via Mobile Services:

var azure = require('azure');
var qs = require('querystring');
var appSettings = require('mobileservice-config').appSettings;

function insert(item, user, request) {
    // Get storage account settings from app settings. 
    var accountName = appSettings.STORAGE_ACCOUNT_NAME;
    var accountKey = appSettings.STORAGE_ACCOUNT_ACCESS_KEY;
    var host = accountName + '.blob.core.windows.net';

    if ((typeof item.containerName !== "undefined") && (
    item.containerName !== null)) {
        // Set the BLOB store container name on the item, which must be lowercase.
        item.containerName = item.containerName.toLowerCase();

        // If it does not already exist, create the container 
        // with public read access for blobs.        
        var blobService = azure.createBlobService(accountName, accountKey, host);
        blobService.createContainerIfNotExists(item.containerName, {
            publicAccessLevel: 'blob'
        }, function(error) {
            if (!error) {

                // Provide write access to the container for the next 5 mins.        
                var sharedAccessPolicy = {
                    AccessPolicy: {
                        Permissions: azure.Constants.BlobConstants.SharedAccessPermissions.WRITE,
                        Expiry: new Date(new Date().getTime() + 5 * 60 * 1000)
                    }
                };

                // Generate the upload URL with SAS for the new image.
                var sasQueryUrl = 
                blobService.generateSharedAccessSignature(item.containerName, 
                item.resourceName, sharedAccessPolicy);

                // Set the query string.
                item.sasQueryString = qs.stringify(sasQueryUrl.queryString);

                // Set the full path on the new new item, 
                // which is used for data binding on the client. 
                item.imagePath = sasQueryUrl.baseUrl + sasQueryUrl.path;

            } else {
                console.error(error);
            }
            request.execute();
        });
    } else {
        request.execute();
    }
}

Like I said, on the C#/Azure side, nothing has changed to my knowledge. I had been playing around with the file upload on the Cordova side, but since the file gets successfully to the C# Web API, I can't see how that would be an issue.

I am getting errors in my Azure Storage logs, but I'm not sure if this is truly an error or not. The error I am getting is a 409 on the CreateContainer action. The message is ClientOtherError. The reason I'm not sure this is related or not is because of the createContainerIfNotExists method. I'm not sure if that would generate an error if the container already exists (doesn't seem like it should but you never know).

2
Is your Web API code running on local computer or in Azure? Can you please check the clock on your computer and see if its off by more than 15 minutes?Gaurav Mantri
@GauravMantri Sorry, I should have mentioned that I tried that. I'm running the API locally for testing but in Azure for production (tested against both). My clock is accurate and the timezone isn't an issue. I even put a start time for the upload of 15 hours in the past just to be sure, and that didn't work.IAmTimCorey
Have you tried Date.UTC() instead of new Date().getTime()?b2zw2a
One thing I noticed in your request URL is that % sign is getting encoded as %25 which should not be happening. Your request URL should end with %3D which is URL encoding for =. Can you please check why this is happening?Gaurav Mantri
@plentysmart SAS is calculated in Mobile Service which is running in Azure. In Azure everything is in UTC, so Date().getTime() would return UTC date/time.Gaurav Mantri

2 Answers

3
votes

One thing I noticed in your request URL is that % sign is getting encoded as %25 which should not be happening. Your request URL should end with %3D which is URL encoding for =.

-1
votes

It looks like you are missing Authentication header. See documentation:

Authorization - Required. Specifies the authentication scheme, account name, and signature. For more information, see Authentication for the Azure Storage Services.

Details how to set this header can be found here

EDIT: Wrong, as pointed out in comment - "An authenticated request must include the Authorization header. If this header is not included, the request is anonymous and may only succeed against a container or blob that is marked for public access , or against a container, blob, queue, or table for which a shared access signature has been provided for delegated access"