Autenticación de HMAC: referencia de API REST
Puede autenticar las solicitudes HTTP mediante el esquema de autenticación HMAC-SHA256. (HMAC hace referencia al código de autenticación de mensajes basado en hash). Estas solicitudes se deben transmitir a través de TLS.
Requisitos previos
- Credencial - <Id. de clave de acceso>
- Secreto: valor de clave de acceso descodificado en Base64.
base64_decode(<Access Key Value>)
Los valores para la credencial (también denominada id
) y el secreto (también denominado value
) se deben obtener de la instancia de Azure App Configuration. Para ello, pueden usar Azure Portal o la CLI de Azure.
Proporcione a cada solicitud todos los encabezados HTTP necesarios para la autenticación. Los mínimos requeridos son:
Encabezado de solicitud | Descripción |
---|---|
Host | Host de Internet y número de puerto. Para más información, consulte la sección 3.2.2. |
Fecha | Fecha y hora en que se ha originado la solicitud. No puede superar los 15 minutos de la hora universal coordinada actual (hora del meridiano de Greenwich). El valor es una fecha HTTP, como se explica en la sección 3.3.1. |
x-ms-date | Igual que el elemento Date anterior. Se puede usar en su lugar cuando el agente no puede acceder directamente al encabezado de solicitud Date o cuando un proxy lo modifica. Si se proporcionan x-ms-date y Date , x-ms-date tiene prioridad. |
x-ms-content-sha256 | Hash SHA256 codificado en Base64 del cuerpo de la solicitud. Se debe proporcionar aunque no haya ningún cuerpo. base64_encode(SHA256(body)) |
Autorización | Información de autenticación requerida por el esquema HMAC-SHA256. Más adelante en este artículo se explican el formato y los detalles. |
Ejemplo:
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}
Encabezado de autorización
Sintaxis
Authorization
: HMAC-SHA256 Credential
=<value>&SignedHeaders
=<value>&Signature
=<value>
Argumento | Descripción |
---|---|
HMAC-SHA256 | Esquema de autorización. (requerido) |
Credential: | Identificador de la clave de acceso que se usa para procesar la firma. (requerido) |
SignedHeaders | Encabezados de solicitud HTTP agregados a la firma. (requerido) |
Signature | HMACSHA256 codificado en Base64 de String-To-Sign. (requerido) |
Credential:
Identificador de la clave de acceso que se usa para procesar la firma.
Encabezados firmados
Nombres de encabezados de solicitud HTTP, separados por puntos y coma, necesarios para firmar la solicitud. Estos encabezados HTTP deben proporcionarse correctamente también con la solicitud. No use espacios en blanco.
Encabezados de solicitud HTTP requeridos
x-ms-date
[or Date
];host
;x-ms-content-sha256
También se pueden agregar otros encabezados de solicitud HTTP a la firma. Solo tiene que anexarlos al argumento SignedHeaders
.
Ejemplo:
x-ms-date;host;x-ms-content-sha256;Content-Type
;Accept
Firma
Hash HMACSHA256 codificado en base64 de String-To-Sign. Usa la clave de acceso identificada por Credential
. base64_encode(HMACSHA256(String-To-Sign, Secret))
String-To-Sign
Es una representación canónica de la solicitud:
String-To-Sign=
HTTP_METHOD + '\n' + path_and_query + '\n' + signed_headers_values
Argumento | Descripción |
---|---|
HTTP_METHOD | Nombre del método HTTP en mayúsculas usado con la solicitud. Para obtener más información, consulte la sección 9. |
path_and_query | Concatenación de la ruta de acceso del URI absoluto de la solicitud y la cadena de consulta. Para obtener más información, consulte la sección 3.3. |
signed_headers_values | Valores separados por puntos y coma de todos los encabezados de solicitud HTTP enumerados en SignedHeaders . El formato sigue la semántica SignedHeaders . |
Ejemplo:
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
Errors
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256, Bearer
Motivo: No se ha proporcionado un encabezado de solicitud de autorización con el esquema HMAC-SHA256.
Solución: Proporcione un encabezado de solicitud HTTP Authorization
válido.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="The access token has expired", Bearer
Motivo: El encabezado de solicitud Date
o x-ms-date
supera los 15 minutos de la hora universal coordinada actual (hora del meridiano de Greenwich).
Solución: Proporcione la fecha y hora correctas.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="Invalid access token date", Bearer
Motivo: Encabezado de solicitud Date
o x-ms-date
no válido o que falta.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="[Credential][SignedHeaders][Signature] is required", Bearer
Motivo: Falta un parámetro obligatorio del encabezado de solicitud Authorization
.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="Invalid Credential", Bearer
Motivo: No se encuentra el elemento [Host
]/[id. de clave de acceso] proporcionado.
Solución: Compruebe el parámetro Credential
del encabezado de la solicitud Authorization
. Asegúrese de que sea un identificador de clave de acceso válido y asegúrese de que el encabezado Host
apunta a la cuenta registrada.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="Invalid Signature", Bearer
Motivo: el elemento Signature
proporcionado no coincide con lo esperado por el servidor.
Solución: asegúrese de que String-To-Sign
sea correcto. Asegúrese de que Secret
sea correcto y se use correctamente (descodificado en Base64 antes de usar).
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="Signed request header 'xxx' is not provided", Bearer
Motivo:falta el encabezado de solicitud obligatorio por el parámetro SignedHeaders
del encabezado Authorization
.
Solución: Proporcione el encabezado obligatorio con el valor correcto.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="XXX is required as a signed header", Bearer
Motivo: falta un parámetro de SignedHeaders
.
Solución: compruebe los requisitos mínimos de los Encabezados firmados.
Fragmentos de código
JavaScript
Requisitos previos: 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 }
];
}
C#
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, string 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(Convert.FromBase64String(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());
}
}
}
}
Java
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);
}
Golang
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"io/ioutil"
"net/http"
"strings"
"time"
)
//SignRequest Setup the auth header for accessing Azure App Configuration 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))
}
Python
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("%a, %d %b %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
}
PowerShell
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
Bash
Requisitos previos:
Requisito previo | Get-Help | Versiones probadas |
---|---|---|
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"