googleauth 1.8.1 → 1.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.
@@ -42,6 +42,7 @@ module Google
42
42
 
43
43
  attr_reader :expires_at
44
44
  attr_accessor :access_token
45
+ attr_accessor :universe_domain
45
46
 
46
47
  def expires_within? seconds
47
48
  # This method is needed for BaseClient
@@ -75,7 +76,7 @@ module Google
75
76
  # The retrieved subject token.
76
77
  #
77
78
  def retrieve_subject_token!
78
- raise NotImplementedError
79
+ raise NoMethodError, "retrieve_subject_token! not implemented"
79
80
  end
80
81
 
81
82
  # Returns whether the credentials represent a workforce pool (True) or
@@ -85,8 +86,7 @@ module Google
85
86
  # true if the credentials represent a workforce pool.
86
87
  # false if they represent a workload.
87
88
  def is_workforce_pool?
88
- pattern = "//iam\.googleapis\.com/locations/[^/]+/workforcePools/"
89
- /#{pattern}/.match?(@audience || "")
89
+ %r{/iam\.googleapis\.com/locations/[^/]+/workforcePools/}.match?(@audience || "")
90
90
  end
91
91
 
92
92
  private
@@ -111,6 +111,7 @@ module Google
111
111
  @quota_project_id = options[:quota_project_id]
112
112
  @project_id = nil
113
113
  @workforce_pool_user_project = options[:workforce_pool_user_project]
114
+ @universe_domain = options[:universe_domain] || "googleapis.com"
114
115
 
115
116
  @expires_at = nil
116
117
  @access_token = nil
@@ -128,7 +129,7 @@ module Google
128
129
  if @client_id.nil? && @workforce_pool_user_project
129
130
  additional_options = { userProject: @workforce_pool_user_project }
130
131
  end
131
- @sts_client.exchange_token(
132
+ token_request = {
132
133
  audience: @audience,
133
134
  grant_type: STS_GRANT_TYPE,
134
135
  subject_token: retrieve_subject_token!,
@@ -136,10 +137,31 @@ module Google
136
137
  scopes: @service_account_impersonation_url ? IAM_SCOPE : @scope,
137
138
  requested_token_type: STS_REQUESTED_TOKEN_TYPE,
138
139
  additional_options: additional_options
139
- )
140
+ }
141
+ log_token_request token_request
142
+ @sts_client.exchange_token token_request
143
+ end
144
+
145
+ def log_token_request token_request
146
+ logger&.info do
147
+ Google::Logging::Message.from(
148
+ message: "Requesting access token from #{token_request[:grant_type]}",
149
+ "credentialsId" => object_id
150
+ )
151
+ end
152
+ logger&.debug do
153
+ digest = Digest::SHA256.hexdigest token_request[:subject_token].to_s
154
+ loggable_request = token_request.merge subject_token: "(sha256:#{digest})"
155
+ Google::Logging::Message.from(
156
+ message: "Request data",
157
+ "request" => loggable_request,
158
+ "credentialsId" => object_id
159
+ )
160
+ end
140
161
  end
141
162
 
142
163
  def get_impersonated_access_token token, _options = {}
164
+ log_impersonated_token_request token
143
165
  response = connection.post @service_account_impersonation_url do |req|
144
166
  req.headers["Authorization"] = "Bearer #{token}"
145
167
  req.headers["Content-Type"] = "application/json"
@@ -152,6 +174,16 @@ module Google
152
174
 
153
175
  MultiJson.load response.body
154
176
  end
177
+
178
+ def log_impersonated_token_request original_token
179
+ logger&.info do
180
+ digest = Digest::SHA256.hexdigest original_token
181
+ Google::Logging::Message.from(
182
+ message: "Requesting impersonated access token with original token (sha256:#{digest})",
183
+ "credentialsId" => object_id
184
+ )
185
+ end
186
+ end
155
187
  end
156
188
  end
157
189
  end
@@ -73,7 +73,8 @@ module Google
73
73
  subject_token_type: user_creds[:subject_token_type],
