gitkit-ruby-client 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. data/README.rdoc +64 -0
  2. data/lib/gitkit_client.rb +290 -0
  3. data/lib/rpc_helper.rb +209 -0
  4. metadata +130 -0
@@ -0,0 +1,64 @@
1
+ = Google Identity Toolkit Ruby Client
2
+
3
+ Google Identity Toolkit (Gitkit) ruby client library is for third party sites to easily integrate with Gitkit service.
4
+
5
+ === Installation
6
+
7
+ gem install gitkit-ruby-client
8
+
9
+ === Examples
10
+
11
+ # Create a server config file or download it from Google Developer Console
12
+ # The config file contains Gitkit library config in json format
13
+ # {
14
+ # "clientId" : "oauth2-web-client-id.apps.googleusercontent.com",
15
+ # "serviceAccountEmail" : "service-account-email@developer.gserviceaccount.com",
16
+ # "serviceAccountPrivateKeyFile" : "path-to-service-account-private-key-file.p12",
17
+ # "widgetUrl" : "full-url-of-gitkit-widget-on-your-site",
18
+ # "cookieName" : "gtoken",
19
+ # "serverKey" : "devconsole-server-key"
20
+ # }
21
+
22
+ # Create a Gitkit client
23
+ gitkit_client = GitkitLib::GitkitClient.create_from_config_file 'gitkit-server-config.json'
24
+
25
+ # Verify the Gitkit token in the incoming http request
26
+ token = request.cookies["gtoken"]
27
+ user = gitkit_client.verify_gitkit_token token
28
+
29
+ # Upload passwords
30
+ def calc_sha1(key, plain_text, salt)
31
+ hmac = OpenSSL::HMAC.new key, 'sha1'
32
+ hmac << plain_text
33
+ hmac << salt
34
+ hmac.digest
35
+ end
36
+
37
+ hash_key = 'hash-key'
38
+
39
+ user1 = GitkitLib::GitkitUser.new
40
+ user1.email = '1234@example.com'
41
+ user1.user_id = '1234'
42
+ user1.salt = 'salt-1'
43
+ user1.password_hash = calc_sha1(hash_key, '1111', 'salt-1')
44
+
45
+ user2 = GitkitLib::GitkitUser.new
46
+ user2.email = '5678@example.com'
47
+ user2.user_id = '5678'
48
+ user2.salt = 'salt-2'
49
+ user2.password_hash = calc_sha1(hash_key, '5555', 'salt-2')
50
+ user2.name = '56 78'
51
+
52
+ gitkit_client.upload_users 'HMAC_SHA1', hash_key, [user1, user2]
53
+
54
+ # Get user by email
55
+ user = gitkit_client.get_user_by_email('1234@example.com')
56
+
57
+ # Get user by id
58
+ user = gitkit_client.get_user_by_id('5678')
59
+
60
+ # Delete a user
61
+ gitkit_client.delete_user '5678'
62
+
63
+ # Download all accounts
64
+ gitkit_client.get_all_users(2) { |account| pp account}
@@ -0,0 +1,290 @@
1
+ # Copyright 2014 Google Inc. All Rights Reserved.
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 'addressable/uri'
16
+ require 'jwt'
17
+ require 'json'
18
+ require 'rpc_helper'
19
+ require 'uri'
20
+
21
+ module GitkitLib
22
+
23
+ class GitkitClient
24
+
25
+ # Create a client from json config file
26
+ #
27
+ # @param [String] file file name of the json-format config
28
+ def self.create_from_config_file(file)
29
+ config = JSON.parse File.open(file, 'rb') { |io| io.read }
30
+ p12key = File.open(config['serviceAccountPrivateKeyFile'], 'rb') {
31
+ |io| io.read }
32
+ new(
33
+ config['clientId'],
34
+ config['serviceAccountEmail'],
35
+ p12key,
36
+ config['widgetUrl'],
37
+ config['serverApiKey'])
38
+ end
39
+
40
+ # Initializes a GitkitClient.
41
+ #
42
+ # @param [String] client_id Google oauth2 web client id of this site
43
+ # @param [String] service_account_email Google service account email
44
+ # @param [String] service_account_key Google service account private p12 key
45
+ # @param [String] widget_url url to host the Gitkit widget
46
+ # @param [String] server_api_key server-side Google API key
47
+ def initialize(client_id, service_account_email, service_account_key,
48
+ widget_url, server_api_key = nil)
49
+ @client_id = client_id
50
+ @widget_url = widget_url
51
+ @rpc_helper = RpcHelper.new(service_account_email, service_account_key,
52
+ server_api_key)
53
+ @certificates = {}
54
+ end
55
+
56
+ # Verifies a Gitkit token
57
+ #
58
+ # @param [String] token_string the token to be verified
59
+ # @return [GitkitUser, nil] for valid token, [nil, String] otherwise with
60
+ # error_message.
61
+ def verify_gitkit_token(token_string)
62
+ if token_string.nil?
63
+ return nil, 'no token'
64
+ end
65
+ begin
66
+ parsed_token, _ = JWT.decode(token_string, nil, true) {|header|
67
+ # key finder
68
+ key_id = header['kid']
69
+ unless @certificates.has_key? key_id
70
+ @certificates = Hash[get_certs.map {|key,cert|
71
+ [key, OpenSSL::X509::Certificate.new(cert)]}]
72
+ end
73
+ @certificates[key_id].public_key
74
+ }
75
+ # check expiration time
76
+ if Time.new.to_i > parsed_token['exp']
77
+ return nil, 'token expired'
78
+ end
79
+ # check audience
80
+ if parsed_token['aud'] != @client_id
81
+ return nil, 'audience mismatch'
82
+ end
83
+ GitkitUser.parse_from_api_response parsed_token
84
+ rescue JWT::DecodeError => e
85
+ return nil, e.message
86
+ end
87
+ end
88
+
89
+ # Gets Gitkit public certs
90
+ #
91
+ # @api private
92
+ def get_certs
93
+ @rpc_helper.get_gitkit_certs()
94
+ end
95
+
96
+ # Gets user info by email
97
+ #
98
+ # @param [String] email user email
99
+ # @return [GitkitUser] for the email
100
+ def get_user_by_email(email)
101
+ response = @rpc_helper.get_user_by_email email
102
+ GitkitUser.parse_from_api_response response.fetch('users', [{}])[0]
103
+ end
104
+
105
+ # Gets user info by user id
106
+ #
107
+ # @param [String] id user id
108
+ # @return [GitkitUser] for the id
109
+ def get_user_by_id(id)
110
+ response = @rpc_helper.get_user_by_id id
111
+ GitkitUser.parse_from_api_response response.fetch('users', [{}])[0]
112
+ end
113
+
114
+ # Downloads all user accounts from Gitkit service
115
+ #
116
+ # @param [Fixnum] max_results pagination size of each request
117
+ # @yield [GitkitUser] individual user account
118
+ def get_all_users(max_results = 10)
119
+ next_page_token = nil
120
+ while true
121
+ next_page_token, accounts = @rpc_helper.download_account(
122
+ next_page_token, max_results)
123
+ accounts.each { |account|
124
+ yield GitkitUser.parse_from_api_response account }
125
+ if not next_page_token or accounts.length == 0
126
+ break
127
+ end
128
+ end
129
+ end
130
+
131
+ # Uploads multiple accounts to Gitkit service
132
+ #
133
+ # @param [String] hash_algorithm password hash algorithm
134
+ # @param [String] hash_key key of the hash algorithm
135
+ # @param [Array<GitkitUser>] accounts user accounts to be uploaded
136
+ def upload_users(hash_algorithm, hash_key, accounts)
137
+ account_request = accounts.collect { |account| account.to_request }
138
+ @rpc_helper.upload_account hash_algorithm, JWT.base64url_encode(hash_key),
139
+ account_request
140
+ end
141
+
142
+ # Deletes a user account from Gitkit service
143
+ #
144
+ # @param [String] local_id user id to be deleted
145
+ def delete_user(local_id)
146
+ @rpc_helper.delete_account local_id
147
+ end
148
+
149
+ # Get one-time out-of-band code for ResetPassword/ChangeEmail request
150
+ #
151
+ # @param [String] full_url the full URL of incoming request
152
+ # @param [Hash{String=>String}] param dict of HTTP POST params
153
+ # @param [String] user_ip end user's IP address
154
+ # @param [String] gitkit_token the gitkit token if user logged in
155
+ #
156
+ # @return [Hash] {
157
+ # email: user email who is asking reset password
158
+ # oobLink: the generated link to be send to user's email
159
+ # action: OobAction
160
+ # response_body: the http body to be returned
161
+ # }
162
+ def get_oob_result(full_url, param, user_ip, gitkit_token=nil)
163
+ if param.has_key? 'action'
164
+ begin
165
+ if param['action'] == 'resetPassword'
166
+ oob_link = build_oob_link(full_url,
167
+ password_reset_request(param, user_ip),
168
+ param['action'])
169
+ return password_reset_response(oob_link, param)
170
+ elsif param['action'] == 'changeEmail'
171
+ unless gitkit_token
172
+ return failure_msg('login is required')
173
+ end
174
+ oob_link = build_oob_link(full_url,
175
+ change_email_request(param, user_ip, gitkit_token),
176
+ param['action'])
177
+ return email_change_response(oob_link, param)
178
+ end
179
+ rescue GitkitClientError => error
180
+ return failure_msg(error.message)
181
+ end
182
+ end
183
+ failure_msg('unknown request type')
184
+ end
185
+
186
+ def password_reset_request(param, user_ip)
187
+ {
188
+ 'email' => param['email'],
189
+ 'userIp' => user_ip,
190
+ 'challenge' => param['challenge'],
191
+ 'captchaResp' => param['response'],
192
+ 'requestType' => 'PASSWORD_RESET'
193
+ }
194
+ end
195
+
196
+ def change_email_request(param, user_ip, gitkit_token)
197
+ {
198
+ 'email' => param['oldEmail'],
199
+ 'newEmail' => param['newEmail'],
200
+ 'userIp' => user_ip,
201
+ 'idToken' => gitkit_token,
202
+ 'requestType' => 'NEW_EMAIL_ACCEPT'
203
+ }
204
+ end
205
+
206
+ def build_oob_link(full_url, param, mode)
207
+ code = @rpc_helper.get_oob_code(param)
208
+ if code
209
+ oob_link = Addressable::URI.parse full_url
210
+ oob_link.path = @widget_url
211
+ oob_link.query_values = { 'mode' => mode, 'oobCode' => code}
212
+ return oob_link.to_s
213
+ end
214
+ nil
215
+ end
216
+
217
+ def failure_msg(msg)
218
+ {:response_body => {'error' => msg}}.to_json
219
+ end
220
+
221
+ def email_change_response(oob_link, param)
222
+ {
223
+ :oldEmail => param['email'],
224
+ :newEmail => param['newEmail'],
225
+ :oobLink => oob_link,
226
+ :action => :CHANGE_EMAIL,
227
+ :response_body => {'success' => true}.to_json
228
+ }
229
+ end
230
+
231
+ def password_reset_response(oob_link, param)
232
+ {
233
+ :email => param['email'],
234
+ :oobLink => oob_link,
235
+ :action => :RESET_PASSWORD,
236
+ :response_body => {'success' => true}.to_json
237
+ }
238
+ end
239
+ end
240
+
241
+ class GitkitUser
242
+ attr_accessor :email, :user_id, :name, :photo_url, :provider_id,
243
+ :email_verified, :password_hash, :salt, :password, :provider_info
244
+
245
+ def self.parse_from_api_response(api_response)
246
+ user = self.new
247
+ user.email = api_response.fetch('email', nil)
248
+ user.user_id = api_response.fetch('user_id',
249
+ api_response.fetch('localId', nil))
250
+ user.name = api_response.fetch('displayName', nil)
251
+ user.photo_url = api_response.fetch('photoUrl', nil)
252
+ user.provider_id = api_response.fetch('provider_id',
253
+ api_response.fetch('providerId', nil))
254
+ user.email_verified = api_response.fetch('emailVerified',
255
+ api_response.fetch('verified', nil))
256
+ user.password_hash = api_response.fetch('passwordHash', nil)
257
+ user.salt = api_response.fetch('salt', nil)
258
+ user.password = api_response.fetch('password', nil)
259
+ user.provider_info = api_response.fetch('providerUserInfo', {})
260
+ user
261
+ end
262
+
263
+ # Convert to gitkit api request (a dict)
264
+ def to_request
265
+ request = {}
266
+ request['email'] = @email if @email
267
+ request['localId'] = @user_id if @user_id
268
+ request['displayName'] = @name if @name
269
+ request['photoUrl'] = @photo_url if @photo_url
270
+ request['emailVerified'] = @email_verified if @email_verified != nil
271
+ request['passwordHash'] =
272
+ JWT.base64url_encode @password_hash if @password_hash
273
+ request['salt'] = JWT.base64url_encode @salt if @salt
274
+ request['providerUserInfo'] = @provider_info if @provider_info != nil
275
+ request
276
+ end
277
+ end
278
+
279
+ class GitkitClientError < StandardError
280
+ def initialize(message)
281
+ super
282
+ end
283
+ end
284
+
285
+ class GitkitServerError < StandardError
286
+ def initialize(message)
287
+ super
288
+ end
289
+ end
290
+ end
@@ -0,0 +1,209 @@
1
+ # Copyright 2014 Google Inc. All Rights Reserved.
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 'jwt'
16
+ require 'json'
17
+ require 'faraday'
18
+ require 'gitkit_client'
19
+ require 'openssl'
20
+
21
+ module GitkitLib
22
+ class RpcHelper
23
+ attr_accessor :access_token, :token_issued_at, :token_duration
24
+
25
+ TOKEN_ENDPOINT = 'https://accounts.google.com/o/oauth2/token'
26
+ GITKIT_SCOPE = 'https://www.googleapis.com/auth/identitytoolkit'
27
+ GITKIT_API_URL =
28
+ 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/'
29
+
30
+ def initialize(service_account_email, service_account_key, server_api_key,
31
+ google_token_endpoint = TOKEN_ENDPOINT)
32
+ @service_account_email = service_account_email
33
+ @google_api_url = google_token_endpoint
34
+ @connection = Faraday::Connection.new
35
+ @service_account_key =
36
+ OpenSSL::PKCS12.new(service_account_key, 'notasecret').key
37
+ @server_api_key = server_api_key
38
+ @token_duration = 3600
39
+ @token_issued_at = 0
40
+ @access_token = nil
41
+ end
42
+
43
+ # GetAccountInfo by email
44
+ #
45
+ # @api private
46
+ # @param [String] email account email to be queried
47
+ # @return [JSON] account info
48
+ def get_user_by_email(email)
49
+ invoke_gitkit_api('getAccountInfo', {'email' => [email]})
50
+ end
51
+
52
+ # GetAccountInfo by id
53
+ #
54
+ # @api private
55
+ # @param [String] id account id to be queried
56
+ # @return [JSON] account info
57
+ def get_user_by_id(id)
58
+ invoke_gitkit_api('getAccountInfo', {'localId' => [id]})
59
+ end
60
+
61
+ # Get out-of-band code for ResetPassword/ChangeEmail etc. operation
62
+ #
63
+ # @api private
64
+ # @param [Hash<String, String>] request the oob request
65
+ # @return <String> the oob code
66
+ def get_oob_code(request)
67
+ response = invoke_gitkit_api('getOobConfirmationCode', request)
68
+ response.fetch('oobCode', nil)
69
+ end
70
+
71
+ # Download all accounts
72
+ #
73
+ # @api private
74
+ # @param [String] next_page_token pagination token for next page
75
+ # @param [Fixnum] max_results pagination size
76
+ # @return [Array<JSON>] user account info
77
+ def download_account(next_page_token, max_results)
78
+ param = {}
79
+ if next_page_token
80
+ param['nextPageToken'] = next_page_token
81
+ end
82
+ if max_results
83
+ param['maxResults'] = max_results
84
+ end
85
+ response = invoke_gitkit_api('downloadAccount', param)
86
+ return response.fetch('nextPageToken', nil), response.fetch('users', {})
87
+ end
88
+
89
+ # Delete an account
90
+ #
91
+ # @api private
92
+ # @param <String> local_id user id to be deleted
93
+ def delete_account(local_id)
94
+ invoke_gitkit_api('deleteAccount', {'localId' => local_id})
95
+ end
96
+
97
+ # Upload batch accounts
98
+ #
99
+ # @api private
100
+ # @param <String> hash_algorithm hash algorithm
101
+ # @param <String> hash_key hash key
102
+ # @param <Array<GitkitUser>> accounts account to be uploaded
103
+ def upload_account(hash_algorithm, hash_key, accounts)
104
+ param = {
105
+ 'hashAlgorithm' => hash_algorithm,
106
+ 'signerKey' => hash_key,
107
+ 'users' => accounts
108
+ }
109
+ invoke_gitkit_api('uploadAccount', param)
110
+ end
111
+
112
+ # Creates a signed jwt assertion
113
+ #
114
+ # @api private
115
+ # @return [String] jwt assertion
116
+ def sign_assertion
117
+ now = Time.new
118
+ assertion = {
119
+ 'iss' => @service_account_email,
120
+ 'scope' => GITKIT_SCOPE,
121
+ 'aud' => @google_api_url,
122
+ 'exp' => (now + @token_duration).to_i,
123
+ 'iat' => now.to_i
124
+ }
125
+ JWT.encode(assertion, @service_account_key, 'RS256')
126
+ end
127
+
128
+ # Get an access token, from Google server if cached one is expired
129
+ #
130
+ # @api private
131
+ def fetch_access_token
132
+ if is_token_expired
133
+ assertion = sign_assertion
134
+ post_body = {
135
+ 'assertion' => assertion,
136
+ 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer'}
137
+ headers = {'Content-type' => 'application/x-www-form-urlencoded'}
138
+ response = @connection.post(RpcHelper::TOKEN_ENDPOINT, post_body,
139
+ headers)
140
+ @access_token = JSON.parse(response.env[:body])['access_token']
141
+ @token_issued_at = Time.new.to_i
142
+ end
143
+ @access_token
144
+ end
145
+
146
+ # Check whether the cached access token is expired
147
+ #
148
+ # @api private
149
+ # @return <Boolean> whether the access token is expired
150
+ def is_token_expired
151
+ @access_token == nil ||
152
+ Time.new.to_i > @token_issued_at + @token_duration - 30
153
+ end
154
+
155
+ # Invoke Gitkit API, with optional access token for service account
156
+ # operations
157
+ #
158
+ # @api private
159
+ # @param [String] method Gitkit API method name
160
+ # @param [Hash<String, String>] params api request params
161
+ # @param [bool] need_service_account whether the request needs to be
162
+ # authenticated
163
+ # @return <JSON> the Gitkit api response
164
+ def invoke_gitkit_api(method, params, need_service_account=true)
165
+ post_body = JSON.generate(params)
166
+ headers = {'Content-type' => 'application/json'}
167
+ if need_service_account
168
+ @connection.authorization :Bearer, fetch_access_token
169
+ end
170
+ response = @connection.post(GITKIT_API_URL + method, post_body, headers)
171
+ check_gitkit_error JSON.parse(response.env[:body])
172
+ end
173
+
174
+ # Download the Gitkit public certs
175
+ #
176
+ # @api private
177
+ # @return <JSON> the public certs
178
+ def get_gitkit_certs
179
+ if @server_api_key.nil?
180
+ @connection.authorization :Bearer, fetch_access_token
181
+ response = @connection.get(GITKIT_API_URL + 'publicKeys')
182
+ else
183
+ response = @connection.get [GITKIT_API_URL, 'publicKeys?key=',
184
+ @server_api_key].join
185
+ end
186
+ MultiJson.load response.body
187
+ end
188
+
189
+ # Checks the Gitkit response
190
+ #
191
+ # @api private
192
+ # @param [JSON] response the response received
193
+ # @return [JSON] the response if no error
194
+ def check_gitkit_error(response)
195
+ if response.has_key? 'error'
196
+ error = response['error']
197
+ if error.has_key? 'code'
198
+ code = error['code']
199
+ raise GitkitClientError, error['message'] if code.to_s.match(/^4/)
200
+ raise GitkitServerError, error['message']
201
+ else
202
+ raise GitkitServerError, 'null error code from Gitkit server'
203
+ end
204
+ else
205
+ response
206
+ end
207
+ end
208
+ end
209
+ end
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gitkit-ruby-client
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease:
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 0
10
+ version: 1.0.0
11
+ platform: ruby
12
+ authors:
13
+ - Jin Liu
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2014-05-15 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: addressable
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 7
29
+ segments:
30
+ - 2
31
+ - 3
32
+ - 2
33
+ version: 2.3.2
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: faraday
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ hash: 59
45
+ segments:
46
+ - 0
47
+ - 9
48
+ - 0
49
+ version: 0.9.0
50
+ type: :runtime
51
+ version_requirements: *id002
52
+ - !ruby/object:Gem::Dependency
53
+ name: multi_json
54
+ prerelease: false
55
+ requirement: &id003 !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ hash: 23
61
+ segments:
62
+ - 1
63
+ - 0
64
+ - 0
65
+ version: 1.0.0
66
+ type: :runtime
67
+ version_requirements: *id003
68
+ - !ruby/object:Gem::Dependency
69
+ name: jwt
70
+ prerelease: false
71
+ requirement: &id004 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - "="
75
+ - !ruby/object:Gem::Version
76
+ hash: 13
77
+ segments:
78
+ - 0
79
+ - 1
80
+ - 11
81
+ version: 0.1.11
82
+ type: :runtime
83
+ version_requirements: *id004
84
+ description: Google Identity Toolkit Ruby client library
85
+ email:
86
+ executables: []
87
+
88
+ extensions: []
89
+
90
+ extra_rdoc_files:
91
+ - README.rdoc
92
+ files:
93
+ - lib/gitkit_client.rb
94
+ - lib/rpc_helper.rb
95
+ - README.rdoc
96
+ homepage: https://developers.google.com/identity-toolkit/v3
97
+ licenses: []
98
+
99
+ post_install_message:
100
+ rdoc_options: []
101
+
102
+ require_paths:
103
+ - lib
104
+ required_ruby_version: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ hash: 3
110
+ segments:
111
+ - 0
112
+ version: "0"
113
+ required_rubygems_version: !ruby/object:Gem::Requirement
114
+ none: false
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ hash: 3
119
+ segments:
120
+ - 0
121
+ version: "0"
122
+ requirements: []
123
+
124
+ rubyforge_project:
125
+ rubygems_version: 1.8.15
126
+ signing_key:
127
+ specification_version: 4
128
+ summary: Google Identity Toolkit Ruby client
129
+ test_files: []
130
+