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 +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
|