googleauth 1.3.0 → 1.11.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 +93 -0
- data/README.md +43 -2
- data/lib/googleauth/application_default.rb +5 -9
- data/lib/googleauth/base_client.rb +80 -0
- data/lib/googleauth/client_id.rb +25 -8
- data/lib/googleauth/compute_engine.rb +65 -35
- data/lib/googleauth/credentials.rb +32 -30
- data/lib/googleauth/credentials_loader.rb +5 -13
- data/lib/googleauth/default_credentials.rb +5 -2
- data/lib/googleauth/external_account/aws_credentials.rb +378 -0
- data/lib/googleauth/external_account/base_credentials.rb +159 -0
- 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 +94 -0
- data/lib/googleauth/helpers/connection.rb +35 -0
- data/lib/googleauth/id_tokens.rb +2 -2
- data/lib/googleauth/json_key_reader.rb +2 -1
- data/lib/googleauth/oauth2/sts_client.rb +109 -0
- data/lib/googleauth/scope_util.rb +35 -2
- data/lib/googleauth/service_account.rb +25 -11
- data/lib/googleauth/signet.rb +14 -38
- data/lib/googleauth/user_authorizer.rb +66 -9
- data/lib/googleauth/user_refresh.rb +4 -2
- data/lib/googleauth/version.rb +1 -1
- data/lib/googleauth/web_user_authorizer.rb +19 -8
- metadata +29 -20
@@ -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
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# Copyright 2022 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 "uri"
|
17
|
+
require "googleauth/credentials_loader"
|
18
|
+
require "googleauth/external_account/aws_credentials"
|
19
|
+
require "googleauth/external_account/identity_pool_credentials"
|
20
|
+
require "googleauth/external_account/pluggable_credentials"
|
21
|
+
|
22
|
+
module Google
|
23
|
+
# Module Auth provides classes that provide Google-specific authorization
|
24
|
+
# used to access Google APIs.
|
25
|
+
module Auth
|
26
|
+
# Authenticates requests using External Account credentials, such
|
27
|
+
# as those provided by the AWS provider.
|
28
|
+
module ExternalAccount
|
29
|
+
# Provides an entrypoint for all Exernal Account credential classes.
|
30
|
+
class Credentials
|
31
|
+
# The subject token type used for AWS external_account credentials.
|
32
|
+
AWS_SUBJECT_TOKEN_TYPE = "urn:ietf:params:aws:token-type:aws4_request".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
|
35
|
+
|
36
|
+
# Create a ExternalAccount::Credentials
|
37
|
+
#
|
38
|
+
# @param json_key_io [IO] an IO from which the JSON key can be read
|
39
|
+
# @param scope [String,Array,nil] the scope(s) to access
|
40
|
+
def self.make_creds options = {}
|
41
|
+
json_key_io, scope = options.values_at :json_key_io, :scope
|
42
|
+
|
43
|
+
raise "A json file is required for external account credentials." unless json_key_io
|
44
|
+
user_creds = read_json_key json_key_io
|
45
|
+
|
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
|
48
|
+
|
49
|
+
raise MISSING_CREDENTIAL_SOURCE if user_creds[:credential_source].nil?
|
50
|
+
user_creds[:scope] = scope
|
51
|
+
make_external_account_credentials user_creds
|
52
|
+
end
|
53
|
+
|
54
|
+
# Reads the required fields from the JSON.
|
55
|
+
def self.read_json_key json_key_io
|
56
|
+
json_key = MultiJson.load json_key_io.read, symbolize_keys: true
|
57
|
+
wanted = [
|
58
|
+
:audience, :subject_token_type, :token_url, :credential_source
|
59
|
+
]
|
60
|
+
wanted.each do |key|
|
61
|
+
raise "the json is missing the #{key} field" unless json_key.key? key
|
62
|
+
end
|
63
|
+
json_key
|
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
|
+
universe_domain: user_creds[:universe_domain]
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
def make_external_account_credentials user_creds
|
82
|
+
unless user_creds[:credential_source][:file].nil? && user_creds[:credential_source][:url].nil?
|
83
|
+
return Google::Auth::ExternalAccount::IdentityPoolCredentials.new user_creds
|
84
|
+
end
|
85
|
+
unless user_creds[:credential_source][:executable].nil?
|
86
|
+
return Google::Auth::ExternalAccount::PluggableAuthCredentials.new user_creds
|
87
|
+
end
|
88
|
+
raise INVALID_EXTERNAL_ACCOUNT_TYPE
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,35 @@
|
|
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 "faraday"
|
16
|
+
|
17
|
+
module Google
|
18
|
+
# Module Auth provides classes that provide Google-specific authorization
|
19
|
+
# used to access Google APIs.
|
20
|
+
module Auth
|
21
|
+
# Helpers provides utility methods for Google::Auth.
|
22
|
+
module Helpers
|
23
|
+
# Connection provides a Faraday connection for use with Google::Auth.
|
24
|
+
module Connection
|
25
|
+
module_function
|
26
|
+
|
27
|
+
attr_accessor :default_connection
|
28
|
+
|
29
|
+
def connection
|
30
|
+
@default_connection || Faraday.default_connection
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
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
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# Copyright 2023 Google LLC
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require "googleauth/helpers/connection"
|
16
|
+
|
17
|
+
module Google
|
18
|
+
module Auth
|
19
|
+
module OAuth2
|
20
|
+
# OAuth 2.0 Token Exchange Spec.
|
21
|
+
# This module defines a token exchange utility based on the
|
22
|
+
# [OAuth 2.0 Token Exchange](https://tools.ietf.org/html/rfc8693) spec. This will be mainly
|
23
|
+
# used to exchange external credentials for GCP access tokens in workload identity pools to
|
24
|
+
# access Google APIs.
|
25
|
+
# The implementation will support various types of client authentication as allowed in the spec.
|
26
|
+
#
|
27
|
+
# A deviation on the spec will be for additional Google specific options that cannot be easily
|
28
|
+
# mapped to parameters defined in the RFC.
|
29
|
+
# The returned dictionary response will be based on the [rfc8693 section 2.2.1]
|
30
|
+
# (https://tools.ietf.org/html/rfc8693#section-2.2.1) spec JSON response.
|
31
|
+
#
|
32
|
+
class STSClient
|
33
|
+
include Helpers::Connection
|
34
|
+
|
35
|
+
URLENCODED_HEADERS = { "Content-Type": "application/x-www-form-urlencoded" }.freeze
|
36
|
+
|
37
|
+
# Create a new instance of the STSClient.
|
38
|
+
#
|
39
|
+
# @param [String] token_exchange_endpoint
|
40
|
+
# The token exchange endpoint.
|
41
|
+
def initialize options = {}
|
42
|
+
raise "Token exchange endpoint can not be nil" if options[:token_exchange_endpoint].nil?
|
43
|
+
self.default_connection = options[:connection]
|
44
|
+
@token_exchange_endpoint = options[:token_exchange_endpoint]
|
45
|
+
end
|
46
|
+
|
47
|
+
# Exchanges the provided token for another type of token based on the
|
48
|
+
# rfc8693 spec
|
49
|
+
#
|
50
|
+
# @param [Faraday instance] connection
|
51
|
+
# A callable faraday instance used to make HTTP requests.
|
52
|
+
# @param [String] grant_type
|
53
|
+
# The OAuth 2.0 token exchange grant type.
|
54
|
+
# @param [String] subject_token
|
55
|
+
# The OAuth 2.0 token exchange subject token.
|
56
|
+
# @param [String] subject_token_type
|
57
|
+
# The OAuth 2.0 token exchange subject token type.
|
58
|
+
# @param [String] resource
|
59
|
+
# The optional OAuth 2.0 token exchange resource field.
|
60
|
+
# @param [String] audience
|
61
|
+
# The optional OAuth 2.0 token exchange audience field.
|
62
|
+
# @param [Array<String>] scopes
|
63
|
+
# The optional list of scopes to use.
|
64
|
+
# @param [String] requested_token_type
|
65
|
+
# The optional OAuth 2.0 token exchange requested token type.
|
66
|
+
# @param additional_headers (Hash<String,String>):
|
67
|
+
# The optional additional headers to pass to the token exchange endpoint.
|
68
|
+
#
|
69
|
+
# @return [Hash] A hash containing the token exchange response.
|
70
|
+
def exchange_token options = {}
|
71
|
+
missing_required_opts = [:grant_type, :subject_token, :subject_token_type] - options.keys
|
72
|
+
unless missing_required_opts.empty?
|
73
|
+
raise ArgumentError, "Missing required options: #{missing_required_opts.join ', '}"
|
74
|
+
end
|
75
|
+
|
76
|
+
# TODO: Add the ability to add authentication to the headers
|
77
|
+
headers = URLENCODED_HEADERS.dup.merge(options[:additional_headers] || {})
|
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 = {}
|
93
|
+
request_body = {
|
94
|
+
grant_type: options[:grant_type],
|
95
|
+
audience: options[:audience],
|
96
|
+
scope: Array(options[:scopes])&.join(" ") || [],
|
97
|
+
requested_token_type: options[:requested_token_type],
|
98
|
+
subject_token: options[:subject_token],
|
99
|
+
subject_token_type: options[:subject_token_type]
|
100
|
+
}
|
101
|
+
unless options[:additional_options].nil?
|
102
|
+
request_body[:options] = CGI.escape MultiJson.dump(options[:additional_options], symbolize_name: true)
|
103
|
+
end
|
104
|
+
request_body
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|