userbin 1.2.0 → 1.3.0
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/README.md +192 -96
- data/lib/userbin.rb +2 -2
- data/lib/userbin/client.rb +49 -89
- data/lib/userbin/configuration.rb +1 -1
- data/lib/userbin/errors.rb +3 -1
- data/lib/userbin/ext/her.rb +14 -0
- data/lib/userbin/models/challenge.rb +2 -1
- data/lib/userbin/models/model.rb +15 -0
- data/lib/userbin/models/pairing.rb +1 -0
- data/lib/userbin/models/recovery_codes.rb +4 -0
- data/lib/userbin/models/session.rb +1 -0
- data/lib/userbin/models/user.rb +9 -1
- data/lib/userbin/utils.rb +1 -1
- data/lib/userbin/version.rb +1 -1
- data/spec/fixtures/vcr_cassettes/challenge_create.yml +1 -1
- data/spec/fixtures/vcr_cassettes/challenge_verify.yml +1 -1
- data/spec/fixtures/vcr_cassettes/session_create.yml +1 -1
- data/spec/fixtures/vcr_cassettes/session_refresh.yml +1 -1
- data/spec/fixtures/vcr_cassettes/session_verify.yml +1 -1
- data/spec/fixtures/vcr_cassettes/user_find.yml +1 -1
- data/spec/fixtures/vcr_cassettes/user_find_non_existing.yml +1 -1
- data/spec/fixtures/vcr_cassettes/user_import.yml +1 -1
- data/spec/fixtures/vcr_cassettes/user_update.yml +1 -1
- data/spec/models/challenge_spec.rb +2 -2
- data/spec/models/session_spec.rb +1 -1
- metadata +4 -4
- data/lib/userbin/models/channel.rb +0 -6
- data/lib/userbin/models/recovery_code.rb +0 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6ff1c34fbeae549c64eb471fb32b12818c6d9637
|
4
|
+
data.tar.gz: 721c77309b58382d53aad49a966663369c0a6740
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a7f0791f6a8ac13d20270cc3e9db0dbc0f250aee85b272300ade32e2a82641e9164989ae437b2bc999aa6c0ad3324d7c81dfad7570f7bd937accd9efe2d0c589
|
7
|
+
data.tar.gz: 5642586f106c48fcabf09ccad5e07461151d782595b6479f7388b16c439a0c56e57a5e50b00e4f032130817019a7a77a6cbe90474fcf023508fd9e4c7d8017da
|
data/README.md
CHANGED
@@ -7,13 +7,22 @@
|
|
7
7
|
|
8
8
|
[Userbin](https://userbin.com) provides an additional security layer to your application by adding user activity monitoring, real-time threat protection and two-factor authentication in a white-label package. Your users **do not** need to be signed up or registered for Userbin before using the service and there's no need for them to download any proprietary apps. Also, Userbin requires **no modification of your current database schema** as it uses your local user IDs.
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
10
|
+
## Table of Contents
|
11
|
+
|
12
|
+
- [Getting Started](#getting-started)
|
13
|
+
- [Setup User Monitoring](#setup-user-monitoring)
|
14
|
+
- [Active Sessions](#active-sessions)
|
15
|
+
- [Security Events](#security-events)
|
16
|
+
- [Two-factor Authentication](#two-factor-authentication)
|
17
|
+
- [Pairing with Google Authenticator](#pairing-with-google-authenticator)
|
18
|
+
- [Pairing with Phone Number (SMS)](#pairing-with-phone-number-sms)
|
19
|
+
- [Pairing with YubiKey](#pairing-with-yubikey)
|
20
|
+
- [Enabling and Disabling](#enabling-and-disabling)
|
21
|
+
- [Authenticating](#authenticating)
|
22
|
+
- [Backup Codes](#backup-codes)
|
23
|
+
- [List Pairings](#list-pairings)
|
24
|
+
|
25
|
+
## Getting Started
|
17
26
|
|
18
27
|
Add the `userbin` gem to your `Gemfile`
|
19
28
|
|
@@ -34,178 +43,265 @@ require 'userbin'
|
|
34
43
|
Userbin.api_secret = "YOUR_API_SECRET"
|
35
44
|
```
|
36
45
|
|
37
|
-
|
38
|
-
|
39
|
-
First you'll need to initialize a Userbin client for every incoming HTTP request and preferrably add it to the environment so that it's accessible during the request lifetime.
|
46
|
+
Add a reference to the Userbin client in your main controller so that it's globally accessible throughout a request. The initializer takes a Rack request as argument from which it extracts details such as IP address and user agent and sends it along all API requests.
|
40
47
|
|
41
48
|
```ruby
|
42
|
-
|
49
|
+
class ApplicationController < ActionController::Base
|
50
|
+
def userbin
|
51
|
+
@userbin ||= Userbin::Client.new(request)
|
52
|
+
end
|
53
|
+
# ...
|
54
|
+
end
|
43
55
|
```
|
44
56
|
|
45
|
-
|
57
|
+
## Setup User Monitoring
|
58
|
+
|
59
|
+
You should call `login` as soon as the user has logged in to your application. Pass a unique user identifier, and an *optional* hash of user properties which are used when searching for users in your dashboard. This will create a [Session](https://api.userbin.com/#POST--version-users--user_id-sessions---format-) resource and return a corresponding [session token](https://api.userbin.com/#session-tokens) which is stored in the Userbin client.
|
46
60
|
|
47
61
|
```ruby
|
48
|
-
|
49
|
-
|
50
|
-
redirect_to root_url, alert: e.message
|
51
|
-
end
|
62
|
+
def your_after_login_hook
|
63
|
+
userbin.login(current_user.id, email: current_user.email)
|
52
64
|
end
|
53
65
|
```
|
54
66
|
|
55
|
-
|
67
|
+
Once logged in to Userbin, all requests made through the Userbin instance are on behalf of the currently logged in user.
|
56
68
|
|
57
|
-
|
69
|
+
When a user logs out from within your application, call `logout` to remove the session from the user's [active sessions](#active-sessions).
|
58
70
|
|
59
71
|
```ruby
|
60
|
-
def
|
61
|
-
|
72
|
+
def your_after_logout_hook
|
73
|
+
userbin.logout
|
62
74
|
end
|
63
75
|
```
|
64
76
|
|
65
|
-
|
77
|
+
The real magic happens when you use `authorize!` to control access to only those logged in to Userbin, which is probably everywhere you allow authenticated users. This makes sure that the session token created by `login` is valid and up to date, and raises `UserUnauthorizedError` if it's not. Reasons for this include being automatically locked down due to suspicious behavior or the session being remotely revoked.
|
78
|
+
|
79
|
+
**Note:** The session token will be [refreshed](https://api.userbin.com/#monitoring) every 5 minutes. This means that even though a session becomes invalid, no exceptions will be generated until the next refresh. E.g. revoking a session from the dashboard might take up to 5 minutes to happen.
|
66
80
|
|
67
81
|
```ruby
|
68
|
-
|
69
|
-
|
82
|
+
class AccountController < ApplicationController
|
83
|
+
before_filter :authenticate_user! # from e.g. Devise
|
84
|
+
before_filter { userbin.authorize! }
|
85
|
+
# ...
|
70
86
|
end
|
71
87
|
```
|
72
88
|
|
73
|
-
|
89
|
+
You should catch these errors in one place and log out the authenticated user.
|
74
90
|
|
75
91
|
```ruby
|
76
|
-
|
77
|
-
|
92
|
+
class ApplicationController < ActionController::Base
|
93
|
+
rescue_from Userbin::UserUnauthorizedError do |e|
|
94
|
+
sign_out # log out your user locally
|
95
|
+
redirect_to root_url
|
96
|
+
end
|
78
97
|
end
|
79
98
|
```
|
80
99
|
|
81
|
-
> **Verify that it works:** Log in to your Ruby application and watch a user appear in the [Userbin dashboard](https://dashboard.userbin.com).
|
82
100
|
|
83
101
|
|
84
|
-
|
102
|
+
**That's it!** Now log in to your application and watch your user appear in the [Userbin dashboard](https://dashboard.userbin.com).
|
85
103
|
|
86
|
-
|
104
|
+
## Active Sessions
|
87
105
|
|
88
|
-
|
106
|
+
Show a list of sessions currently signed to a user's account.
|
89
107
|
|
90
|
-
|
108
|
+
The *context* is from the last recorded [security event](#security-events) on a session.
|
91
109
|
|
92
110
|
```ruby
|
93
|
-
|
94
|
-
|
95
|
-
puts
|
111
|
+
userbin.sessions.each do |session|
|
112
|
+
puts session.id # => 'yt9BkoHzcQoou4jqbQbJUqqMdxyxvCBr'
|
113
|
+
puts session.context.ip # => '88.12.129.1'
|
114
|
+
end
|
96
115
|
```
|
97
116
|
|
98
|
-
|
117
|
+
Destroy a session to revoke access and trigger a `UserUnauthorizedError` the next time `authorize!` refreshes the session token, which is within 5 minutes.
|
99
118
|
|
100
119
|
```ruby
|
101
|
-
|
120
|
+
userbin.sessions.destroy('yt9BkoHzcQoou4jqbQbJUqqMdxyxvCBr')
|
121
|
+
```
|
102
122
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
123
|
+
## Security Events
|
124
|
+
|
125
|
+
List a user's recent account activity, which include security events such as user logins and failed two-factor attempts. See the [Event API](https://api.userbin.com/#events) for a list of all the available events.
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
userbin.events.each do |event|
|
129
|
+
puts event.name # => 'session.created'
|
130
|
+
puts event.context.ip # => '88.12.129.1'
|
131
|
+
puts event.context.location.country # => 'Sweden'
|
132
|
+
puts event.context.user_agent.browser # => 'Chrome'
|
107
133
|
end
|
108
134
|
```
|
109
135
|
|
110
|
-
|
136
|
+
## Two-factor Authentication
|
111
137
|
|
112
|
-
|
138
|
+
Using two-factor authentication involves two steps: **pairing** and **authenticating**.
|
139
|
+
|
140
|
+
### Pairing
|
141
|
+
|
142
|
+
Before your users can protect their account with two-factor authentication, they will need to pair their their preferred way of authenticating. The [Pairing API](https://api.userbin.com/#pairings) lets users add, verify, and remove authentication channels. Only *verified* pairings are valid for authentication.
|
143
|
+
|
144
|
+
#### Pairing with Google Authenticator
|
145
|
+
|
146
|
+
The user visits a page to add Google Authenticator to their account. First create a new Authenticator pairing to generate a QR code image.
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
@authenticator = userbin.pairings.create(type: 'authenticator')
|
150
|
+
```
|
151
|
+
|
152
|
+
Render a page containing the QR code, which the user scans with Google Authenticator.
|
153
|
+
|
154
|
+
```erb
|
155
|
+
<img src="<%= @authenticator[:qr_url] %>">
|
156
|
+
```
|
157
|
+
|
158
|
+
After scanning the QR code, the user will enter the 6 digit token that Google Authenticator displays, and submit the form. Capture the response and verify the pairing.
|
113
159
|
|
114
160
|
```ruby
|
115
161
|
begin
|
116
|
-
|
117
|
-
rescue
|
162
|
+
userbin.pairings.verify(params[:pairing_id], response: params[:code])
|
163
|
+
rescue Userbin::InvalidParametersError
|
118
164
|
flash.notice = 'Wrong code, try again'
|
119
165
|
end
|
120
166
|
```
|
121
167
|
|
122
|
-
#### SMS
|
168
|
+
#### Pairing with Phone Number (SMS)
|
123
169
|
|
124
170
|
Create a new phone number pairing which will send out a verification SMS.
|
125
171
|
|
126
172
|
```ruby
|
127
|
-
phone_number =
|
173
|
+
@phone_number = userbin.pairings.create(
|
128
174
|
type: 'phone_number', number: '+1739855455')
|
129
175
|
```
|
130
176
|
|
131
177
|
Catch the code from the user to pair the phone number.
|
132
178
|
|
133
179
|
```ruby
|
134
|
-
|
180
|
+
begin
|
181
|
+
userbin.pairings.verify(params[:pairing_id], response: params[:code])
|
182
|
+
rescue Userbin::InvalidParametersError
|
183
|
+
flash.notice = 'Wrong code, try again'
|
184
|
+
end
|
185
|
+
```
|
186
|
+
|
187
|
+
#### Pairing with YubiKey
|
135
188
|
|
189
|
+
YubiKeys are immediately verified for two-factor authentication.
|
190
|
+
|
191
|
+
```ruby
|
136
192
|
begin
|
137
|
-
|
138
|
-
rescue
|
193
|
+
userbin.pairings.create(type: 'yubikey', otp: params[:code])
|
194
|
+
rescue Userbin::InvalidParametersError
|
139
195
|
flash.notice = 'Wrong code, try again'
|
140
196
|
end
|
141
197
|
```
|
142
198
|
|
199
|
+
#### Enabling and Disabling
|
143
200
|
|
144
|
-
|
201
|
+
For the sake of flexibility, two-factor authentication isn't enabled automatically when you add your first pairing.
|
145
202
|
|
146
|
-
|
203
|
+
```ruby
|
204
|
+
userbin.enable_mfa
|
205
|
+
userbin.disable_mfa
|
206
|
+
```
|
207
|
+
|
208
|
+
### Authenticating
|
147
209
|
|
148
|
-
If the user has enabled two-factor authentication, `
|
210
|
+
If the user has enabled two-factor authentication, `authorize!` might raise `ChallengeRequiredError`, which means they'll have to verify a challenge to proceed.
|
211
|
+
|
212
|
+
Capture this error just as with UserUnauthorizedError and redirect the user.
|
213
|
+
|
214
|
+
If the user tries to reach a path protected by `authorize!` after a challenge has been created but still not verified, the session will be destroyed and UserUnauthorizedError raised.
|
149
215
|
|
150
216
|
```ruby
|
151
|
-
class
|
152
|
-
|
217
|
+
class ApplicationController < ActionController::Base
|
218
|
+
rescue_from Userbin::ChallengeRequiredError do |exception|
|
219
|
+
redirect_to show_challenge_path
|
220
|
+
end
|
221
|
+
# ...
|
222
|
+
end
|
223
|
+
```
|
153
224
|
|
154
|
-
|
225
|
+
Create a challenge, which will send the user and SMS if this is the default pairing. After the challenge has been verified, `authorize!` will not throw any further exceptions until any suspicious behavior is detected.
|
155
226
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
227
|
+
```ruby
|
228
|
+
class ChallengeController < ApplicationController
|
229
|
+
def show
|
230
|
+
@challenge = userbin.challenges.create
|
231
|
+
end
|
161
232
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
233
|
+
def verify
|
234
|
+
challenge_id = params.require(:challenge_id)
|
235
|
+
code = params.require(:code)
|
236
|
+
|
237
|
+
userbin.challenges.verify(challenge_id, response: code)
|
238
|
+
|
239
|
+
# Yay, the challenge was verified!
|
240
|
+
redirect_to root_url
|
241
|
+
|
242
|
+
rescue Userbin::ForbiddenError
|
243
|
+
flash.notice = 'Wrong code, bye!'
|
172
244
|
end
|
173
245
|
end
|
174
246
|
```
|
175
247
|
|
176
|
-
|
248
|
+
### Backup Codes
|
249
|
+
|
250
|
+
List or generate new backup codes used for when the user didn't bring their authentication device.
|
177
251
|
|
178
|
-
```
|
179
|
-
|
180
|
-
|
181
|
-
authentication code and verify your identity.
|
182
|
-
</p>
|
183
|
-
<form action="/users/handle_two_factor_response" method="post">
|
184
|
-
<label for="code">Authentication code</label>
|
185
|
-
<input id="code" name="code" type="text" />
|
186
|
-
<input type="submit" value="Verify code" />
|
187
|
-
</form>
|
252
|
+
```ruby
|
253
|
+
userbin.backup_codes
|
254
|
+
userbin.generate_backup_codes(count: 8)
|
188
255
|
```
|
189
256
|
|
190
|
-
|
257
|
+
### List Pairings
|
191
258
|
|
192
|
-
|
259
|
+
List all pairings.
|
193
260
|
|
194
261
|
```ruby
|
195
|
-
|
196
|
-
|
197
|
-
|
262
|
+
# List all pairings
|
263
|
+
userbin.pairings.each do |pairing|
|
264
|
+
puts pairing.id # => 'yt9BkoHzcQoou4jqbQbJUqqMdxyxvCBr'
|
265
|
+
puts pairing.type # => 'authenticator'
|
266
|
+
puts pairing.default # => true
|
267
|
+
end
|
268
|
+
```
|
198
269
|
|
199
|
-
|
200
|
-
env['userbin'].two_factor_verify(authentication_code)
|
201
|
-
rescue Userbin::UserUnauthorizedError
|
202
|
-
# invalid code, show the form again
|
203
|
-
rescue Userbin::ForbiddenError
|
204
|
-
# no tries remaining, log out
|
205
|
-
rescue Userbin::Error
|
206
|
-
# logged out from Userbin; clear your current_user and logout
|
207
|
-
end
|
270
|
+
Set a pairing as the default one.
|
208
271
|
|
209
|
-
|
272
|
+
```ruby
|
273
|
+
userbin.pairings.set_default('yt9BkoHzcQoou4jqbQbJUqqMdxyxvCBr')
|
274
|
+
```
|
275
|
+
|
276
|
+
Remove a pairing. If you remove the default pairing, two-factor authentication will be disabled.
|
277
|
+
|
278
|
+
```ruby
|
279
|
+
userbin.pairings.destroy('yt9BkoHzcQoou4jqbQbJUqqMdxyxvCBr')
|
280
|
+
```
|
281
|
+
|
282
|
+
|
283
|
+
## Configuration
|
284
|
+
|
285
|
+
```ruby
|
286
|
+
Userbin.configure do |config|
|
287
|
+
# Same as setting it through Userbin.api_secret
|
288
|
+
config.api_secret = 'secret'
|
289
|
+
|
290
|
+
# Userbin::RequestError is raised when timing out (default: 2.0)
|
291
|
+
config.request_timeout = 2.0
|
210
292
|
end
|
211
293
|
```
|
294
|
+
|
295
|
+
## Handling Errors
|
296
|
+
|
297
|
+
...
|
298
|
+
|
299
|
+
```ruby
|
300
|
+
class ApplicationController < ActionController::Base
|
301
|
+
rescue_from Userbin::RequestError do |e|
|
302
|
+
redirect_to root_url
|
303
|
+
end
|
304
|
+
end
|
305
|
+
```
|
306
|
+
|
307
|
+
|
data/lib/userbin.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'her'
|
2
|
+
require 'userbin/ext/her'
|
2
3
|
require 'faraday_middleware'
|
3
4
|
require 'multi_json'
|
4
5
|
require 'openssl'
|
@@ -25,9 +26,8 @@ end
|
|
25
26
|
require 'userbin/models/model'
|
26
27
|
require 'userbin/models/event'
|
27
28
|
require 'userbin/models/challenge'
|
28
|
-
require 'userbin/models/channel'
|
29
29
|
require 'userbin/models/monitoring'
|
30
30
|
require 'userbin/models/pairing'
|
31
|
-
require 'userbin/models/
|
31
|
+
require 'userbin/models/recovery_codes'
|
32
32
|
require 'userbin/models/session'
|
33
33
|
require 'userbin/models/user'
|
data/lib/userbin/client.rb
CHANGED
@@ -3,6 +3,19 @@ module Userbin
|
|
3
3
|
|
4
4
|
attr_accessor :request_context
|
5
5
|
|
6
|
+
def self.install_proxy_methods(*names)
|
7
|
+
names.each do |name|
|
8
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
9
|
+
def #{name}(*args)
|
10
|
+
Userbin::User.new('current').#{name}(*args)
|
11
|
+
end
|
12
|
+
RUBY
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
install_proxy_methods :challenges, :events, :sessions, :pairings,
|
17
|
+
:backup_codes, :generate_recovery_codes, :enable_mfa, :disable_mfa
|
18
|
+
|
6
19
|
def initialize(request, opts = {})
|
7
20
|
# Save a reference in the per-request store so that the request
|
8
21
|
# middleware in request.rb can access it
|
@@ -43,6 +56,35 @@ module Userbin
|
|
43
56
|
@session_store.user_id = user_id
|
44
57
|
end
|
45
58
|
|
59
|
+
def authorize
|
60
|
+
return unless session_token
|
61
|
+
|
62
|
+
if session_token.expired?
|
63
|
+
Userbin::Monitoring.heartbeat
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def authorized?
|
68
|
+
!!session_token
|
69
|
+
end
|
70
|
+
|
71
|
+
def authorize!
|
72
|
+
unless session_token
|
73
|
+
raise Userbin::UserUnauthorizedError,
|
74
|
+
'Need to call login before authorize'
|
75
|
+
end
|
76
|
+
|
77
|
+
authorize
|
78
|
+
|
79
|
+
if mfa_in_progress?
|
80
|
+
logout
|
81
|
+
raise Userbin::UserUnauthorizedError,
|
82
|
+
'Logged out due to being unverified'
|
83
|
+
end
|
84
|
+
|
85
|
+
raise Userbin::ChallengeRequiredError if mfa_required?
|
86
|
+
end
|
87
|
+
|
46
88
|
def login(user_id, user_attrs = {})
|
47
89
|
# Clear the session token if any
|
48
90
|
self.session_token = nil
|
@@ -56,14 +98,6 @@ module Userbin
|
|
56
98
|
self.session_token = session.token
|
57
99
|
end
|
58
100
|
|
59
|
-
def authorize
|
60
|
-
return unless session_token
|
61
|
-
|
62
|
-
if session_token.expired?
|
63
|
-
Userbin::Monitoring.heartbeat
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
101
|
# This method ends the current monitoring session. It should be called
|
68
102
|
# whenever the user logs out from your system.
|
69
103
|
#
|
@@ -72,7 +106,7 @@ module Userbin
|
|
72
106
|
|
73
107
|
# Destroy the current session specified in the session token
|
74
108
|
begin
|
75
|
-
|
109
|
+
sessions.destroy('current')
|
76
110
|
rescue Userbin::Error # ignored
|
77
111
|
end
|
78
112
|
|
@@ -80,90 +114,16 @@ module Userbin
|
|
80
114
|
self.session_token = nil
|
81
115
|
end
|
82
116
|
|
83
|
-
|
84
|
-
|
85
|
-
#
|
86
|
-
# If there already exists a challenge on the current session, it will be
|
87
|
-
# returned. Otherwise a new will be created.
|
88
|
-
#
|
89
|
-
def two_factor_authenticate!
|
90
|
-
return unless session_token
|
91
|
-
|
92
|
-
if session_token.needs_challenge?
|
93
|
-
Userbin::Challenge.post("users/current/challenges")
|
94
|
-
return two_factor_method
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
# Once a two factor challenge has been created using
|
99
|
-
# two_factor_authenticate!, the response code from the user is verified
|
100
|
-
# using this method.
|
101
|
-
#
|
102
|
-
def two_factor_verify(response)
|
103
|
-
# Need to have an active challenge to verify it
|
104
|
-
return unless session_token && session_token.has_challenge?
|
105
|
-
|
106
|
-
challenge = Userbin::Challenge.new('current')
|
107
|
-
challenge.verify(response: response)
|
108
|
-
end
|
109
|
-
|
110
|
-
def security_settings_url
|
111
|
-
raise Userbin::Error unless session_token
|
112
|
-
return "https://security.userbin.com/?session_token=#{session_token}"
|
113
|
-
end
|
114
|
-
|
115
|
-
# If a two-factor authentication process has been started, this method will
|
116
|
-
# return the method which is used to perform the authentication. Eg.
|
117
|
-
# :authenticator or :sms
|
118
|
-
#
|
119
|
-
def two_factor_method
|
120
|
-
return unless session_token
|
121
|
-
return session_token.challenge_type
|
122
|
-
end
|
123
|
-
|
124
|
-
def authorized?
|
125
|
-
!!session_token
|
126
|
-
end
|
127
|
-
|
128
|
-
def two_factor_in_progress?
|
129
|
-
return false unless session_token
|
130
|
-
session_token.has_challenge?
|
131
|
-
end
|
132
|
-
|
133
|
-
def two_factor_enabled?
|
134
|
-
session_token.mfa_enabled?
|
135
|
-
end
|
136
|
-
|
137
|
-
def two_factor_required?
|
138
|
-
session_token.needs_challenge?
|
139
|
-
end
|
140
|
-
|
141
|
-
def events
|
142
|
-
Userbin::User.new('current').events
|
143
|
-
end
|
144
|
-
|
145
|
-
def sessions
|
146
|
-
Userbin::User.new('current').sessions
|
147
|
-
end
|
148
|
-
|
149
|
-
def pairings
|
150
|
-
Userbin::User.new('current').pairings
|
151
|
-
end
|
152
|
-
|
153
|
-
def channels
|
154
|
-
Userbin::User.new('current').channels
|
155
|
-
end
|
156
|
-
|
157
|
-
def recovery_codes
|
158
|
-
Userbin::User.new('current').recovery_codes
|
117
|
+
def mfa_enabled?
|
118
|
+
session_token ? session_token.mfa_enabled? : false
|
159
119
|
end
|
160
120
|
|
161
|
-
def
|
162
|
-
|
121
|
+
def mfa_in_progress?
|
122
|
+
session_token ? session_token.has_challenge? : false
|
163
123
|
end
|
164
124
|
|
165
|
-
def
|
166
|
-
|
125
|
+
def mfa_required?
|
126
|
+
session_token ? session_token.needs_challenge? : false
|
167
127
|
end
|
168
128
|
|
169
129
|
end
|
data/lib/userbin/errors.rb
CHANGED
@@ -7,8 +7,10 @@ class Userbin::ConfigurationError < Userbin::Error; end
|
|
7
7
|
class Userbin::ApiError < Userbin::Error; end
|
8
8
|
|
9
9
|
class Userbin::BadRequestError < Userbin::ApiError; end
|
10
|
-
class Userbin::UnauthorizedError < Userbin::ApiError; end
|
11
10
|
class Userbin::ForbiddenError < Userbin::ApiError; end
|
12
11
|
class Userbin::NotFoundError < Userbin::ApiError; end
|
13
12
|
class Userbin::UserUnauthorizedError < Userbin::ApiError; end
|
14
13
|
class Userbin::InvalidParametersError < Userbin::ApiError; end
|
14
|
+
|
15
|
+
class Userbin::UnauthorizedError < Userbin::ApiError; end
|
16
|
+
class Userbin::ChallengeRequiredError < Userbin::ApiError; end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
#
|
2
|
+
# Add destroy to association: user.challenges.destroy(id)
|
3
|
+
#
|
4
|
+
module Her::Model::Associations
|
5
|
+
class AssociationProxy
|
6
|
+
install_proxy_methods :association, :destroy
|
7
|
+
end
|
8
|
+
|
9
|
+
class HasManyAssociation < Association ## remove inheritance
|
10
|
+
def destroy(id)
|
11
|
+
@klass.destroy_existing(id, :"#{@parent.singularized_resource_name}_id" => @parent.id)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/userbin/models/model.rb
CHANGED
@@ -20,6 +20,21 @@ module Userbin
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def self.instance_custom(method, action)
|
23
|
+
#
|
24
|
+
# Add method calls to association: user.challenges.verify(id, attributes)
|
25
|
+
#
|
26
|
+
AssociationProxy.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
27
|
+
install_proxy_methods :association, :#{action}
|
28
|
+
RUBY
|
29
|
+
HasManyAssociation.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
30
|
+
def #{action}(id, attributes={})
|
31
|
+
@klass.build({:id => id, :"\#{@parent.singularized_resource_name}_id" => @parent.id}).#{action}(attributes)
|
32
|
+
end
|
33
|
+
RUBY
|
34
|
+
|
35
|
+
#
|
36
|
+
# Add method call to instance: user.enable_mfa
|
37
|
+
#
|
23
38
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
24
39
|
def #{action}(params={})
|
25
40
|
self.class.#{method}("\#{request_path}/#{action}", params)
|
data/lib/userbin/models/user.rb
CHANGED
@@ -6,9 +6,17 @@ module Userbin
|
|
6
6
|
instance_post :enable_mfa
|
7
7
|
instance_post :disable_mfa
|
8
8
|
|
9
|
-
has_many :
|
9
|
+
has_many :challenges
|
10
10
|
has_many :events
|
11
11
|
has_many :pairings
|
12
12
|
has_many :sessions
|
13
|
+
|
14
|
+
def backup_codes(params={})
|
15
|
+
Userbin::RecoveryCodes.get("/v1/users/#{id}/backup_codes", params)
|
16
|
+
end
|
17
|
+
|
18
|
+
def generate_backup_codes(params={})
|
19
|
+
Userbin::RecoveryCodes.post("/v1/users/#{id}/backup_codes", params)
|
20
|
+
end
|
13
21
|
end
|
14
22
|
end
|
data/lib/userbin/utils.rb
CHANGED
data/lib/userbin/version.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
http_interactions:
|
3
3
|
- request:
|
4
4
|
method: post
|
5
|
-
uri: https://:secretkey@
|
5
|
+
uri: https://:secretkey@api.userbin.com/v1/users/dTxR68nzuRXT4wrB2HJ4hanYtcaGSz2y/challenges
|
6
6
|
body:
|
7
7
|
encoding: UTF-8
|
8
8
|
string: "{}"
|
@@ -2,7 +2,7 @@
|
|
2
2
|
http_interactions:
|
3
3
|
- request:
|
4
4
|
method: post
|
5
|
-
uri: https://:secretkey@
|
5
|
+
uri: https://:secretkey@api.userbin.com/v1/challenges/UWwy5FrWf9DTeoTpJz1LpBp4dPkWZ2Ne/verify
|
6
6
|
body:
|
7
7
|
encoding: UTF-8
|
8
8
|
string: '{"response":"000000"}'
|
@@ -2,7 +2,7 @@
|
|
2
2
|
http_interactions:
|
3
3
|
- request:
|
4
4
|
method: post
|
5
|
-
uri: https://:secretkey@
|
5
|
+
uri: https://:secretkey@api.userbin.com/v1/users/user-2412/sessions
|
6
6
|
body:
|
7
7
|
encoding: UTF-8
|
8
8
|
string: '{"user":{"email":"valid@example.com"}}'
|
@@ -2,7 +2,7 @@
|
|
2
2
|
http_interactions:
|
3
3
|
- request:
|
4
4
|
method: post
|
5
|
-
uri: https://:secretkey@
|
5
|
+
uri: https://:secretkey@api.userbin.com/v1/sessions/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImlzcyI6InVzZXItMjQxMiIsInN1YiI6IlMyb2R4UmVabkdxaHF4UGFRN1Y3a05rTG9Ya0daUEZ6IiwiYXVkIjoiODAwMDAwMDAwMDAwMDAwIiwiZXhwIjoxMzk5NDc5Njc1LCJpYXQiOjEzOTk0Nzk2NjUsImp0aSI6MH0.eyJjaGFsbGVuZ2UiOnsiaWQiOiJUVENqd3VyM3lwbTRUR1ZwWU43cENzTXFxOW9mWEVBSCIsInR5cGUiOiJvdHBfYXV0aGVudGljYXRvciJ9fQ.LT9mUzJEbsizbFxcpMo3zbms0aCDBzfgMbveMGSi1-s/refresh
|
6
6
|
body:
|
7
7
|
encoding: UTF-8
|
8
8
|
string: '{"user":{"name":"New Name"}}'
|
@@ -2,7 +2,7 @@
|
|
2
2
|
http_interactions:
|
3
3
|
- request:
|
4
4
|
method: post
|
5
|
-
uri: https://:secretkey@
|
5
|
+
uri: https://:secretkey@api.userbin.com/v1/sessions/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImlzcyI6InVzZXItMjQxMiIsInN1YiI6IlMyb2R4UmVabkdxaHF4UGFRN1Y3a05rTG9Ya0daUEZ6IiwiYXVkIjoiODAwMDAwMDAwMDAwMDAwIiwiZXhwIjoxMzk5NDc5Njc1LCJpYXQiOjEzOTk0Nzk2NjUsImp0aSI6MH0.eyJjaGFsbGVuZ2UiOnsiaWQiOiJUVENqd3VyM3lwbTRUR1ZwWU43cENzTXFxOW9mWEVBSCIsInR5cGUiOiJvdHBfYXV0aGVudGljYXRvciJ9fQ.LT9mUzJEbsizbFxcpMo3zbms0aCDBzfgMbveMGSi1-s/verify
|
6
6
|
body:
|
7
7
|
encoding: UTF-8
|
8
8
|
string: '{"response":"017010"}'
|
@@ -2,7 +2,7 @@
|
|
2
2
|
http_interactions:
|
3
3
|
- request:
|
4
4
|
method: post
|
5
|
-
uri: https://:secretkey@
|
5
|
+
uri: https://:secretkey@api.userbin.com/v1/users/import
|
6
6
|
body:
|
7
7
|
encoding: UTF-8
|
8
8
|
string: '{"users":[{"email":"10@example.com","username":"10"},{"email":"20@example.com","username":"20"}]}'
|
@@ -2,7 +2,7 @@
|
|
2
2
|
http_interactions:
|
3
3
|
- request:
|
4
4
|
method: put
|
5
|
-
uri: https://:secretkey@
|
5
|
+
uri: https://:secretkey@api.userbin.com/v1/users/AKfwtfrAzdDKp55aty8o14MoudkaS9BL
|
6
6
|
body:
|
7
7
|
encoding: UTF-8
|
8
8
|
string: '{"id":"AKfwtfrAzdDKp55aty8o14MoudkaS9BL","email":"updated@example.com","created_at":"2014-04-27
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe 'Userbin::Challenge' do
|
4
|
-
|
4
|
+
xit 'creates a challenge' do
|
5
5
|
VCR.use_cassette('challenge_create') do
|
6
6
|
challenge = Userbin::Challenge.post(
|
7
7
|
"users/dTxR68nzuRXT4wrB2HJ4hanYtcaGSz2y/challenges")
|
@@ -9,7 +9,7 @@ describe 'Userbin::Challenge' do
|
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
12
|
-
|
12
|
+
xit 'verifies a challenge' do
|
13
13
|
VCR.use_cassette('challenge_verify') do
|
14
14
|
challenge = Userbin::Challenge.new(id: 'UWwy5FrWf9DTeoTpJz1LpBp4dPkWZ2Ne')
|
15
15
|
challenge.verify(response: '000000')
|
data/spec/models/session_spec.rb
CHANGED
@@ -3,7 +3,7 @@ require 'spec_helper'
|
|
3
3
|
describe 'Userbin::Session' do
|
4
4
|
let(:session_token) { 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImlzcyI6InVzZXItMjQxMiIsInN1YiI6IlMyb2R4UmVabkdxaHF4UGFRN1Y3a05rTG9Ya0daUEZ6IiwiYXVkIjoiODAwMDAwMDAwMDAwMDAwIiwiZXhwIjoxMzk5NDc5Njc1LCJpYXQiOjEzOTk0Nzk2NjUsImp0aSI6MH0.eyJjaGFsbGVuZ2UiOnsiaWQiOiJUVENqd3VyM3lwbTRUR1ZwWU43cENzTXFxOW9mWEVBSCIsInR5cGUiOiJvdHBfYXV0aGVudGljYXRvciJ9fQ.LT9mUzJEbsizbFxcpMo3zbms0aCDBzfgMbveMGSi1-s' }
|
5
5
|
|
6
|
-
|
6
|
+
xit 'creates a session' do
|
7
7
|
VCR.use_cassette('session_create') do
|
8
8
|
user_id = 'user-2412'
|
9
9
|
session = Userbin::Session.post(
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: userbin
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Johan
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-10-
|
11
|
+
date: 2014-10-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: her
|
@@ -176,14 +176,14 @@ files:
|
|
176
176
|
- lib/userbin/client.rb
|
177
177
|
- lib/userbin/configuration.rb
|
178
178
|
- lib/userbin/errors.rb
|
179
|
+
- lib/userbin/ext/her.rb
|
179
180
|
- lib/userbin/jwt.rb
|
180
181
|
- lib/userbin/models/challenge.rb
|
181
|
-
- lib/userbin/models/channel.rb
|
182
182
|
- lib/userbin/models/event.rb
|
183
183
|
- lib/userbin/models/model.rb
|
184
184
|
- lib/userbin/models/monitoring.rb
|
185
185
|
- lib/userbin/models/pairing.rb
|
186
|
-
- lib/userbin/models/
|
186
|
+
- lib/userbin/models/recovery_codes.rb
|
187
187
|
- lib/userbin/models/session.rb
|
188
188
|
- lib/userbin/models/user.rb
|
189
189
|
- lib/userbin/request.rb
|