googleauth 0.1.0 → 0.16.2

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 (78) hide show
  1. checksums.yaml +5 -5
  2. data/.github/CODEOWNERS +7 -0
  3. data/.github/CONTRIBUTING.md +74 -0
  4. data/.github/ISSUE_TEMPLATE/bug_report.md +36 -0
  5. data/.github/ISSUE_TEMPLATE/feature_request.md +21 -0
  6. data/.github/ISSUE_TEMPLATE/support_request.md +7 -0
  7. data/.github/renovate.json +6 -0
  8. data/.github/sync-repo-settings.yaml +18 -0
  9. data/.github/workflows/ci.yml +55 -0
  10. data/.github/workflows/release-please.yml +39 -0
  11. data/.gitignore +3 -0
  12. data/.kokoro/populate-secrets.sh +76 -0
  13. data/.kokoro/release.cfg +52 -0
  14. data/.kokoro/release.sh +18 -0
  15. data/.kokoro/trampoline_v2.sh +489 -0
  16. data/.repo-metadata.json +5 -0
  17. data/.rubocop.yml +17 -0
  18. data/.toys/.toys.rb +45 -0
  19. data/.toys/ci.rb +43 -0
  20. data/.toys/kokoro/.toys.rb +66 -0
  21. data/.toys/kokoro/publish-docs.rb +67 -0
  22. data/.toys/kokoro/publish-gem.rb +53 -0
  23. data/.toys/linkinator.rb +43 -0
  24. data/.trampolinerc +48 -0
  25. data/CHANGELOG.md +199 -0
  26. data/CODE_OF_CONDUCT.md +43 -0
  27. data/Gemfile +22 -1
  28. data/{COPYING → LICENSE} +0 -0
  29. data/README.md +140 -17
  30. data/googleauth.gemspec +28 -28
  31. data/integration/helper.rb +31 -0
  32. data/integration/id_tokens/key_source_test.rb +74 -0
  33. data/lib/googleauth.rb +7 -37
  34. data/lib/googleauth/application_default.rb +81 -0
  35. data/lib/googleauth/client_id.rb +104 -0
  36. data/lib/googleauth/compute_engine.rb +73 -26
  37. data/lib/googleauth/credentials.rb +561 -0
  38. data/lib/googleauth/credentials_loader.rb +207 -0
  39. data/lib/googleauth/default_credentials.rb +93 -0
  40. data/lib/googleauth/iam.rb +75 -0
  41. data/lib/googleauth/id_tokens.rb +233 -0
  42. data/lib/googleauth/id_tokens/errors.rb +71 -0
  43. data/lib/googleauth/id_tokens/key_sources.rb +396 -0
  44. data/lib/googleauth/id_tokens/verifier.rb +142 -0
  45. data/lib/googleauth/json_key_reader.rb +50 -0
  46. data/lib/googleauth/scope_util.rb +61 -0
  47. data/lib/googleauth/service_account.rb +177 -67
  48. data/lib/googleauth/signet.rb +69 -8
  49. data/lib/googleauth/stores/file_token_store.rb +65 -0
  50. data/lib/googleauth/stores/redis_token_store.rb +96 -0
  51. data/lib/googleauth/token_store.rb +69 -0
  52. data/lib/googleauth/user_authorizer.rb +285 -0
  53. data/lib/googleauth/user_refresh.rb +129 -0
  54. data/lib/googleauth/version.rb +1 -1
  55. data/lib/googleauth/web_user_authorizer.rb +295 -0
  56. data/spec/googleauth/apply_auth_examples.rb +96 -94
  57. data/spec/googleauth/client_id_spec.rb +160 -0
  58. data/spec/googleauth/compute_engine_spec.rb +125 -55
  59. data/spec/googleauth/credentials_spec.rb +600 -0
  60. data/spec/googleauth/get_application_default_spec.rb +232 -80
  61. data/spec/googleauth/iam_spec.rb +80 -0
  62. data/spec/googleauth/scope_util_spec.rb +77 -0
  63. data/spec/googleauth/service_account_spec.rb +422 -68
  64. data/spec/googleauth/signet_spec.rb +101 -25
  65. data/spec/googleauth/stores/file_token_store_spec.rb +57 -0
  66. data/spec/googleauth/stores/redis_token_store_spec.rb +50 -0
  67. data/spec/googleauth/stores/store_examples.rb +58 -0
  68. data/spec/googleauth/user_authorizer_spec.rb +343 -0
  69. data/spec/googleauth/user_refresh_spec.rb +359 -0
  70. data/spec/googleauth/web_user_authorizer_spec.rb +172 -0
  71. data/spec/spec_helper.rb +51 -10
  72. data/test/helper.rb +33 -0
  73. data/test/id_tokens/key_sources_test.rb +240 -0
  74. data/test/id_tokens/verifier_test.rb +269 -0
  75. metadata +114 -75
  76. data/.travis.yml +0 -18
  77. data/CONTRIBUTING.md +0 -32
  78. data/Rakefile +0 -15
