googleauth 1.5.2 → 1.7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc7032e8f5f0a060f621bd405feccbea08b70b5a8ed7a3d33676ddf1926160a7
4
- data.tar.gz: 23a199f6a6333ed269c8f564a028533802450c3e7cb100debf5e7e8d940dc8b3
3
+ metadata.gz: 5437d56e0c86ce235d37a202af1a28219a9caeb0bc0f8abac5540cc1d73edf28
4
+ data.tar.gz: f56369065e2abc56fb51abccc5003264f8c9ef3e745c202e06e5cb4c6b083d84
5
5
  SHA512:
6
- metadata.gz: 8b2142a0bdeffd451c3f98c97c8414b26a63939dcd5b4430cb9299b3f1ecdf12d8cc39b8cf813531034dc63121764600c386b372ffe819fdd202c51b070acef2
7
- data.tar.gz: 56e4c86362466b27ad68ec388138fb469b44e604aa60f40454f7499210f61c16e19e559ecd9358023a9de74a8f21289410f5855e128598ac1206c341c328997d
6
+ metadata.gz: f350fb9178517f4782c1dcf08804f5b0ec6bb12ed6dd460ff9c7a875b5929146bf357d797a52cb976878ef25ca8e3439d90b41dfb0e44fbf0b1cfcdf1109ec85
7
+ data.tar.gz: 5a1811530c2a2f5321937bdc90f157f7b55cf0b4c77c7c8f98e87474cfd22b06154987279003cefcb3f8cb400f690364078f9dd0302286f6cbd6d94afde826b3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Release History
2
2
 
