Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions gems/smithy-client/lib/smithy-client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@

# identity and auth

require_relative 'smithy-client/auth'
require_relative 'smithy-client/auth_scheme'
require_relative 'smithy-client/identity_provider'
require_relative 'smithy-client/null_signer'
require_relative 'smithy-client/refreshing_identity_provider'

# stubbing
Expand Down
60 changes: 60 additions & 0 deletions gems/smithy-client/lib/smithy-client/api_key_signer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# frozen_string_literal: true

module Smithy
module Client
# Signs requests with the ApiKey identity.
class ApiKeySigner
def initialize(options = {})
@name = options[:name]
@in = options[:in]
@scheme = options[:scheme]
end
Comment on lines +6 to +11
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these intended to be public interfaces - if so, we should include more documentation about init options? Here, other signers and auth scheme.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I intended them to be API private for now. I'm also not sure yet so I'm going to wait on documentation.


def sign_request(context)
case @in
when 'header' then sign_in_header(context.http_request, context.config.api_key_provider)
when 'query' then sign_in_query_param(context.http_request, context.config.api_key_provider)
end
end

def presign_url(_context)
raise NotImplementedError
end

private

def sign_in_header(http_request, provider)
http_request.headers.delete(@name)
value = "#{@scheme} #{provider.identity.key}".strip
http_request.headers[@name] = value
end

def sign_in_query_param(http_request, provider)
remove_query_param(http_request)
append_query_param(http_request, provider)
end

def append_query_param(http_request, provider)
value = provider.identity.key
if http_request.endpoint.query
http_request.endpoint.query += "&#{@name}=#{value}"
else
http_request.endpoint.query = "#{@name}=#{value}"
end
end

def remove_query_param(http_request)
return unless http_request.endpoint.query

parsed = CGI.parse(http_request.endpoint.query)
parsed.delete(@name)
# encode_www_form ignores query params without values
# (CGI parses these as empty lists)
parsed.each do |key, values|
parsed[key] = values.empty? ? nil : values
end
http_request.endpoint.query = URI.encode_www_form(parsed)
end
end
end
end
106 changes: 106 additions & 0 deletions gems/smithy-client/lib/smithy-client/auth.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# frozen_string_literal: true

module Smithy
module Client
# @api private
module Auth
ResolvedAuth = Struct.new(:scheme_id, :signer, :signer_properties, :identity_provider, keyword_init: true)

class << self
def resolve(context, endpoint_properties = {})
if endpoint_properties.key?('authSchemes')
resolve_with_endpoint_auth(context.config, endpoint_properties['authSchemes'])
else
auth_parameters = context.client.class.auth_parameters.create(context)
resolve_without_endpoint_auth(context.config, auth_parameters)
end
end

private

def resolve_with_endpoint_auth(config, endpoint_auth_schemes)
endpoint_auth_schemes_map = config.endpoint_auth_schemes
normalized_endpoint_schemes = []
endpoint_auth_schemes.each do |scheme|
normalized_scheme_id = endpoint_auth_schemes_map[scheme['name']]
next unless normalized_scheme_id

properties = {}
scheme.each do |key, value|
next if key == 'name'

properties[key] = value
end
normalized_endpoint_schemes << { scheme_id: normalized_scheme_id, signer_properties: properties }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming we'll need to make changes to sigv4 auth in V4 as well following these changes. Have you tested with V4?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I tested with v4 and I opened a separate PR.

end
resolved_auth_options = prioritize_auth_options(normalized_endpoint_schemes, config.auth_scheme_preference)
resolve_auth_scheme(config.auth_schemes, resolved_auth_options)
end

def resolve_without_endpoint_auth(config, auth_parameters)
auth_options = config.auth_resolver.resolve(auth_parameters)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm understanding this correctly, endpoint auth completely overrides the modeled auths?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. I think we should do it that way. It's less complex and I think endpoint auth is guaranteed to work.