@@ -0,0 +1,142 @@
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
+ options = { algorithms: key.algorithm }
109
+ decoded_token = JWT.decode token, key.key, true, options
110
+ payload = decoded_token.first
111
+ rescue JWT::ExpiredSignature
112
+ raise ExpiredTokenError, "Token signature is expired"
113
+ rescue JWT::DecodeError
114
+ nil # Try the next key
115
+ end
116
+
117
+ normalize_and_verify_payload payload, aud, azp, iss
118
+ end
119
+
120
+ def normalize_and_verify_payload payload, aud, azp, iss
121
+ return nil unless payload
122
+
123
+ # Map the legacy "cid" claim to the canonical "azp"
124
+ payload["azp"] ||= payload["cid"] if payload.key? "cid"
125
+
126
+ # Payload content validation
127
+ if aud && (Array(aud) & Array(payload["aud"])).empty?
128
+ raise AudienceMismatchError, "Token aud mismatch: #{payload['aud']}"
129
+ end
130
+ if azp && (Array(azp) & Array(payload["azp"])).empty?
131
+ raise AuthorizedPartyMismatchError, "Token azp mismatch: #{payload['azp']}"
132
+ end
133
+ if iss && (Array(iss) & Array(payload["iss"])).empty?
134
+ raise IssuerMismatchError, "Token iss mismatch: #{payload['iss']}"
135
+ end
136
+
137
+ payload
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,50 @@
1
+ # Copyright 2015, Google Inc.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are
6
+ # met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright
9
+ # notice, this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above
11
+ # copyright notice, this list of conditions and the following disclaimer
12
+ # in the documentation and/or other materials provided with the
13
+ # distribution.
14
+ # * Neither the name of Google Inc. nor the names of its
15
+ # contributors may be used to endorse or promote products derived from
16
+ # this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
+
30
+ module Google
31
+ # Module Auth provides classes that provide Google-specific authorization
32
+ # used to access Google APIs.
33
+ module Auth
34
+ # JsonKeyReader contains the behaviour used to read private key and
35
+ # client email fields from the service account
36
+ module JsonKeyReader
37
+ def read_json_key json_key_io
38
+ json_key = MultiJson.load json_key_io.read
39
+ raise "missing client_email" unless json_key.key? "client_email"
40
+ raise "missing private_key" unless json_key.key? "private_key"
41
+ [
42
+ json_key["private_key"],
43
+ json_key["client_email"],
44
+ json_key["project_id"],
45
+ json_key["quota_project_id"]
46
+ ]
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,61 @@
1
+ # Copyright 2015, Google Inc.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are
6
+ # met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright
9
+ # notice, this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above
11
+ # copyright notice, this list of conditions and the following disclaimer
12
+ # in the documentation and/or other materials provided with the
13
+ # distribution.
14
+ # * Neither the name of Google Inc. nor the names of its
15
+ # contributors may be used to endorse or promote products derived from
16
+ # this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
+
30
+ require "googleauth/signet"
31
+ require "googleauth/credentials_loader"
32
+ require "multi_json"
33
+
34
+ module Google
35
+ module Auth
36
+ # Small utility for normalizing scopes into canonical form
37
+ module ScopeUtil
38
+ ALIASES = {
39
+ "email" => "https://www.googleapis.com/auth/userinfo.email",
40
+ "profile" => "https://www.googleapis.com/auth/userinfo.profile",
41
+ "openid" => "https://www.googleapis.com/auth/plus.me"
42
+ }.freeze
43
+
44
+ def self.normalize scope
45
+ list = as_array scope
46
+ list.map { |item| ALIASES[item] || item }
47
+ end
48
+
49
+ def self.as_array scope
50
+ case scope
51
+ when Array
52
+ scope
53
+ when String
54
+ scope.split
55
+ else
56
+ raise "Invalid scope value. Must be string or array"
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -27,91 +27,201 @@
27
27
  # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