74
74
  token_url: user_creds[:token_url],
75
75
  credential_source: user_creds[:credential_source],
76
- service_account_impersonation_url: user_creds[:service_account_impersonation_url]
76
+ service_account_impersonation_url: user_creds[:service_account_impersonation_url],
77
+ universe_domain: user_creds[:universe_domain]
77
78
  )
78
79
  end
79
80
 
@@ -24,7 +24,13 @@ module Google
24
24
  module Connection
25
25
  module_function
26
26
 
27
- attr_accessor :default_connection
27
+ def default_connection
28
+ @default_connection
29
+ end
30
+
31
+ def default_connection= conn
32
+ @default_connection = conn
33
+ end
28
34
 
29
35
  def connection
30
36
  @default_connection || Faraday.default_connection
@@ -168,7 +168,6 @@ module Google
168
168
  aud: nil,
169
169
  azp: nil,
170
170
  iss: OIDC_ISSUERS
171
-
172
171
  verifier = Verifier.new key_source: oidc_key_source,
173
172
  aud: aud,
174
173
  azp: azp,
@@ -206,7 +205,6 @@ module Google
206
205
  aud: nil,
207
206
  azp: nil,
208
207
  iss: IAP_ISSUERS
209
-
210
208
  verifier = Verifier.new key_source: iap_key_source,
211
209
  aud: aud,
212
210
  azp: azp,