resolved_auth_options = prioritize_auth_options(auth_options, config.auth_scheme_preference)
resolve_auth_scheme(config.auth_schemes, resolved_auth_options)
end

def prioritize_auth_options(auth_options, auth_scheme_preference)
return auth_options if auth_scheme_preference.empty?

auth_options_by_id = {}
auth_options.each do |option|
auth_options_by_id[option[:scheme_id]] = option
end

preferred_options = []
auth_scheme_preference.each do |scheme_id|
option = auth_options_by_id[scheme_id]
next unless option

preferred_options << option
end

preferred_options.empty? ? auth_options : preferred_options
end

def resolve_auth_scheme(auth_schemes, auth_options)
raise 'No auth options were resolved' if auth_options.empty?

failures = []
auth_options.each do |auth_option|
scheme_id = auth_option[:scheme_id]
if scheme_id == 'smithy.api#noAuth'
return ResolvedAuth.new(
scheme_id: 'smithy.api#noAuth', signer: NullSigner.new, signer_properties: {},
identity_provider: nil
)
end
auth_scheme = auth_schemes[scheme_id]
error = validate_auth_scheme(auth_scheme, scheme_id)
unless error
return ResolvedAuth.new(
scheme_id: scheme_id,
signer: auth_scheme.signer,
signer_properties: auth_option[:signer_properties] || {},
identity_provider: auth_scheme.identity_provider
)
end

failures << error
end

raise failures.join("\n")
end

def validate_auth_scheme(auth_scheme, scheme_id)
return "Auth scheme #{scheme_id} was not enabled for this request" unless auth_scheme

identity_provider = auth_scheme.identity_provider
return "Auth scheme #{scheme_id} did not have an identity provider configured" unless identity_provider
return "Auth scheme #{scheme_id} failed to resolve identity" unless identity_provider.set?

nil
end
end
end
end
end
23 changes: 23 additions & 0 deletions gems/smithy-client/lib/smithy-client/auth_scheme.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

module Smithy
module Client
# Contains information about configured authentication schemes.
class AuthScheme
def initialize(options = {})
@scheme_id = options[:scheme_id]
@identity_provider = options[:identity_provider]
@signer = options[:signer]
end

# @return [String]
attr_reader :scheme_id

# @return [IdentityProvider]
attr_reader :identity_provider

# @return [Signer]
attr_reader :signer
end
end
end
18 changes: 18 additions & 0 deletions gems/smithy-client/lib/smithy-client/bearer_token_signer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module Smithy
module Client
# Signs requests with the BearerToken identity.
class BearerTokenSigner
def sign_request(context)
context.http_request.headers.delete('Authorization')
provider = context.config.bearer_token_provider
context.http_request.headers['Authorization'] = "Bearer #{provider.identity.token}"
end

def presign_url(_context)
raise NotImplementedError
end
end
end
end
31 changes: 31 additions & 0 deletions gems/smithy-client/lib/smithy-client/login_signer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

module Smithy
module Client
# Signs requests with the Login identity.
class LoginSigner
def initialize(options = {})
@scheme_id = options[:scheme_id]
end

def sign_request(context)
raise NotImplementedError unless @scheme_id == 'smithy.api#httpBasicAuth'

sign_with_basic(context.http_request, context.config.login_provider)
end

def presign_url(_context)
raise NotImplementedError
end

private

def sign_with_basic(http_request, provider)
http_request.headers.delete('Authorization')
identity = provider.identity
encoded = Base64.strict_encode64("#{identity.username}:#{identity.password}")
http_request.headers['Authorization'] = "Basic #{encoded}"
end
end
end
end
16 changes: 16 additions & 0 deletions gems/smithy-client/lib/smithy-client/null_signer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

module Smithy
module Client
# A signer that does not sign requests. Used for anonymous authentication.
class NullSigner
def sign_request(_context)
# no-op
end