28
  # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
29
 
30
- require 'googleauth/signet'
31
- require 'memoist'
32
- require 'multi_json'
33
- require 'openssl'
34
- require 'rbconfig'
35
-
36
- # Reads the private key and client email fields from service account JSON key.
37
- def read_json_key(json_key_io)
38
- json_key = MultiJson.load(json_key_io.read)
39
- fail 'missing client_email' unless json_key.key?('client_email')
40
- fail 'missing private_key' unless json_key.key?('private_key')
41
- [json_key['private_key'], json_key['client_email']]
42
- end
30
+ require "googleauth/signet"
31
+ require "googleauth/credentials_loader"
32
+ require "googleauth/json_key_reader"
33
+ require "jwt"
34
+ require "multi_json"
35
+ require "stringio"
43
36
 
44
37
  module Google
45
38
  # Module Auth provides classes that provide Google-specific authorization
46
39
  # used to access Google APIs.
47
40
  module Auth
48
- # Authenticates requests using Google's Service Account credentials.
41
+ # Authenticates requests using Google's Service Account credentials via an
42
+ # OAuth access token.
49
43
  #
50
44
  # This class allows authorizing requests for service accounts directly
51
45
  # from credentials from a json key file downloaded from the developer
52
46
  # console (via 'Generate new Json Key').
53
47
  #
54
- # cf [Application Default Credentials](http://goo.gl/mkAHpZ)
48
+ # cf [Application Default Credentials](https://cloud.google.com/docs/authentication/production)
55
49
  class ServiceAccountCredentials < Signet::OAuth2::Client
56
- ENV_VAR = 'GOOGLE_APPLICATION_CREDENTIALS'
57
- NOT_FOUND_ERROR =
58
- "Unable to read the credential file specified by #{ENV_VAR}"
59
- TOKEN_CRED_URI = 'https://www.googleapis.com/oauth2/v3/token'
60
- WELL_KNOWN_PATH = 'gcloud/application_default_credentials.json'
61
- WELL_KNOWN_ERROR = 'Unable to read the default credential file'
62
-
63
- class << self
64
- extend Memoist
65
-
66
- # determines if the current OS is windows
67
- def windows?
68
- RbConfig::CONFIG['host_os'] =~ /Windows|mswin/
69
- end
70
- memoize :windows?
71
-
72
- # Creates an instance from the path specified in an environment
73
- # variable.
74
- #
75
- # @param scope [string|array] the scope(s) to access
76
- def from_env(scope)
77
- return nil unless ENV.key?(ENV_VAR)
78
- path = ENV[ENV_VAR]
79
- fail 'file #{path} does not exist' unless File.exist?(path)
80
- File.open(path) do |f|
81
- return new(scope, f)
82
- end
83
- rescue StandardError => e
84
- raise "#{NOT_FOUND_ERROR}: #{e}"
50
+ TOKEN_CRED_URI = "https://www.googleapis.com/oauth2/v4/token".freeze
51
+ extend CredentialsLoader
52
+ extend JsonKeyReader
53
+ attr_reader :project_id
54
+ attr_reader :quota_project_id
55
+
56
+ def enable_self_signed_jwt?
57
+ @enable_self_signed_jwt
58
+ end
59
+
60
+ # Creates a ServiceAccountCredentials.
61
+ #
62
+ # @param json_key_io [IO] an IO from which the JSON key can be read
63
+ # @param scope [string|array|nil] the scope(s) to access
64
+ def self.make_creds options = {}
65
+ json_key_io, scope, enable_self_signed_jwt, target_audience, audience, token_credential_uri =
66
+ options.values_at :json_key_io, :scope, :enable_self_signed_jwt, :target_audience,
67
+ :audience, :token_credential_uri
68
+ raise ArgumentError, "Cannot specify both scope and target_audience" if scope && target_audience
69
+
70
+ if json_key_io
71
+ private_key, client_email, project_id, quota_project_id = read_json_key json_key_io
72
+ else
73
+ private_key = unescape ENV[CredentialsLoader::PRIVATE_KEY_VAR]
74
+ client_email = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
75
+ project_id = ENV[CredentialsLoader::PROJECT_ID_VAR]
76
+ quota_project_id = nil
85
77
  end
