googleauth 0.8.1 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +7 -0
  3. data/.github/workflows/release.yml +39 -0
  4. data/.kokoro/build.bat +9 -1
  5. data/.kokoro/continuous/linux.cfg +12 -2
  6. data/.kokoro/continuous/osx.cfg +5 -0
  7. data/.kokoro/continuous/post.cfg +30 -0
  8. data/.kokoro/continuous/windows.cfg +27 -1
  9. data/.kokoro/presubmit/linux.cfg +11 -1
  10. data/.kokoro/presubmit/osx.cfg +5 -0
  11. data/.kokoro/presubmit/windows.cfg +27 -1
  12. data/.kokoro/release.cfg +42 -1
  13. data/.kokoro/trampoline.bat +10 -0
  14. data/.repo-metadata.json +5 -0
  15. data/.rubocop.yml +8 -2
  16. data/CHANGELOG.md +94 -20
  17. data/Gemfile +7 -7
  18. data/{COPYING → LICENSE} +0 -0
  19. data/README.md +12 -15
  20. data/Rakefile +48 -5
  21. data/googleauth.gemspec +7 -3
  22. data/integration/helper.rb +31 -0
  23. data/integration/id_tokens/key_source_test.rb +74 -0
  24. data/lib/googleauth.rb +1 -0
  25. data/lib/googleauth/application_default.rb +2 -2
  26. data/lib/googleauth/compute_engine.rb +45 -20
  27. data/lib/googleauth/credentials.rb +445 -71
  28. data/lib/googleauth/credentials_loader.rb +11 -9
  29. data/lib/googleauth/iam.rb +1 -1
  30. data/lib/googleauth/id_tokens.rb +233 -0
  31. data/lib/googleauth/id_tokens/errors.rb +71 -0
  32. data/lib/googleauth/id_tokens/key_sources.rb +396 -0
  33. data/lib/googleauth/id_tokens/verifier.rb +142 -0
  34. data/lib/googleauth/json_key_reader.rb +6 -2
  35. data/lib/googleauth/scope_util.rb +1 -1
  36. data/lib/googleauth/service_account.rb +42 -23
  37. data/lib/googleauth/signet.rb +9 -6
  38. data/lib/googleauth/stores/file_token_store.rb +1 -0
  39. data/lib/googleauth/stores/redis_token_store.rb +1 -0
  40. data/lib/googleauth/user_authorizer.rb +6 -1
  41. data/lib/googleauth/user_refresh.rb +2 -2
  42. data/lib/googleauth/version.rb +1 -1
  43. data/lib/googleauth/web_user_authorizer.rb +16 -14
  44. data/rakelib/devsite_builder.rb +45 -0
  45. data/rakelib/link_checker.rb +64 -0
  46. data/rakelib/repo_metadata.rb +59 -0
  47. data/spec/googleauth/apply_auth_examples.rb +28 -5
  48. data/spec/googleauth/compute_engine_spec.rb +69 -13
  49. data/spec/googleauth/credentials_spec.rb +492 -165
  50. data/spec/googleauth/service_account_spec.rb +31 -16
  51. data/spec/googleauth/signet_spec.rb +46 -7
  52. data/spec/googleauth/user_authorizer_spec.rb +21 -1
  53. data/spec/googleauth/user_refresh_spec.rb +1 -1
  54. data/spec/googleauth/web_user_authorizer_spec.rb +6 -0
  55. data/test/helper.rb +33 -0
  56. data/test/id_tokens/key_sources_test.rb +240 -0
  57. data/test/id_tokens/verifier_test.rb +269 -0
  58. metadata +49 -13
  59. data/.kokoro/windows.sh +0 -4
data/Gemfile CHANGED
@@ -8,18 +8,18 @@ group :development do
8
8
  gem "coveralls", "~> 0.7"
9
9
  gem "fakefs", "~> 0.6"
10
10
  gem "fakeredis", "~> 0.5"
11
- gem "google-style", "~> 0.2"
11
+ gem "google-style", "~> 1.25.1"
12
12
  gem "logging", "~> 2.0"
13
+ gem "minitest", "~> 5.14"
14
+ gem "minitest-focus", "~> 1.1"
13
15
  gem "rack-test", "~> 0.6"
14
- gem "rake", "~> 10.0"
16
+ gem "rake", "~> 13.0"
15
17
  gem "redis", "~> 3.2"
16
18
  gem "rspec", "~> 3.0"
17
19
  gem "simplecov", "~> 0.9"
18
20
  gem "sinatra"
19
- gem "webmock", "~> 1.21"
21
+ gem "webmock", "~> 3.8"
20
22
  end
21
23
 
22
- platforms :jruby do
23
- group :development do
24
- end
25
- end
24
+ gem "faraday", ">= 0.17.3", "< 2.0"
25
+ gem "gems", "~> 1.2"
File without changes
data/README.md CHANGED
@@ -1,15 +1,13 @@
1
1
  # Google Auth Library for Ruby
2
2
 
3
3
  <dl>
4
- <dt>Homepage</dt><dd><a href="http://www.github.com/google/google-auth-library-ruby">http://www.github.com/google/google-auth-library-ruby</a></dd>
4
+ <dt>Homepage</dt><dd><a href="http://www.github.com/googleapis/google-auth-library-ruby">http://www.github.com/googleapis/google-auth-library-ruby</a></dd>
5
5
  <dt>Authors</dt><dd><a href="mailto:temiola@google.com">Tim Emiola</a></dd>
6
6
  <dt>Copyright</dt><dd>Copyright © 2015 Google, Inc.</dd>
7
7
  <dt>License</dt><dd>Apache 2.0</dd>
8
8
  </dl>
9
9
 
