tozny-auth 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +2 -1
- data/.rubocop.yml +8 -0
- data/README.md +35 -1
- data/Rakefile +8 -4
- data/lib/tozny/auth/common.rb +14 -11
- data/lib/tozny/auth/version.rb +1 -1
- data/lib/tozny/realm.rb +126 -67
- data/lib/tozny/user.rb +62 -21
- data/tozny-auth.gemspec +2 -1
- data/tozny-auth.iml +11 -3
- metadata +18 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2eb729af3e457366a65763b14a64592dad16f316
|
4
|
+
data.tar.gz: e623a5995a6f6a59227d30f9e68a1b3ee5f881ae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e2fc60e32ed36a9543a24a1550e8acff4fb424f4d2c6399f412c7dc0dc9467c362346bde8c5b14679a27a6a46725c0d41f63cfb116defb599d664a80e8169389
|
7
|
+
data.tar.gz: f2fb6af758d2b234a850fe6b924c5e7c395776b7191c08637d70bd879f1cabb49035fa1ca9001fd276a757b088f1723535f66c88e2085f77c75ebc0108630f7e
|
data/.gitignore
CHANGED
data/.rubocop.yml
ADDED
data/README.md
CHANGED
@@ -16,7 +16,7 @@ Or install it yourself as:
|
|
16
16
|
|
17
17
|
$ gem install tozny-auth
|
18
18
|
|
19
|
-
## Usage
|
19
|
+
## Usage (Authentication)
|
20
20
|
In your template, include jQuery and the Tozny jQuery library:
|
21
21
|
```html
|
22
22
|
<script src="https://code.jquery.com/jquery-1.10.2.min.js"></script>
|
@@ -45,6 +45,40 @@ if params[:tozny_action] == 'tozny_login'
|
|
45
45
|
end
|
46
46
|
```
|
47
47
|
|
48
|
+
## Usage (SMS OTP / 2FA)
|
49
|
+
|
50
|
+
To send a one-time-password (via SMS)
|
51
|
+
```ruby
|
52
|
+
require 'tozny/auth'
|
53
|
+
realm_key_id = 'sid_123456789'
|
54
|
+
realm_secret = '6f75.....190a8dbc7'
|
55
|
+
tozny = Tozny::Realm.new(realm_key_id, realm_secret)
|
56
|
+
|
57
|
+
tozny.otp_challenge('sms-otp-6', '8005551234', nil, {foo: 'bar'})
|
58
|
+
#or alternatively (for a 6 digit OTP -- you cannot do an 8 digit OTP using the following method)
|
59
|
+
tozny.sms_otp('8005551234', {foo: 'bar'})
|
60
|
+
#or, if you don't need custom data, and you have unauthenticated OTP enabled in your realm's admin console:
|
61
|
+
tozny.user_api.otp_challenge('sms-otp-6', '8005551234')
|
62
|
+
#finally, if you already have an otp 'presence' you can use that instead of the type and destination:
|
63
|
+
tozny.otp_challenge(presence='237fa....af794')
|
64
|
+
```
|
65
|
+
|
66
|
+
To verify the OTP the end-user enters based on the session
|
67
|
+
```ruby
|
68
|
+
require 'tozny/auth'
|
69
|
+
realm_key_id = 'sid_123456789'
|
70
|
+
tozny_user = Tozny::User.new(realm_key_id) # Note: Tozny::Realm#user_api is an instance of Tozny::User pre-set to the realm
|
71
|
+
|
72
|
+
session_id = '2392e...134' # this should be the session_id you got back from otp_challenge
|
73
|
+
otp = '123456' # this should be the OTP entered by the user
|
74
|
+
|
75
|
+
if tozny_user.otp_result(session_id, otp).key?(:signed_data)
|
76
|
+
# the OTP was correct
|
77
|
+
else
|
78
|
+
# you can try another OTP until the session expires
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
48
82
|
## Contributing
|
49
83
|
|
50
84
|
Bug reports and pull requests are welcome on GitHub at https://github.com/tozny/sdk-ruby
|
data/Rakefile
CHANGED
@@ -1,10 +1,14 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rubocop/rake_task'
|
3
4
|
|
4
5
|
Rake::TestTask.new(:test) do |t|
|
5
|
-
t.libs <<
|
6
|
-
t.libs <<
|
6
|
+
t.libs << 'test'
|
7
|
+
t.libs << 'lib'
|
7
8
|
t.test_files = FileList['test/**/*_test.rb']
|
8
9
|
end
|
9
10
|
|
10
11
|
task :default => :test
|
12
|
+
|
13
|
+
RuboCop::RakeTask.new
|
14
|
+
|
data/lib/tozny/auth/common.rb
CHANGED
@@ -12,9 +12,9 @@ module Tozny
|
|
12
12
|
# @return [String] the base64url-encoded string
|
13
13
|
def self.base64url_encode(str)
|
14
14
|
Base64::strict_encode64(
|
15
|
-
str
|
16
|
-
)
|
17
|
-
.tr('+/', '-_')
|
15
|
+
str
|
16
|
+
)
|
17
|
+
.tr('+/', '-_')
|
18
18
|
.tr('=', '')
|
19
19
|
end
|
20
20
|
|
@@ -22,15 +22,18 @@ module Tozny
|
|
22
22
|
# @param [String] str the base64url-encoded string
|
23
23
|
# @return [String] the decoded plaintext string
|
24
24
|
def self.base64url_decode(str)
|
25
|
-
Base64::strict_decode64(
|
26
|
-
|
25
|
+
Base64::strict_decode64(
|
26
|
+
str.tr('-_', '+/')
|
27
|
+
.ljust(str.length+(str.length % 4), '='))
|
28
|
+
# replace - with + and _ with /
|
29
|
+
# then add padding
|
27
30
|
end
|
28
31
|
|
29
32
|
# checks the HMAC/SHA256 signature of a string
|
30
33
|
# @param [String] signature the signature to check against
|
31
34
|
# @param [String] str the signed data to check the signature against
|
32
35
|
# @param [String] secret the secret to check the signature against
|
33
|
-
# @return [
|
36
|
+
# @return [TrueClass, FalseClass] whether the signature matched
|
34
37
|
def self.check_signature(signature, str, secret)
|
35
38
|
expected_sig = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), secret, str)
|
36
39
|
expected_sig == signature
|
@@ -44,11 +47,11 @@ module Tozny
|
|
44
47
|
# @return [Hash] a hash including the signed_data and a signature
|
45
48
|
def self.encode_and_sign(data, secret)
|
46
49
|
encoded_data = base64url_encode(data)
|
47
|
-
sig=OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), secret, encoded_data)
|
50
|
+
sig = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), secret, encoded_data)
|
48
51
|
encoded_sig = base64url_encode(sig)
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
+
{
|
53
|
+
signed_data: encoded_data,
|
54
|
+
signature: encoded_sig
|
52
55
|
}
|
53
56
|
end
|
54
57
|
|
@@ -58,4 +61,4 @@ module Tozny
|
|
58
61
|
OpenSSL::Digest::SHA256.hexdigest SecureRandom.random_bytes(8)
|
59
62
|
end
|
60
63
|
end
|
61
|
-
end
|
64
|
+
end
|
data/lib/tozny/auth/version.rb
CHANGED
data/lib/tozny/realm.rb
CHANGED
@@ -3,27 +3,26 @@ require 'json'
|
|
3
3
|
require 'net/http'
|
4
4
|
require 'uri'
|
5
5
|
|
6
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
7
|
+
|
6
8
|
module Tozny
|
7
9
|
class Realm
|
8
10
|
attr_accessor :realm_key_id, :realm_secret, :api_url, :user_api
|
9
11
|
|
10
12
|
def initialize(realm_key_id, realm_secret, api_url = nil)
|
11
|
-
#
|
12
|
-
#self.realm_secret = realm_secret
|
13
|
-
|
14
|
-
#set the API URL
|
13
|
+
# set the API URL
|
15
14
|
if !api_url.nil?
|
16
15
|
self.api_url = api_url
|
17
16
|
elsif !(ENV['API_URL'].nil?)
|
18
|
-
self.api_url=ENV['API_URL']
|
17
|
+
self.api_url = ENV['API_URL']
|
19
18
|
else
|
20
|
-
self.api_url='https://api.tozny.com/index.php'
|
19
|
+
self.api_url = 'https://api.tozny.com/index.php'
|
21
20
|
end
|
22
|
-
unless self.api_url.is_a? URI #don't try to parse a URI instance into a URI, as this will break
|
21
|
+
unless self.api_url.is_a? URI # don't try to parse a URI instance into a URI, as this will break
|
23
22
|
self.api_url = URI.parse(self.api_url)
|
24
23
|
end
|
25
24
|
|
26
|
-
|
25
|
+
set_new_realm(realm_key_id, realm_secret)
|
27
26
|
|
28
27
|
end
|
29
28
|
|
@@ -31,11 +30,11 @@ module Tozny
|
|
31
30
|
# @param [String] realm_key_id
|
32
31
|
# @param [String] realm_secret
|
33
32
|
# @return [TrueClass] will always return true
|
34
|
-
def set_new_realm
|
33
|
+
def set_new_realm(realm_key_id, realm_secret)
|
35
34
|
self.realm_key_id = realm_key_id
|
36
35
|
self.realm_secret = realm_secret
|
37
|
-
if
|
38
|
-
|
36
|
+
if user_api.is_a? ::Tozny::User
|
37
|
+
user_api.set_new_realm(realm_key_id)
|
39
38
|
else
|
40
39
|
self.user_api = ::Tozny::User.new(realm_key_id, api_url)
|
41
40
|
end
|
@@ -60,12 +59,12 @@ module Tozny
|
|
60
59
|
# @param [String] user_id the user_id of the login to check
|
61
60
|
# @param [String] session_id the session_id of the login to check
|
62
61
|
# @return [Hash] the return from the API
|
63
|
-
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
|
64
|
-
raw_call(
|
65
|
-
:
|
66
|
-
:
|
67
|
-
:
|
68
|
-
|
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'
|
69
68
|
end
|
70
69
|
|
71
70
|
# Add a user to a closed realm
|
@@ -74,17 +73,15 @@ module Tozny
|
|
74
73
|
# @param [String, OpenSSL::PKey::RSA] pub_key the public key of the user to be added. Only necessary
|
75
74
|
# @return [Hash, FalseClass] the user in its current (incomplete if defer is 'true' state)
|
76
75
|
# @raise ArgumentError if there is no pubkey when there should be one
|
77
|
-
def user_add(defer = 'false', meta, pub_key)
|
76
|
+
def user_add(defer = 'false', meta = nil, pub_key)
|
78
77
|
unless pub_key.nil?
|
79
|
-
if pub_key.is_a? String
|
80
|
-
pub_key = OpenSSL::PKey::RSA.new pub_key
|
81
|
-
end
|
78
|
+
pub_key = OpenSSL::PKey::RSA.new pub_key if pub_key.is_a? String
|
82
79
|
pub_key = pub_key.public_key if pub_key.private?
|
83
80
|
end
|
84
81
|
|
85
82
|
request_obj = {
|
86
|
-
|
87
|
-
|
83
|
+
method: 'realm.user_add',
|
84
|
+
defer: defer
|
88
85
|
}
|
89
86
|
if defer == 'false'
|
90
87
|
raise ArgumentError, 'Must provide a public key if not using deferred enrollment' if pub_key.nil?
|
@@ -106,31 +103,31 @@ module Tozny
|
|
106
103
|
# @param [Hash{Symbol,String=>Object}] meta the metadata fields to update, along with their new values
|
107
104
|
# @return [Hash] the updated user
|
108
105
|
def user_update(user_id, meta)
|
109
|
-
raw_call(
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
106
|
+
raw_call(
|
107
|
+
method: 'realm.user_update',
|
108
|
+
user_id: user_id,
|
109
|
+
extra_fields: Tozny::Core.base64url_encode(meta.to_json)
|
110
|
+
)
|
114
111
|
end
|
115
112
|
|
116
113
|
# @param [String] user_id
|
117
114
|
# @return [Hash] the result of the request to the API
|
118
115
|
def user_delete(user_id)
|
119
|
-
raw_call(
|
120
|
-
|
121
|
-
|
122
|
-
|
116
|
+
raw_call(
|
117
|
+
method: 'realm.user_delete',
|
118
|
+
user_id: user_id
|
119
|
+
)
|
123
120
|
end
|
124
121
|
|
125
122
|
# retrieve a user's information
|
126
123
|
# * Note: all meta fields are stored as strings
|
127
124
|
# @param [String] user_id the id or email (if is_id = false) of the user to get
|
128
|
-
# @param [
|
125
|
+
# @param [TrueClass, FalseClass] is_id true if looking up the user by id, false if looking up by email. defaults to true
|
129
126
|
# @return [Hash] the user's information
|
130
127
|
# @raise ArgumentError on failed lookup
|
131
128
|
def user_get(user_id, is_id = true)
|
132
129
|
request_obj = {
|
133
|
-
|
130
|
+
method: 'realm.user_get'
|
134
131
|
}
|
135
132
|
if is_id
|
136
133
|
request_obj[:user_id] = user_id
|
@@ -139,8 +136,8 @@ module Tozny
|
|
139
136
|
end
|
140
137
|
|
141
138
|
user = raw_call(request_obj)
|
142
|
-
if user.nil?
|
143
|
-
raise ArgumentError, ('No user was found for '+(is_id ? 'id' : 'email')+': '+user_id+'.')
|
139
|
+
if user.nil? || user[:results].nil?
|
140
|
+
raise ArgumentError, ('No user was found for ' + (is_id ? 'id' : 'email') + ': ' + user_id + '.')
|
144
141
|
end
|
145
142
|
user[:results]
|
146
143
|
end
|
@@ -149,10 +146,10 @@ module Tozny
|
|
149
146
|
# @param [String] user_id
|
150
147
|
# @return [Hash] the result of the call: keys include :user_id, :temp_key, :secret_enrollment_url, and :key_id
|
151
148
|
def user_device_add(user_id)
|
152
|
-
raw_call(
|
153
|
-
|
154
|
-
|
155
|
-
|
149
|
+
raw_call(
|
150
|
+
method: 'realm.user_device_add',
|
151
|
+
user_id: user_id
|
152
|
+
)
|
156
153
|
end
|
157
154
|
|
158
155
|
# create an OOB challenge question session
|
@@ -163,53 +160,115 @@ module Tozny
|
|
163
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
|
164
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
|
165
162
|
# @param [String] user_id optional. The user who should answer the question.
|
163
|
+
# @raise ArgumentError on invalid question type
|
166
164
|
# TODO: support URI objects instead of strings for success and error
|
167
165
|
# @return [Hash] the result of the API call
|
168
166
|
def question_challenge(question, success_url, error_url, user_id)
|
169
|
-
raise ArgumentError, 'question must either be a string or an options hash as specified' unless question.is_a?String
|
170
|
-
final_question = nil #scope final_question and prevent linting errors
|
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
|
171
169
|
if question.is_a?Hash
|
172
170
|
final_question = question
|
173
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
|
174
179
|
else
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
}
|
180
|
-
final_question[:success] = success_url if success_url.is_a?String
|
181
|
-
final_question[:error] = error_url if error_url.is_a?String
|
182
|
-
else
|
183
|
-
final_question = {
|
184
|
-
:type => 'question',
|
185
|
-
:question => question
|
186
|
-
}
|
187
|
-
end
|
180
|
+
final_question = {
|
181
|
+
type: 'question',
|
182
|
+
question: question
|
183
|
+
}
|
188
184
|
end
|
189
185
|
request_obj = {
|
190
|
-
|
191
|
-
|
186
|
+
method: 'realm.question_challenge',
|
187
|
+
question: final_question
|
192
188
|
}
|
193
189
|
request_obj[:user_id] = user_id if user_id.is_a?String
|
194
190
|
raw_call request_obj
|
195
191
|
end
|
196
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
|
+
|
197
256
|
# perform a raw(ish) API call
|
198
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
|
199
258
|
# @return [Object] The parsed result of the request. NOTE: most types will be stringified for most requests
|
200
259
|
def raw_call(request_obj)
|
201
|
-
request_obj[:nonce] = Tozny::Core.generate_nonce #generate the nonce
|
202
|
-
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?
|
203
|
-
unless request_obj.key?('realm_key_id') || request_obj.key?(:realm_key_id) #check for both string and symbol
|
204
|
-
#TODO: how should we handle conflicts of symbol and string keys?
|
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?
|
205
264
|
request_obj[:realm_key_id] = realm_key_id
|
206
265
|
end
|
207
|
-
encoded_params = Tozny::Core.encode_and_sign(request_obj.to_json, realm_secret) #make a proper request of it.
|
208
|
-
request_url = api_url #copy the URL to a local variable so that we can add the query params
|
209
|
-
request_url.query = URI.encode_www_form encoded_params #encode signed_data and signature as query params
|
210
|
-
#p request_url
|
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
|
211
270
|
http_result = Net::HTTP.get(request_url)
|
212
|
-
JSON.parse(http_result,
|
271
|
+
JSON.parse(http_result, symbolize_names: true) # TODO: handle errors
|
213
272
|
end
|
214
273
|
end
|
215
|
-
end
|
274
|
+
end
|
data/lib/tozny/user.rb
CHANGED
@@ -4,49 +4,90 @@ module Tozny
|
|
4
4
|
class User
|
5
5
|
attr_accessor :realm_key_id, :api_url
|
6
6
|
def initialize(realm_key_id, api_url = nil)
|
7
|
-
|
8
7
|
if !api_url.nil?
|
9
8
|
self.api_url = api_url
|
10
|
-
elsif !(ENV['API_URL'].nil?)
|
11
|
-
self.api_url=ENV['API_URL']
|
9
|
+
elsif !(ENV['API_URL'].nil?) # rubocop:disable all
|
10
|
+
self.api_url = ENV['API_URL']
|
12
11
|
else
|
13
|
-
self.api_url='https://api.tozny.com/index.php'
|
12
|
+
self.api_url = 'https://api.tozny.com/index.php'
|
14
13
|
end
|
15
14
|
|
16
|
-
unless self.api_url.is_a? URI
|
17
|
-
self.api_url = URI.parse(self.api_url)
|
18
|
-
end
|
15
|
+
self.api_url = URI.parse(self.api_url) unless self.api_url.is_a? URI
|
19
16
|
|
20
|
-
|
17
|
+
set_new_realm(realm_key_id)
|
21
18
|
end
|
22
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
23
|
def check_session_status(session_id)
|
24
|
-
raw_call(
|
25
|
-
:
|
26
|
-
:
|
27
|
-
|
24
|
+
raw_call(
|
25
|
+
method: 'user.check_session_status',
|
26
|
+
session_id: session_id
|
27
|
+
).key?(:signed_data)
|
28
28
|
end
|
29
29
|
|
30
30
|
# use a new realm_key_id
|
31
31
|
# @param [String] realm_key_id
|
32
32
|
# @return [TrueClass] will always return true
|
33
|
-
def set_new_realm
|
33
|
+
def set_new_realm(realm_key_id) # rubocop:disable Style/AccessorMethodName
|
34
34
|
self.realm_key_id = realm_key_id
|
35
35
|
true
|
36
36
|
end
|
37
37
|
|
38
|
-
#
|
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
|
39
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
|
40
82
|
# @return [Object] The parsed result of the request. NOTE: most types will be stringified for most requests
|
41
83
|
def raw_call(request_obj)
|
42
|
-
unless request_obj.key?('realm_key_id') || request_obj.key?(:realm_key_id) #check for both string and symbol
|
43
|
-
#TODO: how should we handle conflicts of symbol and string keys?
|
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?
|
44
86
|
request_obj[:realm_key_id] = realm_key_id
|
45
87
|
end
|
46
|
-
request_url = api_url #copy the URL to a local variable so that we can add the query params
|
47
|
-
request_url.query = URI.encode_www_form request_obj #encode request as query params
|
48
|
-
|
49
|
-
JSON.parse(Net::HTTP.get(request_url), {:symbolize_names => true})
|
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)
|
50
91
|
end
|
51
92
|
end
|
52
|
-
end
|
93
|
+
end
|
data/tozny-auth.gemspec
CHANGED
@@ -30,7 +30,8 @@ Gem::Specification.new do |spec|
|
|
30
30
|
spec.add_development_dependency 'bundler', '~> 1.12'
|
31
31
|
spec.add_development_dependency 'rake', '~> 10.0'
|
32
32
|
spec.add_development_dependency 'minitest', '~> 5.0'
|
33
|
+
spec.add_development_dependency 'rubocop', '~> 0.41.1'
|
33
34
|
|
34
35
|
# Only with ruby 2.0.x
|
35
|
-
spec.required_ruby_version = '~> 2
|
36
|
+
spec.required_ruby_version = '~> 2'
|
36
37
|
end
|
data/tozny-auth.iml
CHANGED
@@ -19,9 +19,17 @@
|
|
19
19
|
<excludeFolder url="file://$MODULE_DIR$/.bundle" />
|
20
20
|
<excludeFolder url="file://$MODULE_DIR$/vendor/bundle" />
|
21
21
|
</content>
|
22
|
-
<orderEntry type="jdk" jdkName="ruby-2.
|
22
|
+
<orderEntry type="jdk" jdkName="ruby-2.2.4-p230" jdkType="RUBY_SDK" />
|
23
23
|
<orderEntry type="sourceFolder" forTests="false" />
|
24
|
-
<orderEntry type="library" scope="PROVIDED" name="
|
25
|
-
<orderEntry type="library" scope="PROVIDED" name="
|
24
|
+
<orderEntry type="library" scope="PROVIDED" name="ast (v2.3.0, ruby-2.2.4-p230) [gem]" level="application" />
|
25
|
+
<orderEntry type="library" scope="PROVIDED" name="bundler (v1.12.5, ruby-2.2.4-p230) [gem]" level="application" />
|
26
|
+
<orderEntry type="library" scope="PROVIDED" name="minitest (v5.9.0, ruby-2.2.4-p230) [gem]" level="application" />
|
27
|
+
<orderEntry type="library" scope="PROVIDED" name="parser (v2.3.1.2, ruby-2.2.4-p230) [gem]" level="application" />
|
28
|
+
<orderEntry type="library" scope="PROVIDED" name="powerpack (v0.1.1, ruby-2.2.4-p230) [gem]" level="application" />
|
29
|
+
<orderEntry type="library" scope="PROVIDED" name="rainbow (v2.1.0, ruby-2.2.4-p230) [gem]" level="application" />
|
30
|
+
<orderEntry type="library" scope="PROVIDED" name="rake (v10.1.0, ruby-2.2.4-p230) [gem]" level="application" />
|
31
|
+
<orderEntry type="library" scope="PROVIDED" name="rubocop (v0.41.1, ruby-2.2.4-p230) [gem]" level="application" />
|
32
|
+
<orderEntry type="library" scope="PROVIDED" name="ruby-progressbar (v1.8.1, ruby-2.2.4-p230) [gem]" level="application" />
|
33
|
+
<orderEntry type="library" scope="PROVIDED" name="unicode-display_width (v1.1.0, ruby-2.2.4-p230) [gem]" level="application" />
|
26
34
|
</component>
|
27
35
|
</module>
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tozny-auth
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.5
|
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-
|
11
|
+
date: 2016-06-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '5.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.41.1
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.41.1
|
55
69
|
description: A set of methods to more conveniently access the Tozny authentication
|
56
70
|
API as a RP of Tozny from Ruby
|
57
71
|
email:
|
@@ -61,6 +75,7 @@ extensions: []
|
|
61
75
|
extra_rdoc_files: []
|
62
76
|
files:
|
63
77
|
- ".gitignore"
|
78
|
+
- ".rubocop.yml"
|
64
79
|
- ".travis.yml"
|
65
80
|
- Gemfile
|
66
81
|
- LICENSE
|
@@ -87,7 +102,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
87
102
|
requirements:
|
88
103
|
- - "~>"
|
89
104
|
- !ruby/object:Gem::Version
|
90
|
-
version: '2
|
105
|
+
version: '2'
|
91
106
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
92
107
|
requirements:
|
93
108
|
- - ">="
|