googleauth 0.10.0 → 0.14.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +7 -0
  3. data/.kokoro/continuous/linux.cfg +2 -2
  4. data/.kokoro/continuous/post.cfg +30 -0
  5. data/.kokoro/presubmit/linux.cfg +1 -1
  6. data/.kokoro/release.cfg +1 -1
  7. data/.repo-metadata.json +5 -0
  8. data/.rubocop.yml +5 -4
  9. data/CHANGELOG.md +27 -0
  10. data/Gemfile +5 -2
  11. data/{COPYING → LICENSE} +0 -0
  12. data/README.md +4 -5
  13. data/Rakefile +45 -3
  14. data/googleauth.gemspec +5 -3
  15. data/integration/helper.rb +31 -0
  16. data/integration/id_tokens/key_source_test.rb +74 -0
  17. data/lib/googleauth.rb +1 -0
  18. data/lib/googleauth/application_default.rb +2 -2
  19. data/lib/googleauth/compute_engine.rb +36 -6
  20. data/lib/googleauth/credentials.rb +89 -22
  21. data/lib/googleauth/id_tokens.rb +233 -0
  22. data/lib/googleauth/id_tokens/errors.rb +71 -0
  23. data/lib/googleauth/id_tokens/key_sources.rb +394 -0
  24. data/lib/googleauth/id_tokens/verifier.rb +144 -0
  25. data/lib/googleauth/json_key_reader.rb +6 -2
  26. data/lib/googleauth/service_account.rb +16 -7
  27. data/lib/googleauth/signet.rb +3 -2
  28. data/lib/googleauth/user_authorizer.rb +6 -1
  29. data/lib/googleauth/user_refresh.rb +1 -1
  30. data/lib/googleauth/version.rb +1 -1
  31. data/rakelib/devsite_builder.rb +45 -0
  32. data/rakelib/link_checker.rb +64 -0
  33. data/rakelib/repo_metadata.rb +59 -0
  34. data/spec/googleauth/apply_auth_examples.rb +28 -5
  35. data/spec/googleauth/compute_engine_spec.rb +48 -13
  36. data/spec/googleauth/credentials_spec.rb +17 -6
  37. data/spec/googleauth/service_account_spec.rb +23 -16
  38. data/spec/googleauth/signet_spec.rb +15 -7
  39. data/spec/googleauth/user_authorizer_spec.rb +21 -1
  40. data/spec/googleauth/user_refresh_spec.rb +1 -1
  41. data/test/helper.rb +33 -0
  42. data/test/id_tokens/key_sources_test.rb +240 -0
  43. data/test/id_tokens/verifier_test.rb +269 -0
  44. metadata +46 -12
@@ -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
@@ -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.10.0".freeze
34
+ VERSION = "0.14.0".freeze
35
35
  end
36
36
  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
@@ -0,0 +1,59 @@
1
+ require "json"
2
+
3
+ class RepoMetadata
4
+ attr_reader :data
5
+
6
+ def initialize data
7
+ @data = data
8
+ normalize_data!
9
+ end
10
+
11
+ def allowed_fields
12
+ [
13
+ "name", "version", "language", "distribution-name",
14
+ "product-page", "github-repository", "issue-tracker"
15
+ ]
16
+ end
17
+
18
+ def build output_directory
19
+ fields = @data.to_a.map { |kv| "--#{kv[0]} #{kv[1]}" }
20
+ Dir.chdir output_directory do
21
+ cmd "python3 -m docuploader create-metadata #{fields.join ' '}"
22
+ end
23
+ end
24
+
25
+ def normalize_data!
26
+ require_relative "../lib/googleauth/version.rb"
27
+
28
+ @data.delete_if { |k, _| !allowed_fields.include?(k) }
29
+ @data["version"] = "v#{Google::Auth::VERSION}"
30
+ end
31
+
32
+ def [] key
33
+ data[key]
34
+ end
35
+
36
+ def []= key, value
37
+ @data[key] = value
38
+ end
39
+
40
+ def cmd line
41
+ puts line
42
+ output = `#{line}`
43
+ puts output
44
+ output
45
+ end
46
+
47
+ def self.from_source source
48
+ if source.is_a? RepoMetadata
49
+ data = source.data
50
+ elsif source.is_a? Hash
51
+ data = source
52
+ elsif File.file? source
53
+ data = JSON.parse File.read(source)
54
+ else
55
+ raise "Source must be a path, hash, or RepoMetadata instance"
56
+ end
57
+ RepoMetadata.new data
58
+ end
59
+ end
@@ -45,26 +45,37 @@ shared_examples "apply/apply! are OK" do
45
45
  # auth client
46
46
  describe "#fetch_access_token" do
47
47
  let(:token) { "1/abcdef1234567890" }
48
- let :stub do
48
+ let :access_stub do
49
49
  make_auth_stubs access_token: token
50
50
  end
51
+ let :id_stub do
52
+ make_auth_stubs id_token: token
53
+ end
51
54
 
52
55
  it "should set access_token to the fetched value" do
53
- stub
56
+ access_stub
54
57
  @client.fetch_access_token!
55
58
  expect(@client.access_token).to eq(token)
56
- expect(stub).to have_been_requested
59
+ expect(access_stub).to have_been_requested
60
+ end
61
+
62
+ it "should set id_token to the fetched value" do
63
+ skip unless @id_client
64
+ id_stub
65
+ @id_client.fetch_access_token!
66
+ expect(@id_client.id_token).to eq(token)
67
+ expect(id_stub).to have_been_requested
57
68
  end
58
69
 
