lib/cloud_controller/uaa/uaa_token_decoder.rb
require 'uaa/info'
module VCAP::CloudController
class UaaTokenDecoder
class BadToken < StandardError
end
attr_reader :config
def initialize(uaa_config, grace_period_in_seconds: 0, alternate_reference_time: nil)
@config = uaa_config
@logger = Steno.logger('cc.uaa_token_decoder')
raise ArgumentError.new('grace period should be an integer') unless grace_period_in_seconds.is_a? Integer
raise ArgumentError.new('grace period and alternate reference time cannot be used together') if (grace_period_in_seconds != 0) && !alternate_reference_time.nil?
@alternate_reference_time = alternate_reference_time
@grace_period_in_seconds = grace_period_in_seconds
return unless grace_period_in_seconds < 0
@grace_period_in_seconds = 0
@logger.warn("negative grace period interval '#{grace_period_in_seconds}' is invalid, changed to 0")
end
def decode_token(auth_token)
return unless token_format_valid?(auth_token)
if symmetric_key
decode_token_with_symmetric_key(auth_token)
else
decode_token_with_asymmetric_key(auth_token)
end
rescue CF::UAA::TokenExpired => e
@logger.warn('Token expired')
raise BadToken.new(e.message)
rescue CF::UAA::DecodeError, CF::UAA::AuthError => e
@logger.warn("Invalid bearer token: #{e.inspect} #{e.backtrace}")
raise BadToken.new(e.message)
end
private
def token_format_valid?(auth_token)
auth_token && auth_token.upcase.start_with?('BEARER')
end
def decode_token_with_symmetric_key(auth_token)
last_error = nil
thekeys = [symmetric_key, symmetric_key2]
thekeys.each do |key|
return decode_token_with_key(auth_token, skey: key)
rescue CF::UAA::InvalidSignature => e
last_error = e
end
raise last_error
end
def decode_token_with_asymmetric_key(auth_token)
tries = 2
last_error = nil
while tries > 0
tries -= 1
# If we uncover issues due to attempting to decode with every
# key, we can revisit: https://www.pivotaltracker.com/story/show/132270761
asymmetric_key.value.each do |key|
return decode_token_with_key(auth_token, pkey: key)
rescue CF::UAA::InvalidSignature => e
last_error = e
end
asymmetric_key.refresh
end
raise last_error
end
def decode_token_with_key(auth_token, options)
time = Time.now.utc.to_i
if @alternate_reference_time
time = @alternate_reference_time
@logger.info("using alternate reference time of #{Time.at(@alternate_reference_time)} to calculate token expiry instead of current time")
end
options = { audience_ids: config[:resource_id] }.merge(options)
token = CF::UAA::TokenCoder.new(options).decode_at_reference_time(auth_token, time - @grace_period_in_seconds)
expiration_time = token['exp'] || token[:exp]
@logger.warn("token currently expired but accepted within grace period of #{@grace_period_in_seconds} seconds") if expiration_time && expiration_time < time
raise BadToken.new('Incorrect token') unless access_token?(token)
if token['iss'] != uaa_issuer
@uaa_issuer = nil
raise BadToken.new('Incorrect issuer') if token['iss'] != uaa_issuer
end
token
end
def symmetric_key
config[:symmetric_secret]
end
def symmetric_key2
config[:symmetric_secret2]
end
def asymmetric_key
@asymmetric_key ||= UaaVerificationKeys.new(uaa_username_lookup_client.info)
end
def uaa_username_lookup_client
::CloudController::DependencyLocator.instance.uaa_username_lookup_client
end
def uaa_issuer
@uaa_issuer ||= with_request_error_handling do
fetch_uaa_issuer
end
end
def fetch_uaa_issuer
response = http_client.get('.well-known/openid-configuration')
raise "Could not retrieve issuer information from UAA: #{response.status}" unless response.status == 200
Oj.load(response.body).fetch('issuer')
end
def http_client
uaa_target = config[:internal_url]
uaa_ca = config[:ca_file]
client = HTTPClient.new(base_url: uaa_target)
client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_PEER
client.ssl_config.set_trust_ca(uaa_ca) if uaa_ca.present?
client
end
def with_request_error_handling
tries ||= 3
yield
rescue StandardError
retry unless (tries -= 1).zero?
raise
end
def access_token?(token)
token['jti'] && token['jti'][-2..] != '-r'
end
end
end