“HTTP 403 Server failed to authenticate the request” When Using Shared Access Signatures


 

One of the more common Azure Storage shared access signature issues I see is “403 Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.”  The challenge with this error is that it can often feel very random.  When you run your code on some computers it works fine, but on other computers you get a 403.  Or if you return a collection of SAS URLs to a client some of those URLs work fine and others get a 403.

 

The code to generate the SAS is typically very simple:

 1: string sas = azureContainer.GetSharedAccessSignature (new SharedAccessPolicy ()
 2: {
 3:     SharedAccessStartTime =  DateTime.UtcNow,
 4:     SharedAccessExpiryTime = DateTime.UtcNow.AddHours(1),
 5:     Permissions = SharedAccessPermissions.Write | SharedAccessPermissions.Read
 6: });

So how can it behave so randomly?

 

 

 

Troubleshooting

As with most Azure Storage problems, we want to start with Fiddler.  Download and install Fiddler and let it run and capture traffic while you reproduce the problem.  The Raw results will look something like the following:

 

Request

GET http://teststorage.blob.core.windows.net/Images/TestImage.png?st=2013-08-27T10%3A36%3A43Z&se=2013-08-27T11%3A31%3A43Z&sr=b&sp=r&sig=l95QElg18CEa55%2BXuhJIQz56gFFs%1FppYz%2E024uj3aYc%3D HTTP/1.1
Accept: image/png, image/svg+xml, image/*;q=0.8, */*;q=0.5
Accept-Language: en-US
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)
Accept-Encoding: gzip, deflate, peerdist
Proxy-Connection: Keep-Alive
Host: teststorage.blob.core.windows.net
X-P2P-PeerDist: Version=1.0

Response

HTTP/1.1 403 Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.
Proxy-Connection: Keep-Alive
Connection: Keep-Alive
Content-Length: 422
Via: 1.1 APS-PRXY-10
Date: Tue, 27 Aug 2013 10:36:41 GMT
Content-Type: application/xml
Server: Microsoft-HTTPAPI/2.0
x-ms-request-id: bf0d4d25-0110-4719-945c-afae8cbcdf0b
AuthenticationFailedServer failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.
RequestId:bf0d4d25-0110-4719-945c-afae8cbcdf0b
Time:2013-08-27T10:36:42.3077388Z Signature not valid in the specified time frame

 

I have highlighted the key portions of the Fiddler trace.  Notice the error message indicates that the shared access signature’s time frame is not valid. 

Looking at the Request URL and comparing it to the SAS documentation we can see that the start time is set for 10:36:43.  Given the code above we now that this is the value being returned on the client machine when calling DateTime.UtcNow.

Looking at the Response’s Date header we can see that the server side time is 10:36:41.  2 seconds earlier than the start time in the Request URL that the client created.  This means that the client machine’s system time is at least 2 seconds faster than the system time for the front end authentication server handling that particular Azure Storage request.  And the Azure Storage authentication server is going to reject this request because the client is attempting to access the storage resource 2 seconds earlier than the shared access signature allows.  Clock drift like this is not an uncommon scenario.

So depending on which machine is generating the SAS URL, how recently the system time was synchronized, the time delta between the SAS-generating machine and the storage servers, and the speed of the client issuing the SAS request, you will randomly get HTTP 403 responses.

 

 

 

Resolution

As we know from the SAS documentation the Start Time is optional (from MSDN: “Optional. The time at which the shared access signature becomes valid, in an ISO 8061 format. If omitted, start time for this call is assumed to be the time when the storage service receives the request.”).  If you are going to specify DateTime.Now as your start time, then why specify a start time at all?  The only reason you would want to specify a start time is if you were intentionally trying to time-delay a client’s access to a storage resource.

To resolve this issue simply remove the StartTime from the code such that it looks like this:

 1: string sas = azureContainer.GetSharedAccessSignature (new SharedAccessPolicy ()
 2: {
 3:     SharedAccessExpiryTime = DateTime.UtcNow.AddHours(1),
 4:     Permissions = SharedAccessPermissions.Write | SharedAccessPermissions.Read
 5: });

 


