Skip to content

Commit 4969009

Browse files
author
Matt Muller
committed
Working auth scheme approach
1 parent 427c44e commit 4969009

25 files changed

Lines changed: 297 additions & 172 deletions

gems/smithy-client/lib/smithy-client.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@
5151
# identity and auth
5252

5353
require_relative 'smithy-client/auth'
54+
require_relative 'smithy-client/auth_scheme'
5455
require_relative 'smithy-client/identity_provider'
56+
require_relative 'smithy-client/null_signer'
5557
require_relative 'smithy-client/refreshing_identity_provider'
5658

5759
# stubbing
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# frozen_string_literal: true
2+
3+
module Smithy
4+
module Client
5+
# Signs requests with the ApiKey identity.
6+
class ApiKeySigner
7+
def initialize(options = {})
8+
@name = options[:name]
9+
@in = options[:in]
10+
@scheme = options[:scheme]
11+
end
12+
13+
def sign_request(context)
14+
case @in
15+
when 'header' then sign_in_header(context.http_request, context.config.api_key_provider)
16+
when 'query' then sign_in_query_param(context.http_request, context.config.api_key_provider)
17+
end
18+
end
19+
20+
def presign_url(_context)
21+
raise NotImplementedError
22+
end
23+
24+
private
25+
26+
def sign_in_header(http_request, provider)
27+
http_request.headers.delete(@name)
28+
value = "#{@scheme} #{provider.identity.key}".strip
29+
http_request.headers[@name] = value
30+
end
31+
32+
def sign_in_query_param(http_request, provider)
33+
remove_query_param(http_request)
34+
append_query_param(http_request, provider)
35+
end
36+
37+
def append_query_param(http_request, provider)
38+
value = provider.identity.key
39+
if http_request.endpoint.query
40+
http_request.endpoint.query += "&#{@name}=#{value}"
41+
else
42+
http_request.endpoint.query = "#{@name}=#{value}"
43+
end
44+
end
45+
46+
def remove_query_param(http_request)
47+
return unless http_request.endpoint.query
48+
49+
parsed = CGI.parse(http_request.endpoint.query)
50+
parsed.delete(@name)
51+
# encode_www_form ignores query params without values
52+
# (CGI parses these as empty lists)
53+
parsed.each do |key, values|
54+
parsed[key] = values.empty? ? nil : values
55+
end
56+
http_request.endpoint.query = URI.encode_www_form(parsed)
57+
end
58+
end
59+
end
60+
end

gems/smithy-client/lib/smithy-client/auth.rb

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,29 @@ module Client
33
# @api private
44
module Auth
55
class << self
6-
def resolve_auth(context, endpoint_properties = {})
6+
def resolve(context, endpoint_properties = {})
77
if endpoint_properties.key?('authSchemes')
8-
resolve_auth_scheme_with_endpoint(context, endpoint_properties['authSchemes'])
8+
resolve_with_endpoint_auth(context, endpoint_properties['authSchemes'])
99
else
10-
resolve_auth_scheme_without_endpoint(context)
10+
resolve_without_endpoint_auth(context)
1111
end
1212
end
1313

1414
private
1515

16-
def resolve_auth_scheme_with_endpoint(context, endpoint_auth_schemes)
16+
def resolve_with_endpoint_auth(context, endpoint_auth_schemes)
1717
normalized_endpoint_schemes = []
1818
endpoint_auth_schemes.each do |scheme|
1919
scheme_id = context.config.endpoint_auth_schemes[scheme['name']]
2020
next unless scheme_id
2121

22-
normalized_scheme = { scheme_id: scheme_id }
22+
properties = {}
2323
scheme.each do |key, value|
2424
next if key == 'name'
2525

