googleauth 0.1.0 → 0.16.2
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 +5 -5
- data/.github/CODEOWNERS +7 -0
- data/.github/CONTRIBUTING.md +74 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +36 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +21 -0
- data/.github/ISSUE_TEMPLATE/support_request.md +7 -0
- data/.github/renovate.json +6 -0
- data/.github/sync-repo-settings.yaml +18 -0
- data/.github/workflows/ci.yml +55 -0
- data/.github/workflows/release-please.yml +39 -0
- data/.gitignore +3 -0
- data/.kokoro/populate-secrets.sh +76 -0
- data/.kokoro/release.cfg +52 -0
- data/.kokoro/release.sh +18 -0
- data/.kokoro/trampoline_v2.sh +489 -0
- data/.repo-metadata.json +5 -0
- data/.rubocop.yml +17 -0
- data/.toys/.toys.rb +45 -0
- data/.toys/ci.rb +43 -0
- data/.toys/kokoro/.toys.rb +66 -0
- data/.toys/kokoro/publish-docs.rb +67 -0
- data/.toys/kokoro/publish-gem.rb +53 -0
- data/.toys/linkinator.rb +43 -0
- data/.trampolinerc +48 -0
- data/CHANGELOG.md +199 -0
- data/CODE_OF_CONDUCT.md +43 -0
- data/Gemfile +22 -1
- data/{COPYING → LICENSE} +0 -0
- data/README.md +140 -17
- data/googleauth.gemspec +28 -28
- data/integration/helper.rb +31 -0
- data/integration/id_tokens/key_source_test.rb +74 -0
- data/lib/googleauth.rb +7 -37
- data/lib/googleauth/application_default.rb +81 -0
- data/lib/googleauth/client_id.rb +104 -0
- data/lib/googleauth/compute_engine.rb +73 -26
- data/lib/googleauth/credentials.rb +561 -0
- data/lib/googleauth/credentials_loader.rb +207 -0
- data/lib/googleauth/default_credentials.rb +93 -0
- data/lib/googleauth/iam.rb +75 -0
- data/lib/googleauth/id_tokens.rb +233 -0
- data/lib/googleauth/id_tokens/errors.rb +71 -0
- data/lib/googleauth/id_tokens/key_sources.rb +396 -0
- data/lib/googleauth/id_tokens/verifier.rb +142 -0
- data/lib/googleauth/json_key_reader.rb +50 -0
- data/lib/googleauth/scope_util.rb +61 -0
- data/lib/googleauth/service_account.rb +177 -67
- data/lib/googleauth/signet.rb +69 -8
- data/lib/googleauth/stores/file_token_store.rb +65 -0
- data/lib/googleauth/stores/redis_token_store.rb +96 -0
- data/lib/googleauth/token_store.rb +69 -0
- data/lib/googleauth/user_authorizer.rb +285 -0
- data/lib/googleauth/user_refresh.rb +129 -0
- data/lib/googleauth/version.rb +1 -1
- data/lib/googleauth/web_user_authorizer.rb +295 -0
- data/spec/googleauth/apply_auth_examples.rb +96 -94
- data/spec/googleauth/client_id_spec.rb +160 -0
- data/spec/googleauth/compute_engine_spec.rb +125 -55
- data/spec/googleauth/credentials_spec.rb +600 -0
- data/spec/googleauth/get_application_default_spec.rb +232 -80
- data/spec/googleauth/iam_spec.rb +80 -0
- data/spec/googleauth/scope_util_spec.rb +77 -0
- data/spec/googleauth/service_account_spec.rb +422 -68
- data/spec/googleauth/signet_spec.rb +101 -25
- data/spec/googleauth/stores/file_token_store_spec.rb +57 -0
- data/spec/googleauth/stores/redis_token_store_spec.rb +50 -0
- data/spec/googleauth/stores/store_examples.rb +58 -0
- data/spec/googleauth/user_authorizer_spec.rb +343 -0
- data/spec/googleauth/user_refresh_spec.rb +359 -0
- data/spec/googleauth/web_user_authorizer_spec.rb +172 -0
- data/spec/spec_helper.rb +51 -10
- data/test/helper.rb +33 -0
- data/test/id_tokens/key_sources_test.rb +240 -0
- data/test/id_tokens/verifier_test.rb +269 -0
- metadata +114 -75
- data/.travis.yml +0 -18
- data/CONTRIBUTING.md +0 -32
- data/Rakefile +0 -15
@@ -0,0 +1,142 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2020 Google LLC
|
4
|
+
#
|
5
|
+
# Redistribution and use in source and binary forms, with or without
|
6
|
+
# modification, are permitted provided that the following conditions are
|
7
|
+
# met:
|
8
|
+
#
|
9
|
+
# * Redistributions of source code must retain the above copyright
|
10
|
+
# notice, this list of conditions and the following disclaimer.
|
11
|
+
# * Redistributions in binary form must reproduce the above
|
12
|
+
# copyright notice, this list of conditions and the following disclaimer
|
13
|
+
# in the documentation and/or other materials provided with the
|
14
|
+
# distribution.
|
15
|
+
# * Neither the name of Google Inc. nor the names of its
|
16
|
+
# contributors may be used to endorse or promote products derived from
|
17
|
+
# this software without specific prior written permission.
|
18
|
+
#
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
20
|
+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
21
|
+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
22
|
+
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
23
|
+
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
24
|
+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
25
|
+
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
26
|
+
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
27
|
+
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
28
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
29
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
30
|
+
|
31
|
+
require "jwt"
|
32
|
+
|
33
|
+
module Google
|
34
|
+
module Auth
|
35
|
+
module IDTokens
|
36
|
+
##
|
37
|
+
# An object that can verify ID tokens.
|
38
|
+
#
|
39
|
+
# A verifier maintains a set of default settings, including the key
|
40
|
+
# source and fields to verify. However, individual verification calls can
|
41
|
+
# override any of these settings.
|
42
|
+
#
|
43
|
+
class Verifier
|
44
|
+
##
|
45
|
+
# Create a verifier.
|
46
|
+
#
|
47
|
+
# @param key_source [key source] The default key source to use. All
|
48
|
+
# verification calls must have a key source, so if no default key
|
49
|
+
# source is provided here, then calls to {#verify} _must_ provide
|
50
|
+
# a key source.
|
51
|
+
# @param aud [String,nil] The default audience (`aud`) check, or `nil`
|
52
|
+
# for no check.
|
53
|
+
# @param azp [String,nil] The default authorized party (`azp`) check,
|
54
|
+
# or `nil` for no check.
|
55
|
+
# @param iss [String,nil] The default issuer (`iss`) check, or `nil`
|
56
|
+
# for no check.
|
57
|
+
#
|
58
|
+
def initialize key_source: nil,
|
59
|
+
aud: nil,
|
60
|
+
azp: nil,
|
61
|
+
iss: nil
|
62
|
+
@key_source = key_source
|
63
|
+
@aud = aud
|
64
|
+
@azp = azp
|
65
|
+
@iss = iss
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# Verify the given token.
|
70
|
+
#
|
71
|
+
# @param token [String] the ID token to verify.
|
72
|
+
# @param key_source [key source] If given, override the key source.
|
73
|
+
# @param aud [String,nil] If given, override the `aud` check.
|
74
|
+
# @param azp [String,nil] If given, override the `azp` check.
|
75
|
+
# @param iss [String,nil] If given, override the `iss` check.
|
76
|
+
#
|
77
|
+
# @return [Hash] the decoded payload, if verification succeeded.
|
78
|
+
# @raise [KeySourceError] if the key source failed to obtain public keys
|
79
|
+
# @raise [VerificationError] if the token verification failed.
|
80
|
+
# Additional data may be available in the error subclass and message.
|
81
|
+
#
|
82
|
+
def verify token,
|
83
|
+
key_source: :default,
|
84
|
+
aud: :default,
|
85
|
+
azp: :default,
|
86
|
+
iss: :default
|
87
|
+
key_source = @key_source if key_source == :default
|
88
|
+
aud = @aud if aud == :default
|
89
|
+
azp = @azp if azp == :default
|
90
|
+
iss = @iss if iss == :default
|
91
|
+
|
92
|
+
raise KeySourceError, "No key sources" unless key_source
|
93
|
+
keys = key_source.current_keys
|
94
|
+
payload = decode_token token, keys, aud, azp, iss
|
95
|
+
unless payload
|
96
|
+
keys = key_source.refresh_keys
|
97
|
+
payload = decode_token token, keys, aud, azp, iss
|
98
|
+
end
|
99
|
+
raise SignatureError, "Token not verified as issued by Google" unless payload
|
100
|
+
payload
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def decode_token token, keys, aud, azp, iss
|
106
|
+
payload = nil
|
107
|
+
keys.find do |key|
|
108
|
+
options = { algorithms: key.algorithm }
|
109
|
+
decoded_token = JWT.decode token, key.key, true, options
|
110
|
+
payload = decoded_token.first
|
111
|
+
rescue JWT::ExpiredSignature
|
112
|
+
raise ExpiredTokenError, "Token signature is expired"
|
113
|
+
rescue JWT::DecodeError
|
114
|
+
nil # Try the next key
|
115
|
+
end
|
116
|
+
|
117
|
+
normalize_and_verify_payload payload, aud, azp, iss
|
118
|
+
end
|
119
|
+
|
120
|
+
def normalize_and_verify_payload payload, aud, azp, iss
|
121
|
+
return nil unless payload
|
122
|
+
|
123
|
+
# Map the legacy "cid" claim to the canonical "azp"
|
124
|
+
payload["azp"] ||= payload["cid"] if payload.key? "cid"
|
125
|
+
|
126
|
+
# Payload content validation
|
127
|
+
if aud && (Array(aud) & Array(payload["aud"])).empty?
|
128
|
+
raise AudienceMismatchError, "Token aud mismatch: #{payload['aud']}"
|
129
|
+
end
|
130
|
+
if azp && (Array(azp) & Array(payload["azp"])).empty?
|
131
|
+
raise AuthorizedPartyMismatchError, "Token azp mismatch: #{payload['azp']}"
|
132
|
+
end
|
133
|
+
if iss && (Array(iss) & Array(payload["iss"])).empty?
|
134
|
+
raise IssuerMismatchError, "Token iss mismatch: #{payload['iss']}"
|
135
|
+
end
|
136
|
+
|
137
|
+
payload
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# Copyright 2015, Google Inc.
|
2
|
+
# All rights reserved.
|
3
|
+
#
|
4
|
+
# Redistribution and use in source and binary forms, with or without
|
5
|
+
# modification, are permitted provided that the following conditions are
|
6
|
+
# met:
|
7
|
+
#
|
8
|
+
# * Redistributions of source code must retain the above copyright
|
9
|
+
# notice, this list of conditions and the following disclaimer.
|
10
|
+
# * Redistributions in binary form must reproduce the above
|
11
|
+
# copyright notice, this list of conditions and the following disclaimer
|
12
|
+
# in the documentation and/or other materials provided with the
|
13
|
+
# distribution.
|
14
|
+
# * Neither the name of Google Inc. nor the names of its
|
15
|
+
# contributors may be used to endorse or promote products derived from
|
16
|
+
# this software without specific prior written permission.
|
17
|
+
#
|
18
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
19
|
+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
20
|
+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
21
|
+
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
22
|
+
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
23
|
+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
24
|
+
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
25
|
+
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
26
|
+
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
27
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
28
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
29
|
+
|
30
|
+
module Google
|
31
|
+
# Module Auth provides classes that provide Google-specific authorization
|
32
|
+
# used to access Google APIs.
|
33
|
+
module Auth
|
34
|
+
# JsonKeyReader contains the behaviour used to read private key and
|
35
|
+
# client email fields from the service account
|
36
|
+
module JsonKeyReader
|
37
|
+
def read_json_key json_key_io
|
38
|
+
json_key = MultiJson.load json_key_io.read
|
39
|
+
raise "missing client_email" unless json_key.key? "client_email"
|
40
|
+
raise "missing private_key" unless json_key.key? "private_key"
|
41
|
+
[
|
42
|
+
json_key["private_key"],
|
43
|
+
json_key["client_email"],
|
44
|
+
json_key["project_id"],
|
45
|
+
json_key["quota_project_id"]
|
46
|
+
]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# Copyright 2015, Google Inc.
|
2
|
+
# All rights reserved.
|
3
|
+
#
|
4
|
+
# Redistribution and use in source and binary forms, with or without
|
5
|
+
# modification, are permitted provided that the following conditions are
|
6
|
+
# met:
|
7
|
+
#
|
8
|
+
# * Redistributions of source code must retain the above copyright
|
9
|
+
# notice, this list of conditions and the following disclaimer.
|
10
|
+
# * Redistributions in binary form must reproduce the above
|
11
|
+
# copyright notice, this list of conditions and the following disclaimer
|
12
|
+
# in the documentation and/or other materials provided with the
|
13
|
+
# distribution.
|
14
|
+
# * Neither the name of Google Inc. nor the names of its
|
15
|
+
# contributors may be used to endorse or promote products derived from
|
16
|
+
# this software without specific prior written permission.
|
17
|
+
#
|
18
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
19
|
+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
20
|
+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
21
|
+
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
22
|
+
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
23
|
+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
24
|
+
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
25
|
+
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
26
|
+
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
27
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
28
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
29
|
+
|
30
|
+
require "googleauth/signet"
|
31
|
+
require "googleauth/credentials_loader"
|
32
|
+
require "multi_json"
|
33
|
+
|
34
|
+
module Google
|
35
|
+
module Auth
|
36
|
+
# Small utility for normalizing scopes into canonical form
|
37
|
+
module ScopeUtil
|
38
|
+
ALIASES = {
|
39
|
+
"email" => "https://www.googleapis.com/auth/userinfo.email",
|
40
|
+
"profile" => "https://www.googleapis.com/auth/userinfo.profile",
|
41
|
+
"openid" => "https://www.googleapis.com/auth/plus.me"
|
42
|
+
}.freeze
|
43
|
+
|
44
|
+
def self.normalize scope
|
45
|
+
list = as_array scope
|
46
|
+
list.map { |item| ALIASES[item] || item }
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.as_array scope
|
50
|
+
case scope
|
51
|
+
when Array
|
52
|
+
scope
|
53
|
+
when String
|
54
|
+
scope.split
|
55
|
+
else
|
56
|
+
raise "Invalid scope value. Must be string or array"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -27,91 +27,201 @@
|
|
27
27
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
28
28
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
29
29
|
|
30
|
-
require
|
31
|
-
require
|
32
|
-
require
|
33
|
-
require
|
34
|
-
require
|
35
|
-
|
36
|
-
# Reads the private key and client email fields from service account JSON key.
|
37
|
-
def read_json_key(json_key_io)
|
38
|
-
json_key = MultiJson.load(json_key_io.read)
|
39
|
-
fail 'missing client_email' unless json_key.key?('client_email')
|
40
|
-
fail 'missing private_key' unless json_key.key?('private_key')
|
41
|
-
[json_key['private_key'], json_key['client_email']]
|
42
|
-
end
|
30
|
+
require "googleauth/signet"
|
31
|
+
require "googleauth/credentials_loader"
|
32
|
+
require "googleauth/json_key_reader"
|
33
|
+
require "jwt"
|
34
|
+
require "multi_json"
|
35
|
+
require "stringio"
|
43
36
|
|
44
37
|
module Google
|
45
38
|
# Module Auth provides classes that provide Google-specific authorization
|
46
39
|
# used to access Google APIs.
|
47
40
|
module Auth
|
48
|
-
# Authenticates requests using Google's Service Account credentials
|
41
|
+
# Authenticates requests using Google's Service Account credentials via an
|
42
|
+
# OAuth access token.
|
49
43
|
#
|
50
44
|
# This class allows authorizing requests for service accounts directly
|
51
45
|
# from credentials from a json key file downloaded from the developer
|
52
46
|
# console (via 'Generate new Json Key').
|
53
47
|
#
|
54
|
-
# cf [Application Default Credentials](
|
48
|
+
# cf [Application Default Credentials](https://cloud.google.com/docs/authentication/production)
|
55
49
|
class ServiceAccountCredentials < Signet::OAuth2::Client
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
rescue StandardError => e
|
84
|
-
raise "#{NOT_FOUND_ERROR}: #{e}"
|
50
|
+
TOKEN_CRED_URI = "https://www.googleapis.com/oauth2/v4/token".freeze
|
51
|
+
extend CredentialsLoader
|
52
|
+
extend JsonKeyReader
|
53
|
+
attr_reader :project_id
|
54
|
+
attr_reader :quota_project_id
|
55
|
+
|
56
|
+
def enable_self_signed_jwt?
|
57
|
+
@enable_self_signed_jwt
|
58
|
+
end
|
59
|
+
|
60
|
+
# Creates a ServiceAccountCredentials.
|
61
|
+
#
|
62
|
+
# @param json_key_io [IO] an IO from which the JSON key can be read
|
63
|
+
# @param scope [string|array|nil] the scope(s) to access
|
64
|
+
def self.make_creds options = {}
|
65
|
+
json_key_io, scope, enable_self_signed_jwt, target_audience, audience, token_credential_uri =
|
66
|
+
options.values_at :json_key_io, :scope, :enable_self_signed_jwt, :target_audience,
|
67
|
+
:audience, :token_credential_uri
|
68
|
+
raise ArgumentError, "Cannot specify both scope and target_audience" if scope && target_audience
|
69
|
+
|
70
|
+
if json_key_io
|
71
|
+
private_key, client_email, project_id, quota_project_id = read_json_key json_key_io
|
72
|
+
else
|
73
|
+
private_key = unescape ENV[CredentialsLoader::PRIVATE_KEY_VAR]
|
74
|
+
client_email = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
|
75
|
+
project_id = ENV[CredentialsLoader::PROJECT_ID_VAR]
|
76
|
+
quota_project_id = nil
|
85
77
|
end
|
78
|
+
project_id ||= CredentialsLoader.load_gcloud_project_id
|
79
|
+
|
80
|
+
new(token_credential_uri: token_credential_uri || TOKEN_CRED_URI,
|
81
|
+
audience: audience || TOKEN_CRED_URI,
|
82
|
+
scope: scope,
|
83
|
+
enable_self_signed_jwt: enable_self_signed_jwt,
|
84
|
+
target_audience: target_audience,
|
85
|
+
issuer: client_email,
|
86
|
+
signing_key: OpenSSL::PKey::RSA.new(private_key),
|
87
|
+
project_id: project_id,
|
88
|
+
quota_project_id: quota_project_id)
|
89
|
+
.configure_connection(options)
|
90
|
+
end
|
86
91
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
92
|
+
# Handles certain escape sequences that sometimes appear in input.
|
93
|
+
# Specifically, interprets the "\n" sequence for newline, and removes
|
94
|
+
# enclosing quotes.
|
95
|
+
def self.unescape str
|
96
|
+
str = str.gsub '\n', "\n"
|
97
|
+
str = str[1..-2] if str.start_with?('"') && str.end_with?('"')
|
98
|
+
str
|
99
|
+
end
|
100
|
+
|
101
|
+
def initialize options = {}
|
102
|
+
@project_id = options[:project_id]
|
103
|
+
@quota_project_id = options[:quota_project_id]
|
104
|
+
@enable_self_signed_jwt = options[:enable_self_signed_jwt] ? true : false
|
105
|
+
super options
|
106
|
+
end
|
107
|
+
|
108
|
+
# Extends the base class to use a transient
|
109
|
+
# ServiceAccountJwtHeaderCredentials for certain cases.
|
110
|
+
def apply! a_hash, opts = {}
|
111
|
+
# Use a self-singed JWT if there's no information that can be used to
|
112
|
+
# obtain an OAuth token, OR if there are scopes but also an assertion
|
113
|
+
# that they are default scopes that shouldn't be used to fetch a token.
|
114
|
+
if target_audience.nil? && (scope.nil? || enable_self_signed_jwt?)
|
115
|
+
apply_self_signed_jwt! a_hash
|
116
|
+
else
|
117
|
+
super
|
101
118
|
end
|
102
119
|
end
|
103
120
|
|
104
|
-
|
121
|
+
private
|
122
|
+
|
123
|
+
def apply_self_signed_jwt! a_hash
|
124
|
+
# Use the ServiceAccountJwtHeaderCredentials using the same cred values
|
125
|
+
cred_json = {
|
126
|
+
private_key: @signing_key.to_s,
|
127
|
+
client_email: @issuer,
|
128
|
+
project_id: @project_id,
|
129
|
+
quota_project_id: @quota_project_id
|
130
|
+
}
|
131
|
+
key_io = StringIO.new MultiJson.dump(cred_json)
|
132
|
+
alt = ServiceAccountJwtHeaderCredentials.make_creds json_key_io: key_io
|
133
|
+
alt.apply! a_hash
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Authenticates requests using Google's Service Account credentials via
|
138
|
+
# JWT Header.
|
139
|
+
#
|
140
|
+
# This class allows authorizing requests for service accounts directly
|
141
|
+
# from credentials from a json key file downloaded from the developer
|
142
|
+
# console (via 'Generate new Json Key'). It is not part of any OAuth2
|
143
|
+
# flow, rather it creates a JWT and sends that as a credential.
|
144
|
+
#
|
145
|
+
# cf [Application Default Credentials](https://cloud.google.com/docs/authentication/production)
|
146
|
+
class ServiceAccountJwtHeaderCredentials
|
147
|
+
JWT_AUD_URI_KEY = :jwt_aud_uri
|
148
|
+
AUTH_METADATA_KEY = Signet::OAuth2::AUTH_METADATA_KEY
|
149
|
+
TOKEN_CRED_URI = "https://www.googleapis.com/oauth2/v4/token".freeze
|
150
|
+
SIGNING_ALGORITHM = "RS256".freeze
|
151
|
+
EXPIRY = 60
|
152
|
+
extend CredentialsLoader
|
153
|
+
extend JsonKeyReader
|
154
|
+
attr_reader :project_id
|
155
|
+
attr_reader :quota_project_id
|
156
|
+
|
157
|
+
# make_creds proxies the construction of a credentials instance
|
158
|
+
#
|
159
|
+
# make_creds is used by the methods in CredentialsLoader.
|
160
|
+
#
|
161
|
+
# By default, it calls #new with 2 args, the second one being an
|
162
|
+
# optional scope. Here's the constructor only has one param, so
|
163
|
+
# we modify make_creds to reflect this.
|
164
|
+
def self.make_creds *args
|
165
|
+
new json_key_io: args[0][:json_key_io]
|
166
|
+
end
|
167
|
+
|
168
|
+
# Initializes a ServiceAccountJwtHeaderCredentials.
|
105
169
|
#
|
106
|
-
# @param scope [string|array] the scope(s) to access
|
107
170
|
# @param json_key_io [IO] an IO from which the JSON key can be read
|
108
|
-
def initialize
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
171
|
+
def initialize options = {}
|
172
|
+
json_key_io = options[:json_key_io]
|
173
|
+
if json_key_io
|
174
|
+
@private_key, @issuer, @project_id, @quota_project_id =
|
175
|
+
self.class.read_json_key json_key_io
|
176
|
+
else
|
177
|
+
@private_key = ENV[CredentialsLoader::PRIVATE_KEY_VAR]
|
178
|
+
@issuer = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
|
179
|
+
@project_id = ENV[CredentialsLoader::PROJECT_ID_VAR]
|
180
|
+
@quota_project_id = nil
|
181
|
+
end
|
182
|
+
@project_id ||= CredentialsLoader.load_gcloud_project_id
|
183
|
+
@signing_key = OpenSSL::PKey::RSA.new @private_key
|
184
|
+
end
|
185
|
+
|
186
|
+
# Construct a jwt token if the JWT_AUD_URI key is present in the input
|
187
|
+
# hash.
|
188
|
+
#
|
189
|
+
# The jwt token is used as the value of a 'Bearer '.
|
190
|
+
def apply! a_hash, opts = {}
|
191
|
+
jwt_aud_uri = a_hash.delete JWT_AUD_URI_KEY
|
192
|
+
return a_hash if jwt_aud_uri.nil?
|
193
|
+
jwt_token = new_jwt_token jwt_aud_uri, opts
|
194
|
+
a_hash[AUTH_METADATA_KEY] = "Bearer #{jwt_token}"
|
195
|
+
a_hash
|
196
|
+
end
|
197
|
+
|
198
|
+
# Returns a clone of a_hash updated with the authoriation header
|
199
|
+
def apply a_hash, opts = {}
|
200
|
+
a_copy = a_hash.clone
|
201
|
+
apply! a_copy, opts
|
202
|
+
a_copy
|
203
|
+
end
|
204
|
+
|
205
|
+
# Returns a reference to the #apply method, suitable for passing as
|
206
|
+
# a closure
|
207
|
+
def updater_proc
|
208
|
+
proc { |a_hash, opts = {}| apply a_hash, opts }
|
209
|
+
end
|
210
|
+
|
211
|
+
protected
|
212
|
+
|
213
|
+
# Creates a jwt uri token.
|
214
|
+
def new_jwt_token jwt_aud_uri, options = {}
|
215
|
+
now = Time.new
|
216
|
+
skew = options[:skew] || 60
|
217
|
+
assertion = {
|
218
|
+
"iss" => @issuer,
|
219
|
+
"sub" => @issuer,
|
220
|
+
"aud" => jwt_aud_uri,
|
221
|
+
"exp" => (now + EXPIRY).to_i,
|
222
|
+
"iat" => (now - skew).to_i
|
223
|
+
}
|
224
|
+
JWT.encode assertion, @signing_key, SIGNING_ALGORITHM
|
115
225
|
end
|
116
226
|
end
|
117
227
|
end
|