I'm attempting to hit this "issue token" API for Azure Communication Services, but I get a 401 with
{"error":{"code":"Denied","message":"Request 'x-ms-content-sha256' differs from generated content hash."}}
The confusing part is that I can _successfully_ call the "create" API using the same exact code to sign requests. I'm developing in Ruby. Here's my code.
require 'httparty'
require 'json'
require 'digest'
require 'base64'
# This method authenticates successfully and returns the expected response.
def create_acs_user
path_and_query = '/identities?api-version=2020-07-20-preview2'
content = ''
send_acs_post_request(path_and_query, content)['id']
end
# This method gets {"error":{"code":"Denied","message":"Request 'x-ms-content-sha256' differs from generated content hash."}}
def issue_user_access_token
acs_user_id = ERB::Util.url_encode(params[:acs_user_id])
path_and_query = "/identities/#{acs_user_id}/token?api-version=2020-07-20-preview2"
content = { 'scopes' => ['voip'] }.to_json
send_acs_post_request(path_and_query, content)
end
def send_acs_post_request(path_and_query, content)
connection_str = ENV[CONNECTION_STR]
raise "Missing environment variable #{CONNECTION_STR}!" unless connection_str
endpoint, access_key = connection_str.split(';')
access_key.delete_prefix!('accesskey=')
endpoint.delete_prefix!('endpoint=')
endpoint.delete_suffix!('/')
uri = "#{endpoint}#{path_and_query}"
verb = 'POST'
date_header_value = Time.now.httpdate
host_header_value = endpoint.delete_prefix('https://')
content_header_value = Digest::SHA256.base64digest(content)
str_to_sign = "#{verb}\n"\
"#{path_and_query}\n"\
"#{date_header_value};"\
"#{host_header_value};"\
"#{content_header_value}"
.force_encoding('utf-8')
.strip
signature = Base64.encode64(
OpenSSL::HMAC.digest(
'SHA256',
Base64.decode64(access_key),
str_to_sign
)
).strip
authorization = "HMAC-SHA256 "\
"SignedHeaders=x-ms-date;host;x-ms-content-sha256&"\
"Signature=#{signature}"
response = HTTParty.post(uri, {
headers: {
'x-ms-date' => date_header_value,
'x-ms-content-sha256' => content_header_value,
'authorization' => authorization
}}
)
JSON.parse(response.body)
end
In order to get some more insight, I used the Javascript client library to make the same request like this...
const tokenResponse = await identityClient.issueToken(identityResponse, ["voip"]);
const { token, expiresOn } = tokenResponse;
I checked the value of the x-ms-content-sha256 header and used by the client library and verified that it is identical to the value I calculated in my Ruby code.
Any help on this would be greatly appreciated! :)
Hi bwang-ith,
Can you please provide identity for which you are trying to issue access token?
Hi Blake, I'm not a Ruby man so please be patient with me :-) Nevertheless, it seems that you don't send any content in the request at all. Could you please double check this part:
response = HTTParty.post(uri, {
headers: {
'x-ms-date' => date_header_value,
'x-ms-content-sha256' => content_header_value,
'authorization' => authorization
}}
)
@smsohan I have a feeling that this about url escaping, considering that create user route is working, but I see acs_user_id = ERB::Util.url_encode(params[:acs_user_id]) in the code. I am not ruby expert, maybe you can help
Let me take a look at it.
I have a local repro of the issue. Now looking at what's going on.
@bwang-ith From looking at it, I see that the code is not passing the content to the body. Do you want to try adding this?
response = HTTParty.post(uri, {
body: content # This was missing in the code
headers: {
'x-ms-date' => date_header_value,
'x-ms-content-sha256' => content_header_value,
'authorization' => authorization
}}
)
@smsohan I have a feeling that this about url escaping, considering that create user route is working, but I see
acs_user_id = ERB::Util.url_encode(params[:acs_user_id])in the code. I am not ruby expert, maybe you can help
This is correct. We don't want to URL escape the ACS User ID. The following line can be removed.
acs_user_id = ERB::Util.url_encode(params[:acs_user_id])
This worked for me:
require 'httparty'
require 'json'
require 'digest'
require 'base64'
CONNECTION_STR='CONN'
# This method authenticates successfully and returns the expected response.
def create_acs_user
path_and_query = '/identities?api-version=2020-07-20-preview2'
content = ''
send_acs_post_request(path_and_query, content)['id']
end
# This method gets {"error":{"code":"Denied","message":"Request 'x-ms-content-sha256' differs from generated content hash."}}
def issue_user_access_token(acs_user_id)
#acs_user_id = ERB::Util.url_encode(params[:acs_user_id])
path_and_query = "/identities/#{acs_user_id}/token?api-version=2020-07-20-preview2"
content = { 'scopes' => ['voip'] }.to_json
send_acs_post_request(path_and_query, content)
end
def send_acs_post_request(path_and_query, content)
connection_str = ENV[CONNECTION_STR]
raise "Missing environment variable #{CONNECTION_STR}!" unless connection_str
puts "connection string is #{connection_str}"
endpoint, access_key = connection_str.split(';')
access_key.delete_prefix!('accesskey=')
endpoint.delete_prefix!('endpoint=')
endpoint.delete_suffix!('/')
uri = "#{endpoint}#{path_and_query}"
verb = 'POST'
date_header_value = Time.now.httpdate
host_header_value = endpoint.delete_prefix('https://')
content_header_value = Digest::SHA256.base64digest(content)
str_to_sign = "#{verb}\n"\
"#{path_and_query}\n"\
"#{date_header_value};"\
"#{host_header_value};"\
"#{content_header_value}"
.force_encoding('utf-8')
.strip
signature = Base64.encode64(
OpenSSL::HMAC.digest(
'SHA256',
Base64.decode64(access_key),
str_to_sign
)
).strip
authorization = "HMAC-SHA256 "\
"SignedHeaders=x-ms-date;host;x-ms-content-sha256&"\
"Signature=#{signature}"
response = HTTParty.post(uri, {
body: content,
headers: {
'x-ms-date' => date_header_value,
'x-ms-content-sha256' => content_header_value,
'authorization' => authorization,
'Accept' => 'application/json',
'Content-Type' => 'application/json'
},
debug_output: $stdout
}
)
puts "BODY= #{response}"
JSON.parse(response.body)
end
id = create_acs_user
pp id
pp issue_user_access_token(id)
Closing this at there is a solution