78
+ project_id ||= CredentialsLoader.load_gcloud_project_id
79
+
80
+ new(token_credential_uri: token_credential_uri || TOKEN_CRED_URI,
81
+ audience: audience || TOKEN_CRED_URI,
82
+ scope: scope,
83
+ enable_self_signed_jwt: enable_self_signed_jwt,
84
+ target_audience: target_audience,
85
+ issuer: client_email,
86
+ signing_key: OpenSSL::PKey::RSA.new(private_key),
87
+ project_id: project_id,
88
+ quota_project_id: quota_project_id)
89
+ .configure_connection(options)
90
+ end
86
91
 
87
- # Creates an instance from a well known path.
88
- #
89
- # @param scope [string|array] the scope(s) to access
90
- def from_well_known_path(scope)
91
- home_var, base = windows? ? 'APPDATA' : 'HOME', WELL_KNOWN_PATH
92
- root = ENV[home_var].nil? ? '' : ENV[home_var]
93
- base = File.join('.config', base) unless windows?
94
- path = File.join(root, base)
95
- return nil unless File.exist?(path)
96
- File.open(path) do |f|
97
- return new(scope, f)
98
- end
99
- rescue StandardError => e
100
- raise "#{WELL_KNOWN_ERROR}: #{e}"
92
+ # Handles certain escape sequences that sometimes appear in input.
93
+ # Specifically, interprets the "\n" sequence for newline, and removes
94
+ # enclosing quotes.
95
+ def self.unescape str
96
+ str = str.gsub '\n', "\n"
97
+ str = str[1..-2] if str.start_with?('"') && str.end_with?('"')
98
+ str
99
+ end
100
+
101
+ def initialize options = {}
102
+ @project_id = options[:project_id]
103
+ @quota_project_id = options[:quota_project_id]
104
+ @enable_self_signed_jwt = options[:enable_self_signed_jwt] ? true : false
105
+ super options
106
+ end
107
+
108
+ # Extends the base class to use a transient
109
+ # ServiceAccountJwtHeaderCredentials for certain cases.
110
+ def apply! a_hash, opts = {}
111
+ # Use a self-singed JWT if there's no information that can be used to
112
+ # obtain an OAuth token, OR if there are scopes but also an assertion
113
+ # that they are default scopes that shouldn't be used to fetch a token.
114
+ if target_audience.nil? && (scope.nil? || enable_self_signed_jwt?)
115
+ apply_self_signed_jwt! a_hash
116
+ else
117
+ super
101
118
  end
102
119
  end
103
120
 
