Azure-rest-api-specs: Can't properly sign request to "issue token" API

Created on 20 Nov 2020  路  9Comments  路  Source: Azure/azure-rest-api-specs

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! :)

Communication Service Attention question

All 9 comments

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

Was this page helpful?
0 / 5 - 0 ratings