@@ -0,0 +1,282 @@
1
+ # Copyright 2024 Google, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "googleauth/signet"
16
+ require "googleauth/base_client"
17
+ require "googleauth/helpers/connection"
18
+
19
+ module Google
20
+ module Auth
21
+ # Authenticates requests using impersonation from base credentials.
22
+ # This is a two-step process: first authentication claim from the base credentials is created
23
+ # and then that claim is exchanged for a short-lived token at an IAMCredentials endpoint.
24
+ # The short-lived token and its expiration time are cached.
25
+ class ImpersonatedServiceAccountCredentials
26
+ # @private
27
+ ERROR_SUFFIX = <<~ERROR.freeze
28
+ when trying to get security access token
29
+ from IAM Credentials endpoint using the credentials provided.
30
+ ERROR
31
+
32
+ # @private
33
+ IAM_SCOPE = ["https://www.googleapis.com/auth/iam".freeze].freeze
34
+
35
+ # BaseClient most importantly implements the `:updater_proc` getter,
36
+ # that returns a reference to an `apply!` method that updates
37
+ # a hash argument provided with the authorization header containing
38
+ # the access token (impersonation token in this case).
39
+ include Google::Auth::BaseClient
40
+
41
+ include Helpers::Connection
42
+
43
+ # @return [Object] The original authenticated credentials used to fetch short-lived impersonation access tokens
44
+ attr_reader :base_credentials
45
+
46
+ # @return [Object] The modified version of base credentials, tailored for impersonation purposes
47
+ # with necessary scope adjustments
48
+ attr_reader :source_credentials
49
+
50
+ # @return [String] The URL endpoint used to generate an impersonation token. This URL should follow a specific
51
+ # format to specify the impersonated service account.
52
+ attr_reader :impersonation_url
53
+
54
+ # @return [Array<String>, String] The scope(s) required for the impersonated access token,
55
+ # indicating the permissions needed for the short-lived token
56
+ attr_reader :scope
57
+
58
+ # @return [String, nil] The short-lived impersonation access token, retrieved and cached
59
+ # after making the impersonation request
60
+ attr_reader :access_token
61
+
62
+ # @return [Time, nil] The expiration time of the current access token, used to determine
63
+ # if the token is still valid
64
+ attr_reader :expires_at
65
+
66
+ # Create a ImpersonatedServiceAccountCredentials
67
+ # When you use service account impersonation, you start with an authenticated principal
68
+ # (e.g. your user account or a service account)
69
+ # and request short-lived credentials for a service account
70
+ # that has the authorization that your use case requires.
71
+ #
72
+ # @param options [Hash] A hash of options to configure the credentials.
73
+ # @option options [Object] :base_credentials (required) The authenticated principal.
74
+ # It will be used as following:
75
+ # * will be duplicated (with IAM scope) to create the source credentials if it supports duplication
76
+ # * as source credentials otherwise.
77
+ # @option options [String] :impersonation_url (required) The URL to impersonate the service account.
78
+ # This URL should follow the format:
79
+ # `https://iamcredentials.{universe_domain}/v1/projects/-/serviceAccounts/{source_sa_email}:generateAccessToken`,
80
+ # where:
81
+ # - `{universe_domain}` is the domain of the IAMCredentials API endpoint (e.g., `googleapis.com`).
82
+ # - `{source_sa_email}` is the email address of the service account to impersonate.
83
+ # @option options [Array<String>, String] :scope (required) The scope(s) for the short-lived impersonation token,
84
+ # defining the permissions required for the token.
85
+ # @option options [Object] :source_credentials The authenticated principal that will be used
86
+ # to fetch the short-lived impersonation access token. It is an alternative to providing the base credentials.
87
+ #
88
+ # @return [Google::Auth::ImpersonatedServiceAccountCredentials]
89
+ def self.make_creds options = {}
90
+ new options
91
+ end
92
+
93
+ # Initializes a new instance of ImpersonatedServiceAccountCredentials.
94
+ #
95
+ # @param options [Hash] A hash of options to configure the credentials.
96
+ # @option options [Object] :base_credentials (required) The authenticated principal.
97
+ # It will be used as following:
98
+ # * will be duplicated (with IAM scope) to create the source credentials if it supports duplication
99
+ # * as source credentials otherwise.
100
+ # @option options [String] :impersonation_url (required) The URL to impersonate the service account.
101
+ # This URL should follow the format:
102
+ # `https://iamcredentials.{universe_domain}/v1/projects/-/serviceAccounts/{source_sa_email}:generateAccessToken`,
103
+ # where:
104
+ # - `{universe_domain}` is the domain of the IAMCredentials API endpoint (e.g., `googleapis.com`).
105
+ # - `{source_sa_email}` is the email address of the service account to impersonate.
106
+ # @option options [Array<String>, String] :scope (required) The scope(s) for the short-lived impersonation token,
107
+ # defining the permissions required for the token.
108
+ # @option options [Object] :source_credentials The authenticated principal that will be used
109
+ # to fetch the short-lived impersonation access token. It is an alternative to providing the base credentials.
110
+ # It is redundant to provide both source and base credentials as only source will be used,
111
+ # but it can be done, e.g. when duplicating existing credentials.
112
+ #
113
+ # @raise [ArgumentError] If any of the required options are missing.
114
+ #
115
+ # @return [Google::Auth::ImpersonatedServiceAccountCredentials]
116
+ def initialize options = {}
117
+ @base_credentials, @impersonation_url, @scope =
118
+ options.values_at :base_credentials,
119
+ :impersonation_url,
120
+ :scope
121
+
122
+ # Fail-fast checks for required parameters
123
+ if @base_credentials.nil? && !options.key?(:source_credentials)
124
+ raise ArgumentError, "Missing required option: either :base_credentials or :source_credentials"
125
+ end
126
+ raise ArgumentError, "Missing required option: :impersonation_url" if @impersonation_url.nil?
127
+ raise ArgumentError, "Missing required option: :scope" if @scope.nil?
128
+
129
+ # Some credentials (all Signet-based ones and this one) include scope and a bunch of transient state
130
+ # (e.g. refresh status) as part of themselves
131
+ # so a copy needs to be created with the scope overriden and transient state dropped.
132
+ #
133
+ # If a credentials does not support `duplicate` we'll try to use it as is assuming it has a broad enough scope.
134
+ # This might result in an "access denied" error downstream when the token from that credentials is being used
135
+ # for the token exchange.
136
+ @source_credentials = if options.key? :source_credentials
137
+ options[:source_credentials]
138
+ elsif @base_credentials.respond_to? :duplicate
139
+ @base_credentials.duplicate({
140
+ scope: IAM_SCOPE
141
+ })
142
+ else
143
+ @base_credentials
144
+ end
145
+ end
146
+
147
+ # Determines whether the current access token expires within the specified number of seconds.
148
+ #
149
+ # @param seconds [Integer] The number of seconds to check against the token's expiration time.
150
+ #
151
+ # @return [Boolean] Whether the access token expires within the given time frame
152
+ def expires_within? seconds
153
+ # This method is needed for BaseClient
154
+ @expires_at && @expires_at - Time.now.utc < seconds
155
+ end
156
+
157
+ # The universe domain of the impersonated credentials.
158
+ # Effectively this retrieves the universe domain of the source credentials.
159
+ #
160
+ # @return [String] The universe domain of the credentials.
161
+ def universe_domain
162
+ @source_credentials.universe_domain
163
+ end
164
+
165
+ # @return [Logger, nil] The logger of the credentials.
166
+ def logger
167
+ @source_credentials.logger if source_credentials.respond_to? :logger
168
+ end
169
+
170
+ # Creates a duplicate of these credentials without transient token state
171
+ #
172
+ # @param options [Hash] Overrides for the credentials parameters.
173
+ # The following keys are recognized
174
+ # * `base_credentials` the base credentials used to initialize the impersonation
175
+ # * `source_credentials` the authenticated credentials which usually would be
176
+ # base credentials with scope overridden to IAM_SCOPE
177
+ # * `impersonation_url` the URL to use to make an impersonation token exchange
178
+ # * `scope` the scope(s) to access
179
+ #
180
+ # @return [Google::Auth::ImpersonatedServiceAccountCredentials]
181
+ def duplicate options = {}
182
+ options = deep_hash_normalize options
183
+
184
+ options = {
185
+ base_credentials: @base_credentials,
186
+ source_credentials: @source_credentials,
187
+ impersonation_url: @impersonation_url,
188
+ scope: @scope
189
+ }.merge(options)
190
+
191
+ self.class.new options
192
+ end
193
+
194
+ private
195
+
196
+ # Generates a new impersonation access token by exchanging the source credentials' token
197
+ # at the impersonation URL.
198
+ #
199
+ # This method first fetches an access token from the source credentials and then exchanges it
200
+ # for an impersonation token using the specified impersonation URL. The generated token and
201
+ # its expiration time are cached for subsequent use.
202
+ #
203
+ # @param _options [Hash] (optional) Additional options for token retrieval (currently unused).
204
+ #
205
+ # @raise [Signet::UnexpectedStatusError] If the response status is 403 or 500.
206
+ # @raise [Signet::AuthorizationError] For other unexpected response statuses.
207
+ #
208
+ # @return [String] The newly generated impersonation access token.
209
+ def fetch_access_token! _options = {}
210
+ auth_header = {}
211
+ auth_header = @source_credentials.updater_proc.call auth_header
212
+
213
+ resp = connection.post @impersonation_url do |req|
214
+ req.headers.merge! auth_header
215
+ req.headers["Content-Type"] = "application/json"
216
+ req.body = MultiJson.dump({ scope: @scope })
217
+ end
218
+
219
+ case resp.status
220
+ when 200
221
+ response = MultiJson.load resp.body
222
+ self.expires_at = response["expireTime"]
223
+ @access_token = response["accessToken"]
224
+ access_token
225
+ when 403, 500
226
+ msg = "Unexpected error code #{resp.status}.\n #{resp.env.response_body} #{ERROR_SUFFIX}"
227
+ raise Signet::UnexpectedStatusError, msg
228
+ else
229
+ msg = "Unexpected error code #{resp.status}.\n #{resp.env.response_body} #{ERROR_SUFFIX}"
230
+ raise Signet::AuthorizationError, msg
231
+ end
232
+ end
233
+
234
+ # Setter for the expires_at value that makes sure it is converted
235
+ # to Time object.
236
+ def expires_at= new_expires_at
237
+ @expires_at = normalize_timestamp new_expires_at
238
+ end
239
+
240
+ # Returns the type of token (access_token).
241
+ # This method is needed for BaseClient.
242
+ def token_type
243
+ :access_token
244
+ end
245
+
246
+ # Normalizes a timestamp to a Time object.
247
+ #
248
+ # @param time [Time, String, nil] The timestamp to normalize.
249
+ #
250
+ # @return [Time, nil] The normalized Time object, or nil if the input is nil.
251
+ #
252
+ # @raise [RuntimeError] If the input is not a Time, String, or nil.
253
+ def normalize_timestamp time
254
+ case time
255
+ when NilClass
256
+ nil
257
+ when Time
258
+ time
259
+ when String
260
+ Time.parse time
261
+ else
262
+ raise "Invalid time value #{time}"
263
+ end
264
+ end
265
+
266
+ # Convert all keys in this hash (nested) to symbols for uniform retrieval
267
+ def recursive_hash_normalize_keys val
268
+ if val.is_a? Hash
269
+ deep_hash_normalize val
270
+ else
271
+ val
272
+ end
273
+ end
274
+
275
+ def deep_hash_normalize old_hash
276
+ sym_hash = {}
277
+ old_hash&.each { |k, v| sym_hash[k.to_sym] = recursive_hash_normalize_keys v }
278
+ sym_hash
279
+ end
280
+ end
281
+ end
282
+ end
@@ -27,7 +27,8 @@ module Google
27
27
  json_key["private_key"],
