googleauth 0.9.0 → 0.13.1

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/.kokoro/continuous/linux.cfg +12 -2
  3. data/.kokoro/continuous/osx.cfg +5 -0
  4. data/.kokoro/continuous/post.cfg +30 -0
  5. data/.kokoro/continuous/windows.cfg +10 -0
  6. data/.kokoro/presubmit/linux.cfg +11 -1
  7. data/.kokoro/presubmit/osx.cfg +5 -0
  8. data/.kokoro/presubmit/windows.cfg +10 -0
  9. data/.kokoro/release.cfg +42 -1
  10. data/.repo-metadata.json +5 -0
  11. data/.rubocop.yml +12 -35
  12. data/CHANGELOG.md +32 -0
  13. data/Gemfile +8 -3
  14. data/README.md +7 -11
  15. data/Rakefile +48 -5
  16. data/googleauth.gemspec +7 -4
  17. data/integration/helper.rb +31 -0
  18. data/integration/id_tokens/key_source_test.rb +74 -0
  19. data/lib/googleauth.rb +1 -0
  20. data/lib/googleauth/application_default.rb +9 -9
  21. data/lib/googleauth/compute_engine.rb +30 -27
  22. data/lib/googleauth/credentials.rb +92 -22
  23. data/lib/googleauth/credentials_loader.rb +14 -15
  24. data/lib/googleauth/id_tokens.rb +233 -0
  25. data/lib/googleauth/id_tokens/errors.rb +71 -0
  26. data/lib/googleauth/id_tokens/key_sources.rb +394 -0
  27. data/lib/googleauth/id_tokens/verifier.rb +144 -0
  28. data/lib/googleauth/json_key_reader.rb +6 -2
  29. data/lib/googleauth/service_account.rb +16 -7
  30. data/lib/googleauth/signet.rb +8 -6
  31. data/lib/googleauth/user_authorizer.rb +8 -3
  32. data/lib/googleauth/user_refresh.rb +1 -1
  33. data/lib/googleauth/version.rb +1 -1
  34. data/lib/googleauth/web_user_authorizer.rb +1 -1
  35. data/rakelib/devsite_builder.rb +45 -0
  36. data/rakelib/link_checker.rb +64 -0
  37. data/rakelib/repo_metadata.rb +59 -0
  38. data/spec/googleauth/apply_auth_examples.rb +28 -5
  39. data/spec/googleauth/compute_engine_spec.rb +37 -13
  40. data/spec/googleauth/credentials_spec.rb +25 -6
  41. data/spec/googleauth/service_account_spec.rb +23 -16
  42. data/spec/googleauth/signet_spec.rb +15 -7
  43. data/spec/googleauth/user_authorizer_spec.rb +21 -1
  44. data/spec/googleauth/user_refresh_spec.rb +1 -1
  45. data/test/helper.rb +33 -0
  46. data/test/id_tokens/key_sources_test.rb +240 -0
  47. data/test/id_tokens/verifier_test.rb +269 -0
  48. metadata +45 -12
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2020 Google LLC
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are
7
+ # met:
8
+ #
9
+ # * Redistributions of source code must retain the above copyright
10
+ # notice, this list of conditions and the following disclaimer.
11
+ # * Redistributions in binary form must reproduce the above
12
+ # copyright notice, this list of conditions and the following disclaimer
13
+ # in the documentation and/or other materials provided with the
14
+ # distribution.
15
+ # * Neither the name of Google Inc. nor the names of its
16
+ # contributors may be used to endorse or promote products derived from
17
+ # this software without specific prior written permission.
18
+ #
19
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ require "jwt"
32
+
33
+ module Google
34
+ module Auth
35
+ module IDTokens
36
+ ##
37
+ # An object that can verify ID tokens.
38
+ #
39
+ # A verifier maintains a set of default settings, including the key
40
+ # source and fields to verify. However, individual verification calls can
41
+ # override any of these settings.
42
+ #
43
+ class Verifier
44
+ ##
45
+ # Create a verifier.
46
+ #
47
+ # @param key_source [key source] The default key source to use. All
48
+ # verification calls must have a key source, so if no default key
49
+ # source is provided here, then calls to {#verify} _must_ provide
50
+ # a key source.
51
+ # @param aud [String,nil] The default audience (`aud`) check, or `nil`
52
+ # for no check.
53
+ # @param azp [String,nil] The default authorized party (`azp`) check,
54
+ # or `nil` for no check.
55
+ # @param iss [String,nil] The default issuer (`iss`) check, or `nil`
56
+ # for no check.
57
+ #
58
+ def initialize key_source: nil,
59
+ aud: nil,
60
+ azp: nil,
61
+ iss: nil
62
+ @key_source = key_source
63
+ @aud = aud
64
+ @azp = azp
65
+ @iss = iss
66
+ end
67
+
68
+ ##
69
+ # Verify the given token.
70
+ #
71
+ # @param token [String] the ID token to verify.
72
+ # @param key_source [key source] If given, override the key source.
73
+ # @param aud [String,nil] If given, override the `aud` check.
74
+ # @param azp [String,nil] If given, override the `azp` check.
75
+ # @param iss [String,nil] If given, override the `iss` check.
76
+ #
77
+ # @return [Hash] the decoded payload, if verification succeeded.
78
+ # @raise [KeySourceError] if the key source failed to obtain public keys
79
+ # @raise [VerificationError] if the token verification failed.
80
+ # Additional data may be available in the error subclass and message.
81
+ #
82
+ def verify token,
83
+ key_source: :default,
84
+ aud: :default,
85
+ azp: :default,
86
+ iss: :default
87
+ key_source = @key_source if key_source == :default
88
+ aud = @aud if aud == :default
89
+ azp = @azp if azp == :default
90
+ iss = @iss if iss == :default
91
+
92
+ raise KeySourceError, "No key sources" unless key_source
93
+ keys = key_source.current_keys
94
+ payload = decode_token token, keys, aud, azp, iss
95
+ unless payload
96
+ keys = key_source.refresh_keys
97
+ payload = decode_token token, keys, aud, azp, iss
98
+ end
99
+ raise SignatureError, "Token not verified as issued by Google" unless payload
100
+ payload
101
+ end
102
+
103
+ private
104
+
105
+ def decode_token token, keys, aud, azp, iss
106
+ payload = nil
107
+ keys.find do |key|
108
+ begin
109
+ options = { algorithms: key.algorithm }
110
+ decoded_token = JWT.decode token, key.key, true, options
111
+ payload = decoded_token.first
112
+ rescue JWT::ExpiredSignature
113
+ raise ExpiredTokenError, "Token signature is expired"
114
+ rescue JWT::DecodeError
115
+ nil # Try the next key
116
+ end
117
+ end
118
+
119
+ normalize_and_verify_payload payload, aud, azp, iss
120
+ end
121
+
122
+ def normalize_and_verify_payload payload, aud, azp, iss
123
+ return nil unless payload
124
+
125
+ # Map the legacy "cid" claim to the canonical "azp"
126
+ payload["azp"] ||= payload["cid"] if payload.key? "cid"
127
+
128
+ # Payload content validation
129
+ if aud && (Array(aud) & Array(payload["aud"])).empty?
130
+ raise AudienceMismatchError, "Token aud mismatch: #{payload['aud']}"
131
+ end
132
+ if azp && (Array(azp) & Array(payload["azp"])).empty?
133
+ raise AuthorizedPartyMismatchError, "Token azp mismatch: #{payload['azp']}"
134
+ end
135
+ if iss && (Array(iss) & Array(payload["iss"])).empty?
136
+ raise IssuerMismatchError, "Token iss mismatch: #{payload['iss']}"
137
+ end
138
+
139
+ payload
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -38,8 +38,12 @@ module Google
38
38
  json_key = MultiJson.load json_key_io.read