def presign_url(_context)
# no-op
end
end
end
end
65 changes: 10 additions & 55 deletions gems/smithy-client/lib/smithy-client/plugins/http_api_key_auth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require_relative '../api_key'
require_relative '../api_key_provider'
require_relative '../api_key_signer'

module Smithy
module Client
Expand All @@ -27,64 +28,18 @@ class that responds to #identity and returns a {Smithy::Client::ApiKey}.
provider if provider.set?
end

def after_initialize(client)
client.config.auth_schemes['smithy.api#httpApiKeyAuth'] = client.config.api_key_provider
option(:api_key_signer) do |config|
trait = config.service.traits['smithy.api#httpApiKeyAuth']
ApiKeySigner.new(name: trait['name'], in: trait['in'], scheme: trait['scheme'])
end

# @api private
class Handler < Client::Handler
def call(context)
sign(context) if context.auth[:scheme_id] == 'smithy.api#httpApiKeyAuth'
@handler.call(context)
end

private

def sign(context)
properties = context.config.service.traits['smithy.api#httpApiKeyAuth']
http_request = context.http_request
identity = context.config.api_key_provider.identity
case properties['in']
when 'header' then sign_in_header(properties, http_request, identity)
when 'query' then sign_in_query_param(properties, http_request, identity)
end
end

def sign_in_header(properties, http_request, identity)
http_request.headers.delete(properties['name'])
value = "#{properties['scheme']} #{identity.key}".strip
http_request.headers[properties['name']] = value
end

def sign_in_query_param(properties, http_request, identity)
name = properties['name']
remove_query_param(http_request, name)
append_query_param(http_request, name, identity.key)
end

def append_query_param(request, name, value)
if request.endpoint.query
request.endpoint.query += "&#{name}=#{value}"
else
request.endpoint.query = "#{name}=#{value}"
end
end

def remove_query_param(request, name)
return unless request.endpoint.query

parsed = CGI.parse(request.endpoint.query)
parsed.delete(name)
# encode_www_form ignores query params without values
# (CGI parses these as empty lists)
parsed.each do |key, values|
parsed[key] = values.empty? ? nil : values
end
request.endpoint.query = URI.encode_www_form(parsed)
end
def after_initialize(client)
client.config.auth_schemes['smithy.api#httpApiKeyAuth'] = AuthScheme.new(
identity_provider: client.config.api_key_provider,
scheme_id: 'smithy.api#httpApiKeyAuth',
signer: client.config.api_key_signer
)
end

handler(Handler, step: :sign)
end
end
end
Expand Down
30 changes: 9 additions & 21 deletions gems/smithy-client/lib/smithy-client/plugins/http_basic_auth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require_relative '../login'
require_relative '../login_provider'
require_relative '../login_signer'

module Smithy
module Client
Expand Down Expand Up @@ -35,30 +36,17 @@ class that responds to #identity and returns a {Smithy::Client::Login}.
provider if provider.set?
end

def after_initialize(client)
client.config.auth_schemes['smithy.api#httpBasicAuth'] = client.config.login_provider
option(:login_signer) do |_config|
LoginSigner.new(scheme_id: 'smithy.api#httpBasicAuth')
end

# @api private
class Handler < Client::Handler
def call(context)
sign(context) if context.auth[:scheme_id] == 'smithy.api#httpBasicAuth'
@handler.call(context)
end

private

def sign(context)
http_request = context.http_request
identity = context.config.login_provider.identity
http_request.headers.delete('Authorization')
identity_string = "#{identity.username}:#{identity.password}"
encoded = Base64.strict_encode64(identity_string)
http_request.headers['Authorization'] = "Basic #{encoded}"
end
def after_initialize(client)
client.config.auth_schemes['smithy.api#httpBasicAuth'] = AuthScheme.new(
identity_provider: client.config.login_provider,
scheme_id: 'smithy.api#httpBasicAuth',
signer: client.config.login_signer
)
end

handler(Handler, step: :sign)
end
end
end
Expand Down
Loading
Loading