HMAC 인증 - REST API 참조
HMAC-SHA256 인증 체계를 사용하여 HTTP 요청을 인증할 수 있습니다. (HMAC는 해시 기반 메시지 인증 코드를 참조합니다.) 이러한 요청은 TLS를 통해 전송되어야 합니다.
필수 조건
- 자격 증명 - <액세스 키 ID>
- 비밀 - base64 디코딩된 액세스 키 값입니다.
base64_decode(<Access Key Value>)
자격 증명(id
(이)라고도 함) 및 비밀(value
(이)라고도 함)에 대한 값은 Azure App Configuration 인스턴스에서 가져와야 합니다. 이 작업은 Azure portal 또는 Azure CLI를 사용하여 수행할 수 있습니다.
인증에 필요한 모든 HTTP 헤더를 각 요청에 제공합니다. 필요한 최소값은 다음과 같습니다.
요청 헤더 | 설명 |
---|---|
호스트 | 인터넷 호스트 및 포트 번호입니다. 자세한 내용은 3.2.2 섹션을 참조하세요. |
날짜 | 요청이 실행된 날짜 및 시간입니다. 현재 협정 세계시(그리니치 표준시)에서 15분 이상 떨어져 있을 수 없습니다. 이 값은 섹션 3.3.1에 설명된 대로 HTTP 날짜입니다. |
x-ms-date | 위의 Date 과(와) 동일합니다. 에이전트가 Date 요청 헤더에 직접 액세스할 수 없거나 프록시가 수정하는 경우 대신 사용할 수 있습니다. x-ms-date 및 Date 가 둘 다 제공되면 x-ms-date 이(가) 우선적으로 적용됩니다. |
x-ms-content-sha256 | 요청 본문의 base64로 인코딩된 SHA256 해시입니다. 본문이 없더라도 제공되어야 합니다. base64_encode(SHA256(body)) |
Authorization | HMAC-SHA256 체계에 필요한 인증 정보입니다. 형식 및 세부 정보는 이 문서의 뒷부분에 설명되어 있습니다. |
예제:
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}
권한 부여 헤더
구문
Authorization
: HMAC-SHA256 Credential
=<value>&SignedHeaders
=<value>&Signature
=<value>
인수 | 설명 |
---|---|
HMAC-SHA256 | 권한 부여 체계. (필수) |
자격 증명 | 서명을 계산하는 데 사용되는 액세스 키의 ID입니다. (필수) |
SignedHeaders | 서명에 추가된 HTTP 요청 헤더입니다. (필수) |
서명 | String-To-Sign의 base64로 인코딩된 HMACSHA256입니다. (필수) |
자격 증명
서명을 계산하는 데 사용되는 액세스 키의 ID입니다.
서명된 헤더
요청에 서명하는 데 필요한 HTTP 요청 헤더 이름(세미콜론으로 구분)입니다. 이러한 HTTP 헤더는 요청과 함께 올바르게 제공되어야 합니다. 공백을 사용하지 마세요.
필수 HTTP 요청 헤더
x-ms-date
[또는 Date
];host
;x-ms-content-sha256
다른 모든 HTTP 요청 헤더를 서명에 추가할 수도 있습니다. SignedHeaders
인수에 추가하기만 하면 됩니다.
예제:
x-ms-date;host;x-ms-content-sha256;Content-Type
;Accept
서명
String-To-Sign의 Base64로 인코딩된 HMACSHA256 해시입니다. Credential
(으)로 식별되는 액세스 키를 사용합니다. base64_encode(HMACSHA256(String-To-Sign, Secret))
String-To-Sign
요청의 정식 표현입니다.
String-To-Sign=
HTTP_METHOD + '\n' + path_and_query + '\n' + signed_headers_values
인수 | 설명 |
---|---|
HTTP_METHOD | 요청과 함께 사용되는 대문자 HTTP 메서드 이름입니다. 자세한 내용은 섹션 9를 참조하세요. |
path_and_query | 요청 절대 URI 경로 및 쿼리 문자열의 연결입니다. 자세한 내용은 섹션 3.3을 참조하세요. |
signed_headers_values | SignedHeaders 에 나열된 모든 HTTP 요청 헤더의 세미콜론으로 구분된 값입니다. 형식은 SignedHeaders 의미 체계를 따릅니다. |
예제:
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
이유: HMAC-SHA256 스키마를 사용한 권한 부여 요청 헤더는 제공되지 않습니다.
해결 방법: 유효한 Authorization
HTTP 요청 헤더를 제공합니다.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="The access token has expired", Bearer
이유: Date
또는 x-ms-date
요청 헤더가 현재 협정 세계시(그리니치 표준시)에서 15분 이상 떨어져 있습니다.
솔루션: 올바른 날짜와 시간을 제공합니다.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="Invalid access token date", Bearer
이유: 누락되었Date
거나x-ms-date
유효하지 않은 요청 헤더입니다.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="[Credential][SignedHeaders][Signature] is required", Bearer
이유: Authorization
요청 헤더에서 필수 매개 변수가 누락되었습니다.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="Invalid Credential", Bearer
이유: 제공된 [Host
]/[액세스 키 ID]를 찾을 수 없습니다.
솔루션: Authorization
요청 헤더의 Credential
매개 변수를 확인합니다. 유효한 액세스 키 ID인지 확인하고 Host
헤더가 등록된 계정을 가리키는지 확인합니다.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="Invalid Signature", Bearer
이유: 제공된 Signature
이(가) 서버에서 예상하는 것과 일치하지 않습니다.
솔루션: String-To-Sign
이(가) 올바른지 확인합니다. Secret
이(가) 올바르고 적절하게 사용되는지 확인합니다(사용하기 전에 base64로 디코딩됨).
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="Signed request header 'xxx' is not provided", Bearer
이유: Authorization
헤더의 SignedHeaders
매개 변수에 필요한 요청 헤더가 누락되었습니다.
솔루션: 올바른 값으로 필요한 헤더를 제공합니다.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="XXX is required as a signed header", Bearer
이유: SignedHeaders
에 매개 변수가 누락되었습니다.
솔루션: 서명된 헤더 최소 요구 사항을 확인합니다.
코드 조각
JavaScript
필수 구성 요소: 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
필수 구성 요소:
전제 조건 | 명령 | 테스트된 버전 |
---|---|---|
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"