10
10
  [![Gem Version](https://badge.fury.io/rb/googleauth.svg)](http://badge.fury.io/rb/googleauth)
11
- [![Build Status](https://secure.travis-ci.org/google/google-auth-library-ruby.svg)](http://travis-ci.org/google/google-auth-library-ruby)
12
- [![Coverage Status](https://coveralls.io/repos/google/google-auth-library-ruby/badge.svg)](https://coveralls.io/r/google/google-auth-library-ruby)
13
11
 
14
12
  ## Description
15
13
 
@@ -180,18 +178,18 @@ access and refresh tokens. Two storage implementations are included:
180
178
  * Google::Auth::Stores::RedisTokenStore
181
179
 
182
180
  Custom storage implementations can also be used. See
183
- [token_store.rb](lib/googleauth/token_store.rb) for additional details.
181
+ [token_store.rb](https://googleapis.dev/ruby/googleauth/latest/Google/Auth/TokenStore.html) for additional details.
184
182
 
185
183
  ## Supported Ruby Versions
186
184
 
187
- This library is currently supported on Ruby 1.9+.
185
+ This library is supported on Ruby 2.5+.
188
186
 
189
- However, Ruby 2.4 or later is strongly recommended, as earlier releases have
190
- reached or are nearing end-of-life. After March 31, 2019, Google will provide
191
- official support only for Ruby versions that are considered current and
192
- supported by Ruby Core (that is, Ruby versions that are either in normal
193
- maintenance or in security maintenance).
194
- See https://www.ruby-lang.org/en/downloads/branches/ for further details.
187
+ Google provides official support for Ruby versions that are actively supported
188
+ by Ruby Core—that is, Ruby versions that are either in normal maintenance or in
189
+ security maintenance, and not end of life. Currently, this means Ruby 2.5 and
190
+ later. Older versions of Ruby _may_ still work, but are unsupported and not
191
+ recommended. See https://www.ruby-lang.org/en/downloads/branches/ for details
192
+ about the Ruby support schedule.
195
193
 
196
194
  ## License
197
195
 
@@ -210,7 +208,6 @@ hesitate to
210
208
  [ask questions](http://stackoverflow.com/questions/tagged/google-auth-library-ruby)
211
209
  about the client or APIs on [StackOverflow](http://stackoverflow.com).
212
210
 
213
- [google-apis-ruby-client]: (https://github.com/google/google-api-ruby-client)
214
- [application default credentials]: (https://developers.google.com/accounts/docs/application-default-credentials)
215
- [contributing]: https://github.com/google/google-auth-library-ruby/tree/master/CONTRIBUTING.md
216
- [copying]: https://github.com/google/google-auth-library-ruby/tree/master/COPYING
211
+ [application default credentials]: https://developers.google.com/accounts/docs/application-default-credentials
212
+ [contributing]: https://github.com/googleapis/google-auth-library-ruby/tree/master/.github/CONTRIBUTING.md
213
+ [copying]: https://github.com/googleapis/google-auth-library-ruby/tree/master/COPYING
data/Rakefile CHANGED
@@ -1,18 +1,40 @@
1
1
  # -*- ruby -*-
2
+ require "json"
2
3
  require "bundler/gem_tasks"
3
4
 
5
+ require "rubocop/rake_task"
6
+ RuboCop::RakeTask.new
7
+
8
+ require "rake/testtask"
9
+
10
+ desc "Run tests."
11
+ Rake::TestTask.new do |t|
12
+ t.libs << "test"
13
+ t.test_files = FileList["test/**/*_test.rb"]
14
+ t.warning = false
15
+ end
16
+
17
+ desc "Run integration tests."
18
+ Rake::TestTask.new("integration") do |t|
19
+ t.libs << "integration"
20
+ t.test_files = FileList["integration/**/*_test.rb"]
21
+ t.warning = false
22
+ end
23
+
4
24
  task :ci do
5
25
  header "Using Ruby - #{RUBY_VERSION}"
6
26
  sh "bundle exec rubocop"
27
+ Rake::Task["test"].invoke
28
+ Rake::Task["integration"].invoke
7
29
  sh "bundle exec rspec"
8
30
  end
9
31
 
10
- task :release, :tag do |_t, args|
32
+ task :release_gem, :tag do |_t, args|
11
33
  tag = args[:tag]
12
34
  raise "You must provide a tag to release." if tag.nil?
13
35
 
14
36
  # Verify the tag format "vVERSION"
15
- m = tag.match(/v(?<version>\S*)/)
37
+ m = tag.match /v(?<version>\S*)/
16
38
  raise "Tag #{tag} does not match the expected format." if m.nil?
17
39
 
18
40
  version = m[:version]
@@ -33,17 +55,24 @@ task :release, :tag do |_t, args|
33
55
  sh "bundle exec rake build"
34
56
  end
35
57
 
36
- path_to_be_pushed = "pkg/#{version}.gem"
58
+ path_to_be_pushed = "pkg/googleauth-#{version}.gem"
59
+ gem_was_published = nil
37
60
  if File.file? path_to_be_pushed
38
61
  begin
39
- ::Gems.push File.new(path_to_be_pushed)
62
+ response = ::Gems.push File.new(path_to_be_pushed)
63
+ puts response
64
+ raise unless response.include? "Successfully registered gem:"
65
+ gem_was_published = true
40
66
  puts "Successfully built and pushed googleauth for version #{version}"
41
67
  rescue StandardError => e
68
+ gem_was_published = false
42
69
  puts "Error while releasing googleauth version #{version}: #{e.message}"
43
70
  end
44
71
  else
45
72
  raise "Cannot build googleauth for version #{version}"
46
73
  end
74
+
75
+ Rake::Task["kokoro:publish_docs"].invoke if gem_was_published
47
76
  end
48
77
 
49
78
  namespace :kokoro do
@@ -63,6 +92,14 @@ namespace :kokoro do
63
92
  Rake::Task["ci"].invoke
64
93
  end
65
94
 
95
+ task :post do
96
+ require_relative "rakelib/link_checker.rb"
97
+
98
+ link_checker = LinkChecker.new
99
+ link_checker.run
100
+ exit link_checker.exit_status
101
+ end
102
+
66
103
  task :nightly do
67
104
  Rake::Task["ci"].invoke
68
105
  end
@@ -75,7 +112,13 @@ namespace :kokoro do
75
112
  .first.split("(").last.split(")").first || "0.1.0"
76
113
  end
77
114
  Rake::Task["kokoro:load_env_vars"].invoke
78
- Rake::Task["release"].invoke "v/#{version}"
115
+ Rake::Task["release_gem"].invoke "v#{version}"
116
+ end
117
+
118
+ task :publish_docs do
119
+ require_relative "rakelib/devsite_builder.rb"
120
+
121
+ DevsiteBuilder.new(__dir__).publish
79
122
  end
80
123
  end
81
124
 
data/googleauth.gemspec CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |gem|
9
9
  gem.version = Google::Auth::VERSION
10
10
  gem.authors = ["Tim Emiola"]
11
11
  gem.email = "temiola@google.com"
12
- gem.homepage = "https://github.com/google/google-auth-library-ruby"
12
+ gem.homepage = "https://github.com/googleapis/google-auth-library-ruby"
13
13
  gem.summary = "Google Auth Library for Ruby"
14
14
  gem.license = "Apache-2.0"
15
15
  gem.description = <<-DESCRIPTION
@@ -24,12 +24,16 @@ Gem::Specification.new do |gem|
24
24
  File.basename f
25
25
  end
26
26
  gem.require_paths = ["lib"]
27
+
27
28
  gem.platform = Gem::Platform::RUBY
29
+ gem.required_ruby_version = ">= 2.5"
28
30
 
29
- gem.add_dependency "faraday", "~> 0.12"
31
+ gem.add_dependency "faraday", ">= 0.17.3", "< 2.0"
30
32
  gem.add_dependency "jwt", ">= 1.4", "< 3.0"
31
33
  gem.add_dependency "memoist", "~> 0.16"
32
34
  gem.add_dependency "multi_json", "~> 1.11"
33
35
  gem.add_dependency "os", ">= 0.9", "< 2.0"
34
- gem.add_dependency "signet", "~> 0.7"
36
+ gem.add_dependency "signet", "~> 0.14"
37
+
38
+ gem.add_development_dependency "yard", "~> 0.9"
35
39
  end
@@ -0,0 +1,31 @@
1
+ # Copyright 2020 Google LLC
2
+ #
3
+ # Redistribution and use in source and binary forms, with or without
4
+ # modification, are permitted provided that the following conditions are
5
+ # met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright
8
+ # notice, this list of conditions and the following disclaimer.
9
+ # * Redistributions in binary form must reproduce the above
10
+ # copyright notice, this list of conditions and the following disclaimer
11
+ # in the documentation and/or other materials provided with the
12
+ # distribution.
13
+ # * Neither the name of Google Inc. nor the names of its
14
+ # contributors may be used to endorse or promote products derived from
15
+ # this software without specific prior written permission.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
+
29
+ require "minitest/autorun"
30
+ require "minitest/focus"
31
+ require "googleauth"
@@ -0,0 +1,74 @@
1
+ # Copyright 2020 Google LLC
2
+ #
3
+ # Redistribution and use in source and binary forms, with or without
4
+ # modification, are permitted provided that the following conditions are
5
+ # met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright
8
+ # notice, this list of conditions and the following disclaimer.
9
+ # * Redistributions in binary form must reproduce the above
10
+ # copyright notice, this list of conditions and the following disclaimer
11
+ # in the documentation and/or other materials provided with the
12
+ # distribution.
13
+ # * Neither the name of Google Inc. nor the names of its
14
+ # contributors may be used to endorse or promote products derived from
15
+ # this software without specific prior written permission.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
+
29
+ require "helper"
30
+
31
+ describe Google::Auth::IDTokens do
32
+ describe "key source" do
33
+ let(:legacy_oidc_key_source) {
34
+ Google::Auth::IDTokens::X509CertHttpKeySource.new "https://www.googleapis.com/oauth2/v1/certs"
35
+ }
36
+ let(:oidc_key_source) { Google::Auth::IDTokens.oidc_key_source }
37
+ let(:iap_key_source) { Google::Auth::IDTokens.iap_key_source }
38
+
39
+ it "Gets real keys from the OAuth2 V1 cert URL" do
40
+ keys = legacy_oidc_key_source.refresh_keys
41
+ refute_empty keys
42
+ keys.each do |key|
43
+ assert_kind_of OpenSSL::PKey::RSA, key.key
44
+ refute key.key.private?
45
+ assert_equal "RS256", key.algorithm
46
+ end
47
+ end
48
+
49
+ it "Gets real keys from the OAuth2 V3 cert URL" do
50
+ keys = oidc_key_source.refresh_keys
51
+ refute_empty keys
52
+ keys.each do |key|
53
+ assert_kind_of OpenSSL::PKey::RSA, key.key
54
+ refute key.key.private?
55
+ assert_equal "RS256", key.algorithm
56
+ end
57
+ end
58
+
59
+ it "Gets the same keys from the OAuth2 V1 and V3 cert URLs" do
60
+ keys_v1 = legacy_oidc_key_source.refresh_keys.map(&:key).map(&:export).sort
61
+ keys_v3 = oidc_key_source.refresh_keys.map(&:key).map(&:export).sort
62
+ assert_equal keys_v1, keys_v3
63
+ end
64
+
65
+ it "Gets real keys from the IAP public key URL" do
66
+ keys = iap_key_source.refresh_keys
67
+ refute_empty keys
68
+ keys.each do |key|
69
+ assert_kind_of OpenSSL::PKey::EC, key.key
70
+ assert_equal "ES256", key.algorithm
71
+ end
72
+ end
73
+ end
74
+ end
data/lib/googleauth.rb CHANGED
@@ -31,5 +31,6 @@ require "googleauth/application_default"
31
31
  require "googleauth/client_id"
32
32
  require "googleauth/credentials"
33
33
  require "googleauth/default_credentials"
34
+ require "googleauth/id_tokens"
34
35
  require "googleauth/user_authorizer"
35
36
  require "googleauth/web_user_authorizer"
@@ -47,7 +47,7 @@ module Google
47
47
  #
48
48
  # Use this to obtain the Application Default Credentials for accessing
49
49
  # Google APIs. Application Default Credentials are described in detail
50
- # at http://goo.gl/IUuyuX.
50
+ # at https://cloud.google.com/docs/authentication/production.
51
51
  #
52
52
  # If supplied, scope is used to create the credentials instance, when it can
53
53
  # be applied. E.g, on google compute engine and for user credentials the
@@ -75,7 +75,7 @@ module Google
75
75
  GCECredentials.unmemoize_all
76
76
  raise NOT_FOUND_ERROR
77
77
  end
78
- GCECredentials.new
78
+ GCECredentials.new scope: scope
79
79
  end
80
80
  end
81
81
  end
@@ -51,30 +51,47 @@ module Google
51
51
  class GCECredentials < Signet::OAuth2::Client
52
52
  # The IP Address is used in the URIs to speed up failures on non-GCE
53
53
  # systems.
54
- COMPUTE_AUTH_TOKEN_URI = "http://169.254.169.254/computeMetadata/v1/"\
55
- "instance/service-accounts/default/token".freeze
54
+ DEFAULT_METADATA_HOST = "169.254.169.254".freeze
55
+
56
+ # @private Unused and deprecated
57
+ COMPUTE_AUTH_TOKEN_URI =
58
+ "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token".freeze
59
+ # @private Unused and deprecated
60
+ COMPUTE_ID_TOKEN_URI =
61
+ "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity".freeze
62
+ # @private Unused and deprecated
56
63
  COMPUTE_CHECK_URI = "http://169.254.169.254".freeze
57
64
 
58
65
  class << self
59
66
  extend Memoist
60
67
 
68
+ def metadata_host
69
+ ENV.fetch "GCE_METADATA_HOST", DEFAULT_METADATA_HOST
70
+ end
71
+
72
+ def compute_check_uri
73
+ "http://#{metadata_host}".freeze
74
+ end
75
+
76
+ def compute_auth_token_uri
77
+ "#{compute_check_uri}/computeMetadata/v1/instance/service-accounts/default/token".freeze
78
+ end
79
+
80
+ def compute_id_token_uri
81
+ "#{compute_check_uri}/computeMetadata/v1/instance/service-accounts/default/identity".freeze
82
+ end
83
+
61
84
  # Detect if this appear to be a GCE instance, by checking if metadata
62
- # is available
85
+ # is available.
63
86
  def on_gce? options = {}
87
+ # TODO: This should use google-cloud-env instead.
64
88
  c = options[:connection] || Faraday.default_connection
65
- resp = c.get COMPUTE_CHECK_URI do |req|
66
- # Comment from: oauth2client/client.py
67
- #
68
- # Note: the explicit `timeout` below is a workaround. The underlying
69
- # issue is that resolving an unknown host on some networks will take
70
- # 20-30 seconds; making this timeout short fixes the issue, but
71
- # could lead to false negatives in the event that we are on GCE, but
72
- # the metadata resolution was particularly slow. The latter case is
73
- # "unlikely".
74
- req.options.timeout = 0.1
89
+ headers = { "Metadata-Flavor" => "Google" }
90
+ resp = c.get compute_check_uri, nil, headers do |req|
91
+ req.options.timeout = 1.0
92
+ req.options.open_timeout = 0.1
75
93
  end
76
94
  return false unless resp.status == 200
77
- return false unless resp.headers.key? "Metadata-Flavor"
78
95
  resp.headers["Metadata-Flavor"] == "Google"
79
96
  rescue Faraday::TimeoutError, Faraday::ConnectionFailed
80
97
  false
@@ -88,17 +105,25 @@ module Google
88
105
  def fetch_access_token options = {}
89
106
  c = options[:connection] || Faraday.default_connection
90
107
  retry_with_error do
91
- headers = { "Metadata-Flavor" => "Google" }
92
- resp = c.get COMPUTE_AUTH_TOKEN_URI, nil, headers
108
+ uri = target_audience ? GCECredentials.compute_id_token_uri : GCECredentials.compute_auth_token_uri
109
+ query = target_audience ? { "audience" => target_audience, "format" => "full" } : {}
110
+ query[:scopes] = Array(scope).join "," if scope
111
+ resp = c.get uri, query, "Metadata-Flavor" => "Google"
93
112
  case resp.status
94
113
  when 200
95
- Signet::OAuth2.parse_credentials(resp.body,
96
- resp.headers["content-type"])
114
+ content_type = resp.headers["content-type"]
115
+ if content_type == "text/html"
116
+ { (target_audience ? "id_token" : "access_token") => resp.body }
117
+ else
118
+ Signet::OAuth2.parse_credentials resp.body, content_type
119
+ end
120
+ when 403, 500
121
+ msg = "Unexpected error code #{resp.status} #{UNEXPECTED_ERROR_SUFFIX}"
122
+ raise Signet::UnexpectedStatusError, msg
97
123
  when 404
98
124
  raise Signet::AuthorizationError, NO_METADATA_SERVER_ERROR
99
125
  else
100
- msg = "Unexpected error code #{resp.status}" \
101
- "#{UNEXPECTED_ERROR_SUFFIX}"
126
+ msg = "Unexpected error code #{resp.status} #{UNEXPECTED_ERROR_SUFFIX}"
102
127
  raise Signet::AuthorizationError, msg
103
128
  end
104
129
  end
@@ -35,63 +35,382 @@ require "googleauth/credentials_loader"
35
35
 
36
36
  module Google
37
37
  module Auth
38
- # This class is intended to be inherited by API-specific classes
39
- # which overrides the SCOPE constant.
40
- class Credentials
38
+ ##
39
+ # Credentials is a high-level base class used by Google's API client
40
+ # libraries to represent the authentication when connecting to an API.
41
+ # In most cases, it is subclassed by API-specific credential classes that
42
+ # can be instantiated by clients.
43
+ #
44
+ # ## Options
45
+ #
46
+ # Credentials classes are configured with options that dictate default
47
+ # values for parameters such as scope and audience. These defaults are
48
+ # expressed as class attributes, and may differ from endpoint to endpoint.
49
+ # Normally, an API client will provide subclasses specific to each
50
+ # endpoint, configured with appropriate values.
51
+ #
52
+ # Note that these options inherit up the class hierarchy. If a particular
53
+ # options is not set for a subclass, its superclass is queried.
54
+ #
55
+ # Some older users of this class set options via constants. This usage is
56
+ # deprecated. For example, instead of setting the `AUDIENCE` constant on
57
+ # your subclass, call the `audience=` method.
58
+ #
59
+ # ## Example
60
+ #
61
+ # class MyCredentials < Google::Auth::Credentials
62
+ # # Set the default scope for these credentials
63
+ # self.scope = "http://example.com/my_scope"
64
+ # end
65
+ #
66
+ # # creds is a credentials object suitable for Google API clients
67
+ # creds = MyCredentials.default
68
+ # creds.scope # => ["http://example.com/my_scope"]
69
+ #
70
+ # class SubCredentials < MyCredentials
71
+ # # Override the default scope for this subclass
72
+ # self.scope = "http://example.com/sub_scope"
73
+ # end
74
+ #
75
+ # creds2 = SubCredentials.default
76
+ # creds2.scope # => ["http://example.com/sub_scope"]
77
+ #
78
+ class Credentials # rubocop:disable Metrics/ClassLength
79
+ ##
80
+ # The default token credential URI to be used when none is provided during initialization.
41
81
  TOKEN_CREDENTIAL_URI = "https://oauth2.googleapis.com/token".freeze
82
+
83
+ ##
84
+ # The default target audience ID to be used when none is provided during initialization.
42
85
  AUDIENCE = "https://oauth2.googleapis.com/token".freeze
43
- SCOPE = [].freeze
44
- PATH_ENV_VARS = [].freeze
45
- JSON_ENV_VARS = [].freeze
46
- DEFAULT_PATHS = [].freeze
47
86
 
87
+ @audience = @scope = @target_audience = @env_vars = @paths = @token_credential_uri = nil
88
+
89
+ ##
90
+ # The default token credential URI to be used when none is provided during initialization.
91
+ # The URI is the authorization server's HTTP endpoint capable of issuing tokens and
92
+ # refreshing expired tokens.
93
+ #
94
+ # @return [String]
95
+ #
96
+ def self.token_credential_uri
97
+ lookup_auth_param :token_credential_uri do
98
+ lookup_local_constant :TOKEN_CREDENTIAL_URI
99
+ end
100
+ end
101
+
102
+ ##
103
+ # Set the default token credential URI to be used when none is provided during initialization.
104
+ #
105
+ # @param [String] new_token_credential_uri
106
+ #
107
+ def self.token_credential_uri= new_token_credential_uri
108
+ @token_credential_uri = new_token_credential_uri
109
+ end
110
+
111
+ ##
112
+ # The default target audience ID to be used when none is provided during initialization.
113
+ # Used only by the assertion grant type.
114
+ #
115
+ # @return [String]
116
+ #
117
+ def self.audience
118
+ lookup_auth_param :audience do
119
+ lookup_local_constant :AUDIENCE
120
+ end
121
+ end
122
+
123
+ ##
124
+ # Sets the default target audience ID to be used when none is provided during initialization.
125
+ #
126
+ # @param [String] new_audience
127
+ #
128
+ def self.audience= new_audience
129
+ @audience = new_audience
130
+ end
131
+
132
+ ##
133
+ # The default scope to be used when none is provided during initialization.
134
+ # A scope is an access range defined by the authorization server.
135
+ # The scope can be a single value or a list of values.
136
+ #
137
+ # Either {#scope} or {#target_audience}, but not both, should be non-nil.
138
+ # If {#scope} is set, this credential will produce access tokens.
139
+ # If {#target_audience} is set, this credential will produce ID tokens.
140
+ #
141
+ # @return [String, Array<String>, nil]
142
+ #
143
+ def self.scope
144
+ lookup_auth_param :scope do
145
+ vals = lookup_local_constant :SCOPE
146
+ vals ? Array(vals).flatten.uniq : nil
147
+ end
148
+ end
149
+
150
+ ##
151
+ # Sets the default scope to be used when none is provided during initialization.
152
+ #
153
+ # Either {#scope} or {#target_audience}, but not both, should be non-nil.
154
+ # If {#scope} is set, this credential will produce access tokens.
155
+ # If {#target_audience} is set, this credential will produce ID tokens.
156
+ #
157
+ # @param [String, Array<String>, nil] new_scope
158
+ #
159
+ def self.scope= new_scope
160
+ new_scope = Array new_scope unless new_scope.nil?
161
+ @scope = new_scope
162
+ end
163
+
164
+ ##
165
+ # The default final target audience for ID tokens, to be used when none
166
+ # is provided during initialization.
167
+ #
168
+ # Either {#scope} or {#target_audience}, but not both, should be non-nil.
169
+ # If {#scope} is set, this credential will produce access tokens.
170
+ # If {#target_audience} is set, this credential will produce ID tokens.
171
+ #
172
+ # @return [String, nil]
173
+ #
174
+ def self.target_audience
175
+ lookup_auth_param :target_audience
176
+ end
177
+
178
+ ##
179
+ # Sets the default final target audience for ID tokens, to be used when none
180
+ # is provided during initialization.
181
+ #
182
+ # Either {#scope} or {#target_audience}, but not both, should be non-nil.
183
+ # If {#scope} is set, this credential will produce access tokens.
184
+ # If {#target_audience} is set, this credential will produce ID tokens.
185
+ #
186
+ # @param [String, nil] new_target_audience
187
+ #
188
+ def self.target_audience= new_target_audience
189
+ @target_audience = new_target_audience
190
+ end
191
+
192
+ ##
193
+ # The environment variables to search for credentials. Values can either be a file path to the
194
+ # credentials file, or the JSON contents of the credentials file.
195
+ # The env_vars will never be nil. If there are no vars, the empty array is returned.
196
+ #
197
+ # @return [Array<String>]
198
+ #
199
+ def self.env_vars
200
+ env_vars_internal || []
201
+ end
202
+
203
+ ##
204
+ # @private
205
+ # Internal recursive lookup for env_vars.
206
+ #
207
+ def self.env_vars_internal
208
+ lookup_auth_param :env_vars, :env_vars_internal do
209
+ # Pull values when PATH_ENV_VARS or JSON_ENV_VARS constants exists.
210
+ path_env_vars = lookup_local_constant :PATH_ENV_VARS
211
+ json_env_vars = lookup_local_constant :JSON_ENV_VARS
212
+ (Array(path_env_vars) + Array(json_env_vars)).flatten.uniq if path_env_vars || json_env_vars
213
+ end
214
+ end
215
+
216
+ ##
217
+ # Sets the environment variables to search for credentials.
218
+ # Setting to `nil` "unsets" the value, and defaults to the superclass
219
+ # (or to the empty array if there is no superclass).
220
+ #
221
+ # @param [String, Array<String>, nil] new_env_vars
222
+ #
223
+ def self.env_vars= new_env_vars
224
+ new_env_vars = Array new_env_vars unless new_env_vars.nil?
225
+ @env_vars = new_env_vars
226
+ end
227
+
228
+ ##
229
+ # The file paths to search for credentials files.
230
+ # The paths will never be nil. If there are no paths, the empty array is returned.
231
+ #
232
+ # @return [Array<String>]
233
+ #
234
+ def self.paths
235
+ paths_internal || []
236
+ end
237
+
238
+ ##
239
+ # @private
240
+ # Internal recursive lookup for paths.
241
+ #
242
+ def self.paths_internal
243
+ lookup_auth_param :paths, :paths_internal do
244
+ # Pull in values if the DEFAULT_PATHS constant exists.
245
+ vals = lookup_local_constant :DEFAULT_PATHS
246
+ vals ? Array(vals).flatten.uniq : nil
247
+ end
248
+ end
249
+
250
+ ##
251
+ # Set the file paths to search for credentials files.
252
+ # Setting to `nil` "unsets" the value, and defaults to the superclass
253
+ # (or to the empty array if there is no superclass).
254
+ #
255
+ # @param [String, Array<String>, nil] new_paths
256
+ #
257
+ def self.paths= new_paths
258
+ new_paths = Array new_paths unless new_paths.nil?
259
+ @paths = new_paths
260
+ end
261
+
262
+ ##
263
+ # @private
264
+ # Return the given parameter value, defaulting up the class hierarchy.
265
+ #
266
+ # First returns the value of the instance variable, if set.
267
+ # Next, calls the given block if provided. (This is generally used to
268
+ # look up legacy constant-based values.)
269
+ # Otherwise, calls the superclass method if present.
270
+ # Returns nil if all steps fail.
271
+ #
272
+ # @param name [Symbol] The parameter name
273
+ # @param method_name [Symbol] The lookup method name, if different
274
+ # @return [Object] The value
275
+ #
276
+ def self.lookup_auth_param name, method_name = name
277
+ val = instance_variable_get "@#{name}".to_sym
278
+ val = yield if val.nil? && block_given?
279
+ return val unless val.nil?
280
+ return superclass.send method_name if superclass.respond_to? method_name
281
+ nil
282
+ end
283
+
284
+ ##
285
+ # @private
286
+ # Return the value of the given constant if it is defined directly in
287
+ # this class, or nil if not.
288
+ #
289
+ # @param [Symbol] Name of the constant
290
+ # @return [Object] The value
291
+ #
292
+ def self.lookup_local_constant name
293
+ const_defined?(name, false) ? const_get(name) : nil
294
+ end
295
+
296
+ ##
297
+ # The Signet::OAuth2::Client object the Credentials instance is using.
298
+ #
299
+ # @return [Signet::OAuth2::Client]
300
+ #
48
301
  attr_accessor :client
49
- attr_reader :project_id
50
302
 
51
- # Delegate client methods to the client object.
303
+ ##
304
+ # Identifier for the project the client is authenticating with.
305
+ #
306
+ # @return [String]
307
+ #
308
+ attr_reader :project_id
309
+
310
+ ##
311
+ # Identifier for a separate project used for billing/quota, if any.
312
+ #
313
+ # @return [String,nil]
314
+ #
315
+ attr_reader :quota_project_id
316
+
317
+ # @private Delegate client methods to the client object.
52
318
  extend Forwardable
319
+
320
+ ##
321
+ # @!attribute [r] token_credential_uri
322
+ # @return [String] The token credential URI. The URI is the authorization server's HTTP
323
+ # endpoint capable of issuing tokens and refreshing expired tokens.
324
+ #
325
+ # @!attribute [r] audience
326
+ # @return [String] The target audience ID when issuing assertions. Used only by the
327
+ # assertion grant type.
328
+ #
329
+ # @!attribute [r] scope
330
+ # @return [String, Array<String>] The scope for this client. A scope is an access range
331
+ # defined by the authorization server. The scope can be a single value or a list of values.
332
+ #
333
+ # @!attribute [r] target_audience
334
+ # @return [String] The final target audience for ID tokens returned by this credential.
335
+ #
336
+ # @!attribute [r] issuer
337
+ # @return [String] The issuer ID associated with this client.
338
+ #
339
+ # @!attribute [r] signing_key
340
+ # @return [String, OpenSSL::PKey] The signing key associated with this client.
341
+ #
342
+ # @!attribute [r] updater_proc
343
+ # @return [Proc] Returns a reference to the {Signet::OAuth2::Client#apply} method,
344
+ # suitable for passing as a closure.
345
+ #
53
346
  def_delegators :@client,
54
347
  :token_credential_uri, :audience,
55
- :scope, :issuer, :signing_key, :updater_proc
348
+ :scope, :issuer, :signing_key, :updater_proc, :target_audience
56
349
 
57
- # rubocop:disable Metrics/AbcSize
350
+ ##
351
+ # Creates a new Credentials instance with the provided auth credentials, and with the default
352
+ # values configured on the class.
353
+ #
354
+ # @param [String, Hash, Signet::OAuth2::Client] keyfile
355
+ # The keyfile can be provided as one of the following:
356
+ #
357
+ # * The path to a JSON keyfile (as a +String+)
358
+ # * The contents of a JSON keyfile (as a +Hash+)
359
+ # * A +Signet::OAuth2::Client+ object
360
+ # @param [Hash] options
361
+ # The options for configuring the credentials instance. The following is supported:
362
+ #
363
+ # * +:scope+ - the scope for the client
364
+ # * +"project_id"+ (and optionally +"project"+) - the project identifier for the client
365
+ # * +:connection_builder+ - the connection builder to use for the client
366
+ # * +:default_connection+ - the default connection to use for the client
367
+ #
58
368
  def initialize keyfile, options = {}
59
- scope = options[:scope]
60
369
  verify_keyfile_provided! keyfile
61
370
  @project_id = options["project_id"] || options["project"]
62
- if keyfile.is_a? Signet::OAuth2::Client
63
- @client = keyfile
64
- @project_id ||= keyfile.project_id if keyfile.respond_to? :project_id
65
- elsif keyfile.is_a? Hash
66
- hash = stringify_hash_keys keyfile
67
- hash["scope"] ||= scope
68
- @client = init_client hash, options
69
- @project_id ||= (hash["project_id"] || hash["project"])
371
+ @quota_project_id = options["quota_project_id"]
372
+ case keyfile
373
+ when Signet::OAuth2::Client
374
+ update_from_signet keyfile
375
+ when Hash
376
+ update_from_hash keyfile, options
70
377
  else
71
- verify_keyfile_exists! keyfile
72
- json = JSON.parse ::File.read(keyfile)
73
- json["scope"] ||= scope
74
- @project_id ||= (json["project_id"] || json["project"])
75
- @client = init_client json, options
378
+ update_from_filepath keyfile, options
76
379
  end
77
380
  CredentialsLoader.warn_if_cloud_sdk_credentials @client.client_id
78
381
  @project_id ||= CredentialsLoader.load_gcloud_project_id
79
382
  @client.fetch_access_token!
383
+ @env_vars = nil
384
+ @paths = nil
385
+ @scope = nil
80
386
  end
81
- # rubocop:enable Metrics/AbcSize
82
387
 
83
- # Returns the default credentials checking, in this order, the path env
84
- # evironment variables, json environment variables, default paths. If the
85
- # previously stated locations do not contain keyfile information,
86
- # this method defaults to use the application default.
388
+ ##
389
+ # Creates a new Credentials instance with auth credentials acquired by searching the
390
+ # environment variables and paths configured on the class, and with the default values
391
+ # configured on the class.
392
+ #
393
+ # The auth credentials are searched for in the following order:
394
+ #
395
+ # 1. configured environment variables (see {Credentials.env_vars})
396
+ # 2. configured default file paths (see {Credentials.paths})
397
+ # 3. application default (see {Google::Auth.get_application_default})
398
+ #
399
+ # @param [Hash] options
400
+ # The options for configuring the credentials instance. The following is supported:
401
+ #
402
+ # * +:scope+ - the scope for the client
403
+ # * +"project_id"+ (and optionally +"project"+) - the project identifier for the client
404
+ # * +:connection_builder+ - the connection builder to use for the client
405
+ # * +:default_connection+ - the default connection to use for the client
406
+ #
407
+ # @return [Credentials]
408
+ #
87
409
  def self.default options = {}
88
- # First try to find keyfile file from environment variables.
89
- client = from_path_vars options
410
+ # First try to find keyfile file or json from environment variables.
411
+ client = from_env_vars options
90
412
 
91
- # Second try to find keyfile json from environment variables.
92
- client ||= from_json_vars options
93
-
94
- # Third try to find keyfile file from known file paths.
413
+ # Second try to find keyfile file from known file paths.
95
414
  client ||= from_default_paths options
96
415
 
97
416
  # Finally get instantiated client from Google::Auth
@@ -99,49 +418,68 @@ module Google
99
418
  client
100
419
  end
101
420
 
102
- def self.from_path_vars options
103
- self::PATH_ENV_VARS
104
- .map { |v| ENV[v] }
105
- .compact
106
- .select { |p| ::File.file? p }
107
- .each do |file|
108
- return new file, options
109
- end
110
- nil
111
- end
112
-
113
- def self.from_json_vars options
114
- json = lambda do |v|
115
- unless ENV[v].nil?
116
- begin
117
- JSON.parse ENV[v]
118
- rescue StandardError
119
- nil
421
+ ##
422
+ # @private Lookup Credentials from environment variables.
423
+ def self.from_env_vars options
424
+ env_vars.each do |env_var|
425
+ str = ENV[env_var]
426
+ next if str.nil?
427
+ io =
428
+ if ::File.file? str
429
+ ::StringIO.new ::File.read str
430
+ else
431
+ json = ::JSON.parse str rescue nil
432
+ json ? ::StringIO.new(str) : nil
120
433
  end
121
- end
434
+ next if io.nil?
435
+ return from_io io, options
122
436
  end
123
- self::JSON_ENV_VARS.map(&json).compact.each { |hash| return new hash, options }
124
437
  nil
125
438
  end
126
439
 
440
+ ##
441
+ # @private Lookup Credentials from default file paths.
127
442
  def self.from_default_paths options
128
- self::DEFAULT_PATHS
129
- .select { |p| ::File.file? p }
130
- .each do |file|
131
- return new file, options
132
- end
443
+ paths.each do |path|
444
+ next unless path && ::File.file?(path)
445
+ io = ::StringIO.new ::File.read path
446
+ return from_io io, options
447
+ end
133
448
  nil
134
449
  end
135
450
 
451
+ ##
452
+ # @private Lookup Credentials using Google::Auth.get_application_default.
136
453
  def self.from_application_default options
137
- scope = options[:scope] || self::SCOPE
138
- client = Google::Auth.get_application_default scope
454
+ scope = options[:scope] || self.scope
455
+ auth_opts = {
456
+ token_credential_uri: options[:token_credential_uri] || token_credential_uri,
457
+ audience: options[:audience] || audience,
458
+ target_audience: options[:target_audience] || target_audience,
459
+ enable_self_signed_jwt: options[:enable_self_signed_jwt] && options[:scope].nil?
460
+ }
461
+ client = Google::Auth.get_application_default scope, auth_opts
139
462
  new client, options
140
463
  end
141
- private_class_method :from_path_vars,
142
- :from_json_vars,
464
+
465
+ # @private Read credentials from a JSON stream.
466
+ def self.from_io io, options
467
+ creds_input = {
468
+ json_key_io: io,
469
+ scope: options[:scope] || scope,
470
+ target_audience: options[:target_audience] || target_audience,
471
+ enable_self_signed_jwt: options[:enable_self_signed_jwt] && options[:scope].nil?,
472
+ token_credential_uri: options[:token_credential_uri] || token_credential_uri,
473
+ audience: options[:audience] || audience
474
+ }
475
+ client = Google::Auth::DefaultCredentials.make_creds creds_input
476
+ new client
477
+ end
478
+
479
+ private_class_method :from_env_vars,
143
480
  :from_default_paths,
144
- :from_application_default
481
+ :from_application_default,
482
+ :from_io
145
483
 
146
484
  protected
147
485
 
@@ -166,22 +504,58 @@ module Google
166
504
 
167
505
  # returns a new Hash with string keys instead of symbol keys.
168
506
  def stringify_hash_keys hash
169
- Hash[hash.map { |k, v| [k.to_s, v] }]
507
+ hash.to_h.transform_keys(&:to_s)
170
508
  end
171
509
 
510
+ # rubocop:disable Metrics/AbcSize
511
+
172
512
  def client_options options
173
513
  # Keyfile options have higher priority over constructor defaults
174
- options["token_credential_uri"] ||= self.class::TOKEN_CREDENTIAL_URI
175
- options["audience"] ||= self.class::AUDIENCE
176
- options["scope"] ||= self.class::SCOPE
514
+ options["token_credential_uri"] ||= self.class.token_credential_uri
515
+ options["audience"] ||= self.class.audience
516
+ options["scope"] ||= self.class.scope
517
+ options["target_audience"] ||= self.class.target_audience
518
+
519
+ if !Array(options["scope"]).empty? && options["target_audience"]
520
+ raise ArgumentError, "Cannot specify both scope and target_audience"
521
+ end
177
522
 
523
+ needs_scope = options["target_audience"].nil?
178
524
  # client options for initializing signet client
179
525
  { token_credential_uri: options["token_credential_uri"],
180
526
  audience: options["audience"],
181
- scope: Array(options["scope"]),
527
+ scope: (needs_scope ? Array(options["scope"]) : nil),
528
+ target_audience: options["target_audience"],
182
529
  issuer: options["client_email"],
183
530
  signing_key: OpenSSL::PKey::RSA.new(options["private_key"]) }
184
531
  end
532
+
533
+ # rubocop:enable Metrics/AbcSize
534
+
535
+ def update_from_signet client
536
+ @project_id ||= client.project_id if client.respond_to? :project_id
537
+ @quota_project_id ||= client.quota_project_id if client.respond_to? :quota_project_id
538
+ @client = client
539
+ end
540
+
541
+ def update_from_hash hash, options
542
+ hash = stringify_hash_keys hash
543
+ hash["scope"] ||= options[:scope]
544
+ hash["target_audience"] ||= options[:target_audience]
545
+ @project_id ||= (hash["project_id"] || hash["project"])
546
+ @quota_project_id ||= hash["quota_project_id"]
547
+ @client = init_client hash, options
548
+ end
549
+
550
+ def update_from_filepath path, options
551
+ verify_keyfile_exists! path
552
+ json = JSON.parse ::File.read(path)
553
+ json["scope"] ||= options[:scope]
554
+ json["target_audience"] ||= options[:target_audience]
555
+ @project_id ||= (json["project_id"] || json["project"])
556
+ @quota_project_id ||= json["quota_project_id"]
557
+ @client = init_client json, options
558
+ end
185
559
  end
186
560
  end
187
561
  end