userbin 1.2.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|