googleauth 0.8.1 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
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