googleauth 0.1.0 → 0.16.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +5 -5
  2. data/.github/CODEOWNERS +7 -0
  3. data/.github/CONTRIBUTING.md +74 -0
  4. data/.github/ISSUE_TEMPLATE/bug_report.md +36 -0
  5. data/.github/ISSUE_TEMPLATE/feature_request.md +21 -0
  6. data/.github/ISSUE_TEMPLATE/support_request.md +7 -0
  7. data/.github/renovate.json +6 -0
  8. data/.github/sync-repo-settings.yaml +18 -0
  9. data/.github/workflows/ci.yml +55 -0
  10. data/.github/workflows/release-please.yml +39 -0
  11. data/.gitignore +3 -0
  12. data/.kokoro/populate-secrets.sh +76 -0
  13. data/.kokoro/release.cfg +52 -0
  14. data/.kokoro/release.sh +18 -0
  15. data/.kokoro/trampoline_v2.sh +489 -0
  16. data/.repo-metadata.json +5 -0
  17. data/.rubocop.yml +17 -0
  18. data/.toys/.toys.rb +45 -0
  19. data/.toys/ci.rb +43 -0
  20. data/.toys/kokoro/.toys.rb +66 -0
  21. data/.toys/kokoro/publish-docs.rb +67 -0
  22. data/.toys/kokoro/publish-gem.rb +53 -0
  23. data/.toys/linkinator.rb +43 -0
  24. data/.trampolinerc +48 -0
  25. data/CHANGELOG.md +199 -0
  26. data/CODE_OF_CONDUCT.md +43 -0
  27. data/Gemfile +22 -1
  28. data/{COPYING → LICENSE} +0 -0
  29. data/README.md +140 -17
  30. data/googleauth.gemspec +28 -28
  31. data/integration/helper.rb +31 -0
  32. data/integration/id_tokens/key_source_test.rb +74 -0
  33. data/lib/googleauth.rb +7 -37
  34. data/lib/googleauth/application_default.rb +81 -0
  35. data/lib/googleauth/client_id.rb +104 -0
  36. data/lib/googleauth/compute_engine.rb +73 -26
  37. data/lib/googleauth/credentials.rb +561 -0
  38. data/lib/googleauth/credentials_loader.rb +207 -0
  39. data/lib/googleauth/default_credentials.rb +93 -0
  40. data/lib/googleauth/iam.rb +75 -0
  41. data/lib/googleauth/id_tokens.rb +233 -0
  42. data/lib/googleauth/id_tokens/errors.rb +71 -0
  43. data/lib/googleauth/id_tokens/key_sources.rb +396 -0
  44. data/lib/googleauth/id_tokens/verifier.rb +142 -0
  45. data/lib/googleauth/json_key_reader.rb +50 -0
  46. data/lib/googleauth/scope_util.rb +61 -0
  47. data/lib/googleauth/service_account.rb +177 -67
  48. data/lib/googleauth/signet.rb +69 -8
  49. data/lib/googleauth/stores/file_token_store.rb +65 -0
  50. data/lib/googleauth/stores/redis_token_store.rb +96 -0
  51. data/lib/googleauth/token_store.rb +69 -0
  52. data/lib/googleauth/user_authorizer.rb +285 -0
  53. data/lib/googleauth/user_refresh.rb +129 -0
  54. data/lib/googleauth/version.rb +1 -1
  55. data/lib/googleauth/web_user_authorizer.rb +295 -0
  56. data/spec/googleauth/apply_auth_examples.rb +96 -94
  57. data/spec/googleauth/client_id_spec.rb +160 -0
  58. data/spec/googleauth/compute_engine_spec.rb +125 -55
  59. data/spec/googleauth/credentials_spec.rb +600 -0
  60. data/spec/googleauth/get_application_default_spec.rb +232 -80
  61. data/spec/googleauth/iam_spec.rb +80 -0
  62. data/spec/googleauth/scope_util_spec.rb +77 -0
  63. data/spec/googleauth/service_account_spec.rb +422 -68
  64. data/spec/googleauth/signet_spec.rb +101 -25
  65. data/spec/googleauth/stores/file_token_store_spec.rb +57 -0
  66. data/spec/googleauth/stores/redis_token_store_spec.rb +50 -0
  67. data/spec/googleauth/stores/store_examples.rb +58 -0
  68. data/spec/googleauth/user_authorizer_spec.rb +343 -0
  69. data/spec/googleauth/user_refresh_spec.rb +359 -0
  70. data/spec/googleauth/web_user_authorizer_spec.rb +172 -0
  71. data/spec/spec_helper.rb +51 -10
  72. data/test/helper.rb +33 -0
  73. data/test/id_tokens/key_sources_test.rb +240 -0
  74. data/test/id_tokens/verifier_test.rb +269 -0
  75. metadata +114 -75
  76. data/.travis.yml +0 -18
  77. data/CONTRIBUTING.md +0 -32
  78. 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 'googleauth/signet'
31
- require 'memoist'
32
- require 'multi_json'
33
- require 'openssl'
34
- require 'rbconfig'
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](http://goo.gl/mkAHpZ)
48
+ # cf [Application Default Credentials](https://cloud.google.com/docs/authentication/production)
55
49
  class ServiceAccountCredentials < Signet::OAuth2::Client
56
- ENV_VAR = 'GOOGLE_APPLICATION_CREDENTIALS'
57
- NOT_FOUND_ERROR =
58
- "Unable to read the credential file specified by #{ENV_VAR}"
59
- TOKEN_CRED_URI = 'https://www.googleapis.com/oauth2/v3/token'
60
- WELL_KNOWN_PATH = 'gcloud/application_default_credentials.json'
61
- WELL_KNOWN_ERROR = 'Unable to read the default credential file'
62
-
63
- class << self
64
- extend Memoist
65
-
66
- # determines if the current OS is windows
67
- def windows?
68
- RbConfig::CONFIG['host_os'] =~ /Windows|mswin/
69
- end
70
- memoize :windows?
71
-
72
- # Creates an instance from the path specified in an environment
73
- # variable.
74
- #
75
- # @param scope [string|array] the scope(s) to access
76
- def from_env(scope)
77
- return nil unless ENV.key?(ENV_VAR)
78
- path = ENV[ENV_VAR]
79
- fail 'file #{path} does not exist' unless File.exist?(path)
80
- File.open(path) do |f|
81
- return new(scope, f)
82
- end
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
- # Creates an instance from a well known path.
88
- #
89
- # @param scope [string|array] the scope(s) to access
90
- def from_well_known_path(scope)
91
- home_var, base = windows? ? 'APPDATA' : 'HOME', WELL_KNOWN_PATH
92
- root = ENV[home_var].nil? ? '' : ENV[home_var]
93
- base = File.join('.config', base) unless windows?
94
- path = File.join(root, base)
95
- return nil unless File.exist?(path)
96
- File.open(path) do |f|
97
- return new(scope, f)
98
- end
99
- rescue StandardError => e
100
- raise "#{WELL_KNOWN_ERROR}: #{e}"
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
- # Initializes a ServiceAccountCredentials.
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(scope, json_key_io)
109
- private_key, client_email = read_json_key(json_key_io)
110
- super(token_credential_uri: TOKEN_CRED_URI,
111
- audience: TOKEN_CRED_URI, # TODO: confirm this
112
- scope: scope,
113
- issuer: client_email,
114
- signing_key: OpenSSL::PKey::RSA.new(private_key))
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