28
28
  json_key["client_email"],
29
29
  json_key["project_id"],
30
- json_key["quota_project_id"]
30
+ json_key["quota_project_id"],
31
+ json_key["universe_domain"]
31
32
  ]
32
33
  end
33
34
  end
@@ -12,13 +12,16 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- require "googleauth/signet"
16
- require "googleauth/credentials_loader"
17
- require "googleauth/json_key_reader"
18
15
  require "jwt"
19
16
  require "multi_json"
20
17
  require "stringio"
21
18
 
19
+ require "google/logging/message"
20
+ require "googleauth/signet"
21
+ require "googleauth/credentials_loader"
22
+ require "googleauth/json_key_reader"
23
+ require "googleauth/service_account_jwt_header"
24
+
22
25
  module Google
23
26
  # Module Auth provides classes that provide Google-specific authorization
24
27
  # used to access Google APIs.
@@ -39,7 +42,11 @@ module Google
39
42
  attr_reader :quota_project_id
40
43
 
41
44
  def enable_self_signed_jwt?
42
- @enable_self_signed_jwt
45
+ # Use a self-singed JWT if there's no information that can be used to
46
+ # obtain an OAuth token, OR if there are scopes but also an assertion
47
+ # that they are default scopes that shouldn't be used to fetch a token,
48
+ # OR we are not in the default universe and thus OAuth isn't supported.
49
+ target_audience.nil? && (scope.nil? || @enable_self_signed_jwt || universe_domain != "googleapis.com")
43
50
  end
