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 +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +3 -1
- data/lib/googleauth/external_account/aws_credentials.rb +7 -5
- data/lib/googleauth/external_account/base_credentials.rb +25 -67
- data/lib/googleauth/external_account/external_account_utils.rb +103 -0
- data/lib/googleauth/external_account/identity_pool_credentials.rb +118 -0
- data/lib/googleauth/external_account/pluggable_credentials.rb +156 -0
- data/lib/googleauth/external_account.rb +36 -13
- data/lib/googleauth/id_tokens.rb +2 -2
- data/lib/googleauth/oauth2/sts_client.rb +17 -7
- data/lib/googleauth/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5437d56e0c86ce235d37a202af1a28219a9caeb0bc0f8abac5540cc1d73edf28
|
4
|
+
data.tar.gz: f56369065e2abc56fb51abccc5003264f8c9ef3e745c202e06e5cb4c6b083d84
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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[
|
41
|
-
@region_url = @credential_source[
|
42
|
-
@credential_verification_url = @credential_source[
|
43
|
-
@regional_cred_verification_url = @credential_source[
|
44
|
-
@imdsv2_session_token_url = @credential_source[
|
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
|
-
#
|
79
|
-
#
|
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
|
-
|
83
|
-
|
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
|
-
#
|
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
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
44
|
-
|
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
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
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
|
data/lib/googleauth/id_tokens.rb
CHANGED
@@ -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
|
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
|
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
|
-
|
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
|
data/lib/googleauth/version.rb
CHANGED
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.
|
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-
|
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
|