question

ANSITDelta-0952 avatar image
0 Votes"
ANSITDelta-0952 asked cooldadtx commented

Azure Storage Rest API (Put Blob API)

I am trying to put a blob with Azure rest API. I made a "GET" request successfully but i had issues with "PUT" request. When i try to make "PUT" request I get a 403 error(Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.).

  string uri = string.Format("https://{0}.blob.core.windows.net/{1}/LibraryForm.png",                storageAccountName,containerName);
             Byte[] requestPayload =  File.ReadAllBytes(imagepath);
             using (var httpRequestMessage = new HttpRequestMessage(HttpMethod.Put, uri)
             { Content = (requestPayload == null) ? null : new ByteArrayContent(requestPayload) })
             {
    
                 // Add the request headers for x-ms-date and x-ms-version.
                 DateTime now = DateTime.UtcNow;
                 httpRequestMessage.Headers.Add("x-ms-date", now.ToString("R", CultureInfo.InvariantCulture));
                 httpRequestMessage.Headers.Add("x-ms-version", "2020-06-12");
                 httpRequestMessage.Headers.Add("x-ms-blob-type", "BlockBlob");
                 httpRequestMessage.Headers.Add("x-ms-blob-content-type", "image/png");
    
                 // Add the authorization header.
                 httpRequestMessage.Headers.Authorization = GetAuthorizationHeader(
                    storageAccountName, storageAccountKey, now, httpRequestMessage);
                 // Send the request.
                 using (HttpResponseMessage httpResponseMessage = await new HttpClient().SendAsync(httpRequestMessage))
                 {
                     if (httpResponseMessage.StatusCode == HttpStatusCode.OK)
                     {
                         var str = httpResponseMessage.Content.ReadAsStringAsync().Result;
                         return str;
                     }
                 }
             }
    
    
 internal static AuthenticationHeaderValue GetAuthorizationHeader(string storageAccountName, string storageAccountKey, DateTime now,
         HttpRequestMessage httpRequestMessage, string ifMatch = "", string md5 = "")
         {
             // This is the raw representation of the message signature.
             HttpMethod method = httpRequestMessage.Method;
             String MessageSignature = String.Format("{0}\n\n\n{1}\n{5}\n\n\n\n{2}\n\n\n\n{3}{4}",
                       method.ToString(),
                       (method == HttpMethod.Get || method == HttpMethod.Head) ? String.Empty
                         : httpRequestMessage.Content.Headers.ContentLength.ToString(),
                       ifMatch,
                       GetCanonicalizedHeaders(httpRequestMessage),
                       GetCanonicalizedResource(httpRequestMessage.RequestUri, storageAccountName),
                       md5);
    
             // Now turn it into a byte array.
             byte[] SignatureBytes = Encoding.UTF8.GetBytes(MessageSignature);
    
             // Create the HMACSHA256 version of the storage key.
             HMACSHA256 SHA256 = new HMACSHA256(Convert.FromBase64String(storageAccountKey));
    
             // Compute the hash of the SignatureBytes and convert it to a base64 string.
             string signature = Convert.ToBase64String(SHA256.ComputeHash(SignatureBytes));
    
             // This is the actual header that will be added to the list of request headers.
             // You can stop the code here and look at the value of 'authHV' before it is returned.
             AuthenticationHeaderValue authHV = new AuthenticationHeaderValue("SharedKey",
                 storageAccountName + ":" + Convert.ToBase64String(SHA256.ComputeHash(SignatureBytes)));
             return authHV;
         }
    
 private static string GetCanonicalizedHeaders(HttpRequestMessage httpRequestMessage)
         {
             var headers = from kvp in httpRequestMessage.Headers
                           where kvp.Key.StartsWith("x-ms-", StringComparison.OrdinalIgnoreCase)
                           orderby kvp.Key
                           select new { Key = kvp.Key.ToLowerInvariant(), kvp.Value };
    
             StringBuilder sb = new StringBuilder();
    
             // Create the string in the right format; this is what makes the headers "canonicalized" --
             //   it means put in a standard format. http://en.wikipedia.org/wiki/Canonicalization
             foreach (var kvp in headers)
             {
                 StringBuilder headerBuilder = new StringBuilder(kvp.Key);
                 char separator = ':';
    
                 // Get the value for each header, strip out \r\n if found, then append it with the key.
                 foreach (string headerValues in kvp.Value)
                 {
                     string trimmedValue = headerValues.TrimStart().Replace("\r\n", String.Empty);
                     headerBuilder.Append(separator).Append(trimmedValue);
    
                     // Set this to a comma; this will only be used
                     //   if there are multiple values for one of the headers.
                     separator = ',';
                 }
                 sb.Append(headerBuilder.ToString()).Append("\n");
             }
             return sb.ToString();
         }
    
    
 private static string GetCanonicalizedResource(Uri address, string storageAccountName)
         {
             StringBuilder sb = new StringBuilder("/").Append(storageAccountName).Append(address.AbsolutePath);
    
             // It will have more entries if you have more query parameters.
             NameValueCollection values = HttpUtility.ParseQueryString(address.Query);
    
             foreach (var item in values.AllKeys.OrderBy(k => k))
             {
                 sb.Append('\n').Append(item).Append(':').Append(values[item]);
             }
    
             return sb.ToString().ToLower();
    
         }




dotnet-csharpazure-blob-storage
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

1 Answer

cooldadtx avatar image
2 Votes"
cooldadtx answered cooldadtx commented

Is there any particular reason why you're trying to manually call the API vs use the existing Azure SDK that provides you an easy to use type to do this for you? There are certain complexities when working with the APIs (like size limits) that you need to account for and the SDK will help with that aspect.

Here's what I believe to be the equivalent of what you're doing with the SDK.

//Using UN/PWD here but you can use any supported credential system, see TokenCredential for options
var credentials = new UsernamePasswordCredential(user, pwd, tenantId, clientId);
var client = new BlobClient(new Uri(uri), credentials);

using (var stream = new MemoryStream(requestPayload))
{
   //Create and pass a BlobUploadOptions to set headers and metadata if needed...
   await client.UploadAsync(stream, true);
};


As for the issue with your call I suspect it is the x-ms-date header. DateTime values sent to Azure must be in UTC. Based upon the docs you only have 15 mins and unless you happen to be in the UTC timezone your header value is going to be wrong. Convert the value to Utc before setting the header value.

· 2
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

as per my above code on line no. 8 and 9. I am converting DateTime values in UTC. So please correct me where I am wrong?

0 Votes 0 ·

You're right I didn't notice the Utc in front of the now. Nevertheless some of data you're passing isn't correct so it is failing the call. If you don't want to use the SDK to avoid this issue then capture the network call being made (or dump the HttpRequestMessage contents using a delegating handler.

Take the raw HTTP data and paste it into Postman and try from there. If it fails then the issue is definitely with your request. Use Postman to compare the data you're sending with the requirements of the request as per the doc. Make adjustments and run the test again until you get it working in Postman. Once you do you'll be able to adjust your code accordingly. I still suspect the issue is in your HTTP headers (or lack thereof). I can try to build a raw request to see where it is failing but we personally use the Azure SDK for our API calls as it is much easier. It'll take a little bit.

0 Votes 0 ·