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
|
# frozen_string_literal: true
module Aws
class SSOTokenProvider
include TokenProvider
include RefreshingToken
# @api private
SSO_REQUIRED_OPTS = [:sso_region, :sso_session].freeze
# @api private
SSO_LOGIN_GUIDANCE = 'The SSO session associated with this profile has '\
'expired or is otherwise invalid. To refresh this SSO session run '\
'aws sso login with the corresponding profile.'.freeze
# @option options [required, String] :sso_region The AWS region where the
# SSO directory for the given sso_start_url is hosted.
#
# @option options [required, String] :sso_session The SSO Session used to
# for fetching this token.
#
# @option options [SSOOIDC::Client] :client Optional `SSOOIDC::Client`. If not
# provided, a client will be constructed.
#
# @option options [Callable] before_refresh Proc called before
# credentials are refreshed. `before_refresh` is called
# with an instance of this object when
# AWS credentials are required and need to be refreshed.
def initialize(options = {})
missing_keys = SSO_REQUIRED_OPTS.select { |k| options[k].nil? }
unless missing_keys.empty?
raise ArgumentError, "Missing required keys: #{missing_keys}"
end
@sso_session = options.delete(:sso_session)
@sso_region = options.delete(:sso_region)
options[:region] = @sso_region
options[:credentials] = nil
options[:token_provider] = nil
@client = options[:client] || Aws::SSOOIDC::Client.new(options)
super
end
# @return [SSOOIDC::Client]
attr_reader :client
private
def refresh
# token is valid and not in refresh window - do not refresh it.
return if @token && @token.expiration && !near_expiration?
# token may not exist or is out of the expiration window
# attempt to refresh from disk first (another process/application may have refreshed already)
token_json = read_cached_token
@token = Token.new(token_json['accessToken'], token_json['expiresAt'])
return if @token && @token.expiration && !near_expiration?
# The token is expired and needs to be refreshed
if can_refresh_token?(token_json)
begin
current_time = Time.now
resp = @client.create_token(
grant_type: 'refresh_token',
client_id: token_json['clientId'],
client_secret: token_json['clientSecret'],
refresh_token: token_json['refreshToken']
)
token_json['accessToken'] = resp.access_token
token_json['expiresAt'] = current_time + resp.expires_in
@token = Token.new(token_json['accessToken'], token_json['expiresAt'])
if resp.refresh_token
token_json['refreshToken'] = resp.refresh_token
else
token_json.delete('refreshToken')
end
update_token_cache(token_json)
rescue
# refresh has failed, continue attempting to use the token if its not hard expired
end
end
if !@token.expiration || @token.expiration < Time.now
# Token is hard expired, raise an exception
raise Errors::InvalidSSOToken, 'Token is invalid and failed to refresh.'
end
end
def read_cached_token
cached_token = Json.load(File.read(sso_cache_file))
# validation
unless cached_token['accessToken'] && cached_token['expiresAt']
raise ArgumentError, 'Missing required field(s)'
end
cached_token['expiresAt'] = Time.parse(cached_token['expiresAt'])
cached_token
rescue Errno::ENOENT, Aws::Json::ParseError, ArgumentError
raise Errors::InvalidSSOToken, SSO_LOGIN_GUIDANCE
end
def update_token_cache(token_json)
cached_token = token_json.dup
cached_token['expiresAt'] = cached_token['expiresAt'].iso8601
File.write(sso_cache_file, Json.dump(cached_token))
end
def sso_cache_file
sso_session_sha1 = OpenSSL::Digest::SHA1.hexdigest(@sso_session.encode('utf-8'))
File.join(Dir.home, '.aws', 'sso', 'cache', "#{sso_session_sha1}.json")
rescue ArgumentError
# Dir.home raises ArgumentError when ENV['home'] is not set
raise ArgumentError, "Unable to load sso_cache_file: ENV['HOME'] is not set."
end
# return true if all required fields are present
# return false if registrationExpiresAt exists and is later than now
def can_refresh_token?(token_json)
if token_json['clientId'] &&
token_json['clientSecret'] &&
token_json['refreshToken']
return !token_json['registrationExpiresAt'] ||
Time.parse(token_json['registrationExpiresAt']) > Time.now
else
false
end
end
end
end
|