tozny-auth 0.1.6 → 0.1.7
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/.rubocop.yml +7 -7
- data/bin/console +14 -14
- data/bin/setup +8 -8
- data/lib/tozny/auth/common.rb +64 -64
- data/lib/tozny/auth/version.rb +1 -1
- data/lib/tozny/realm.rb +303 -274
- data/lib/tozny/user.rb +153 -93
- metadata +16 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e41382cfb8a2420e58e304d53d9750c71b7046de
|
|
4
|
+
data.tar.gz: b6dc52cfddc4209c57202ccaa3110548fbb8e560
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3efa4daf484e74ad5a7fe404b134e804025f8cc481973b42ce174848d9b6854b46f22682fa3583b1a3f65732b72d5d82219ea908d702ce0e2298f1beae21d610
|
|
7
|
+
data.tar.gz: ba7dbf7c62dcfddb841bfa0c71b1b7224a083e0b392fc3c744b558907c0d1c51f472b927e280a372154734c31feaf28e9deb1a44aa55aff77c6d723cc20e27bd
|
data/.rubocop.yml
CHANGED
|
@@ -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
|
data/bin/console
CHANGED
|
@@ -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
|
data/lib/tozny/auth/common.rb
CHANGED
|
@@ -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
|
data/lib/tozny/auth/version.rb
CHANGED
data/lib/tozny/realm.rb
CHANGED
|
@@ -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
|
-
# @
|
|
200
|
-
# @raise ArgumentError
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
#
|
|
232
|
-
# @
|
|
233
|
-
# @param [String]
|
|
234
|
-
#
|
|
235
|
-
# @
|
|
236
|
-
#
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
data/lib/tozny/user.rb
CHANGED
|
@@ -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
|
-
# @
|
|
55
|
-
# @raise ArgumentError
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
# @return [
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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.
|
|
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-
|
|
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
|
-
-
|
|
78
|
-
-
|
|
79
|
-
-
|
|
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.
|
|
113
|
+
rubygems_version: 2.6.6
|
|
114
114
|
signing_key:
|
|
115
115
|
specification_version: 4
|
|
116
116
|
summary: Tozny Ruby SDK
|