tozny-auth 0.1.6 → 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cfe8301a7185fc15b5db4e783ef36c19c148d3aa
4
- data.tar.gz: e3ee082717e5820b05944c7bbb5fbbf667416eb2
3
+ metadata.gz: e41382cfb8a2420e58e304d53d9750c71b7046de
4
+ data.tar.gz: b6dc52cfddc4209c57202ccaa3110548fbb8e560
5
5
  SHA512:
6
- metadata.gz: a7f9300e08f52fd63a83b19ccfebfbcf84fdf0b0e3bd8335781043fa3da6b3b857e4d64a17d4bf04b2651edc1ed0ea66ce33ef3b252ed718f75a241efa17e4eb
7
- data.tar.gz: 3dfef0a874d67a6415b564ddee401995176e5b2a2cc48b2fdf706ced4eb7a9fd13b99d64679957050fbfe22833f7c67352a453120a02fa2f79f52080e9dd1c04
6
+ metadata.gz: 3efa4daf484e74ad5a7fe404b134e804025f8cc481973b42ce174848d9b6854b46f22682fa3583b1a3f65732b72d5d82219ea908d702ce0e2298f1beae21d610
7
+ data.tar.gz: ba7dbf7c62dcfddb841bfa0c71b1b7224a083e0b392fc3c744b558907c0d1c51f472b927e280a372154734c31feaf28e9deb1a44aa55aff77c6d723cc20e27bd
@@ -1,8 +1,8 @@
1
- Metrics/LineLength:
2
- Enabled: false
3
- Metrics/MethodLength:
4
- Enabled: false
5
- Rails:
6
- Enabled: false
7
- Style/SpaceInsideHashLiteralBraces:
1
+ Metrics/LineLength:
2
+ Enabled: false
3
+ Metrics/MethodLength:
4
+ Enabled: false
5
+ Rails:
6
+ Enabled: false
7
+ Style/SpaceInsideHashLiteralBraces:
8
8
  Enabled: false
@@ -1,14 +1,14 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "bundler/setup"
4
- require "tozny/auth"
5
-
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
-
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
12
-
13
- require "irb"
14
- IRB.start
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "tozny/auth"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup CHANGED
@@ -1,8 +1,8 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -1,64 +1,64 @@
1
- require 'openssl'
2
- require 'base64'
3
- require 'json'
4
- require 'securerandom'
5
-
6
- module Tozny
7
- # utility class for tozny-specific cryptography and encoding
8
- class Core
9
-
10
- # encodes a string according to the base64url specification, including removing padding
11
- # @param [String] str the string to encode
12
- # @return [String] the base64url-encoded string
13
- def self.base64url_encode(str)
14
- Base64::strict_encode64(
15
- str
16
- )
17
- .tr('+/', '-_')
18
- .tr('=', '')
19
- end
20
-
21
- # decodes a padding-stripped base64url string
22
- # @param [String] str the base64url-encoded string
23
- # @return [String] the decoded plaintext string
24
- def self.base64url_decode(str)
25
- Base64::strict_decode64(
26
- str.tr('-_', '+/')
27
- .ljust(str.length+(str.length % 4), '='))
28
- # replace - with + and _ with /
29
- # then add padding
30
- end
31
-
32
- # checks the HMAC/SHA256 signature of a string
33
- # @param [String] signature the signature to check against
34
- # @param [String] str the signed data to check the signature against
35
- # @param [String] secret the secret to check the signature against
36
- # @return [TrueClass, FalseClass] whether the signature matched
37
- def self.check_signature(signature, str, secret)
38
- expected_sig = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), secret, str)
39
- expected_sig == signature
40
- end
41
-
42
- # base64url encodes and signs some data
43
- # * yields a base64url-encoded data object AND base64url-encoded signature
44
- # * the signature signs the base64-encoded data, NOT the raw data
45
- # @param [String] data the raw data to be encoded
46
- # @param [String] secret the secret to sign the encoded data with
47
- # @return [Hash] a hash including the signed_data and a signature
48
- def self.encode_and_sign(data, secret)
49
- encoded_data = base64url_encode(data)
50
- sig = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), secret, encoded_data)
51
- encoded_sig = base64url_encode(sig)
52
- {
53
- signed_data: encoded_data,
54
- signature: encoded_sig
55
- }
56
- end
57
-
58
- # generates a nonce (number used once)
59
- # @return [String] a hexadecimal nonce
60
- def self.generate_nonce
61
- OpenSSL::Digest::SHA256.hexdigest SecureRandom.random_bytes(8)
62
- end
63
- end
64
- end
1
+ require 'openssl'
2
+ require 'base64'
3
+ require 'json'
4
+ require 'securerandom'
5
+
6
+ module Tozny
7
+ # utility class for tozny-specific cryptography and encoding
8
+ class Core
9
+
10
+ # encodes a string according to the base64url specification, including removing padding
11
+ # @param [String] str the string to encode
12
+ # @return [String] the base64url-encoded string
13
+ def self.base64url_encode(str)
14
+ Base64::strict_encode64(
15
+ str
16
+ )
17
+ .tr('+/', '-_')
18
+ .tr('=', '')
19
+ end
20
+
21
+ # decodes a padding-stripped base64url string
22
+ # @param [String] str the base64url-encoded string
23
+ # @return [String] the decoded plaintext string
24
+ def self.base64url_decode(str)
25
+ Base64::strict_decode64(
26
+ str.tr('-_', '+/')
27
+ .ljust(str.length+(str.length % 4), '='))
28
+ # replace - with + and _ with /
29
+ # then add padding
30
+ end
31
+
32
+ # checks the HMAC/SHA256 signature of a string
33
+ # @param [String] signature the signature to check against
34
+ # @param [String] str the signed data to check the signature against
35
+ # @param [String] secret the secret to check the signature against
36
+ # @return [TrueClass, FalseClass] whether the signature matched
37
+ def self.check_signature(signature, str, secret)
38
+ expected_sig = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), secret, str)
39
+ expected_sig == signature
40
+ end
41
+
42
+ # base64url encodes and signs some data
43
+ # * yields a base64url-encoded data object AND base64url-encoded signature
44
+ # * the signature signs the base64-encoded data, NOT the raw data
45
+ # @param [String] data the raw data to be encoded
46
+ # @param [String] secret the secret to sign the encoded data with
47
+ # @return [Hash] a hash including the signed_data and a signature
48
+ def self.encode_and_sign(data, secret)
49
+ encoded_data = base64url_encode(data)
50
+ sig = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), secret, encoded_data)
51
+ encoded_sig = base64url_encode(sig)
52
+ {
53
+ signed_data: encoded_data,
54
+ signature: encoded_sig
55
+ }
56
+ end
57
+
58
+ # generates a nonce (number used once)
59
+ # @return [String] a hexadecimal nonce
60
+ def self.generate_nonce
61
+ OpenSSL::Digest::SHA256.hexdigest SecureRandom.random_bytes(8)
62
+ end
63
+ end
64
+ end
@@ -1,5 +1,5 @@
1
1
  module Tozny