44
51
 
45
52
  # Creates a ServiceAccountCredentials.
@@ -53,12 +60,13 @@ module Google
53
60
  raise ArgumentError, "Cannot specify both scope and target_audience" if scope && target_audience
54
61
 
55
62
  if json_key_io
56
- private_key, client_email, project_id, quota_project_id = read_json_key json_key_io
63
+ private_key, client_email, project_id, quota_project_id, universe_domain = read_json_key json_key_io
57
64
  else
58
65
  private_key = unescape ENV[CredentialsLoader::PRIVATE_KEY_VAR]
59
66
  client_email = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
60
67
  project_id = ENV[CredentialsLoader::PROJECT_ID_VAR]
61
68
  quota_project_id = nil
69
+ universe_domain = nil
62
70
  end
63
71
  project_id ||= CredentialsLoader.load_gcloud_project_id
64
72
 
@@ -70,10 +78,35 @@ module Google
70
78
  issuer: client_email,
71
79
  signing_key: OpenSSL::PKey::RSA.new(private_key),
72
80
  project_id: project_id,
73
- quota_project_id: quota_project_id)
81
+ quota_project_id: quota_project_id,
82
+ universe_domain: universe_domain || "googleapis.com")
74
83
  .configure_connection(options)
75
84
  end
76
85
 
