googleauth 1.5.2 → 1.7.0

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