39
39
  raise "missing client_email" unless json_key.key? "client_email"
40
40
  raise "missing private_key" unless json_key.key? "private_key"
41
- project_id = json_key["project_id"]
42
- [json_key["private_key"], json_key["client_email"], project_id]
41
+ [
42
+ json_key["private_key"],
43
+ json_key["client_email"],
44
+ json_key["project_id"],
45
+ json_key["quota_project_id"]
46
+ ]
43
47
  end
44
48
  end
45
49
  end
@@ -45,34 +45,40 @@ module Google
45
45
  # from credentials from a json key file downloaded from the developer
46
46
  # console (via 'Generate new Json Key').
47
47
  #
48
- # cf [Application Default Credentials](http://goo.gl/mkAHpZ)
48
+ # cf [Application Default Credentials](https://cloud.google.com/docs/authentication/production)
49
49
  class ServiceAccountCredentials < Signet::OAuth2::Client
50
50
  TOKEN_CRED_URI = "https://www.googleapis.com/oauth2/v4/token".freeze
51
51
  extend CredentialsLoader
52
52
  extend JsonKeyReader
53
53
  attr_reader :project_id
54
+ attr_reader :quota_project_id
54
55
 
55
56
  # Creates a ServiceAccountCredentials.
56
57
  #
57
58
  # @param json_key_io [IO] an IO from which the JSON key can be read