86
+ # Creates a duplicate of these credentials
87
+ # without the Signet::OAuth2::Client-specific
88
+ # transient state (e.g. cached tokens)
89
+ #
90
+ # @param options [Hash] Overrides for the credentials parameters.
91
+ # The following keys are recognized in addition to keys in the
92
+ # Signet::OAuth2::Client
93
+ # * `:enable_self_signed_jwt` Whether the self-signed JWT should
94
+ # be used for the authentication
95
+ # * `project_id` the project id to use during the authentication
96
+ # * `quota_project_id` the quota project id to use
97
+ # during the authentication
98
+ def duplicate options = {}
99
+ options = deep_hash_normalize options
100
+ super(
101
+ {
102
+ enable_self_signed_jwt: @enable_self_signed_jwt,
103
+ project_id: project_id,
104
+ quota_project_id: quota_project_id,
105
+ logger: logger
106
+ }.merge(options)
107
+ )
108
+ end
109
+
77
110
  # Handles certain escape sequences that sometimes appear in input.
78
111
  # Specifically, interprets the "\n" sequence for newline, and removes
79
112
  # enclosing quotes.
@@ -93,16 +126,44 @@ module Google
93
126
  # Extends the base class to use a transient
94
127
  # ServiceAccountJwtHeaderCredentials for certain cases.
95
128
  def apply! a_hash, opts = {}
96
- # Use a self-singed JWT if there's no information that can be used to
97
- # obtain an OAuth token, OR if there are scopes but also an assertion
98
- # that they are default scopes that shouldn't be used to fetch a token.
99
- if target_audience.nil? && (scope.nil? || enable_self_signed_jwt?)
129
+ if enable_self_signed_jwt?
100
130
  apply_self_signed_jwt! a_hash
101
131
  else
102
132
  super
103
133
  end
104
134
  end
105
135
 
136
+ # Modifies this logic so it also requires self-signed-jwt to be disabled
137
+ def needs_access_token?
138
+ super && !enable_self_signed_jwt?
139
+ end
140
+
141
+ # Destructively updates these credentials
142
+ #
143
+ # This method is called by `Signet::OAuth2::Client`'s constructor
144
+ #
145
+ # @param options [Hash] Overrides for the credentials parameters.
146
+ # The following keys are recognized in addition to keys in the
147
+ # Signet::OAuth2::Client
148
+ # * `:enable_self_signed_jwt` Whether the self-signed JWT should
149
+ # be used for the authentication
150
+ # * `project_id` the project id to use during the authentication
151
+ # * `quota_project_id` the quota project id to use
152
+ # during the authentication
153
+ # @return [Google::Auth::ServiceAccountCredentials]
154
+ def update! options = {}
155
+ # Normalize all keys to symbols to allow indifferent access.
156
+ options = deep_hash_normalize options
157
+
158
+ @enable_self_signed_jwt = options[:enable_self_signed_jwt] ? true : false
159
+ @project_id = options[:project_id] if options.key? :project_id
160
+ @quota_project_id = options[:quota_project_id] if options.key? :quota_project_id
161
+
162
+ super(options)
163
+
164
+ self
165
+ end
166
+
106
167
  private
107
168
 
108
169
  def apply_self_signed_jwt! a_hash
@@ -115,101 +176,9 @@ module Google
115
176
  }
116
177
  key_io = StringIO.new MultiJson.dump(cred_json)
117
178
  alt = ServiceAccountJwtHeaderCredentials.make_creds json_key_io: key_io, scope: scope
179
+ alt.logger = logger
118
180
  alt.apply! a_hash
119
181
  end
120
182
  end