2
2
  module Auth
3
- VERSION = '0.1.6'
3
+ VERSION = '0.1.7'
4
4
  end
5
5
  end
@@ -1,274 +1,303 @@
1
- require 'tozny/user'
2
- require 'json'
3
- require 'net/http'
4
- require 'uri'
5
-
6
- # rubocop:disable Metrics/PerceivedComplexity
7
-
8
- module Tozny
9
- class Realm
10
- attr_accessor :realm_key_id, :realm_secret, :api_url, :user_api
11
-
12
- def initialize(realm_key_id, realm_secret, api_url = nil)
13
- # set the API URL
14
- if !api_url.nil?
15
- self.api_url = api_url
16
- elsif !(ENV['API_URL'].nil?)
17
- self.api_url = ENV['API_URL']
18
- else
19
- self.api_url = 'https://api.tozny.com/index.php'
20
- end
21
- unless self.api_url.is_a? URI # don't try to parse a URI instance into a URI, as this will break
22
- self.api_url = URI.parse(self.api_url)
23
- end
24
-
25
- set_new_realm(realm_key_id, realm_secret)
26
-
27
- end
28
-
29
- # use a new realm_key_id and realm_secret. updates the user_api handle to reflect this change as well.
30
- # @param [String] realm_key_id
31
- # @param [String] realm_secret
32
- # @return [TrueClass] will always return true
33
- def set_new_realm(realm_key_id, realm_secret)
34
- self.realm_key_id = realm_key_id
35
- self.realm_secret = realm_secret
36
- if user_api.is_a? ::Tozny::User
37
- user_api.set_new_realm(realm_key_id)
38
- else
39
- self.user_api = ::Tozny::User.new(realm_key_id, api_url)
40
- end
41
- true
42
- end
43
-
44
- # verify a login and extract user information from a signed packet forwarded to the server
45
- # @param [String] signed_data the base64URL data to validate
46
- # @param [String] signature the string representation of the signature to check the login with
47
- # @return [Hash, FalseClass] the login information or false if the login did not check out
48
- def check_login_locally(signed_data, signature)
49
- if check_signature(signed_data, signature)
50
- login_info = JSON.parse(::Tozny::Core.base64url_decode(signed_data))
51
- return false if login_info[:expires_at] < Time.now.to_i
52
- login_info
53
- else
54
- false
55
- end
56
- end
57
-
58
- # verify a login from a user and session id. Does not return complete login information.
59
- # @param [String] user_id the user_id of the login to check
60
- # @param [String] session_id the session_id of the login to check
61
- # @return [Hash] the return from the API
62
- def check_login_via_api(user_id, session_id) # NOTE: this only returns true/false. You need to parse the data locally. See Tozny::Core.base64url_decode
63
- raw_call(
64
- method: 'realm.check_valid_login',
65
- user_id: user_id,
66
- session_id: session_id
67
- )[:return] == 'true'
68
- end
69
-
70
- # Add a user to a closed realm
71
- # @param [String] defer 'true' or 'false', defines whether the user should be deferred to later be completed by the app
72
- # @param [Hash] meta any metadata to be added to the user (eg, favorite color or mother's home address). All meta will be stored as strings
73
- # @param [String, OpenSSL::PKey::RSA] pub_key the public key of the user to be added. Only necessary
74
- # @return [Hash, FalseClass] the user in its current (incomplete if defer is 'true' state)
75
- # @raise ArgumentError if there is no pubkey when there should be one
76
- def user_add(defer = 'false', meta = nil, pub_key)
77
- unless pub_key.nil?
78
- pub_key = OpenSSL::PKey::RSA.new pub_key if pub_key.is_a? String
79
- pub_key = pub_key.public_key if pub_key.private?
80
- end
81
-
82
- request_obj = {
83
- method: 'realm.user_add',
84
- defer: defer
85
- }
86
- if defer == 'false'
87
- raise ArgumentError, 'Must provide a public key if not using deferred enrollment' if pub_key.nil?
88
- request_obj[:pub_key] = pub_key
89
- end
90
-
91
- unless meta.nil?
92
- request_obj[:extra_fields] = Tozny::Core.base64url_encode(meta.to_json)
93
- end
94
-
95
- user = raw_call request_obj
96
- return false unless user[:return] == 'ok'
97
- user
98
- end
99
-
100
- # update a user's meta fields
101
- # * Note: all meta fields are stored as strings
102
- # @param [String] user_id
103
- # @param [Hash{Symbol,String=>Object}] meta the metadata fields to update, along with their new values
104
- # @return [Hash] the updated user
105
- def user_update(user_id, meta)
106
- raw_call(
107
- method: 'realm.user_update',
108
- user_id: user_id,
109
- extra_fields: Tozny::Core.base64url_encode(meta.to_json)
110
- )
111
- end
112
-
113
- # @param [String] user_id
114
- # @return [Hash] the result of the request to the API
115
- def user_delete(user_id)
116
- raw_call(
117
- method: 'realm.user_delete',
118
- user_id: user_id
119
- )
120
- end
121
-
122
- # retrieve a user's information
123
- # * Note: all meta fields are stored as strings
124
- # @param [String] user_id the id or email (if is_id = false) of the user to get
125
- # @param [TrueClass, FalseClass] is_id true if looking up the user by id, false if looking up by email. defaults to true
126
- # @return [Hash] the user's information
127
- # @raise ArgumentError on failed lookup
128
- def user_get(user_id, is_id = true)
129
- request_obj = {
130
- method: 'realm.user_get'
131
- }
132
- if is_id
133
- request_obj[:user_id] = user_id
134
- else
135
- request_obj[:tozny_email] = user_id
136
- end
137
-
138
- user = raw_call(request_obj)
139
- if user.nil? || user[:results].nil?
140
- raise ArgumentError, ('No user was found for ' + (is_id ? 'id' : 'email') + ': ' + user_id + '.')
141
- end
142
- user[:results]
143
- end
144
-
145
- # performs a device add call
146
- # @param [String] user_id
147
- # @return [Hash] the result of the call: keys include :user_id, :temp_key, :secret_enrollment_url, and :key_id
148
- def user_device_add(user_id)
149
- raw_call(
150
- method: 'realm.user_device_add',
151
- user_id: user_id
152
- )
153
- end
154
-
155
- # create an OOB challenge question session
156
- # @param [Hash<Symbol, String>, String] question either a question hash, as specified by the options, or the text of a question to be presented to the user. Required.
157
- # @option question [String] :question The text of the question to be presented to the user
158
- # @option question [String] :success The URL the user's browser should be redirected to after successful authentication
159
- # @option question [String] :error The URL the user's browser should be redirected to after unsuccessful authentication
160
- # @param [String] success_url The URL the user's browser should be redirected to after successful authentication if not specified in the question object
161
- # @param [String] error_url The URL the user's browser should be redirected to after unsuccessful authentication if not specified in the question object
162
- # @param [String] user_id optional. The user who should answer the question.
163
- # @raise ArgumentError on invalid question type
164
- # TODO: support URI objects instead of strings for success and error
165
- # @return [Hash] the result of the API call
166
- def question_challenge(question, success_url, error_url, user_id)
167
- raise ArgumentError, 'question must either be a string or an options hash as specified' unless (question.is_a?String) || (question.is_a?Hash)
168
- final_question = nil # scope final_question and prevent linting errors
169
- if question.is_a?Hash
170
- final_question = question
171
- final_question[:type] = 'callback'
172
- elsif (success_url.is_a?String) || (error_url.is_a?String)
173
- final_question = {
174
- type: 'callback',
175
- question: question
176
- }
177
- final_question[:success] = success_url if success_url.is_a?String
178
- final_question[:error] = error_url if error_url.is_a?String
179
- else
180
- final_question = {
181
- type: 'question',
182
- question: question
183
- }
184
- end
185
- request_obj = {
186
- method: 'realm.question_challenge',
187
- question: final_question
188
- }
189
- request_obj[:user_id] = user_id if user_id.is_a?String
190
- raw_call request_obj
191
- end
192
-
193
- # Create an OTP challenge session
194
- # @return [Hash] a hash [session_id, presence] containing an OTP session id and an OTP presence (an alias for a type-destination combination)
195
- # @param [String] type one of 'sms-otp-6', 'sms-otp-8': the type of the OTP to send
196
- # @param [String] destination the destination for the OTP. For an SMS OTP, this should be a phone number
197
- # @param [String] presence can be used instead of 'type' and 'destination': an OTP presence provided by the TOZNY API
198
- # @param [Object] data optional passthru data to be added to the signed response on a successful request
199
- # @raise ArgumentError when not enough information to submit an OTP request
200
- # @raise ArgumentError on invalid request type
201
- def otp_challenge(type = nil, destination = nil, presence = nil, data = nil)
202
- raise ArgumentError, 'must provide either a presence or a type and destination' if (type.nil? || destination.nil?) && presence.nil?
203
- request_obj = {
204
- method: 'realm.otp_challenge'
205
- }
206
- unless data.nil?
207
- data = data.to_json unless data.is_a?String
208
- request_obj[:data] = data
209
- end
210
-
211
- if presence.nil?
212
- raise ArgumentError, "request type must one of 'sms-otp-6' or 'sms-otp-8'" unless %w(sms-otp-6 sms-otp-8).include? type
213
- request_obj[:type] = type
214
- # TODO: consider validating that 'destination' is a valid phone number when 'type' is sms-otp-*
215
- request_obj[:destination] = destination
216
- else
217
- request_obj[:presence] = presence
218
- end
219
- raw_call request_obj
220
- end
221
-
222
- # Shorthand method to send an 6-digit OTP via SMS without a presence token
223
- # @param destination @see(otp_challenge)
224
- # @param data @see(otp_challenge)
225
- # @return @see(otp_challenge)
226
- def sms_otp(destination, data = nil)
227
- # TODO: validate that 'destination' is a phone number
228
- otp_challenge('sms-otp-6', destination, nil, data)
229
- end
230
-
231
- # push to a user's device based off of their id, email, or username
232
- # @raise ArgumentError when not enough information is provided to find the user
233
- # @param [String] session_id optional a valid Tozny session_id
234
- # @param [String] user_id optional a Tozny user_id
235
- # @param [String] email optional an email for a field associated with tozny_email
236
- # @param [String] username optional a username for a field associated with tozny_username
237
- # @return [Hash {Symbol => String, Integer, Array<TrueClass, FalseClass>}] the result of the push attempt. Will be true if any device owned by the user is successfully sent a push.
238
- def user_push(session_id = nil, user_id = nil, email = nil, username = nil)
239
- raise ArgumentError, 'must provide either a Tozny user id, a tozny_email, or a tozny_username in order to find the user to push to' if
240
- user_id.nil? && email.nil? && username.nil?
241
- session_id ||= user_api.login_challenge[:session_id]
242
- request_obj = {
243
- method: 'realm.user_push',
244
- session_id: session_id
245
- }
246
- if !user_id.nil?
247
- request_obj[:user_id] = user_id
248
- elsif !email.nil?
249
- request_obj[:tozny_email] = email
250
- elsif !username.nil?
251
- request_obj[:tozny_username] = username
252
- end
253
- raw_call request_obj
254
- end
255
-
256
- # perform a raw(ish) API call
257
- # @param [Hash{Symbol, String => Object}] request_obj The request to conduct. Should include a :method at the least. Prefer symbol keys to string keys
258
- # @return [Object] The parsed result of the request. NOTE: most types will be stringified for most requests
259
- def raw_call(request_obj)
260
- request_obj[:nonce] = Tozny::Core.generate_nonce # generate the nonce
261
- request_obj[:expires_at] = Time.now.to_i + 5 * 60 # UNIX timestamp for now +5 min TODO: does this work with check_login_via_api, or should it default to a passed in expires_at?
262
- unless request_obj.key?('realm_key_id') || request_obj.key?(:realm_key_id) # check for both string and symbol
263
- # TODO: how should we handle conflicts of symbol and string keys?
264
- request_obj[:realm_key_id] = realm_key_id
265
- end
266
- encoded_params = Tozny::Core.encode_and_sign(request_obj.to_json, realm_secret) # make a proper request of it.
267
- request_url = api_url # copy the URL to a local variable so that we can add the query params
268
- request_url.query = URI.encode_www_form encoded_params # encode signed_data and signature as query params
269
- # p request_url
270
- http_result = Net::HTTP.get(request_url)
271
- JSON.parse(http_result, symbolize_names: true) # TODO: handle errors
272
- end
273
- end
274
- end
1
+ require 'tozny/user'
2
+ require 'json'
3
+ require 'net/http'
4
+ require 'uri'
5
+
6
+ # rubocop:disable Metrics/PerceivedComplexity
7
+
8
+ module Tozny
9
+ class Realm
10
+ attr_accessor :realm_key_id, :realm_secret, :api_url, :user_api
11
+
12
+ def initialize(realm_key_id, realm_secret, api_url = nil)
13
+ # set the API URL
14
+ if !api_url.nil?
15
+ self.api_url = api_url
16
+ elsif !(ENV['API_URL'].nil?)
17
+ self.api_url = ENV['API_URL']
18
+ else
19
+ self.api_url = 'https://api.tozny.com/index.php'
20
+ end
21
+ unless self.api_url.is_a? URI # don't try to parse a URI instance into a URI, as this will break
22
+ self.api_url = URI.parse(self.api_url)
23
+ end
24
+
25
+ set_new_realm(realm_key_id, realm_secret)
26
+
27
+ end
28
+
29
+ # use a new realm_key_id and realm_secret. updates the user_api handle to reflect this change as well.
30
+ # @param [String] realm_key_id
31
+ # @param [String] realm_secret
32
+ # @return [TrueClass] will always return true
33
+ def set_new_realm(realm_key_id, realm_secret)
34
+ self.realm_key_id = realm_key_id
35
+ self.realm_secret = realm_secret
36
+ if user_api.is_a? ::Tozny::User
37
+ user_api.set_new_realm(realm_key_id)
38
+ else
39
+ self.user_api = ::Tozny::User.new(realm_key_id, api_url)
40
+ end
41
+ true
42
+ end
43
+
44
+ # verify a login and extract user information from a signed packet forwarded to the server
45
+ # @param [String] signed_data the base64URL data to validate
46
+ # @param [String] signature the string representation of the signature to check the login with
47
+ # @return [Hash, FalseClass] the login information or false if the login did not check out
48
+ def check_login_locally(signed_data, signature)
49
+ if check_signature(signed_data, signature)
50
+ login_info = JSON.parse(::Tozny::Core.base64url_decode(signed_data))
51
+ return false if login_info[:expires_at] < Time.now.to_i
52
+ login_info
53
+ else
54
+ false
55
+ end
56
+ end
57
+
58
+ # verify a login from a user and session id. Does not return complete login information.
59
+ # @param [String] user_id the user_id of the login to check
60
+ # @param [String] session_id the session_id of the login to check
61
+ # @return [Hash] the return from the API
62
+ def check_login_via_api(user_id, session_id) # NOTE: this only returns true/false. You need to parse the data locally. See Tozny::Core.base64url_decode
63
+ raw_call(
64
+ method: 'realm.check_valid_login',
65
+ user_id: user_id,
66
+ session_id: session_id
67
+ )[:return] == 'true'
68
+ end
69
+
70
+ # Add a user to a closed realm
71
+ # @param [String] defer 'true' or 'false', defines whether the user should be deferred to later be completed by the app
72
+ # @param [Hash] meta any metadata to be added to the user (eg, favorite color or mother's home address). All meta will be stored as strings
73
+ # @param [String, OpenSSL::PKey::RSA] pub_key the public key of the user to be added. Only necessary
74
+ # @return [Hash, FalseClass] the user in its current (incomplete if defer is 'true' state)
75
+ # @raise ArgumentError if there is no pubkey when there should be one
76
+ def user_add(defer = 'false', meta = nil, pub_key)
77
+ unless pub_key.nil?
78
+ pub_key = OpenSSL::PKey::RSA.new pub_key if pub_key.is_a? String
79
+ pub_key = pub_key.public_key if pub_key.private?
80
+ end
81
+
82
+ request_obj = {
83
+ method: 'realm.user_add',
84
+ defer: defer
85
+ }
86
+ if defer == 'false'
87
+ raise ArgumentError, 'Must provide a public key if not using deferred enrollment' if pub_key.nil?
88
+ request_obj[:pub_key] = pub_key
89
+ end
90
+
91
+ unless meta.nil?
92
+ request_obj[:extra_fields] = Tozny::Core.base64url_encode(meta.to_json)
93
+ end
94
+
95
+ user = raw_call request_obj
96
+ return false unless user[:return] == 'ok'
97
+ user
98
+ end
99
+
100
+ # update a user's meta fields
101
+ # * Note: all meta fields are stored as strings
102
+ # @param [String] user_id
103
+ # @param [Hash{Symbol,String=>Object}] meta the metadata fields to update, along with their new values
104
+ # @return [Hash] the updated user
105
+ def user_update(user_id, meta)
106
+ raw_call(
107
+ method: 'realm.user_update',
108
+ user_id: user_id,
109
+ extra_fields: Tozny::Core.base64url_encode(meta.to_json)
110
+ )
111
+ end
112
+
113
+ # @param [String] user_id
114
+ # @return [Hash] the result of the request to the API
115
+ def user_delete(user_id)
116
+ raw_call(
117
+ method: 'realm.user_delete',
118
+ user_id: user_id
119
+ )
120
+ end
121
+
122
+ # retrieve a user's information
123
+ # * Note: all meta fields are stored as strings
124
+ # @param [String] user_id the id or email (if is_id = false) of the user to get
125
+ # @param [TrueClass, FalseClass] is_id true if looking up the user by id, false if looking up by email. defaults to true
126
+ # @return [Hash] the user's information
127
+ # @raise ArgumentError on failed lookup
128
+ def user_get(user_id, is_id = true)
129
+ request_obj = {
130
+ method: 'realm.user_get'
131
+ }
132
+ if is_id
133
+ request_obj[:user_id] = user_id
134
+ else
135
+ request_obj[:tozny_email] = user_id
136
+ end
137
+
138
+ user = raw_call(request_obj)
139
+ if user.nil? || user[:results].nil?
140
+ raise ArgumentError, ('No user was found for ' + (is_id ? 'id' : 'email') + ': ' + user_id + '.')
141
+ end
142
+ user[:results]
143
+ end
144
+
145
+ # performs a device add call
146
+ # @param [String] user_id
147
+ # @return [Hash] the result of the call: keys include :user_id, :temp_key, :secret_enrollment_url, and :key_id
148
+ def user_device_add(user_id)
149
+ raw_call(
150
+ method: 'realm.user_device_add',
151
+ user_id: user_id
152
+ )
153
+ end
154
+
155
+ # create an OOB challenge question session
156
+ # @param [Hash<Symbol, String>, String] question either a question hash, as specified by the options, or the text of a question to be presented to the user. Required.
157
+ # @option question [String] :question The text of the question to be presented to the user
158
+ # @option question [String] :success The URL the user's browser should be redirected to after successful authentication
159
+ # @option question [String] :error The URL the user's browser should be redirected to after unsuccessful authentication
160
+ # @param [String] success_url The URL the user's browser should be redirected to after successful authentication if not specified in the question object
161
+ # @param [String] error_url The URL the user's browser should be redirected to after unsuccessful authentication if not specified in the question object
162
+ # @param [String] user_id optional. The user who should answer the question.
163
+ # @raise ArgumentError on invalid question type
164
+ # TODO: support URI objects instead of strings for success and error
165
+ # @return [Hash] the result of the API call
166
+ def question_challenge(question, success_url, error_url, user_id)
167
+ raise ArgumentError, 'question must either be a string or an options hash as specified' unless (question.is_a?String) || (question.is_a?Hash)
168
+ final_question = nil # scope final_question and prevent linting errors
169
+ if question.is_a?Hash
170
+ final_question = question
171
+ final_question[:type] = 'callback'
172
+ elsif (success_url.is_a?String) || (error_url.is_a?String)
173
+ final_question = {
174
+ type: 'callback',
175
+ question: question
176
+ }
177
+ final_question[:success] = success_url if success_url.is_a?String
178
+ final_question[:error] = error_url if error_url.is_a?String
179
+ else
180
+ final_question = {
181
+ type: 'question',
182
+ question: question
183
+ }
184
+ end
185
+ request_obj = {
186
+ method: 'realm.question_challenge',
187
+ question: final_question
188
+ }
189
+ request_obj[:user_id] = user_id if user_id.is_a?String
190
+ raw_call request_obj
191
+ end
192
+
193
+ # Create an OTP challenge session
194
+ # @return [Hash] a hash [session_id, presence] containing an OTP session id and an OTP presence (an alias for a type-destination combination)
195
+ # @param [String] type one of 'sms-otp-6', 'sms-otp-8': the type of the OTP to send
196
+ # @param [String] destination the destination for the OTP. For an SMS OTP, this should be a phone number
197
+ # @param [String] presence can be used instead of 'type' and 'destination': an OTP presence provided by the TOZNY API
198
+ # @param [Object] data optional passthru data to be added to the signed response on a successful request
199
+ # @param [String] context One of "verify," "authenticate," or "enroll"
200
+ # @raise ArgumentError when not enough information to submit an OTP request
201
+ # @raise ArgumentError on invalid request type
202
+ def otp_challenge(type = nil, destination = nil, presence = nil, data = nil, context = nil)
203
+ raise ArgumentError, 'must provide either a presence or a type and destination' if (type.nil? || destination.nil?) && presence.nil?
204
+ request_obj = {
205
+ method: 'realm.otp_challenge'
206
+ }
207
+ unless data.nil?
208
+ data = data.to_json unless data.is_a?String
209
+ request_obj[:data] = data
210
+ end
211
+
212
+ if presence.nil?
213
+ raise ArgumentError, "request type must one of 'sms-otp-6' or 'sms-otp-8'" unless %w(sms-otp-6 sms-otp-8).include? type
214
+ request_obj[:type] = type
215
+ # TODO: consider validating that 'destination' is a valid phone number when 'type' is sms-otp-*
216
+ request_obj[:destination] = destination
217
+ else
218
+ request_obj[:presence] = presence
219
+ end
220
+
221
+ request_obj[:context] = context unless context.nil?
222
+
223
+ raw_call request_obj
224
+ end
225
+
226
+ # Create a magic link challenge session
227
+ #
228
+ # @param [String] destination The email address or phone number to which we will send a challenge
229
+ # @param [String] endpoint URL endpoint to use as a base for the challenge URL
230
+ # @param [Integer] lifespan Number of seconds for which the challenge URL is valid
231
+ # @param [String] context One of "verify," "authenticate," or "enroll"
232
+ # @param [Bool] send Flag whether or not to send the email (if false will return the OTP URL instead)
233
+ # @param [String] data JSON-encoded string of data to be signed along with the request
234
+ #
235
+ # @return [Hash] a hash of the session and presence for the challenge
236
+ #
237
+ def link_challenge(destination, endpoint, lifespan = nil, context = nil, send = true, data = nil)
238
+ request_obj = {
239
+ method: 'realm.link_challenge',
240
+ destination: destination,
241
+ endpoint: endpoint,
242
+ send: !! send ? 'yes' : 'no' # Hacky double-bang to convert nil to false and anything else into a boolean
243
+ }
244
+ request_obj['lifespan'] = lifespan unless lifespan.nil?
245
+ request_obj['context'] = context unless context.nil?
246
+ request_obj['data'] = data unless data.nil?
247
+
248
+ raw_call request_obj
249
+ end
250
+
251
+ # Shorthand method to send an 6-digit OTP via SMS without a presence token
252
+ # @param destination @see(otp_challenge)
253
+ # @param data @see(otp_challenge)
254
+ # @return @see(otp_challenge)
255
+ def sms_otp(destination, data = nil)
256
+ # TODO: validate that 'destination' is a phone number
257
+ otp_challenge('sms-otp-6', destination, nil, data)
258
+ end
259
+
260
+ # push to a user's device based off of their id, email, or username
261
+ # @raise ArgumentError when not enough information is provided to find the user
262
+ # @param [String] session_id optional a valid Tozny session_id
263
+ # @param [String] user_id optional a Tozny user_id
264
+ # @param [String] email optional an email for a field associated with tozny_email
265
+ # @param [String] username optional a username for a field associated with tozny_username
266
+ # @return [Hash {Symbol => String, Integer, Array<TrueClass, FalseClass>}] the result of the push attempt. Will be true if any device owned by the user is successfully sent a push.
267
+ def user_push(session_id = nil, user_id = nil, email = nil, username = nil)
268
+ raise ArgumentError, 'must provide either a Tozny user id, a tozny_email, or a tozny_username in order to find the user to push to' if
269
+ user_id.nil? && email.nil? && username.nil?
270
+ session_id ||= user_api.login_challenge[:session_id]
271
+ request_obj = {
272
+ method: 'realm.user_push',
273
+ session_id: session_id
274
+ }
275
+ if !user_id.nil?
276
+ request_obj[:user_id] = user_id
277
+ elsif !email.nil?
278
+ request_obj[:tozny_email] = email
279
+ elsif !username.nil?
280
+ request_obj[:tozny_username] = username
281
+ end
282
+ raw_call request_obj
283
+ end
284
+
285
+ # perform a raw(ish) API call
286
+ # @param [Hash{Symbol, String => Object}] request_obj The request to conduct. Should include a :method at the least. Prefer symbol keys to string keys
287
+ # @return [Object] The parsed result of the request. NOTE: most types will be stringified for most requests
288
+ def raw_call(request_obj)
289
+ request_obj[:nonce] = Tozny::Core.generate_nonce # generate the nonce
290
+ request_obj[:expires_at] = Time.now.to_i + 5 * 60 # UNIX timestamp for now +5 min TODO: does this work with check_login_via_api, or should it default to a passed in expires_at?
291
+ unless request_obj.key?('realm_key_id') || request_obj.key?(:realm_key_id) # check for both string and symbol
292
+ # TODO: how should we handle conflicts of symbol and string keys?
293
+ request_obj[:realm_key_id] = realm_key_id
294
+ end
295
+ encoded_params = Tozny::Core.encode_and_sign(request_obj.to_json, realm_secret) # make a proper request of it.
296
+ request_url = api_url # copy the URL to a local variable so that we can add the query params
297
+ request_url.query = URI.encode_www_form encoded_params # encode signed_data and signature as query params
298
+ # p request_url
299
+ http_result = Net::HTTP.get(request_url)
300
+ JSON.parse(http_result, symbolize_names: true) # TODO: handle errors
301
+ end
302
+ end
303
+ end
@@ -1,93 +1,153 @@
1
- require 'tozny/auth/common'
2
-
3
- module Tozny
4
- class User
5
- attr_accessor :realm_key_id, :api_url
6
- def initialize(realm_key_id, api_url = nil)
7
- if !api_url.nil?
8
- self.api_url = api_url
9
- elsif !(ENV['API_URL'].nil?) # rubocop:disable all
10
- self.api_url = ENV['API_URL']
11
- else
12
- self.api_url = 'https://api.tozny.com/index.php'
13
- end
14
-
15
- self.api_url = URI.parse(self.api_url) unless self.api_url.is_a? URI
16
-
17
- set_new_realm(realm_key_id)
18
- end
19
-
20
- # check the status of a session
21
- # @param [String] session_id the session to check
22
- # @return [TrueClass, FalseClass] 'true' if the use has logged in with the session, false otherwise
23
- def check_session_status(session_id)
24
- raw_call(
25
- method: 'user.check_session_status',
26
- session_id: session_id
27
- ).key?(:signed_data)
28
- end
29
-
30
- # use a new realm_key_id
31
- # @param [String] realm_key_id
32
- # @return [TrueClass] will always return true
33
- def set_new_realm(realm_key_id) # rubocop:disable Style/AccessorMethodName
34
- self.realm_key_id = realm_key_id
35
- true
36
- end
37
-
38
- # Generate a new login challenge session
39
- # @param [TrueClass, FalseClass] user_add optional: whether or not to create an enrollment challenge rather than an authentication challenge
40
- # @return [Hash] a challenge session (:challenge, :realm_key_id, :session_id, :qr_url, :mobile_url, :created_at, :presence = "")
41
- def login_challenge(user_add = nil)
42
- request_obj = {
43
- method: 'user.login_challenge'
44
- }
45
- request_obj[:user_add] = user_add if user_add
46
- raw_call request_obj
47
- end
48
-
49
- # Create an OTP challenge session
50
- # @return [Hash] a hash [session_id, presence] containing an OTP session id and an OTP presence (an alias for a type-destination combination)
51
- # @param [String] type one of 'sms-otp-6', 'sms-otp-8': the type of the OTP to send
52
- # @param [String] destination the destination for the OTP. For an SMS OTP, this should be a phone number
53
- # @param [String] presence can be used instead of 'type' and 'destination': an OTP presence provided by the TOZNY API
54
- # @raise ArgumentError when not enough information to submit an OTP request
55
- # @raise ArgumentError on invalid request type
56
- def otp_challenge(type = nil, destination = nil, presence = nil)
57
- raise ArgumentError, 'must provide either a presence or a type and destination' if (type.nil? || destination.nil?) && presence.nil?
58
- request_obj = {
59
- method: 'user.otp_challenge'
60
- }
61
- if presence.nil?
62
- raise ArgumentError, "request type must one of 'sms-otp-6' or 'sms-otp-8'" unless %w(sms-otp-6 sms-otp-8).include? type
63
- request_obj[:type] = type
64
- # TODO: consider validating that 'destination' is a valid phone number when 'type' is sms-otp-*
65
- request_obj[:destination] = destination
66
- else
67
- request_obj[:presence] = presence
68
- end
69
- raw_call request_obj
70
- end
71
-
72
- # Check an OTP against an OTP session
73
- # @param [String] session_id the OTP session to validate
74
- # @param [String, Integer] otp the OTP to check
75
- # @return [Hash] The signed_data and signature containing a session ID and metadata, if any, on success. Otherwise, error[s].
76
- def otp_result(session_id, otp)
77
- raw_call(method: 'user.otp_result', session_id: session_id, otp: otp)
78
- end
79
-
80
- # Perform a raw(ish) API call
81
- # @param [Hash{Symbol, String => Object}] request_obj The request to conduct. Should include a :method at the least. Prefer symbol keys to string keys
82
- # @return [Object] The parsed result of the request. NOTE: most types will be stringified for most requests
83
- def raw_call(request_obj)
84
- unless request_obj.key?('realm_key_id') || request_obj.key?(:realm_key_id) # check for both string and symbol
85
- # TODO: how should we handle conflicts of symbol and string keys?
86
- request_obj[:realm_key_id] = realm_key_id
87
- end
88
- request_url = api_url # copy the URL to a local variable so that we can add the query params
89
- request_url.query = URI.encode_www_form request_obj # encode request as query params
90
- JSON.parse(Net::HTTP.get(request_url), symbolize_names: true)
91
- end
92
- end
93
- end
1
+ require 'tozny/auth/common'
2
+
3
+ module Tozny
4
+ class User
5
+ attr_accessor :realm_key_id, :api_url
6
+ def initialize(realm_key_id, api_url = nil)
7
+ if !api_url.nil?
8
+ self.api_url = api_url
9
+ elsif !(ENV['API_URL'].nil?) # rubocop:disable all
10
+ self.api_url = ENV['API_URL']
11
+ else
12
+ self.api_url = 'https://api.tozny.com/index.php'
13
+ end
14
+
15
+ self.api_url = URI.parse(self.api_url) unless self.api_url.is_a? URI
16
+
17
+ set_new_realm(realm_key_id)
18
+ end
19
+
20
+ # check the status of a session
21
+ # @param [String] session_id the session to check
22
+ # @return [TrueClass, FalseClass] 'true' if the use has logged in with the session, false otherwise
23
+ def check_session_status(session_id)
24
+ raw_call(
25
+ method: 'user.check_session_status',
26
+ session_id: session_id
27
+ ).key?(:signed_data)
28
+ end
29
+
30
+ # use a new realm_key_id
31
+ # @param [String] realm_key_id
32
+ # @return [TrueClass] will always return true
33
+ def set_new_realm(realm_key_id) # rubocop:disable Style/AccessorMethodName
34
+ self.realm_key_id = realm_key_id
35
+ true
36
+ end
37
+
38
+ # Generate a new login challenge session
39
+ # @param [TrueClass, FalseClass] user_add optional: whether or not to create an enrollment challenge rather than an authentication challenge
40
+ # @return [Hash] a challenge session (:challenge, :realm_key_id, :session_id, :qr_url, :mobile_url, :created_at, :presence = "")
41
+ def login_challenge(user_add = nil)
42
+ request_obj = {
43
+ method: 'user.login_challenge'
44
+ }
45
+ request_obj[:user_add] = user_add if user_add
46
+ raw_call request_obj
47
+ end
48
+
49
+ # Create an OTP challenge session
50
+ # @return [Hash] a hash [session_id, presence] containing an OTP session id and an OTP presence (an alias for a type-destination combination)
51
+ # @param [String] type one of 'sms-otp-6', 'sms-otp-8': the type of the OTP to send
52
+ # @param [String] destination the destination for the OTP. For an SMS OTP, this should be a phone number
53
+ # @param [String] presence can be used instead of 'type' and 'destination': an OTP presence provided by the TOZNY API
54
+ # @param [String] context One of "verify," "authenticate," or "enroll"
55
+ # @raise ArgumentError when not enough information to submit an OTP request
56
+ # @raise ArgumentError on invalid request type
57
+ def otp_challenge(type = nil, destination = nil, presence = nil, context = nil)
58
+ raise ArgumentError, 'must provide either a presence or a type and destination' if (type.nil? || destination.nil?) && presence.nil?
59
+ request_obj = {
60
+ method: 'user.otp_challenge'
61
+ }
62
+ if presence.nil?
63
+ raise ArgumentError, "request type must one of 'sms-otp-6' or 'sms-otp-8'" unless %w(sms-otp-6 sms-otp-8).include? type
64
+ request_obj[:type] = type
65
+ # TODO: consider validating that 'destination' is a valid phone number when 'type' is sms-otp-*
66
+ request_obj[:destination] = destination
67
+ else
68
+ request_obj[:presence] = presence
69
+ end
70
+
71
+ request_obj[:context] = context unless context.nil?
72
+
73
+ raw_call request_obj
74
+ end
75
+
76
+ # Create a magic link challenge session
77
+ #
78
+ # @param [String] destination The email address or phone number to which we will send a challenge
79
+ # @param [String] endpoint URL endpoint to use as a base for the challenge URL
80
+ # @param [String] context One of "verify," "authenticate," or "enroll"
81
+ #
82
+ # @return [Hash] a hash of the session and presence for the challenge
83
+ #
84
+ def link_challenge(destination, endpoint, context = nil)
85
+ request_obj = {
86
+ method: 'user.link_challenge',
87
+ destination: destination,
88
+ endpoint: endpoint
89
+ }
90
+ request_obj['context'] = context unless context.nil?
91
+
92
+ raw_call request_obj
93
+ end
94
+
95
+ # Check an OTP against an OTP session
96
+ # @param [String] session_id the OTP session to validate
97
+ # @param [String, Integer] otp the OTP to check
98
+ # @return [Hash] The signed_data and signature containing a session ID and metadata, if any, on success. Otherwise, error[s].
99
+ def otp_result(session_id, otp)
100
+ raw_call(method: 'user.otp_result', session_id: session_id, otp: otp)
101
+ end
102
+
103
+ # Verify a magic link OTP
104
+ #
105
+ # @param [String] otp The OTP (usually embedded in a magic link)to validate
106
+ #
107
+ # @return [Hash]
108
+ #
109
+ def link_result(otp)
110
+ params = {
111
+ method: 'user.link_result',
112
+ realm_key_id: realm_key_id,
113
+ otp: otp
114
+ }
115
+ raw_call(params)
116
+ end
117
+
118
+ # Exchange a signed OTP challenge (from user.link_result or user.otp_result) for either an
119
+ # enrollment challenge (if the original context was "enroll") or a signed user payload (if
120
+ # the original context was "authenticate").
121
+ #
122
+ # @param [String] signed_data Signed data payload
123
+ # @param [String] signature Realm-signed signature of the payload
124
+ # @param [String] session_id If this was an authentication challenge, provide the session ID to close the session
125
+ #
126
+ # @return [Hash]
127
+ #
128
+ def challenge_exchange(signed_data, signature, session_id = nil)
129
+ params = {
130
+ method: 'user.challenge_exchange',
131
+ realm_key_id: realm_key_id,
132
+ signed_data: signed_data,
133
+ signature: signature
134
+ }
135
+ params['session_id'] = session_id unless session_id.nil?
136
+
137
+ raw_call(params)
138
+ end
139
+
140
+ # Perform a raw(ish) API call
141
+ # @param [Hash{Symbol, String => Object}] request_obj The request to conduct. Should include a :method at the least. Prefer symbol keys to string keys
142
+ # @return [Object] The parsed result of the request. NOTE: most types will be stringified for most requests
143
+ def raw_call(request_obj)
144
+ unless request_obj.key?('realm_key_id') || request_obj.key?(:realm_key_id) # check for both string and symbol
145
+ # TODO: how should we handle conflicts of symbol and string keys?
146
+ request_obj[:realm_key_id] = realm_key_id
147
+ end
148
+ request_url = api_url # copy the URL to a local variable so that we can add the query params
149
+ request_url.query = URI.encode_www_form request_obj # encode request as query params
150
+ JSON.parse(Net::HTTP.get(request_url), symbolize_names: true)
151
+ end
152
+ end
153
+ end
metadata CHANGED
@@ -1,69 +1,69 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tozny-auth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ethan Bell / emanb29
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-06-28 00:00:00.000000000 Z
11
+ date: 2016-08-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ~>
18
18
  - !ruby/object:Gem::Version