58
59
  # @param scope [string|array|nil] the scope(s) to access
59
60
  def self.make_creds options = {}
60
- json_key_io, scope = options.values_at :json_key_io, :scope
61
+ json_key_io, scope, target_audience = options.values_at :json_key_io, :scope, :target_audience
62
+ raise ArgumentError, "Cannot specify both scope and target_audience" if scope && target_audience
63
+
61
64
  if json_key_io
62
- private_key, client_email, project_id = read_json_key json_key_io
65
+ private_key, client_email, project_id, quota_project_id = read_json_key json_key_io
63
66
  else
64
67
  private_key = unescape ENV[CredentialsLoader::PRIVATE_KEY_VAR]
65
68
  client_email = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
66
69
  project_id = ENV[CredentialsLoader::PROJECT_ID_VAR]
70
+ quota_project_id = nil
67
71
  end
68
72
  project_id ||= CredentialsLoader.load_gcloud_project_id
69
73
 
70
74
  new(token_credential_uri: TOKEN_CRED_URI,
71
75
  audience: TOKEN_CRED_URI,
72
76
  scope: scope,
77
+ target_audience: target_audience,
73
78
  issuer: client_email,
74
79
  signing_key: OpenSSL::PKey::RSA.new(private_key),
75
- project_id: project_id)
80
+ project_id: project_id,
81
+ quota_project_id: quota_project_id)
76
82
  .configure_connection(options)
77
83
  end
78
84
 
@@ -87,6 +93,7 @@ module Google
87
93
 
88
94
  def initialize options = {}
89
95
  @project_id = options[:project_id]
96
+ @quota_project_id = options[:quota_project_id]
90
97
  super options
91
98
  end
92
99
 
@@ -97,7 +104,7 @@ module Google
97
104
  # authenticate instead.
98
105
  def apply! a_hash, opts = {}
99
106
  # Use the base implementation if scopes are set
100
- unless scope.nil?
107
+ unless scope.nil? && target_audience.nil?
101
108
  super
102
109
  return
103
110
  end
@@ -123,7 +130,7 @@ module Google
123
130
  # console (via 'Generate new Json Key'). It is not part of any OAuth2
124
131
  # flow, rather it creates a JWT and sends that as a credential.
125
132
  #
126
- # cf [Application Default Credentials](http://goo.gl/mkAHpZ)
133
+ # cf [Application Default Credentials](https://cloud.google.com/docs/authentication/production)
127
134
  class ServiceAccountJwtHeaderCredentials
128
135
  JWT_AUD_URI_KEY = :jwt_aud_uri
129
136
  AUTH_METADATA_KEY = Signet::OAuth2::AUTH_METADATA_KEY
@@ -133,6 +140,7 @@ module Google
133
140
  extend CredentialsLoader
134
141
  extend JsonKeyReader
135
142
  attr_reader :project_id
143
+ attr_reader :quota_project_id
136
144
 
137
145
  # make_creds proxies the construction of a credentials instance
138
146
  #
@@ -151,12 +159,13 @@ module Google
151
159
  def initialize options = {}
152
160
  json_key_io = options[:json_key_io]
153
161
  if json_key_io
154
- @private_key, @issuer, @project_id =
162
+ @private_key, @issuer, @project_id, @quota_project_id =
155
163
  self.class.read_json_key json_key_io
156
164
  else
157
165
  @private_key = ENV[CredentialsLoader::PRIVATE_KEY_VAR]
158
166
  @issuer = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
159
167
  @project_id = ENV[CredentialsLoader::PROJECT_ID_VAR]
168
+ @quota_project_id = nil
160
169
  end