121
-
122
- # Authenticates requests using Google's Service Account credentials via
123
- # JWT Header.
124
- #
125
- # This class allows authorizing requests for service accounts directly
126
- # from credentials from a json key file downloaded from the developer
127
- # console (via 'Generate new Json Key'). It is not part of any OAuth2
128
- # flow, rather it creates a JWT and sends that as a credential.
129
- #
130
- # cf [Application Default Credentials](https://cloud.google.com/docs/authentication/production)
131
- class ServiceAccountJwtHeaderCredentials
132
- JWT_AUD_URI_KEY = :jwt_aud_uri
133
- AUTH_METADATA_KEY = Google::Auth::BaseClient::AUTH_METADATA_KEY
134
- TOKEN_CRED_URI = "https://www.googleapis.com/oauth2/v4/token".freeze
135
- SIGNING_ALGORITHM = "RS256".freeze
136
- EXPIRY = 60
137
- extend CredentialsLoader
138
- extend JsonKeyReader
139
- attr_reader :project_id
140
- attr_reader :quota_project_id
141
-
142
- # Create a ServiceAccountJwtHeaderCredentials.
143
- #
144
- # @param json_key_io [IO] an IO from which the JSON key can be read
145
- # @param scope [string|array|nil] the scope(s) to access
146
- def self.make_creds options = {}
147
- json_key_io, scope = options.values_at :json_key_io, :scope
148
- new json_key_io: json_key_io, scope: scope
149
- end
150
-
151
- # Initializes a ServiceAccountJwtHeaderCredentials.
152
- #
153
- # @param json_key_io [IO] an IO from which the JSON key can be read
154
- def initialize options = {}
155
- json_key_io = options[:json_key_io]
156
- if json_key_io
157
- @private_key, @issuer, @project_id, @quota_project_id =
158
- self.class.read_json_key json_key_io
159
- else
160
- @private_key = ENV[CredentialsLoader::PRIVATE_KEY_VAR]
161
- @issuer = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
162
- @project_id = ENV[CredentialsLoader::PROJECT_ID_VAR]
163
- @quota_project_id = nil
164
- end
165
- @project_id ||= CredentialsLoader.load_gcloud_project_id
166
- @signing_key = OpenSSL::PKey::RSA.new @private_key
167
- @scope = options[:scope]
168
- end
169
-
170
- # Construct a jwt token if the JWT_AUD_URI key is present in the input
171
- # hash.
172
- #
173
- # The jwt token is used as the value of a 'Bearer '.
174
- def apply! a_hash, opts = {}
175
- jwt_aud_uri = a_hash.delete JWT_AUD_URI_KEY
176
- return a_hash if jwt_aud_uri.nil? && @scope.nil?
177
- jwt_token = new_jwt_token jwt_aud_uri, opts
178
- a_hash[AUTH_METADATA_KEY] = "Bearer #{jwt_token}"
179
- a_hash
180
- end
181
-
182
- # Returns a clone of a_hash updated with the authoriation header
183
- def apply a_hash, opts = {}
184
- a_copy = a_hash.clone
185
- apply! a_copy, opts
186
- a_copy
187
- end
188
-
189
- # Returns a reference to the #apply method, suitable for passing as
190
- # a closure
191
- def updater_proc
192
- proc { |a_hash, opts = {}| apply a_hash, opts }
193
- end
194
-
195
- # Creates a jwt uri token.
196
- def new_jwt_token jwt_aud_uri = nil, options = {}
197
- now = Time.new
198
- skew = options[:skew] || 60
199
- assertion = {
200
- "iss" => @issuer,
201
- "sub" => @issuer,
202
- "exp" => (now + EXPIRY).to_i,
203
- "iat" => (now - skew).to_i
204
- }
205
-
206
- jwt_aud_uri = nil if @scope
207
-
208
- assertion["scope"] = Array(@scope).join " " if @scope
209
- assertion["aud"] = jwt_aud_uri if jwt_aud_uri
210
-
211
- JWT.encode assertion, @signing_key, SIGNING_ALGORITHM
212
- end
213
- end
214
183
  end
215
184
  end