19
19
  version: '1.12'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ~>
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.12'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ~>
32
32
  - !ruby/object:Gem::Version
33
33
  version: '10.0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ~>
39
39
  - !ruby/object:Gem::Version
40
40
  version: '10.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: minitest
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - ~>
46
46
  - !ruby/object:Gem::Version
47
47
  version: '5.0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - "~>"
52
+ - - ~>
53
53
  - !ruby/object:Gem::Version
54
54
  version: '5.0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rubocop
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - "~>"
59
+ - - ~>
60
60
  - !ruby/object:Gem::Version
61
61
  version: 0.41.1
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - "~>"
66
+ - - ~>
67
67
  - !ruby/object:Gem::Version
68
68
  version: 0.41.1
69
69
  description: A set of methods to more conveniently access the Tozny authentication
@@ -74,9 +74,9 @@ executables: []
74
74
  extensions: []
75
75
  extra_rdoc_files: []
76
76
  files:
77
- - ".gitignore"
78
- - ".rubocop.yml"
79
- - ".travis.yml"
77
+ - .gitignore
78
+ - .rubocop.yml
79
+ - .travis.yml
80
80
  - Gemfile
81
81
  - LICENSE
82
82
  - README.md
@@ -100,17 +100,17 @@ require_paths:
100
100
  - lib
101
101
  required_ruby_version: !ruby/object:Gem::Requirement
102
102
  requirements:
103
- - - "~>"
103
+ - - ~>
104
104
  - !ruby/object:Gem::Version
105
105
  version: '2'
106
106
  required_rubygems_version: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - ">="
108
+ - - '>='
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0'
111
111
  requirements: []
112
112
  rubyforge_project:
113
- rubygems_version: 2.4.5.1
113
+ rubygems_version: 2.6.6
114
114
  signing_key:
115
115
  specification_version: 4
116
116
  summary: Tozny Ruby SDK