59
70
  it "should notify refresh listeners after updating" do
60
- stub
71
+ access_stub
61
72
  expect do |b|
62
73
  @client.on_refresh(&b)
63
74
  @client.fetch_access_token!
64
75
  end.to yield_with_args(have_attributes(
65
76
  access_token: "1/abcdef1234567890"
66
77
  ))
67
- expect(stub).to have_been_requested
78
+ expect(access_stub).to have_been_requested
68
79
  end
69
80
  end
70
81
 
@@ -79,6 +90,18 @@ shared_examples "apply/apply! are OK" do
79
90
  expect(md).to eq(want)
80
91
  expect(stub).to have_been_requested
81
92
  end
93
+
94
+ it "should update the target hash with fetched ID token" do
95
+ skip unless @id_client
96
+ token = "1/abcdef1234567890"
97
+ stub = make_auth_stubs id_token: token
98
+
99
+ md = { foo: "bar" }
100
+ @id_client.apply! md
101
+ want = { :foo => "bar", auth_key => "Bearer #{token}" }
102
+ expect(md).to eq(want)
103
+ expect(stub).to have_been_requested
104
+ end
82
105
  end
83
106
 
84
107
  describe "updater_proc" do
@@ -37,31 +37,52 @@ require "googleauth/compute_engine"
37
37
  require "spec_helper"
38
38
 
39
39
  describe Google::Auth::GCECredentials do
40
- MD_URI = "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token".freeze
40
+ MD_ACCESS_URI = "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token".freeze
41
+ MD_ID_URI = "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://pubsub.googleapis.com/&format=full".freeze
41
42
  GCECredentials = Google::Auth::GCECredentials
42
43
 
43
44
  before :example do
44
45
  @client = GCECredentials.new
46
+ @id_client = GCECredentials.new target_audience: "https://pubsub.googleapis.com/"
45
47
  end
46
48
 
47
- def make_auth_stubs opts = {}
48
- access_token = opts[:access_token] || ""
49
- body = MultiJson.dump("access_token" => access_token,
50
- "token_type" => "Bearer",
51
- "expires_in" => 3600)
52
- stub_request(:get, MD_URI)
53
- .with(headers: { "Metadata-Flavor" => "Google" })
54
- .to_return(body: body,
55
- status: 200,
56
- headers: { "Content-Type" => "application/json" })
49
+ def make_auth_stubs opts
50
+ if opts[:access_token]
51
+ body = MultiJson.dump("access_token" => opts[:access_token],
52
+ "token_type" => "Bearer",
53
+ "expires_in" => 3600)
54
+
55
+ uri = MD_ACCESS_URI
56
+ uri += "?scopes=#{Array(opts[:scope]).join ','}" if opts[:scope]
57
+
58
+ stub_request(:get, uri)
59
+ .with(headers: { "Metadata-Flavor" => "Google" })
60
+ .to_return(body: body,
61
+ status: 200,
62
+ headers: { "Content-Type" => "application/json" })
63
+ elsif opts[:id_token]
64
+ stub_request(:get, MD_ID_URI)
65
+ .with(headers: { "Metadata-Flavor" => "Google" })
66
+ .to_return(body: opts[:id_token],
67
+ status: 200,
68
+ headers: { "Content-Type" => "text/html" })
69
+ end
57
70
  end
58
71
 
59
72
  it_behaves_like "apply/apply! are OK"
60
73
 
61
74
  context "metadata is unavailable" do
62
75
  describe "#fetch_access_token" do
76
+ it "should pass scopes when requesting an access token" do
77
+ scopes = ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/bigtable.data"]
78
+ stub = make_auth_stubs access_token: "1/abcdef1234567890", scope: scopes
79
+ @client = GCECredentials.new(scope: scopes)
80
+ @client.fetch_access_token!
81
+ expect(stub).to have_been_requested
82
+ end
83
+
63
84
  it "should fail if the metadata request returns a 404" do
64
- stub = stub_request(:get, MD_URI)
85
+ stub = stub_request(:get, MD_ACCESS_URI)
65
86
  .to_return(status: 404,
66
87
  headers: { "Metadata-Flavor" => "Google" })
67
88
  expect { @client.fetch_access_token! }
@@ -70,7 +91,7 @@ describe Google::Auth::GCECredentials do
70
91
  end
71
92
 
72
93
  it "should fail if the metadata request returns an unexpected code" do
73
- stub = stub_request(:get, MD_URI)
94
+ stub = stub_request(:get, MD_ACCESS_URI)
74
95
  .to_return(status: 503,
75
96
  headers: { "Metadata-Flavor" => "Google" })
76
97
  expect { @client.fetch_access_token! }
@@ -121,5 +142,19 @@ describe Google::Auth::GCECredentials do
121
142
  expect(GCECredentials.on_gce?({}, true)).to eq(false)
122
143
  expect(stub).to have_been_requested
123
144
  end
145
+
146
+ it "should honor GCE_METADATA_HOST environment variable" do
147
+ ENV["GCE_METADATA_HOST"] = "mymetadata.example.com"
148
+ begin
149
+ stub = stub_request(:get, "http://mymetadata.example.com")
150
+ .with(headers: { "Metadata-Flavor" => "Google" })
151
+ .to_return(status: 200,
152
+ headers: { "Metadata-Flavor" => "Google" })
153
+ expect(GCECredentials.on_gce?({}, true)).to eq(true)
154
+ expect(stub).to have_been_requested
155
+ ensure
156
+ ENV.delete "GCE_METADATA_HOST"
157
+ end
158
+ end
124
159
  end
125
160
  end