161
170
  @project_id ||= CredentialsLoader.load_gcloud_project_id
162
171
  @signing_key = OpenSSL::PKey::RSA.new @private_key
@@ -48,8 +48,9 @@ module Signet
48
48
  def apply! a_hash, opts = {}
49
49
  # fetch the access token there is currently not one, or if the client
50
50
  # has expired
51
- fetch_access_token! opts if access_token.nil? || expires_within?(60)
52
- a_hash[AUTH_METADATA_KEY] = "Bearer #{access_token}"
51
+ token_type = target_audience ? :id_token : :access_token
52
+ fetch_access_token! opts if send(token_type).nil? || expires_within?(60)
53
+ a_hash[AUTH_METADATA_KEY] = "Bearer #{send token_type}"
53
54
  end
54
55
 
55
56
  # Returns a clone of a_hash updated with the authentication token
@@ -66,7 +67,7 @@ module Signet
66
67
  end
67
68
 
68
69
  def on_refresh &block
69
- @refresh_listeners ||= []
70
+ @refresh_listeners = [] unless defined? @refresh_listeners
70
71
  @refresh_listeners << block
71
72
  end
72
73
 
@@ -84,15 +85,16 @@ module Signet
84
85
  end
85
86
 
86
87
  def notify_refresh_listeners
87
- listeners = @refresh_listeners || []
88
+ listeners = defined?(@refresh_listeners) ? @refresh_listeners : []
88
89
  listeners.each do |block|
89
90
  block.call self
90
91
  end
91
92
  end
92
93
 
93
94
  def build_default_connection
94
- return nil unless defined?(@connection_info)
95
- if @connection_info.respond_to? :call
95
+ if !defined?(@connection_info)
96
+ nil
97
+ elsif @connection_info.respond_to? :call
96
98
  @connection_info.call
97
99
  else
98
100
  @connection_info
@@ -129,8 +129,8 @@ module Google
129
129
  data = MultiJson.load saved_token
130
130
 
131
131
  if data.fetch("client_id", @client_id.id) != @client_id.id
132
- raise sprintf(MISMATCHED_CLIENT_ID_ERROR,
133
- data["client_id"], @client_id.id)
132
+ raise format(MISMATCHED_CLIENT_ID_ERROR,
133
+ data["client_id"], @client_id.id)
134
134
  end
135
135
 
136
136
  credentials = UserRefreshCredentials.new(
@@ -271,10 +271,15 @@ module Google
271
271
  # @return [String]
272
272
  # Redirect URI
273
273
  def redirect_uri_for base_url
274
- return @callback_uri unless URI(@callback_uri).scheme.nil?
274
+ return @callback_uri if uri_is_postmessage?(@callback_uri) || !URI(@callback_uri).scheme.nil?
275
275
  raise format(MISSING_ABSOLUTE_URL_ERROR, @callback_uri) if base_url.nil? || URI(base_url).scheme.nil?
276
276
  URI.join(base_url, @callback_uri).to_s
277
277
  end
278
+
279
+ # Check if URI is Google's postmessage flow (not a valid redirect_uri by spec, but allowed)
280
+ def uri_is_postmessage? uri
281
+ uri.to_s.casecmp("postmessage").zero?
282
+ end
278
283
  end
279
284
  end
280
285
  end
@@ -44,7 +44,7 @@ module Google
44
44
  # 'gcloud auth login' saves a file with these contents in well known
45
45
  # location
46
46
  #
47
- # cf [Application Default Credentials](http://goo.gl/mkAHpZ)
47
+ # cf [Application Default Credentials](https://cloud.google.com/docs/authentication/production)
48
48
  class UserRefreshCredentials < Signet::OAuth2::Client
49
49
  TOKEN_CRED_URI = "https://oauth2.googleapis.com/token".freeze
50
50
  AUTHORIZATION_URI = "https://accounts.google.com/o/oauth2/auth".freeze
@@ -31,6 +31,6 @@ module Google
31
31
  # Module Auth provides classes that provide Google-specific authorization
32
32
  # used to access Google APIs.
33
33
  module Auth
34
- VERSION = "0.9.0".freeze
34
+ VERSION = "0.13.1".freeze
35
35
  end
36
36
  end
@@ -171,7 +171,7 @@ module Google
171
171
  options[:state] = MultiJson.dump(state.merge(
172
172
  SESSION_ID_KEY => request.session[XSRF_KEY],
173
173
  CURRENT_URI_KEY => redirect_to
174
- ))
174
+ ))
175
175
  options[:base_url] = request.url
