HTTP requests may be authenticated using the HMAC-SHA256 authentication scheme. These requests must be transmitted over TLS.
Prerequisites:
- Credential - <Access Key ID>
- Secret - base64 decoded Access Key Value.
base64_decode(<Access Key Value>)
The values for credential (also called 'id') and secret (also called 'value') must be obtained from the Azure App Configuration instance. This can be done using the Azure Portal or the Azure CLI.
Provide each request with all HTTP headers required for Authentication. The minimum required are:
Request Header | Description |
---|---|
Host | Internet host and port number. See section 3.2.2 |
Date | Date and Time at which the request was originated. It can not be more than 15 min off from current GMT. The value is an HTTP-date, as described in section 3.3.1 |
x-ms-date | Same as Date above. It can be used instead when the agent can't directly access Date request header or a proxy modifies it. If x-ms-date and Date are both provided, x-ms-date takes precedence. |
x-ms-content-sha256 | base64 encoded SHA256 hash of the request body. It must be provided even if there is no body. base64_encode(SHA256(body)) |
Authorization | Authentication information required by HMAC-SHA256 scheme. Format and details are explained below. |
Example:
Host: {myconfig}.azconfig.io
Date: Fri, 11 May 2018 18:48:36 GMT
x-ms-content-sha256: {SHA256 hash of the request body}
Authorization: HMAC-SHA256 Credential={Access Key ID}&SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature={Signature}
Syntax:
Authorization
: HMAC-SHA256 Credential
=<value>&SignedHeaders
=<value>&Signature
=<value>
Argument | Description |
---|---|
HMAC-SHA256 | Authorization Scheme (required) |
Credential | The ID of the access key used to compute the Signature. (required) |
SignedHeaders | HTTP Request Headers added to the signature. (required) |
Signature | base64 encoded HMACSHA256 of String-To-Sign. (required) |
ID of the access key used to compute the Signature.
Semicolon separated HTTP request header names required to sign the request. These HTTP headers must be correctly provided with the request as well. Don't use whitespaces.
Required HTTP request headers:
x-ms-date
[or Date
];host
;x-ms-content-sha256
Any other HTTP request headers can also be added to the signing. Just append them to the SignedHeaders
argument.
Example:
x-ms-date;host;x-ms-content-sha256;Content-Type
;Accept
Base64 encoded HMACSHA256 hash of the String-To-Sign using the access key identified by Credential
.
base64_encode(HMACSHA256(String-To-Sign, Secret))
It represents canonical representation of the request:
String-To-Sign=
HTTP_METHOD + '\n' + path_and_query + '\n' + signed_headers_values
Argument | Description |
---|---|
HTTP_METHOD | Uppercased HTTP method name used with the request. See section 9 |
path_and_query | Concatenation of request absolute URI path and query string. See section 3.3. |
signed_headers_values | Semicolon separated values of all HTTP request headers listed in SignedHeaders. The format follows SignedHeaders semantic. |
Example:
string-To-Sign=
"GET" + '\n' + // VERB
"/kv?fields=*&api-version=1.0" + '\n' + // path_and_query
"Fri, 11 May 2018 18:48:36 GMT;{myconfig}.azconfig.io;{value of ms-content-sha256 header}" // signed_headers_values
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256, Bearer
Reason: Authorization request header with HMAC-SHA256 scheme is not provided.
Solution: Provide valid Authorization
HTTP request header
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="The access token has expired", Bearer
Reason: Date
or x-ms-date
request header is more than 15 minutes off from the current GMT time.
Solution: Provide correct date and time
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="Invalid access token date", Bearer
Reason: Missing or invalid Date
or x-ms-date
request header
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="[Credential][SignedHeaders][Signature] is required", Bearer
Reason: Missing a required parameter from Authorization
request header
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="Invalid Credential", Bearer
Reason: Provided [Host
]/[Access Key ID] is not found.
Solution: Check the Credential
parameter of the Authorization
request header and make sure it is a valid Access Key ID. Make sure the Host
header points to the registered account.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="Invalid Signature", Bearer
Reason: The Signature
provided doesn't match what the server expects.
Solution: Make sure the String-To-Sign
is correct. Make sure the Secret
is correct and properly used (base64 decoded prior to using). See Examples section.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="Signed request header 'xxx' is not provided", Bearer
Reason: Missing request header required by SignedHeaders
parameter in Authorization
header.
Solution: Provide the required header with correct value.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="XXX is required as a signed header", Bearer
Reason: Missing parameter in SignedHeaders
.
Solution: Check Signed Headers minimum requirements.
Prerequisites: Crypto-JS
function signRequest(host,
method, // GET, PUT, POST, DELETE
url, // path+query
body, // request body (undefined of none)
credential, // access key id
secret) // access key value (base64 encoded)
{
var verb = method.toUpperCase();
var utcNow = new Date().toUTCString();
var contentHash = CryptoJS.SHA256(body).toString(CryptoJS.enc.Base64);
//
// SignedHeaders
var signedHeaders = "x-ms-date;host;x-ms-content-sha256"; // Semicolon separated header names
//
// String-To-Sign
var stringToSign =
verb + '\n' + // VERB
url + '\n' + // path_and_query
utcNow + ';' + host + ';' + contentHash; // Semicolon separated SignedHeaders values
//
// Signature
var signature = CryptoJS.HmacSHA256(stringToSign, CryptoJS.enc.Base64.parse(secret)).toString(CryptoJS.enc.Base64);
//
// Result request headers
return [
{ name: "x-ms-date", value: utcNow },
{ name: "x-ms-content-sha256", value: contentHash },
{ name: "Authorization", value: "HMAC-SHA256 Credential=" + credential + "&SignedHeaders=" + signedHeaders + "&Signature=" + signature }
];
}
using (var client = new HttpClient())
{
var request = new HttpRequestMessage()
{
RequestUri = new Uri("https://{config store name}.azconfig.io/kv?api-version=1.0"),
Method = HttpMethod.Get
};
//
// Sign the request
request.Sign(<Credential>, <Secret>);
await client.SendAsync(request);
}
static class HttpRequestMessageExtensions
{
public static HttpRequestMessage Sign(this HttpRequestMessage request, string credential, byte[] secret)
{
string host = request.RequestUri.Authority;
string verb = request.Method.ToString().ToUpper();
DateTimeOffset utcNow = DateTimeOffset.UtcNow;
string contentHash = Convert.ToBase64String(request.Content.ComputeSha256Hash());
//
// SignedHeaders
string signedHeaders = "date;host;x-ms-content-sha256"; // Semicolon separated header names
//
// String-To-Sign
var stringToSign = $"{verb}\n{request.RequestUri.PathAndQuery}\n{utcNow.ToString("r")};{host};{contentHash}";
//
// Signature
string signature;
using (var hmac = new HMACSHA256(secret))
{
signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.ASCII.GetBytes(stringToSign)));
}
//
// Add headers
request.Headers.Date = utcNow;
request.Headers.Add("x-ms-content-sha256", contentHash);
request.Headers.Authorization = new AuthenticationHeaderValue("HMAC-SHA256", $"Credential={credential}&SignedHeaders={signedHeaders}&Signature={signature}");
return request;
}
}
static class HttpContentExtensions
{
public static byte[] ComputeSha256Hash(this HttpContent content)
{
using (var stream = new MemoryStream())
{
if (content != null)
{
content.CopyToAsync(stream).Wait();
stream.Seek(0, SeekOrigin.Begin);
}
using (var alg = SHA256.Create())
{
return alg.ComputeHash(stream.ToArray());
}
}
}
}
public CloseableHttpResponse signRequest(HttpUriRequest request, String credential, String secret)
throws IOException, URISyntaxException {
Map<String, String> authHeaders = generateHeader(request, credential, secret);
authHeaders.forEach(request::setHeader);
return httpClient.execute(request);
}
private static Map<String, String> generateHeader(HttpUriRequest request, String credential, String secret)
throws URISyntaxException, IOException {
String requestTime = GMT_DATE_FORMAT.format(new Date());
String contentHash = buildContentHash(request);
// SignedHeaders
String signedHeaders = "x-ms-date;host;x-ms-content-sha256";
// Signature
String methodName = request.getRequestLine().getMethod().toUpperCase();
URIBuilder uri = new URIBuilder(request.getRequestLine().getUri());
String scheme = uri.getScheme() + "://";
String requestPath = uri.toString().substring(scheme.length()).substring(uri.getHost().length());
String host = new URIBuilder(request.getRequestLine().getUri()).getHost();
String toSign = String.format("%s\n%s\n%s;%s;%s", methodName, requestPath, requestTime, host, contentHash);
byte[] decodedKey = Base64.getDecoder().decode(secret);
String signature = Base64.getEncoder().encodeToString(new HmacUtils(HMAC_SHA_256, decodedKey).hmac(toSign));
// Compose headers
Map<String, String> headers = new HashMap<>();
headers.put("x-ms-date", requestTime);
headers.put("x-ms-content-sha256", contentHash);
String authorization = String.format("HMAC-SHA256 Credential=%s, SignedHeaders=%s, Signature=%s",
credential, signedHeaders, signature);
headers.put("Authorization", authorization);
return headers;
}
private static String buildContentHash(HttpUriRequest request) throws IOException {
String content = "";
if (request instanceof HttpEntityEnclosingRequest) {
try {
StringWriter writer = new StringWriter();
IOUtils.copy(((HttpEntityEnclosingRequest) request).getEntity().getContent(), writer,
StandardCharsets.UTF_8);
content = writer.toString();
}
finally {
((HttpEntityEnclosingRequest) request).getEntity().getContent().close();
}
}
byte[] digest = new DigestUtils(SHA_256).digest(content);
return Base64.getEncoder().encodeToString(digest);
}
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"io/ioutil"
"net/http"
"strings"
"time"
)
//SignRequest Setup the auth header for accessing Azure AppConfiguration service
func SignRequest(id string, secret string, req *http.Request) error {
method := req.Method
host := req.URL.Host
pathAndQuery := req.URL.Path
if req.URL.RawQuery != "" {
pathAndQuery = pathAndQuery + "?" + req.URL.RawQuery
}
content, err := ioutil.ReadAll(req.Body)
if err != nil {
return err
}
req.Body = ioutil.NopCloser(bytes.NewBuffer(content))
key, err := base64.StdEncoding.DecodeString(secret)
if err != nil {
return err
}
timestamp := time.Now().UTC().Format(http.TimeFormat)
contentHash := getContentHashBase64(content)
stringToSign := fmt.Sprintf("%s\n%s\n%s;%s;%s", strings.ToUpper(method), pathAndQuery, timestamp, host, contentHash)
signature := getHmac(stringToSign, key)
req.Header.Set("x-ms-content-sha256", contentHash)
req.Header.Set("x-ms-date", timestamp)
req.Header.Set("Authorization", "HMAC-SHA256 Credential="+id+", SignedHeaders=x-ms-date;host;x-ms-content-sha256, Signature="+signature)
return nil
}
func getContentHashBase64(content []byte) string {
hasher := sha256.New()
hasher.Write(content)
return base64.StdEncoding.EncodeToString(hasher.Sum(nil))
}
func getHmac(content string, key []byte) string {
hmac := hmac.New(sha256.New, key)
hmac.Write([]byte(content))
return base64.StdEncoding.EncodeToString(hmac.Sum(nil))
}
import base64
import hashlib
import hmac
from datetime import datetime
import six
def sign_request(host,
method, # GET, PUT, POST, DELETE
url, # Path + Query
body, # Request body
credential, # Access Key ID
secret): # Access Key Value
verb = method.upper()
utc_now = str(datetime.utcnow().strftime("%b, %d %Y %H:%M:%S ")) + "GMT"
if six.PY2:
content_digest = hashlib.sha256(bytes(body)).digest()
else:
content_digest = hashlib.sha256(bytes(body, 'utf-8')).digest()
content_hash = base64.b64encode(content_digest).decode('utf-8')
# Signed Headers
signed_headers = "x-ms-date;host;x-ms-content-sha256" # Semicolon separated header names
# String-To-Sign
string_to_sign = verb + '\n' + \
url + '\n' + \
utc_now + ';' + host + ';' + content_hash # Semicolon separated SignedHeaders values
# Decode secret
if six.PY2:
decoded_secret = base64.b64decode(secret)
digest = hmac.new(decoded_secret, bytes(
string_to_sign), hashlib.sha256).digest()
else:
decoded_secret = base64.b64decode(secret, validate=True)
digest = hmac.new(decoded_secret, bytes(
string_to_sign, 'utf-8'), hashlib.sha256).digest()
# Signature
signature = base64.b64encode(digest).decode('utf-8')
# Result request headers
return {
"x-ms-date": utc_now,
"x-ms-content-sha256": content_hash,
"Authorization": "HMAC-SHA256 Credential=" + credential + "&SignedHeaders=" + signed_headers + "&Signature=" + signature
}
function Sign-Request(
[string] $hostname,
[string] $method, # GET, PUT, POST, DELETE
[string] $url, # path+query
[string] $body, # request body
[string] $credential, # access key id
[string] $secret # access key value (base64 encoded)
)
{
$verb = $method.ToUpperInvariant()
$utcNow = (Get-Date).ToUniversalTime().ToString("R", [Globalization.DateTimeFormatInfo]::InvariantInfo)
$contentHash = Compute-SHA256Hash $body
$signedHeaders = "x-ms-date;host;x-ms-content-sha256"; # Semicolon separated header names
$stringToSign = $verb + "`n" +
$url + "`n" +
$utcNow + ";" + $hostname + ";" + $contentHash # Semicolon separated signedHeaders values
$signature = Compute-HMACSHA256Hash $secret $stringToSign
# Return request headers
return @{
"x-ms-date" = $utcNow;
"x-ms-content-sha256" = $contentHash;
"Authorization" = "HMAC-SHA256 Credential=" + $credential + "&SignedHeaders=" + $signedHeaders + "&Signature=" + $signature
}
}
function Compute-SHA256Hash(
[string] $content
)
{
$sha256 = [System.Security.Cryptography.SHA256]::Create()
try {
return [Convert]::ToBase64String($sha256.ComputeHash([Text.Encoding]::ASCII.GetBytes($content)))
}
finally {
$sha256.Dispose()
}
}
function Compute-HMACSHA256Hash(
[string] $secret, # base64 encoded
[string] $content
)
{
$hmac = [System.Security.Cryptography.HMACSHA256]::new([Convert]::FromBase64String($secret))
try {
return [Convert]::ToBase64String($hmac.ComputeHash([Text.Encoding]::ASCII.GetBytes($content)))
}
finally {
$hmac.Dispose()
}
}
# Stop if any error occurs
$ErrorActionPreference = "Stop"
$uri = [System.Uri]::new("https://{myconfig}.azconfig.io/kv?api-version=1.0")
$method = "GET"
$body = $null
$credential = "<Credential>"
$secret = "<Secret>"
$headers = Sign-Request $uri.Authority $method $uri.PathAndQuery $body $credential $secret
Invoke-RestMethod -Uri $uri -Method $method -Headers $headers -Body $body
Prerequisites:
Prerequisite | Command | Versions Tested |
---|---|---|
Bash | bash | 3.5.27, 4.4.23 |
coreutils | tr | 8.28 |
curl | curl | 7.55.1, 7.58.0 |
OpenSSL | openssl | 1.1.0g, 1.1.1a |
util-linux | hexdump | 2.14.1, 2.31.1 |
#!/bin/bash
sign_request () {
local host="$1"
local method="$2" # GET, PUT, POST, DELETE
local url="$3" # path+query
local body="$4" # request body
local credential="$5" # access key id
local secret="$6" # access key value (base64 encoded)
local verb=$(printf "$method" | tr '[:lower:]' '[:upper:]')
local utc_now="$(date -u '+%a, %d %b %Y %H:%M:%S GMT')"
local content_hash="$(printf "$body" | openssl sha256 -binary | base64)"
local signed_headers="x-ms-date;host;x-ms-content-sha256" # Semicolon separated header names
local string_to_sign="$verb\n$url\n$utc_now;$host;$content_hash" # Semicolon separated signed_headers values
local decoded_secret="$(printf "$secret" | base64 -d | hexdump -v -e '/1 "%02x"')"
local signature="$(printf "$string_to_sign" | openssl sha256 -mac HMAC -macopt hexkey:"$decoded_secret" -binary | base64)"
# Output request headers
printf '%s\n' \
"x-ms-date: $utc_now" \
"x-ms-content-sha256: $content_hash" \
"Authorization: HMAC-SHA256 Credential=$credential&SignedHeaders=$signed_headers&Signature=$signature"
}
host="{config store name}.azconfig.io"
method="GET"
url="/kv?api-version=1.0"
body=""
credential="<Credential>"
secret="<Secret>"
headers=$(sign_request "$host" "$method" "$url" "$body" "$credential" "$secret")
while IFS= read -r line; do
header_args+=("-H$line")
done <<< "$headers"
curl -X "$method" -d "$body" "${header_args[@]}" "https://$host$url"