104
- # Initializes a ServiceAccountCredentials.
121
+ private
122
+
123
+ def apply_self_signed_jwt! a_hash
124
+ # Use the ServiceAccountJwtHeaderCredentials using the same cred values
125
+ cred_json = {
126
+ private_key: @signing_key.to_s,
127
+ client_email: @issuer,
128
+ project_id: @project_id,
129
+ quota_project_id: @quota_project_id
130
+ }
131
+ key_io = StringIO.new MultiJson.dump(cred_json)
132
+ alt = ServiceAccountJwtHeaderCredentials.make_creds json_key_io: key_io
133
+ alt.apply! a_hash
134
+ end
135
+ end
136
+
137
+ # Authenticates requests using Google's Service Account credentials via
138
+ # JWT Header.
139
+ #
140
+ # This class allows authorizing requests for service accounts directly
141
+ # from credentials from a json key file downloaded from the developer
142
+ # console (via 'Generate new Json Key'). It is not part of any OAuth2
143
+ # flow, rather it creates a JWT and sends that as a credential.
144
+ #
145
+ # cf [Application Default Credentials](https://cloud.google.com/docs/authentication/production)
146
+ class ServiceAccountJwtHeaderCredentials
147
+ JWT_AUD_URI_KEY = :jwt_aud_uri
148
+ AUTH_METADATA_KEY = Signet::OAuth2::AUTH_METADATA_KEY
149
+ TOKEN_CRED_URI = "https://www.googleapis.com/oauth2/v4/token".freeze
150
+ SIGNING_ALGORITHM = "RS256".freeze
151
+ EXPIRY = 60
152
+ extend CredentialsLoader
153
+ extend JsonKeyReader
154
+ attr_reader :project_id
155
+ attr_reader :quota_project_id
156
+
157
+ # make_creds proxies the construction of a credentials instance
158
+ #
159
+ # make_creds is used by the methods in CredentialsLoader.
160
+ #
161
+ # By default, it calls #new with 2 args, the second one being an
162
+ # optional scope. Here's the constructor only has one param, so
163
+ # we modify make_creds to reflect this.
164
+ def self.make_creds *args
165
+ new json_key_io: args[0][:json_key_io]
166
+ end
167
+
168
+ # Initializes a ServiceAccountJwtHeaderCredentials.
105
169
  #
106
- # @param scope [string|array] the scope(s) to access
107
170
  # @param json_key_io [IO] an IO from which the JSON key can be read
108
- def initialize(scope, json_key_io)
109
- private_key, client_email = read_json_key(json_key_io)
110
- super(token_credential_uri: TOKEN_CRED_URI,
111
- audience: TOKEN_CRED_URI, # TODO: confirm this
112
- scope: scope,
113
- issuer: client_email,
114
- signing_key: OpenSSL::PKey::RSA.new(private_key))
171
+ def initialize options = {}
172
+ json_key_io = options[:json_key_io]
173
+ if json_key_io
174
+ @private_key, @issuer, @project_id, @quota_project_id =
175
+ self.class.read_json_key json_key_io
176
+ else
177
+ @private_key = ENV[CredentialsLoader::PRIVATE_KEY_VAR]
178
+ @issuer = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
179
+ @project_id = ENV[CredentialsLoader::PROJECT_ID_VAR]
180
+ @quota_project_id = nil
181
+ end
182
+ @project_id ||= CredentialsLoader.load_gcloud_project_id
183
+ @signing_key = OpenSSL::PKey::RSA.new @private_key
184
+ end
185
+
186
+ # Construct a jwt token if the JWT_AUD_URI key is present in the input
187
+ # hash.
188
+ #
189
+ # The jwt token is used as the value of a 'Bearer '.
190
+ def apply! a_hash, opts = {}
191
+ jwt_aud_uri = a_hash.delete JWT_AUD_URI_KEY
192
+ return a_hash if jwt_aud_uri.nil?
193
+ jwt_token = new_jwt_token jwt_aud_uri, opts
194
+ a_hash[AUTH_METADATA_KEY] = "Bearer #{jwt_token}"
195
+ a_hash
196
+ end
197
+
198
+ # Returns a clone of a_hash updated with the authoriation header
199
+ def apply a_hash, opts = {}
200
+ a_copy = a_hash.clone
201
+ apply! a_copy, opts
202
+ a_copy
203
+ end
204
+
205
+ # Returns a reference to the #apply method, suitable for passing as
206
+ # a closure
207
+ def updater_proc
208
+ proc { |a_hash, opts = {}| apply a_hash, opts }
209
+ end
210
+
211
+ protected
212
+
213
+ # Creates a jwt uri token.
214
+ def new_jwt_token jwt_aud_uri, options = {}
215
+ now = Time.new
216
+ skew = options[:skew] || 60
217
+ assertion = {
218
+ "iss" => @issuer,
219
+ "sub" => @issuer,
220
+ "aud" => jwt_aud_uri,
221
+ "exp" => (now + EXPIRY).to_i,
222
+ "iat" => (now - skew).to_i
223
+ }
224
+ JWT.encode assertion, @signing_key, SIGNING_ALGORITHM
115
225
  end
116
226
  end
117
227
  end