EDIT 2: The following is the output of the Authorization error:
<?xml version=\"1.0\" encoding=\"utf-8\"?>
<Error>
<Code>AuthenticationFailed</Code>
<Message>Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.\nRequestId:34d738a5-101e-000d-5a14-ed5956000000\nTime:2021-01-17T21:07:38.6231913Z
</Message>
<AuthenticationErrorDetail>Signature did not match. String to sign used was cw\n2021-01-17T19:06:42Z\n2021-01-18T21:06:42Z\n/blob/example-account/example-container/example-blob.json\n\n\nhttps\n2019-02-02\nb\n\n\n\n\n\n
</AuthenticationErrorDetail>
</Error>
I don't really understand... I updated the C# code below to print out the string_to_sign with the \n characters, and it's exactly the same as the string_to_sign from the output above.
NOTE: The SAS token generated from Azure Storage that does work is an Account SAS, while the one I'm generating is a Service SAS. Could Service SAS be restricted in Azure Storage?
EDIT: I tried generating a SAS token directly from Azure Storage, and this did seem to work. It appears to be an account SAS, NOT the service SAS I'm trying to use below.
?sv=2019-12-12&ss=b&srt=o&sp=wac&se=2021-01-18T01:15:13Z&st=2021-01-17T17:15:13Z&spr=https&sig=<signature>
I'm looking to be able to send upload a file to Azure Storage using it's REST API. However, I'm having some trouble getting it to Authorize. I find the documentation a bit conflicting, in some places it says I can include a SAS token in the URI, in others it's in the Authorize header. For context, I'm trying to do it directly from APIM, so in the sample code below it's written with its limited API. This is just a general concept I'm using to generate the authorization string, but I keep getting a 403 when I use it (I'm not sure if I need to do something from the Azure Storage side).
/**
Based on https://docs.microsoft.com/en-us/rest/api/storageservices/create-service-sas
*/
using System;
using System.Collections.Generic;
namespace sas_token
{
class Program
{
static void Main(string[] args)
{
string key = args[0];
Console.WriteLine(generate_blob_sas_token(key));
}
public static string generate_blob_sas_token(string key)
{
const string canonicalizedResource = "canonicalizedResource";
// NOTE: this only works for Blob type files, Tables have a different
// structure
// NOTE: use a List instead of Dictionary since the order of keys in
// Dictionaries is undefined and the signature string requires a very
// specific order
List<KeyValuePair<string, string>> sas_token_properties = new List<KeyValuePair<string, string>>(){
// signedPermissions, select 1..* from [racwdxltmeop], MUST be in that order
new KeyValuePair<string, string>("sp", "cw"),
// signedStart time, date from when the token is valid
// NOTE: because of clock skew between services, even setting the time to
// now may not create an immediately usable token
new KeyValuePair<string, string>("st", DateTime.UtcNow.AddMinutes(-120).ToString("yyyy-MM-ddTHH:mm:ssZ")),
// signedExpiry time, date until the token is valid
new KeyValuePair<string, string>("se", DateTime.UtcNow.AddDays(1).ToString("yyyy-MM-ddTHH:mm:ssZ")),
// canonicalizedResource, must be prefixed with /blob in recent versions
// NOTE: this is NOT included as a query parameter, but is in the signature
// URL = https://myaccount.blob.core.windows.net/music/intro.mp3
// canonicalizedResource = "/blob/myaccount/music/intro.mp3"
new KeyValuePair<string, string>(canonicalizedResource, "/blob/example-account/example-container"),
// signedIdentifier, can be used to identify a Stored Access Policy
new KeyValuePair<string, string>("si", ""),
// signedIP, single or range of allowed IP addresses
new KeyValuePair<string, string>("sip", ""),
// signedProtocol
// [http, https]
new KeyValuePair<string, string>("spr", "https"),
// signedVersion, the version of SAS used (defines which keys are
// required/available)
new KeyValuePair<string, string>("sv", "2019-02-02"),
// signedResource, the type of resource the token is allowed to access
// [b = blob, d = directory, c = container, bv, bs]
new KeyValuePair<string, string>("sr", "b"),
// signedSnapshotTime
new KeyValuePair<string, string>("sst", ""),
// the following specify how the response should be formatted
// Cache-Control
new KeyValuePair<string, string>("rscc", ""),
// Content-Disposition
new KeyValuePair<string, string>("rscd", ""),
// Content-Encoding
new KeyValuePair<string, string>("rsce", ""),
// Content-Language
new KeyValuePair<string, string>("rscl", ""),
// Content-Type
new KeyValuePair<string, string>("rsct", "")
};
// the format is a very specific text string, where values are delimited by new
// lines, and the order of the properties in the string is important!
List<string> values = new List<string>();
foreach (KeyValuePair<string, string> entry in sas_token_properties)
{
values.Add(entry.Value);
}
string string_to_sign = string.Join("\n", new List<string>(values));
Console.WriteLine(string_to_sign.Replace("\n", "\\n"));
System.Security.Cryptography.HMACSHA256 hmac = new System.Security.Cryptography.HMACSHA256(System.Text.Encoding.UTF8.GetBytes(key));
var signature = System.Convert.ToBase64String(hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(string_to_sign)));
// create the query parameters of any set values + the signature
// NOTE: all properties that contribute to the signature must be added
// as query params EXCEPT canonicalizedResource
List<string> parameters = new List<string>();
foreach (KeyValuePair<string, string> entry in sas_token_properties)
{
if (!string.IsNullOrEmpty(entry.Value) && entry.Key != canonicalizedResource)
{
parameters.Add(entry.Key + "=" + System.Net.WebUtility.UrlEncode(entry.Value));
}
}
parameters.Add("sig=" + System.Net.WebUtility.UrlEncode(signature));
string sas_token_querystring = string.Join("&", parameters);
return sas_token_querystring;
}
}
}
I use the output in the following (simplified) APIM policy (I set "sas_token" variable to the output of the function to test the process):
<set-variable name="x-request-body" value="@(context.Request.Body.As<string>())" />
<send-request mode="new" response-variable-name="tokenstate" timeout="20" ignore-error="true">
<set-url>@("https://example-account.blob.core.windows.net/example-container/test.json")</set-url>
<set-method>PUT</set-method>
<set-header name="x-ms-date" exists-action="override">
<value>@(DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"))</value>
</set-header>
<set-header name="x-ms-version" exists-action="override">
<value>2019-02-02</value>
</set-header>
<set-header name="x-ms-blob-type" exists-action="override">
<value>BlockBlob</value>
</set-header>
<set-header name="Authorization" exists-action="override">
<value>@("SharedAccessSignature " + (string)context.Variables["sas_token"])</value>
</set-header>
<set-body>@((string)context.Variables["x-request-body"])</set-body>
</send-request>
For completeness, here's the result from APIM when I trace a test request using {"hello": "then"}:
{
"message": "Request is being forwarded to the backend service. Timeout set to 20 seconds",
"request": {
"method": "PUT",
"url": "https://example-account.blob.core.windows.net/example-container/test.json",
"headers": [
{
"name": "Host",
"value": "example-account.blob.core.windows.net"
},
{
"name": "Content-Length",
"value": 17
},
{
"name": "x-ms-date",
"value": "2021-01-17T16:53:28Z"
},
{
"name": "x-ms-version",
"value": "2019-02-02"
},
{
"name": "x-ms-blob-type",
"value": "BlockBlob"
},
{
"name": "Authorization",
"value": "SharedAccessSignature sp=cw&st=2021-01-17T13%3A42%3A02Z&se=2021-01-18T15%3A42%3A02Z&spr=https&sv=2019-02-02&sr=b&sig=<signature>"
},
{
"name": "X-Forwarded-For",
"value": "205.193.94.40"
}
]
}
}
send-request (92.315 ms)
{
"response": {
"status": {
"code": 403,
"reason": "Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature."
},
"headers": [
{
"name": "x-ms-request-id",
"value": "185d86f5-601e-0038-5cf1-ec3542000000"
},
{
"name": "Content-Length",
"value": "321"
},
{
"name": "Content-Type",
"value": "application/xml"
},
{
"name": "Date",
"value": "Sun, 17 Jan 2021 16:53:28 GMT"
},
{
"name": "Server",
"value": "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0"
}
]
}
}
Also, still newish to C#, so if something can be done better please let me know.
