googleauth 0.9.0 → 0.13.1

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