Comments (13)

  1. scicoria says:

    funny; just had my motherboard replaced and the tech failed to properly set the TimeDate.  this was the issue.

  2. Ravindra Sharma says:

    Removed Starttime, But still i'm getting the same error.

  3. kwill says:

    Ravindra, the blog post has instructions for troubleshooting this type of issue using Fiddler.  Have you done this yet and do you have more information?

  4. Phil says:

    This 403 happens to me frequently even when NOT using a SAS. Eventually it goes away. Appalling.

  5. kwill says:

    Phil, do you have any more details about your problem?  How are you authenticating your requests?  What does a Fiddler trace look like?

  6. senkwe says:

    You can still get this error (as I am now) even when you don't specify an Expiry StartTime.

    Pretty crappy service to be honest.

  7. kwill says:

    senkwe, do you have any additional details about the problem you are seeing?  403 authentication failures can be tricky to troubleshoot, but I have yet to see a single one caused by a problem with the service itself.  If you can share some code and the Request/Response details it would be helpful.

  8. Patrick says:

    Kevin,

    I have been struggling with this random 403 error for hours.  I have rewritten the SaS generation code a million times and it still randomly generates a 403 on file uploads.  Any help would greatly appreciated.

    Thanks,

    Patrick

    This is my SaS code located on Azure:

    **********************************************

           public string GetSaS(string containerName, string blobName)

           {

               var myAccount = CloudStorageAccount.Parse(ConfigurationManager.AppSettings["StorageConnectionString"].ToString());

               var myContainer = myAccount.CreateCloudBlobClient().GetContainerReference(containerName);

               myContainer.CreateIfNotExists();

               var myBlob = myContainer.GetBlockBlobReference(blobName);

               var sharedAccesSignature = myBlob.GetSharedAccessSignature(

                                new Microsoft.WindowsAzure.Storage.Blob.SharedAccessBlobPolicy()

                                {

                                    Permissions = Microsoft.WindowsAzure.Storage.Blob.SharedAccessBlobPermissions.Write | Microsoft.WindowsAzure.Storage.Blob.SharedAccessBlobPermissions.Read,

                                    SharedAccessStartTime = new DateTimeOffset(DateTime.UtcNow.AddMinutes(-5)),

                                    SharedAccessExpiryTime = DateTime.UtcNow.AddMinutes(5),

                                });

               return myBlob.Uri.AbsoluteUri + sharedAccesSignature;

           }

    This is my calling code:

    *****************************

    .controller('AccountDetailCtrl', function ($scope) {

       $scope.uploadfiles = function () {

           var files = fileControl.files;

           for (var i = 0, file; file = files[i]; i++) {

               var reader = new FileReader();

               reader.onloadend = (function (theFile) {

                   return function (e) {

                       $.ajax({

                           type: 'GET',

                           url: 'mytestingzone.azurewebsites.net/…/GetSaS + $("#ContainerName").val() + '&blobName=' + theFile.name,

                           success: function (res, status, xhr) {

                               upload(e.target.result, theFile.type, res);

                           },

                           error: function (res, status, xhr) {

                               alert("Can't get the Shared Access Signature");

                           }

                       });

                   };

               })(file);

               reader.readAsArrayBuffer(file);

           }

       }

    });

    This is the function called from the controller above:

    *************************************************************

    function upload(file, type, url) {

       var ajaxRequest = new XMLHttpRequest();

       ajaxRequest.open('PUT', url, true);

       ajaxRequest.setRequestHeader('Content-Type', type);

       ajaxRequest.setRequestHeader('x-ms-blob-type', 'BlockBlob');

       ajaxRequest.send(file);

    }

  9. Patrick, what does the response body show?  It will probably have a more verbose error that will give you a clue as to the source of the failure.  You can use Fiddler to easily get this information.

  10. Patrick says:

    The response shows the below message. As an FYI, when testing the upload, I have been selecting the same 7 audio files on each attempt.  The error does not occur on the same files each time, but it does occur on a random 3-4 out of the 7.  As an FYI, this is an Ionic/Cordova app running on the Ripple emulator for an iPad3.  The files which do not generate the 403 upload successfully.

    <Error>

    <Code>AuthenticationFailed</Code>

    <Message>

    Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature. RequestId:b153d38a-0001-001c-78c1-e5c92d000000 Time:2015-09-02T20:52:04.1706687Z

    </Message>

    <AuthenticationErrorDetail>Signature fields not well formed.</AuthenticationErrorDetail>

    </Error>

  11. Patrick, there isn't enough information here to determine root cause.  "Signature fields not well formed" is typically a problem with the URL that you are using, perhaps something like a missing &, a URL encoding problem, etc.  Try to compare the URLs of a successful upload to one that is failing.  If you can provide the full HTTP Request/Response including URL and Headers then I can take a look.

  12. Patrick says:

    Kevin,

    Below is some detail.  It is part 1 of 2.  The problem only occurs when I run the code from within an Ionic/Cordova app using the ripple emulator.  I created a simple Azure website and used the same client code calling the same Webapi and it worked like a charm.

    Ripple emulator for iPad3

    Cross Domain Prosy: Local

    Thanks,

    Patrick

    Request for SaS

    *******************

    GET /ripple/xhr_proxy?tinyhippos_apikey=ABC&tinyhippos_rurl=http%3A//bigbend.azurewebsites.net/Home/GetBlobSasUrl%3FblobName%3D01%20-%20Pocohontas.mp3 HTTP/1.1

    Host: localhost:4400

    Connection: keep-alive

    Accept: */*

    X-Ripple-User-Agent: Mozilla/5.0 (iPad; U; CPU OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B367 Safari/531.21.10

    User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36

    Referer: http://localhost:4400/index.html

    Accept-Encoding: gzip, deflate, sdch

    Accept-Language: en-US,en;q=0.8

    Cookie: _ga=GA1.1.75718487.1438959624

    Response to request for SaS

    *********************************

    HTTP/1.1 200 OK

    x-powered-by: ASP.NET

    cache-control: private

    content-length: 193

    content-type: text/html; charset=utf-8

    server: Microsoft-IIS/8.0

    x-aspnetmvc-version: 5.2

    x-aspnet-version: 4.0.30319

    access-control-allow-origin: *

    access-control-allow-methods: GET,POST,DELETE,HEAD,PUT,OPTIONS

    access-control-allow-headers: Origin, X-Olaround-Debug-Mode, Authorization, Accept

    access-control-expose-headers: X-Olaround-Debug-Mode, X-Olaround-Request-Start-Timestamp, X-Olaround-Request-End-Timestamp, X-Olaround-Request-Time, X-Olaround-Request-Method, X-Olaround-Request-Result, X-Olaround-Request-Endpoint

    set-cookie: ARRAffinity=4bb47419dbededa759fef4bd6c0e2c00689e1a7b43c2c4646297f8c7689e6267;Path=/;Domain=bigbend.azurewebsites.net

    date: Thu, 03 Sep 2015 02:01:57 GMT

    Connection: keep-alive

  13. Patrick says:

    Part 2 of 2

    This is passed to the ajax PUT request (quotes not included)–> "interval4storage.blob.core.windows.net/…/01 – Pocohontas.mp3?sv=2015-02-21&sr=b&sig=LP3uydviJDpqfPMver0fxh3Aqdu2J%2BGXaFSMnZRpsbg%3D&se=2015-09-03T02%3A31%3A58Z&sp=rwl"

    PUT to write blob

    *********************

    PUT /ripple/xhr_proxy?tinyhippos_apikey=ABC&tinyhippos_rurl=https%3A//interval4storage.blob.core.windows.net/someimagescontainer/01%20-%20Pocohontas.mp3%3Fsv%3D2015-02-21%26sr%3Db%26sig%3DLP3uydviJDpqfPMver0fxh3Aqdu2J%252BGXaFSMnZRpsbg%253D%26se%3D2015-09-03T02%253A31%253A58Z%26sp%3Drwl HTTP/1.1

    Host: localhost:4400

    Connection: keep-alive

    Content-Length: 5373952

    x-ms-blob-type: BlockBlob

    X-Ripple-User-Agent: Mozilla/5.0 (iPad; U; CPU OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B367 Safari/531.21.10

    User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36

    Origin: http://localhost:4400

    Content-Type: audio/mp3

    Accept: */*

    Referer: http://localhost:4400/index.html

    Accept-Encoding: gzip, deflate, sdch

    Accept-Language: en-US,en;q=0.8

    Cookie: _ga=GA1.1.75718487.1438959624

    Response to PUT to write blob

    ************************************

    HTTP/1.1 403 Forbidden

    X-Powered-By: Express

    Access-Control-Allow-Origin: http://localhost:4400

    Access-Control-Allow-Credentials: true

    Access-Control-Allow-Methods: HEAD, GET, POST, PUT, DELETE, OPTIONS, TRACE, CONNECT, PATCH

    content-length: 408

    content-type: application/xml

    server: Microsoft-HTTPAPI/2.0

    x-ms-request-id: 95203f78-0001-0056-25ec-e5f94a000000

    date: Thu, 03 Sep 2015 02:01:57 GMT

    Connection: keep-alive