26-
normalized_scheme[key] = value
26+
properties[key] = value
2727
end
28-
normalized_endpoint_schemes << normalized_scheme
28+
normalized_endpoint_schemes << { scheme_id: scheme_id, properties: properties }
2929
end
3030
resolved_auth_options = prioritize_auth_options(
3131
normalized_endpoint_schemes,
@@ -34,7 +34,7 @@ def resolve_auth_scheme_with_endpoint(context, endpoint_auth_schemes)
3434
resolve_auth_scheme(context.config.auth_schemes, resolved_auth_options)
3535
end
3636

37-
def resolve_auth_scheme_without_endpoint(context)
37+
def resolve_without_endpoint_auth(context)
3838
auth_parameters = context.client.class.auth_parameters.create(context)
3939
auth_options = context.config.auth_resolver.resolve(auth_parameters)
4040
resolved_auth_options = prioritize_auth_options(auth_options, context.config.auth_scheme_preference)
@@ -66,23 +66,24 @@ def resolve_auth_scheme(auth_schemes, auth_options)
6666
failures = []
6767
auth_options.each do |auth_option|
6868
scheme_id = auth_option[:scheme_id]
69+
if scheme_id == 'smithy.api#noAuth'
70+
return AuthScheme.new(identity_provider: nil, scheme_id: 'smithy.api#noAuth', signer: NullSigner.new)
71+
end
6972

70-
# Anonymous auth does not have a plugin and does not sign
71-
return auth_option if scheme_id == 'smithy.api#noAuth'
72-
73-
error = validate_auth_scheme(auth_schemes, scheme_id)
74-
return auth_option unless error
73+
auth_scheme = auth_schemes[scheme_id]
74+
error = validate_auth_scheme(auth_scheme, scheme_id)
75+
return auth_scheme unless error
7576

7677
failures << error
7778
end
7879

7980
raise failures.join("\n")
8081
end
8182

82-
def validate_auth_scheme(auth_schemes, scheme_id)
83-
return "Auth scheme #{scheme_id} was not enabled for this request" unless auth_schemes.key?(scheme_id)
83+
def validate_auth_scheme(auth_scheme, scheme_id)
84+
return "Auth scheme #{scheme_id} was not enabled for this request" unless auth_scheme
8485

85-
identity_provider = auth_schemes[scheme_id]
86+
identity_provider = auth_scheme.identity_provider
8687
return "Auth scheme #{scheme_id} did not have an identity provider configured" unless identity_provider
8788
return "Auth scheme #{scheme_id} failed to resolve identity" unless identity_provider.set?
8889

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# frozen_string_literal: true
2+
3+
module Smithy
4+
module Client
5+
# Contains information about candidate authentication schemes.
6+
class AuthScheme
7+
def initialize(options = {})
8+
@identity_provider = options[:identity_provider]
9+
@scheme_id = options[:scheme_id]
10+
@signer = options[:signer]
11+
end
12+
13+
# @return [IdentityProvider]
14+
attr_reader :identity_provider
15+
16+
# @return [String]
17+
attr_reader :scheme_id
18+
19+
# @return [Signer]
20+
attr_reader :signer
21+
end
22+
end
23+
end
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# frozen_string_literal: true
2+
3+
module Smithy
4+
module Client
5+
# Signs requests with the BearerToken identity.
6+
class BearerTokenSigner
7+
def sign_request(context)
8+
context.http_request.headers.delete('Authorization')
9+
provider = context.config.bearer_token_provider
10+
context.http_request.headers['Authorization'] = "Bearer #{provider.identity.token}"
11+
end
12+
13+
def presign_url(_context)
14+
raise NotImplementedError
15+
end
16+
end
17+
end
18+
end
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# frozen_string_literal: true
2+
3+
module Smithy
4+
module Client
5+
# Signs requests with the Login identity.
6+
class LoginSigner
7+
def initialize(options = {})
8+
@scheme_id = options[:scheme_id]
9+
end
10+
11+
def sign_request(context)
12+
raise NotImplementedError unless @scheme_id == 'smithy.api#httpBasicAuth'
13+
14+
sign_with_basic(context.http_request, context.config.login_provider)
15+
end
16+
17+
def presign_url(_context)
18+
raise NotImplementedError
19+
end
20+
21+
private
22+
23+
def sign_with_basic(http_request, provider)
24+
http_request.headers.delete('Authorization')
25+
identity = provider.identity
26+
encoded = Base64.strict_encode64("#{identity.username}:#{identity.password}")
27+
http_request.headers['Authorization'] = "Basic #{encoded}"
28+
end
29+
end
30+
end
31+
end
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# frozen_string_literal: true
2+
3+
module Smithy
4+
module Client
5+
# A signer that does not sign requests. Used for anonymous authentication.
6+
class NullSigner
7+
def sign_request(_context)
8+
# no-op
9+
end
10+
11+
def presign_url(_context)
12+
# no-op
13+
end
14+
end
15+
end
16+
end

gems/smithy-client/lib/smithy-client/plugins/http_api_key_auth.rb

Lines changed: 10 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require_relative '../api_key'
44
require_relative '../api_key_provider'
5+
require_relative '../api_key_signer'
56

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

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

34-
# @api private
35-
class Handler < Client::Handler
36-
def call(context)
37-
sign(context) if context.auth[:scheme_id] == 'smithy.api#httpApiKeyAuth'
38-
@handler.call(context)
39-
end
40-
41-
private
42-
43-
def sign(context)
44-
properties = context.config.service.traits['smithy.api#httpApiKeyAuth']
45-
http_request = context.http_request
46-
identity = context.config.api_key_provider.identity
47-
case properties['in']
48-
when 'header' then sign_in_header(properties, http_request, identity)
49-
when 'query' then sign_in_query_param(properties, http_request, identity)
50-
end
51-
end
52-
53-
def sign_in_header(properties, http_request, identity)
54-
http_request.headers.delete(properties['name'])
55-
value = "#{properties['scheme']} #{identity.key}".strip
56-
http_request.headers[properties['name']] = value
57-
end
58-
59-
def sign_in_query_param(properties, http_request, identity)
60-
name = properties['name']
61-
remove_query_param(http_request, name)
62-
append_query_param(http_request, name, identity.key)
63-
end
64-
65-
def append_query_param(request, name, value)
66-
if request.endpoint.query
67-
request.endpoint.query += "&#{name}=#{value}"
68-
else
69-
request.endpoint.query = "#{name}=#{value}"
70-
end
71-
end
72-
73-
def remove_query_param(request, name)
74-
return unless request.endpoint.query
75-
76-
parsed = CGI.parse(request.endpoint.query)
77-
parsed.delete(name)
78-
# encode_www_form ignores query params without values
79-
# (CGI parses these as empty lists)
80-
parsed.each do |key, values|
81-
parsed[key] = values.empty? ? nil : values
82-
end
83-
request.endpoint.query = URI.encode_www_form(parsed)
84-
end
36+
def after_initialize(client)
37+
client.config.auth_schemes['smithy.api#httpApiKeyAuth'] = AuthScheme.new(
38+
identity_provider: client.config.api_key_provider,
39+
scheme_id: 'smithy.api#httpApiKeyAuth',
40+
signer: client.config.api_key_signer
41+
)
8542
end
86-
87-
handler(Handler, step: :sign)
8843
end
8944
end
9045
end

gems/smithy-client/lib/smithy-client/plugins/http_basic_auth.rb

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require_relative '../login'
44
require_relative '../login_provider'
5+
require_relative '../login_signer'
56

67
module Smithy
78
module Client
@@ -35,30 +36,17 @@ class that responds to #identity and returns a {Smithy::Client::Login}.
3536
provider if provider.set?
3637
end
3738

38-
def after_initialize(client)
39-
client.config.auth_schemes['smithy.api#httpBasicAuth'] = client.config.login_provider
39+
option(:login_signer) do |_config|
40+
LoginSigner.new(scheme_id: 'smithy.api#httpBasicAuth')
4041
end
4142

42-
# @api private
43-
class Handler < Client::Handler
44-
def call(context)
45-
sign(context) if context.auth[:scheme_id] == 'smithy.api#httpBasicAuth'
46-
@handler.call(context)
47-
end
48-
49-
private
50-
51-
def sign(context)
52-
http_request = context.http_request
53-
identity = context.config.login_provider.identity
54-
http_request.headers.delete('Authorization')
55-
identity_string = "#{identity.username}:#{identity.password}"
56-
encoded = Base64.strict_encode64(identity_string)
57-
http_request.headers['Authorization'] = "Basic #{encoded}"
58-
end
43+
def after_initialize(client)
44+
client.config.auth_schemes['smithy.api#httpBasicAuth'] = AuthScheme.new(
45+
identity_provider: client.config.login_provider,
46+
scheme_id: 'smithy.api#httpBasicAuth',
47+
signer: client.config.login_signer
48+
)
5949
end
60-
61-
handler(Handler, step: :sign)
6250
end
6351
end
6452
end

0 commit comments

Comments
 (0)