176
176
  super options
177
177
  end
@@ -0,0 +1,45 @@
1
+ require "pathname"
2
+
3
+ require_relative "repo_metadata.rb"
4
+
5
+ class DevsiteBuilder
6
+ def initialize master_dir = "."
7
+ @master_dir = Pathname.new master_dir
8
+ @output_dir = "doc"
9
+ @metadata = RepoMetadata.from_source "#{master_dir}/.repo-metadata.json"
10
+ end
11
+
12
+ def build
13
+ FileUtils.remove_dir @output_dir if Dir.exist? @output_dir
14
+ markup = "--markup markdown"
15
+
16
+ Dir.chdir @master_dir do
17
+ cmds = ["-o #{@output_dir}", markup]
18
+ cmd "yard --verbose #{cmds.join ' '}"
19
+ end
20
+ @metadata.build @master_dir + @output_dir
21
+ end
22
+
23
+ def upload
24
+ Dir.chdir @output_dir do
25
+ opts = [
26
+ "--credentials=#{ENV['KOKORO_KEYSTORE_DIR']}/73713_docuploader_service_account",
27
+ "--staging-bucket=#{ENV.fetch 'STAGING_BUCKET', 'docs-staging'}",
28
+ "--metadata-file=./docs.metadata"
29
+ ]
30
+ cmd "python3 -m docuploader upload . #{opts.join ' '}"
31
+ end
32
+ end
33
+
34
+ def publish
35
+ build
36
+ upload
37
+ end
38
+
39
+ def cmd line
40
+ puts line
41
+ output = `#{line}`
42
+ puts output
43
+ output
44
+ end
45
+ end
@@ -0,0 +1,64 @@
1
+ require "open3"
2
+
3
+ class LinkChecker
4
+ def initialize
5
+ @failed = false
6
+ end
7
+
8
+ def run
9
+ job_info
10
+ git_commit = ENV.fetch "KOKORO_GITHUB_COMMIT", "master"
11
+
12
+ markdown_files = Dir.glob "**/*.md"
13
+ broken_markdown_links = check_links markdown_files,
14
+ "https://github.com/googleapis/google-auth-library-ruby/tree/#{git_commit}",
15
+ " --skip '^(?!(\\Wruby.*google|.*google.*\\Wruby|.*cloud\\.google\\.com))'"
16
+
17
+ broken_devsite_links = check_links ["googleauth"],
18
+ "https://googleapis.dev/ruby",
19
+ "/latest/ --recurse --skip https:.*github.*"
20
+
21
+ puts_broken_links broken_markdown_links
22
+ puts_broken_links broken_devsite_links
23
+ end
24
+
25
+ def check_links location_list, base, tail
26
+ broken_links = Hash.new { |h, k| h[k] = [] }
27
+ location_list.each do |location|
28
+ out, err, st = Open3.capture3 "npx linkinator #{base}/#{location}#{tail}"
29
+ puts out
30
+ unless st.to_i.zero?
31
+ @failed = true
32
+ puts err
33
+ end
34
+ checked_links = out.split "\n"
35
+ checked_links.select! { |link| link =~ /\[\d+\]/ && !link.include?("[200]") }
36
+ unless checked_links.empty?
37
+ @failed = true
38
+ broken_links[location] += checked_links
39
+ end
40
+ end
41
+ broken_links
42
+ end
43
+
44
+ def puts_broken_links link_hash
45
+ link_hash.each do |location, links|
46
+ puts "#{location} contains the following broken links:"
47
+ links.each { |link| puts " #{link}" }
48
+ puts ""
49
+ end
50
+ end
51
+
52
+ def job_info
53
+ line_length = "Using Ruby - #{RUBY_VERSION}".length + 8
54
+ puts ""
55
+ puts "#" * line_length
56
+ puts "### Using Ruby - #{RUBY_VERSION} ###"
57
+ puts "#" * line_length
58
+ puts ""
59
+ end
60
+
61
+ def exit_status
62
+ @failed ? 1 : 0
63
+ end
64
+ end