1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219
|
# encoding: utf-8
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
module MsRestAzure
#
# Class that provides access to authentication token via Managed Service Identity.
#
class MSITokenProvider < MsRest::TokenProvider
private
TOKEN_ACQUIRE_URL = 'http://localhost:{port}/oauth2/token'
REQUEST_BODY_PATTERN = 'resource={resource_uri}'
USER_ASSIGNED_IDENTITY = '{id_type}={user_assigned_identity}'
DEFAULT_SCHEME = 'Bearer'
IMDS_TOKEN_ACQUIRE_URL = 'http://169.254.169.254/metadata/identity/oauth2/token'
# @return [MSIActiveDirectoryServiceSettings] settings.
attr_accessor :settings
# @return [Integer] port number where MSI service is running.
attr_accessor :port
# @return [String] client id for user assigned managed identity
attr_accessor :client_id
# @return [String] object id for user assigned managed identity
attr_accessor :object_id
# @return [String] ms_res id for user assigned managed identity
attr_accessor :msi_res_id
# @return [String] auth token.
attr_writer :token
# @return [Time] the date when the current token expires.
attr_writer :token_expires_on
# @return [Integer] the amount of time we refresh token before it expires.
attr_reader :expiration_threshold
public
# @return [Time] the date when the current token expires.
attr_reader :token_expires_on
# @return [String] the type of token.
attr_reader :token_type
# @return [String] auth token.
attr_reader :token
#
# Creates and initialize new instance of the MSITokenProvider class.
# @param port [Integer] port number where MSI service is running.
# @param settings [ActiveDirectoryServiceSettings] active directory setting.
# @param msi_id [Hash] MSI id for user assigned managed service identity,
# msi_id = {'client_id': 'client id of user assigned identity'}
# or
# msi_id = {'object_id': 'object id of user assigned identity'}
# or
# msi_id = {'msi_rest_id': 'resource id of user assigned identity'}
# The above key,value pairs are mutually exclusive.
def initialize(port = 50342, settings = ActiveDirectoryServiceSettings.get_azure_settings, msi_id = nil)
fail ArgumentError, 'Azure AD settings cannot be nil' if settings.nil?
fail ArgumentError, 'msi_id must include either client_id, object_id or msi_res_id exclusively' if (!msi_id.nil? && msi_id.length > 1)
warn "The 'port' argument is no longer used, and will be removed in a future release" if port != 50342
@port = port
@settings = settings
if !msi_id.nil?
@client_id = msi_id[:client_id] unless msi_id[:client_id].nil?
@object_id = msi_id[:object_id] unless msi_id[:object_id].nil?
@msi_res_id = msi_id[:msi_res_id] unless msi_id[:msi_res_id].nil?
end
@expiration_threshold = 5 * 60
end
#
# Returns the string value which needs to be attached
# to HTTP request header in order to be authorized.
#
# @return [String] authentication headers.
def get_authentication_header
if !ENV['MSI_VM'].nil? && ENV['MSI_VM'].downcase == 'true'
acquire_token if token_expired
else
acquire_token_from_imds_with_retry if token_expired
end
"#{token_type} #{token}"
end
private
def append_header(name, value)
"#{name}=#{value}"
end
def acquire_token_from_imds_with_retry
token_acquire_url = IMDS_TOKEN_ACQUIRE_URL.dup + "?" + append_header('resource', ERB::Util.url_encode(@settings.token_audience)) + '&' + append_header('api-version', '2018-02-01')
token_acquire_url = (token_acquire_url + '&' + append_header('client_id', ERB::Util.url_encode(@client_id))) unless @client_id.nil?
token_acquire_url = (token_acquire_url + '&' + append_header('object_id', ERB::Util.url_encode(@object_id))) unless @object_id.nil?
token_acquire_url = (token_acquire_url + '&' + append_header('msi_res_id', ERB::Util.url_encode(@msi_res_id))) unless @msi_res_id.nil?
url = URI.parse(token_acquire_url)
connection = Faraday.new(:url => url, :ssl => MsRest.ssl_options) do |builder|
builder.adapter Faraday.default_adapter
end
retry_value = 1
max_retry = 20
response = nil
user_defined_time_limit = ENV['USER_DEFINED_IMDS_MAX_RETRY_TIME'].nil? ? 104900:ENV['USER_DEFINED_IMDS_MAX_RETRY_TIME']
total_wait = 0
slots = []
(0..max_retry-1).each do |i|
slots << (100 * ((2 << i) - 1) /1000.to_f)
end
while retry_value <= max_retry && total_wait < user_defined_time_limit
response = connection.get do |request|
request.headers['Metadata'] = 'true'
request.headers['User-Agent'] = "Azure-SDK-For-Ruby/ms_rest_azure/#{MsRestAzure::VERSION}"
end
if response.status == 410 || response.status == 429 || response.status == 404 || (response.status > 499 && response.status < 600)
puts slots.inspect
wait = slots[0..retry_value].sample
wait = wait < 1 ? 3 : wait
wait = (response.status == 410 && wait < 70) ? 70 : wait
retry_value += 1
if (retry_value > max_retry)
break
end
wait = wait > user_defined_time_limit ? user_defined_time_limit : wait
sleep(wait)
total_wait += wait
elsif response.status != 200
fail AzureOperationError, "Couldn't acquire access token from Managed Service Identity, please verify your tenant id, port and settings"
else
break
end
end
if retry_value > max_retry
fail AzureOperationError, "MSI: Failed to acquire tokens after #{max_retry} times"
end
response_body = JSON.load(response.body)
@token = response_body['access_token']
@token_expires_on = Time.at(Integer(response_body['expires_on']))
@token_type = response_body['token_type']
end
#
# Checks whether token is about to expire.
#
# @return [Bool] True if token is about to expire, false otherwise.
def token_expired
@token.nil? || Time.now >= @token_expires_on + expiration_threshold
end
#
# Retrieves a new authentication token.
#
# @return [String] new authentication token.
def acquire_token
token_acquire_url = TOKEN_ACQUIRE_URL.dup
token_acquire_url['{port}'] = @port.to_s
url = URI.parse(token_acquire_url)
connection = Faraday.new(:url => url, :ssl => MsRest.ssl_options) do |builder|
builder.adapter Faraday.default_adapter
end
request_body = REQUEST_BODY_PATTERN.dup
request_body['{resource_uri}'] = ERB::Util.url_encode(@settings.token_audience)
request_body = set_msi_id(request_body, 'client_id', @client_id) unless @client_id.nil?
request_body = set_msi_id(request_body, 'object_id', @object_id) unless @object_id.nil?
request_body = set_msi_id(request_body, 'msi_res_id', @msi_res_id) unless @msi_res_id.nil?
response = connection.post do |request|
request.headers['content-type'] = 'application/x-www-form-urlencoded'
request.headers['Metadata'] = 'true'
request.body = request_body
end
fail AzureOperationError,
'Couldn\'t acquire access token from Managed Service Identity, please verify your tenant id, port and settings' unless response.status == 200
response_body = JSON.load(response.body)
@token = response_body['access_token']
@token_expires_on = Time.at(Integer(response_body['expires_on']))
@token_type = response_body['token_type']
end
#
# Sets user assigned identity value in request body
# @param request_body [String] body of the request used to acquire token
# @param id_type [String] type of id to send 'client_id', 'object_id' or 'msi_res_id'
# @param id_value [String] id of the user assigned identity
#
# @return [String] new request body with the addition of <id_type>=<id_value>.
def set_msi_id(request_body, id_type, id_value)
user_assigned_identity = USER_ASSIGNED_IDENTITY.dup
request_body = [request_body, user_assigned_identity].join(',')
request_body['{id_type}'] = id_type
request_body['{user_assigned_identity}'] = ERB::Util.url_encode(id_value)
return request_body
end
end
end
|