3
+ ### 1.7.0 (2023-07-14)
4
+
5
+ #### Features
6
+
7
+ * Adding support for pluggable auth credentials ([#437](https://github.com/googleapis/google-auth-library-ruby/issues/437))
8
+ #### Documentation
9
+
10
+ * fixed iss argument and description in comments of IDTokens ([#438](https://github.com/googleapis/google-auth-library-ruby/issues/438))
11
+
12
+ ### 1.6.0 (2023-06-20)
13
+
14
+ #### Features
15
+
16
+ * adding identity pool credentials ([#433](https://github.com/googleapis/google-auth-library-ruby/issues/433))
17
+ #### Documentation
18
+
19
+ * deprecation message for discontinuing command line auth flow ([#435](https://github.com/googleapis/google-auth-library-ruby/issues/435))
20
+
3
21
  ### 1.5.2 (2023-04-13)
4
22
 
5
23
  #### Bug Fixes
data/README.md CHANGED
@@ -97,7 +97,9 @@ get('/oauth2callback') do
97
97
  end
98
98
  ```
99
99
 
100
- ### Example (Command Line)
100
+ ### Example (Command Line) [Deprecated]
101
+
102
+ The Google Auth OOB flow has been discontiued on January 31, 2023. The OOB flow is a legacy flow that is no longer considered secure. To continue using Google Auth, please migrate your applications to a more secure flow. For more information on how to do this, please refer to this [OOB Migration](https://developers.google.com/identity/protocols/oauth2/resources/oob-migration) guide.
101
103
 
102
104
  ```ruby
103
105
  require 'googleauth'
@@ -14,6 +14,7 @@
14
14
 
15
15
  require "time"
16
16
  require "googleauth/external_account/base_credentials"
17
+ require "googleauth/external_account/external_account_utils"
17
18
 
18
19
  module Google
19
20
  # Module Auth provides classes that provide Google-specific authorization used to access Google APIs.
@@ -27,6 +28,7 @@ module Google
27
28
  IMDSV2_TOKEN_EXPIRATION_IN_SECONDS = 300
28
29
 
29
30
  include Google::Auth::ExternalAccount::BaseCredentials
31
+ include Google::Auth::ExternalAccount::ExternalAccountUtils
30
32
  extend CredentialsLoader
31
33
 
32
34
  # Will always be nil, but method still gets used.
@@ -37,11 +39,11 @@ module Google
37
39
 
38
40
  @audience = options[:audience]
39
41
  @credential_source = options[:credential_source] || {}
40
- @environment_id = @credential_source["environment_id"]
41
- @region_url = @credential_source["region_url"]
42
- @credential_verification_url = @credential_source["url"]
43
- @regional_cred_verification_url = @credential_source["regional_cred_verification_url"]
44
- @imdsv2_session_token_url = @credential_source["imdsv2_session_token_url"]
42
+ @environment_id = @credential_source[:environment_id]
43
+ @region_url = @credential_source[:region_url]
44
+ @credential_verification_url = @credential_source[:url]
45
+ @regional_cred_verification_url = @credential_source[:regional_cred_verification_url]
46
+ @imdsv2_session_token_url = @credential_source[:imdsv2_session_token_url]
45
47
 
46
48
  # These will be lazily loaded when needed, or will raise an error if not provided
47
49
  @region = nil
@@ -20,15 +20,13 @@ module Google
20
20
  # Module Auth provides classes that provide Google-specific authorization
21
21
  # used to access Google APIs.
22
22
  module Auth
23
- # Authenticates requests using External Account credentials, such
24
- # as those provided by the AWS provider.
25
23
  module ExternalAccount
26
24
  # Authenticates requests using External Account credentials, such
27
- # as those provided by the AWS provider.
25
+ # as those provided by the AWS provider or OIDC provider like Azure, etc.
28
26
  module BaseCredentials
29
27
  # Contains all methods needed for all external account credentials.
30
28
  # Other credentials should call `base_setup` during initialization
31
- # And should define the :retrieve_subject_token method
29
+ # And should define the :retrieve_subject_token! method
32
30
 
33
31
  # External account JSON type identifier.
34
32
  EXTERNAL_ACCOUNT_JSON_TYPE = "external_account".freeze
@@ -36,8 +34,6 @@ module Google
36
34
  STS_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange".freeze
37
35
  # The token exchange requested_token_type. This is always an access_token.
38
36
  STS_REQUESTED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token".freeze
39
- # Cloud resource manager URL used to retrieve project information.
40
- CLOUD_RESOURCE_MANAGER = "https://cloudresourcemanager.googleapis.com/v1/projects/".freeze
41
37
  # Default IAM_SCOPE
42
38
  IAM_SCOPE = ["https://www.googleapis.com/auth/iam".freeze].freeze
43
39
 
@@ -74,52 +70,23 @@ module Google
74
70
  notify_refresh_listeners
75
71
  end
76
72
 
77
- ##
78
- # Retrieves the project ID corresponding to the workload identity or workforce pool.
79
- # For workforce pool credentials, it returns the project ID corresponding to the workforce_pool_user_project.
80
- # When not determinable, None is returned.
73
+ # Retrieves the subject token using the credential_source object.
74
+ # @return [string]
75
+ # The retrieved subject token.
81
76
  #
82
- # The resource may not have permission (resourcemanager.projects.get) to
83
- # call this API or the required scopes may not be selected:
84
- # https://cloud.google.com/resource-manager/reference/rest/v1/projects/get#authorization-scopes
85
- #
86
- # @return [string,nil]
87
- # The project ID corresponding to the workload identity pool or workforce pool if determinable.
88
- #
89
- def project_id
90
- return @project_id unless @project_id.nil?
91
- project_number = self.project_number || @workforce_pool_user_project
92
-
93
- # if we missing either project number or scope, we won't retrieve project_id
94
- return nil if project_number.nil? || @scope.nil?
95
-
96
- url = "#{CLOUD_RESOURCE_MANAGER}#{project_number}"
97
-
98
- response = connection.get url do |req|
99
- req.headers["Authorization"] = "Bearer #{@access_token}"
100
- req.headers["Content-Type"] = "application/json"
101
- end
102
-
103
- if response.status == 200
104
- response_data = MultiJson.load response.body, symbolize_names: true
105
- @project_id = response_data[:projectId]
106
- end
107
-
108
- @project_id
77
+ def retrieve_subject_token!
78
+ raise NotImplementedError
109
79
  end
110
80
 
111
- ##
112
- # Retrieve the project number corresponding to workload identity pool
113
- # STS audience pattern:
114
- # `//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/...`
115
- #
116
- # @return [string, nil]
81
+ # Returns whether the credentials represent a workforce pool (True) or
82
+ # workload (False) based on the credentials' audience.
117
83
  #
118
- def project_number
119
- segments = @audience.split "/"
120
- idx = segments.index "projects"
121
- return nil if idx.nil? || idx + 1 == segments.size
122
- segments[idx + 1]
84
+ # @return [bool]
85
+ # true if the credentials represent a workforce pool.
86
+ # false if they represent a workload.
87
+ def is_workforce_pool?
88
+ pattern = "//iam\.googleapis\.com/locations/[^/]+/workforcePools/"
89
+ /#{pattern}/.match?(@audience || "")
123
90
  end
124
91
 
125
92
  private
@@ -136,13 +103,14 @@ module Google
136
103
  @scope = options[:scope] || IAM_SCOPE
137
104
  @subject_token_type = options[:subject_token_type]
138
105
  @token_url = options[:token_url]
106
+ @token_info_url = options[:token_info_url]
139
107
  @service_account_impersonation_url = options[:service_account_impersonation_url]
140
108
  @service_account_impersonation_options = options[:service_account_impersonation_options] || {}
141
109
  @client_id = options[:client_id]
142
110
  @client_secret = options[:client_secret]
143
111
  @quota_project_id = options[:quota_project_id]
144
112
  @project_id = nil
145
- @workforce_pool_user_project = [:workforce_pool_user_project]
113
+ @workforce_pool_user_project = options[:workforce_pool_user_project]
146
114
 
147
115
  @expires_at = nil
148
116
  @access_token = nil
@@ -151,29 +119,23 @@ module Google
151
119
  token_exchange_endpoint: @token_url,
152
120
  connection: default_connection
153
121
  )
154
- end
155
-
156
- def normalize_timestamp time
157
- case time
158
- when NilClass
159
- nil
160
- when Time
161
- time
162
- when String
163
- Time.parse time
164
- else
165
- raise "Invalid time value #{time}"
166
- end
122
+ return unless @workforce_pool_user_project && !is_workforce_pool?
123
+ raise "workforce_pool_user_project should not be set for non-workforce pool credentials."
167
124
  end
168
125
 
169
126
  def exchange_token
127
+ additional_options = nil
128
+ if @client_id.nil? && @workforce_pool_user_project
129
+ additional_options = { userProject: @workforce_pool_user_project }
130
+ end
170
131
  @sts_client.exchange_token(
171
132
  audience: @audience,
172
133
  grant_type: STS_GRANT_TYPE,
173
134
  subject_token: retrieve_subject_token!,
174
135
  subject_token_type: @subject_token_type,
175
136
  scopes: @service_account_impersonation_url ? IAM_SCOPE : @scope,
176
- requested_token_type: STS_REQUESTED_TOKEN_TYPE
137
+ requested_token_type: STS_REQUESTED_TOKEN_TYPE,
138
+ additional_options: additional_options
177
139
  )
178
140
  end
179
141
 
@@ -190,10 +152,6 @@ module Google
190
152
 
191
153
  MultiJson.load response.body
192
154
  end
193
-
194
- def retrieve_subject_token!
195
- raise NotImplementedError
196
- end
197
155
  end
198
156
  end
199
157
  end
@@ -0,0 +1,103 @@
1
+ # Copyright 2023 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.require "time"
14
+
15
+ require "googleauth/base_client"
16
+ require "googleauth/helpers/connection"
17
+ require "googleauth/oauth2/sts_client"
18
+
19
+ module Google
20
+ # Module Auth provides classes that provide Google-specific authorization
21
+ # used to access Google APIs.
22
+ module Auth
23
+ module ExternalAccount
24
+ # Authenticates requests using External Account credentials, such
25
+ # as those provided by the AWS provider or OIDC provider like Azure, etc.
26
+ module ExternalAccountUtils
27
+ # Cloud resource manager URL used to retrieve project information.
28
+ CLOUD_RESOURCE_MANAGER = "https://cloudresourcemanager.googleapis.com/v1/projects/".freeze
29
+
30
+ ##
31
+ # Retrieves the project ID corresponding to the workload identity or workforce pool.
32
+ # For workforce pool credentials, it returns the project ID corresponding to the workforce_pool_user_project.
33
+ # When not determinable, None is returned.
34
+ #
35
+ # The resource may not have permission (resourcemanager.projects.get) to
36
+ # call this API or the required scopes may not be selected:
37
+ # https://cloud.google.com/resource-manager/reference/rest/v1/projects/get#authorization-scopes
38
+ #
39
+ # @return [string,nil]
40
+ # The project ID corresponding to the workload identity pool or workforce pool if determinable.
41
+ #
42
+ def project_id
43
+ return @project_id unless @project_id.nil?
44
+ project_number = self.project_number || @workforce_pool_user_project
45
+
46
+ # if we missing either project number or scope, we won't retrieve project_id
47
+ return nil if project_number.nil? || @scope.nil?
48
+
49
+ url = "#{CLOUD_RESOURCE_MANAGER}#{project_number}"
50
+ response = connection.get url do |req|
51
+ req.headers["Authorization"] = "Bearer #{@access_token}"
52
+ req.headers["Content-Type"] = "application/json"
53
+ end
54
+
55
+ if response.status == 200
56
+ response_data = MultiJson.load response.body, symbolize_names: true
57
+ @project_id = response_data[:projectId]
58
+ end
59
+
60
+ @project_id
61
+ end
62
+
63
+ ##
64
+ # Retrieve the project number corresponding to workload identity pool
65
+ # STS audience pattern:
66
+ # `//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/...`
67
+ #
68
+ # @return [string, nil]
69
+ #
70
+ def project_number
71
+ segments = @audience.split "/"
72
+ idx = segments.index "projects"
73
+ return nil if idx.nil? || idx + 1 == segments.size
74
+ segments[idx + 1]
75
+ end
76
+
77
+ def normalize_timestamp time
78
+ case time
79
+ when NilClass
80
+ nil
81
+ when Time
82
+ time
83
+ when String
84
+ Time.parse time
85
+ else
86
+ raise "Invalid time value #{time}"
87
+ end
88
+ end
89
+
90
+ def service_account_email
91
+ return nil if @service_account_impersonation_url.nil?
92
+ start_idx = @service_account_impersonation_url.rindex "/"
93
+ end_idx = @service_account_impersonation_url.index ":generateAccessToken"
94
+ if start_idx != -1 && end_idx != -1 && start_idx < end_idx
95
+ start_idx += 1
96
+ return @service_account_impersonation_url[start_idx..end_idx]
97
+ end
98
+ nil
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,118 @@
1
+ # Copyright 2023 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 "time"
16
+ require "googleauth/external_account/base_credentials"
17
+ require "googleauth/external_account/external_account_utils"
18
+
19
+ module Google
20
+ # Module Auth provides classes that provide Google-specific authorization used to access Google APIs.
21
+ module Auth
22
+ module ExternalAccount
23
+ # This module handles the retrieval of credentials from Google Cloud by utilizing the any 3PI
24
+ # provider then exchanging the credentials for a short-lived Google Cloud access token.
25
+ class IdentityPoolCredentials
26
+ include Google::Auth::ExternalAccount::BaseCredentials
27
+ include Google::Auth::ExternalAccount::ExternalAccountUtils
28
+ extend CredentialsLoader
29
+
30
+ # Will always be nil, but method still gets used.
31
+ attr_reader :client_id
32
+
33
+ # Initialize from options map.
34
+ #
35
+ # @param [string] audience
36
+ # @param [hash{symbol => value}] credential_source
37
+ # credential_source is a hash that contains either source file or url.
38
+ # credential_source_format is either text or json. To define how we parse the credential response.
39
+ #
40
+ def initialize options = {}
41
+ base_setup options
42
+
43
+ @audience = options[:audience]
44
+ @credential_source = options[:credential_source] || {}
45
+ @credential_source_file = @credential_source[:file]
46
+ @credential_source_url = @credential_source[:url]
47
+ @credential_source_headers = @credential_source[:headers] || {}
48
+ @credential_source_format = @credential_source[:format] || {}
49
+ @credential_source_format_type = @credential_source_format[:type] || "text"
50
+ validate_credential_source
51
+ end
52
+
53
+ # Implementation of BaseCredentials retrieve_subject_token!
54
+ def retrieve_subject_token!
55
+ content, resource_name = token_data
56
+ if @credential_source_format_type == "text"
57
+ token = content
58
+ else
59
+ begin
60
+ response_data = MultiJson.load content, symbolize_keys: true
61
+ token = response_data[@credential_source_field_name.to_sym]
62
+ rescue StandardError
63
+ raise "Unable to parse subject_token from JSON resource #{resource_name} " \
64
+ "using key #{@credential_source_field_name}"
65
+ end
66
+ end
67
+ raise "Missing subject_token in the credential_source file/response." unless token
68
+ token
69
+ end
70
+
71
+ private
72
+
73
+ def validate_credential_source
74
+ # `environment_id` is only supported in AWS or dedicated future external account credentials.
75
+ unless @credential_source[:environment_id].nil?
76
+ raise "Invalid Identity Pool credential_source field 'environment_id'"
77
+ end
78
+ unless ["json", "text"].include? @credential_source_format_type
79
+ raise "Invalid credential_source format #{@credential_source_format_type}"
80
+ end
81
+ # for JSON types, get the required subject_token field name.
82
+ @credential_source_field_name = @credential_source_format[:subject_token_field_name]
83
+ if @credential_source_format_type == "json" && @credential_source_field_name.nil?
84
+ raise "Missing subject_token_field_name for JSON credential_source format"
85
+ end
86
+ # check file or url must be fulfilled and mutually exclusiveness.
87
+ if @credential_source_file && @credential_source_url
88
+ raise "Ambiguous credential_source. 'file' is mutually exclusive with 'url'."
89
+ end
90
+ return unless (@credential_source_file || @credential_source_url).nil?
91
+ raise "Missing credential_source. A 'file' or 'url' must be provided."
92
+ end
93
+
94
+ def token_data
95
+ @credential_source_file.nil? ? url_data : file_data
96
+ end
97
+
98
+ def file_data
99
+ raise "File #{@credential_source_file} was not found." unless File.exist? @credential_source_file
100
+ content = File.read @credential_source_file, encoding: "utf-8"
101
+ [content, @credential_source_file]
102
+ end
103
+
104
+ def url_data
105
+ begin
106
+ response = connection.get @credential_source_url do |req|
107
+ req.headers.merge! @credential_source_headers
108
+ end
109
+ rescue Faraday::Error => e
110
+ raise "Error retrieving from credential url: #{e}"
111
+ end
112
+ raise "Unable to retrieve Identity Pool subject token #{response.body}" unless response.success?
113
+ [response.body, @credential_source_url]
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,156 @@
1
+ # Copyright 2023 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 "open3"
16
+ require "time"
17
+ require "googleauth/external_account/base_credentials"
18
+ require "googleauth/external_account/external_account_utils"
19
+
20
+ module Google
21
+ # Module Auth provides classes that provide Google-specific authorization used to access Google APIs.
22
+ module Auth
23
+ module ExternalAccount
24
+ # This module handles the retrieval of credentials from Google Cloud by utilizing the any 3PI
25
+ # provider then exchanging the credentials for a short-lived Google Cloud access token.
26
+ class PluggableAuthCredentials
27
+ # constant for pluggable auth enablement in environment variable.
28
+ ENABLE_PLUGGABLE_ENV = "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES".freeze
29
+ EXECUTABLE_SUPPORTED_MAX_VERSION = 1
30
+ EXECUTABLE_TIMEOUT_MILLIS_DEFAULT = 30 * 1000
31
+ EXECUTABLE_TIMEOUT_MILLIS_LOWER_BOUND = 5 * 1000
32
+ EXECUTABLE_TIMEOUT_MILLIS_UPPER_BOUND = 120 * 1000
33
+ ID_TOKEN_TYPE = ["urn:ietf:params:oauth:token-type:jwt", "urn:ietf:params:oauth:token-type:id_token"].freeze
34
+
35
+ include Google::Auth::ExternalAccount::BaseCredentials
36
+ include Google::Auth::ExternalAccount::ExternalAccountUtils
37
+ extend CredentialsLoader
38
+
39
+ # Will always be nil, but method still gets used.
40
+ attr_reader :client_id
41
+
42
+ # Initialize from options map.
43
+ #
44
+ # @param [string] audience
45
+ # @param [hash{symbol => value}] credential_source
46
+ # credential_source is a hash that contains either source file or url.
47
+ # credential_source_format is either text or json. To define how we parse the credential response.
48
+ #
49
+ def initialize options = {}
50
+ base_setup options
51
+
52
+ @audience = options[:audience]
53
+ @credential_source = options[:credential_source] || {}
54
+ @credential_source_executable = @credential_source[:executable]
55
+ raise "Missing excutable source. An 'executable' must be provided" if @credential_source_executable.nil?
56
+ @credential_source_executable_command = @credential_source_executable[:command]
57
+ if @credential_source_executable_command.nil?
58
+ raise "Missing command field. Executable command must be provided."
59
+ end
60
+ @credential_source_executable_timeout_millis = @credential_source_executable[:timeout_millis] ||
61
+ EXECUTABLE_TIMEOUT_MILLIS_DEFAULT
62
+ if @credential_source_executable_timeout_millis < EXECUTABLE_TIMEOUT_MILLIS_LOWER_BOUND ||
63
+ @credential_source_executable_timeout_millis > EXECUTABLE_TIMEOUT_MILLIS_UPPER_BOUND
64
+ raise "Timeout must be between 5 and 120 seconds."
65
+ end
66
+ @credential_source_executable_output_file = @credential_source_executable[:output_file]
67
+ end
68
+
69
+ def retrieve_subject_token!
70
+ unless ENV[ENABLE_PLUGGABLE_ENV] == "1"
71
+ raise "Executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') " \
72
+ "to run."
73
+ end
74
+ # check output file first
75
+ subject_token = load_subject_token_from_output_file
76
+ return subject_token unless subject_token.nil?
77
+ # environment variable injection
78
+ env = inject_environment_variables
79
+ output = subprocess_with_timeout env, @credential_source_executable_command,
80
+ @credential_source_executable_timeout_millis
81
+ response = MultiJson.load output, symbolize_keys: true
82
+ parse_subject_token response
83
+ end
84
+
85
+ private
86
+
87
+ def load_subject_token_from_output_file
88
+ return nil if @credential_source_executable_output_file.nil?
89
+ return nil unless File.exist? @credential_source_executable_output_file
90
+ begin
91
+ content = File.read @credential_source_executable_output_file, encoding: "utf-8"
92
+ response = MultiJson.load content, symbolize_keys: true
93
+ rescue StandardError
94
+ return nil
95
+ end
96
+ begin
97
+ subject_token = parse_subject_token response
98
+ rescue StandardError => e
99
+ return nil if e.message.match(/The token returned by the executable is expired/)
100
+ raise e
101
+ end
102
+ subject_token
103
+ end
104
+
105
+ def parse_subject_token response
106
+ validate_response_schema response
107
+ unless response[:success]
108
+ if response[:code].nil? || response[:message].nil?
109
+ raise "Error code and message fields are required in the response."
110
+ end
111
+ raise "Executable returned unsuccessful response: code: #{response[:code]}, message: #{response[:message]}."
112
+ end
113
+ if response[:expiration_time] && response[:expiration_time] < Time.now.to_i
114
+ raise "The token returned by the executable is expired."
115
+ end
116
+ raise "The executable response is missing the token_type field." if response[:token_type].nil?
117
+ return response[:id_token] if ID_TOKEN_TYPE.include? response[:token_type]
118
+ return response[:saml_response] if response[:token_type] == "urn:ietf:params:oauth:token-type:saml2"
119
+ raise "Executable returned unsupported token type."
120
+ end
121
+
122
+ def validate_response_schema response
123
+ raise "The executable response is missing the version field." if response[:version].nil?
124
+ if response[:version] > EXECUTABLE_SUPPORTED_MAX_VERSION
125
+ raise "Executable returned unsupported version #{response[:version]}."
126
+ end
127
+ raise "The executable response is missing the success field." if response[:success].nil?
128
+ end
129
+
130
+ def inject_environment_variables
131
+ env = ENV.to_h
132
+ env["GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE"] = @audience
133
+ env["GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"] = @subject_token_type
134
+ env["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = "0" # only non-interactive mode we support.
135
+ unless @service_account_impersonation_url.nil?
136
+ env["GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL"] = service_account_email
137
+ end
138
+ unless @credential_source_executable_output_file.nil?
139
+ env["GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE"] = @credential_source_executable_output_file
140
+ end
141
+ env
142
+ end
143
+
144
+ def subprocess_with_timeout environment_vars, command, timeout_seconds
145
+ Timeout.timeout timeout_seconds do
146
+ output, error, status = Open3.capture3 environment_vars, command
147
+ unless status.success?
148
+ raise "Executable exited with non-zero return code #{status.exitstatus}. Error: #{output}, #{error}"
149
+ end
150
+ output
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -16,6 +16,8 @@ require "time"
16
16
  require "uri"
17
17
  require "googleauth/credentials_loader"
18
18
  require "googleauth/external_account/aws_credentials"
19
+ require "googleauth/external_account/identity_pool_credentials"
20
+ require "googleauth/external_account/pluggable_credentials"
19
21
 
20
22
  module Google
21
23
  # Module Auth provides classes that provide Google-specific authorization
@@ -28,7 +30,8 @@ module Google
28
30
  class Credentials
29
31
  # The subject token type used for AWS external_account credentials.
30
32
  AWS_SUBJECT_TOKEN_TYPE = "urn:ietf:params:aws:token-type:aws4_request".freeze
31
- AWS_SUBJECT_TOKEN_INVALID = "aws is the only currently supported external account type".freeze
33
+ MISSING_CREDENTIAL_SOURCE = "missing credential source for external account".freeze
34
+ INVALID_EXTERNAL_ACCOUNT_TYPE = "credential source is not supported external account type".freeze
32
35
 
33
36
  # Create a ExternalAccount::Credentials
34
37
  #
@@ -40,30 +43,50 @@ module Google
40
43
  raise "A json file is required for external account credentials." unless json_key_io
41
44
  user_creds = read_json_key json_key_io
42
45
 
43
- # TODO: check for other External Account Credential types. Currently only AWS is supported.
44
- raise AWS_SUBJECT_TOKEN_INVALID unless user_creds["subject_token_type"] == AWS_SUBJECT_TOKEN_TYPE
46
+ # AWS credentials is determined by aws subject token type
47
+ return make_aws_credentials user_creds, scope if user_creds[:subject_token_type] == AWS_SUBJECT_TOKEN_TYPE
45
48
 
46
- Google::Auth::ExternalAccount::AwsCredentials.new(
47
- audience: user_creds["audience"],
48
- scope: scope,
49
- subject_token_type: user_creds["subject_token_type"],
50
- token_url: user_creds["token_url"],
51
- credential_source: user_creds["credential_source"],
52
- service_account_impersonation_url: user_creds["service_account_impersonation_url"]
53
- )
49
+ raise MISSING_CREDENTIAL_SOURCE if user_creds[:credential_source].nil?
50
+ user_creds[:scope] = scope
51
+ make_external_account_credentials user_creds
54
52
  end
55
53
 
56
54
  # Reads the required fields from the JSON.
57
55
  def self.read_json_key json_key_io
58
- json_key = MultiJson.load json_key_io.read
56
+ json_key = MultiJson.load json_key_io.read, symbolize_keys: true
59
57
  wanted = [
60
- "audience", "subject_token_type", "token_url", "credential_source"
58
+ :audience, :subject_token_type, :token_url, :credential_source
61
59
  ]
62
60
  wanted.each do |key|
63
61
  raise "the json is missing the #{key} field" unless json_key.key? key
64
62
  end
65
63
  json_key
66
64
  end
65
+
66
+ class << self
67
+ private
68
+
69
+ def make_aws_credentials user_creds, scope
70
+ Google::Auth::ExternalAccount::AwsCredentials.new(
71
+ audience: user_creds[:audience],
72
+ scope: scope,
73
+ subject_token_type: user_creds[:subject_token_type],
74
+ token_url: user_creds[:token_url],
75
+ credential_source: user_creds[:credential_source],
76
+ service_account_impersonation_url: user_creds[:service_account_impersonation_url]
77
+ )
78
+ end
79
+
80
+ def make_external_account_credentials user_creds
81
+ unless user_creds[:credential_source][:file].nil? && user_creds[:credential_source][:url].nil?
82
+ return Google::Auth::ExternalAccount::IdentityPoolCredentials.new user_creds
83
+ end
84
+ unless user_creds[:credential_source][:executable].nil?
85
+ return Google::Auth::ExternalAccount::PluggableAuthCredentials.new user_creds
86
+ end
87
+ raise INVALID_EXTERNAL_ACCOUNT_TYPE
88
+ end
89
+ end
67
90
  end
68
91
  end
69
92
  end
@@ -153,7 +153,7 @@ module Google
153
153
  # one of the provided values, or the verification will fail with
154
154
  # {Google::Auth::IDToken::AuthorizedPartyMismatchError}. If `nil`
155
155
  # (the default), no azp checking is performed.
156
- # @param aud [String,Array<String>,nil] The expected audience. At least
156
+ # @param iss [String,Array<String>,nil] The expected issuer. At least
157
157
  # one `iss` field in the token must match at least one of the
158
158
  # provided issuers, or the verification will fail with
159
159
  # {Google::Auth::IDToken::IssuerMismatchError}. If `nil`, no issuer
@@ -191,7 +191,7 @@ module Google
191
191
  # one of the provided values, or the verification will fail with
192
192
  # {Google::Auth::IDToken::AuthorizedPartyMismatchError}. If `nil`
193
193
  # (the default), no azp checking is performed.
194
- # @param aud [String,Array<String>,nil] The expected audience. At least
194
+ # @param iss [String,Array<String>,nil] The expected issuer. At least
195
195
  # one `iss` field in the token must match at least one of the
196
196
  # provided issuers, or the verification will fail with
197
197
  # {Google::Auth::IDToken::IssuerMismatchError}. If `nil`, no issuer
@@ -76,6 +76,20 @@ module Google
76
76
  # TODO: Add the ability to add authentication to the headers
77
77
  headers = URLENCODED_HEADERS.dup.merge(options[:additional_headers] || {})
78
78
 
79
+ request_body = make_request options
80
+
81
+ response = connection.post @token_exchange_endpoint, URI.encode_www_form(request_body), headers
82
+
83
+ if response.status != 200
84
+ raise "Token exchange failed with status #{response.status}"
85
+ end
86
+
87
+ MultiJson.load response.body
88
+ end
89
+
90
+ private
91
+
92
+ def make_request options = {}
79
93
  request_body = {
80
94
  grant_type: options[:grant_type],
81
95
  audience: options[:audience],
@@ -84,14 +98,10 @@ module Google
84
98
  subject_token: options[:subject_token],
85
99
  subject_token_type: options[:subject_token_type]
86
100
  }
87
-
88
- response = connection.post @token_exchange_endpoint, URI.encode_www_form(request_body), headers
89
-
90
- if response.status != 200
91
- raise "Token exchange failed with status #{response.status}"
101
+ unless options[:additional_options].nil?
102
+ request_body[:options] = CGI.escape MultiJson.dump(options[:additional_options], symbolize_name: true)
92
103
  end
93
-
94
- MultiJson.load response.body
104
+ request_body
95
105
  end
96
106
  end
97
107
  end
@@ -16,6 +16,6 @@ module Google
16
16
  # Module Auth provides classes that provide Google-specific authorization
17
17
  # used to access Google APIs.
18
18
  module Auth
19
- VERSION = "1.5.2".freeze
19
+ VERSION = "1.7.0".freeze
20
20
  end
21
21
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: googleauth
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.2
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tim Emiola
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-04-14 00:00:00.000000000 Z
11
+ date: 2023-07-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -143,6 +143,9 @@ files:
143
143
  - lib/googleauth/external_account.rb
144
144
  - lib/googleauth/external_account/aws_credentials.rb
145
145
  - lib/googleauth/external_account/base_credentials.rb
146
+ - lib/googleauth/external_account/external_account_utils.rb
147
+ - lib/googleauth/external_account/identity_pool_credentials.rb
148
+ - lib/googleauth/external_account/pluggable_credentials.rb
146
149
  - lib/googleauth/helpers/connection.rb
147
150
  - lib/googleauth/iam.rb
148
151
  - lib/googleauth/id_tokens.rb