@@ -6,21 +6,29 @@ module Authentication
66 module AuthnIam
77 class Authenticator
88
9- def initialize ( env :, logger : Rails . logger , client : Net ::HTTP )
9+ def initialize (
10+ env :,
11+ logger : Rails . logger ,
12+ client : Net ::HTTP ,
13+ fetch_authenticator_secrets : Authentication ::Util ::FetchAuthenticatorSecrets . new (
14+ optional_variable_names : %w[ optional-signed-headers ]
15+ ) )
1016 @env = env
1117 @logger = logger
1218 @client = client
19+ @fetch_authenticator_secrets = fetch_authenticator_secrets
1320 end
1421
15- VALID_KEYS = %w[ host authorization x-amz-date x-amz-security-token x-amz-content-sha256 ] . freeze
22+ REQUIRED_KEYS = %w[
23+ host
24+ authorization
25+ x-amz-date
26+ x-amz-security-token
27+ x-amz-content-sha256 ] . freeze
1628
1729 def valid? ( input )
18- # input.credentials is JSON holding the AWS signed headers
19- signed_aws_headers = JSON . parse ( input . credentials )
20- . transform_keys ( &:downcase )
21- . select { |k , _ | VALID_KEYS . include? ( k ) }
22- raise Errors ::Authentication ::AuthnIam ::InvalidAWSHeaders ,
23- "Headers validation failed" unless valid_headers? ( signed_aws_headers )
30+ @authenticator_input = input
31+ signed_aws_headers = extract_signed_headers
2432
2533 aws_response = response_from_signed_request ( signed_aws_headers )
2634
@@ -78,7 +86,7 @@ def attempt_signed_request(signed_headers)
7886 return fallback_response if fallback_response . code . to_i == 200
7987 end
8088
81- return response
89+ response
8290 end
8391
8492 def aws_call ( region :, headers :)
@@ -138,12 +146,57 @@ def valid_region?(region)
138146 /\A ([a-z]{2}(-gov)?-[a-z]+-\d )\z / . match? ( region )
139147 end
140148
141- def valid_headers ?( signed_aws_headers )
149+ def valid_host_header ?( signed_aws_headers )
142150 host = signed_aws_headers [ 'host' ]
143151 return true if host . nil? || host . empty?
144152 uri = URI ( "https://#{ host } " )
145153 uri . host &.end_with? ( '.amazonaws.com' )
146154 end
155+
156+ def iam_authenticator_secrets
157+ @iam_authenticator_secrets ||= @fetch_authenticator_secrets . call (
158+ service_id : @authenticator_input . service_id ,
159+ conjur_account : @authenticator_input . account ,
160+ authenticator_name : @authenticator_input . authenticator_name ,
161+ required_variable_names : [ ] ,
162+ )
163+ end
164+
165+ def optional_signed_headers
166+ @optional_signed_headers ||=
167+ ( iam_authenticator_secrets &.dig ( 'optional-signed-headers' )
168+ &.to_s &.split ( ';' )
169+ &.map ( &:downcase )
170+ &.map ( &:strip ) ) || [ ]
171+ end
172+
173+ def extract_signed_headers
174+ input = JSON . parse ( @authenticator_input . credentials ) . transform_keys ( &:downcase )
175+ match = input [ 'authorization' ] &.match ( %r{SignedHeaders=([A-Za-z0-9;_-]+)} )
176+ raise Errors ::Authentication ::AuthnIam ::InvalidAWSHeaders ,
177+ "Failed to extract signed headers" unless match
178+ signed_headers = match [ 1 ] . split ( ';' ) . map ( &:downcase )
179+
180+ missing = signed_headers - input . keys
181+ raise Errors ::Authentication ::AuthnIam ::InvalidAWSHeaders ,
182+ "Missing required signed headers: #{ missing . join ( ', ' ) } " unless missing . empty?
183+
184+ allowed_keys = REQUIRED_KEYS
185+ allowed_keys = allowed_keys | optional_signed_headers unless optional_signed_headers . empty?
186+
187+ unexpected = signed_headers - allowed_keys
188+ raise Errors ::Authentication ::AuthnIam ::InvalidAWSHeaders ,
189+ "Unexpected signed headers found: #{ unexpected . join ( ', ' ) } . " +
190+ "Please use only permitted headers in the signature. " +
191+ "If you need to include optional headers, please ensure " +
192+ "they are secure and then add them to the authenticator " +
193+ "configuration." unless unexpected . empty?
194+
195+ raise Errors ::Authentication ::AuthnIam ::InvalidAWSHeaders ,
196+ "Host header validation failed" unless valid_host_header? ( input )
197+
198+ input . select { |k , _ | allowed_keys . include? ( k ) }
199+ end
147200 end
148201 end
149202end
0 commit comments