userbin 1.5.0 → 1.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bb87601eef49b0f5d845aadecd86b36c2aa2a5a2
4
- data.tar.gz: d35d0620253e7702afbdddd95b1a94c586018b34
3
+ metadata.gz: a838f3a5b083d7baf4689189e9937242ee900f35
4
+ data.tar.gz: 43613aa2135886c221fd39fc2753cd9aed44e8fb
5
5
  SHA512:
6
- metadata.gz: 7a7d1ad419f59374579ecff1a8d3038671da449fc9b7dd4a637ba8db300c711fe07394d6fc9d842e50a935c0055acc2c375d5a5f53f8610588b37371d581feb3
7
- data.tar.gz: 2cda728dfc00d9c03288037a5b6039ff04b40d0d5a150994d4a329a7e34a40f7feaf844b8ec41f4540346548a10b04d69d58674262cc486a3ee7c7169104863f
6
+ metadata.gz: 64882804d2c11cc349d85e92fcbde230106bdb40e75099f94cf33b67ad47904ec587094dc98b18b656b326230e981c535248ed7256f1c1af10c903911efd7e0e
7
+ data.tar.gz: 89dcec756ea9f6856894071b1eae52f8017e48db7e6efb8484115d76cdf6bcd5e9eb0a12873137710a34e0fef44f242de67b702a4e0cfbf1cc760d1fb6029c31
data/README.md CHANGED
@@ -3,14 +3,16 @@
3
3
  [![Build Status](https://travis-ci.org/userbin/userbin-ruby.png)](https://travis-ci.org/userbin/userbin-ruby)
4
4
  [![Gem Version](https://badge.fury.io/rb/userbin.png)](http://badge.fury.io/rb/userbin)
5
5
  [![Dependency Status](https://gemnasium.com/userbin/userbin-ruby.png)](https://gemnasium.com/userbin/userbin-ruby)
6
+ [![Coverage Status](https://coveralls.io/repos/userbin/userbin-ruby/badge.png)](https://coveralls.io/r/userbin/userbin-ruby)
6
7
 
8
+ **[Userbin](https://userbin.com) adds an additional security layer to your application by providing account takeover protection and two-factor authentication in a white-label package**
7
9
 
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.
10
+ 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
11
 
10
12
  ## Table of Contents
11
13
 
14
+ - [Installation](#installation)
12
15
  - [Getting Started](#getting-started)
13
- - [Setup User Monitoring](#setup-user-monitoring)
14
16
  - [Active Sessions](#active-sessions)
15
17
  - [Security Events](#security-events)
16
18
  - [Two-factor Authentication](#two-factor-authentication)
@@ -22,7 +24,7 @@
22
24
  - [Backup Codes](#backup-codes)
23
25
  - [List Pairings](#list-pairings)
24
26
 
25
- ## Getting Started
27
+ ## Installation
26
28
 
27
29
  Add the `userbin` gem to your `Gemfile`
28
30
 
@@ -30,22 +32,17 @@ Add the `userbin` gem to your `Gemfile`
30
32
  gem "userbin"
31
33
  ```
32
34
 
33
- Install the gem
34
-
35
- ```bash
36
- bundle install
37
- ```
38
-
39
35
  Load and configure the library with your Userbin API secret in an initializer or similar.
40
36
 
41
37
  ```ruby
42
- require 'userbin'
43
38
  Userbin.api_secret = "YOUR_API_SECRET"
44
39
  ```
45
40
 
46
- ## Setup User Monitoring
41
+ ## Getting started
47
42
 
48
- 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.
43
+ ### 1. Logging in and out
44
+
45
+ You should call `login` as soon as the user has logged in to your application to start the Userbin session. Pass a unique user identifier, and an *optional* hash of user properties which are used when searching for users in your dashboard.
49
46
 
50
47
  ```ruby
51
48
  def your_after_login_hook
@@ -53,9 +50,7 @@ def your_after_login_hook
53
50
  end
54
51
  ```
55
52
 
56
- Once logged in to Userbin, all requests made through the Userbin instance are on behalf of the currently logged in user.
57
-
58
- When a user logs out from within your application, call `logout` to remove the session from the user's [active sessions](#active-sessions).
53
+ When a user logs out from within your application, call `logout` to tell Userbin to remove the session from the user's [active sessions](#active-sessions).
59
54
 
60
55
  ```ruby
61
56
  def your_after_logout_hook
@@ -63,32 +58,46 @@ def your_after_logout_hook
63
58
  end
64
59
  ```
65
60
 
66
- 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.
61
+ **Check that it works** by logging in to your application and watch your user appear in the [Userbin dashboard](https://dashboard.userbin.com).
67
62
 
68
- **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.
63
+ ### 2. Protecting routes
69
64
 
70
- ```ruby
71
- class AccountController < ApplicationController
72
- before_filter :authenticate_user! # from e.g. Devise
73
- before_filter { userbin.authorize! }
74
- # ...
75
- end
76
- ```
65
+ Call `authorize!` just before your `current_user` is being initialized. Usually you'll want to override your normal authentication filter, e.g. `authenticate_user!` if you're using Devise.
77
66
 
78
- You should catch these errors in one place and log out the authenticated user.
67
+ - `UserUnauthorizedError` will be raised if `login` has not yet been called, or if the session is no longer valid.
68
+ - `ChallengeRequiredError` will be raised when the user has enabled two-factor authentication and is logging in from an untrusted device.
79
69
 
80
70
  ```ruby
81
71
  class ApplicationController < ActionController::Base
82
- rescue_from Userbin::UserUnauthorizedError do |e|
83
- sign_out # log out your user locally
84
- redirect_to root_url
72
+ rescue_from Userbin::UserUnauthorizedError, with: :user_unauthorized
73
+ rescue_from Userbin::ChallengeRequiredError, with: :challenge_required
74
+
75
+ # IMPLEMENT: Override the authentication method from your framework
76
+ def authenticate_user!
77
+ userbin.authorize!
78
+ super
79
+ end
80
+
81
+ # IMPLEMENT: Log out your user locally
82
+ def user_unauthorized
83
+ sign_out
84
+ redirect_to root_path
85
+ end
86
+
87
+ # IMPLEMENT: Redirect to two-factor authentication login
88
+ def challenge_required
89
+ redirect_to show_challenge_path
85
90
  end
86
91
  end
87
92
  ```
88
93
 
94
+ Then use the overridden filter in your protected routes as you normally would, and all your critical flows will be protected from account takeover.
89
95
 
90
-
91
- **That's it!** Now log in to your application and watch your user appear in the [Userbin dashboard](https://dashboard.userbin.com).
96
+ ```ruby
97
+ class AccountController < ApplicationController
98
+ before_filter :authenticate_user!
99
+ end
100
+ ```
92
101
 
93
102
  ## Active Sessions
94
103
 
@@ -12,13 +12,13 @@ require 'userbin/version'
12
12
  require 'userbin/configuration'
13
13
  require 'userbin/client'
14
14
  require 'userbin/errors'
15
- require 'userbin/session_store'
16
- require 'userbin/trusted_token_store'
15
+ require 'userbin/token_store'
17
16
  require 'userbin/jwt'
18
17
  require 'userbin/utils'
19
18
  require 'userbin/request'
20
19
  require 'userbin/session_token'
21
20
 
21
+ require 'userbin/support/cookie_store'
22
22
  require 'userbin/support/rails' if defined?(Rails::Railtie)
23
23
  if defined?(Sinatra::Base)
24
24
  if defined?(Padrino)
@@ -34,6 +34,7 @@ end
34
34
 
35
35
  # These need to be required after setting up Her
36
36
  require 'userbin/models/model'
37
+ require 'userbin/models/account'
37
38
  require 'userbin/models/event'
38
39
  require 'userbin/models/challenge'
39
40
  require 'userbin/models/context'
@@ -17,21 +17,13 @@ module Userbin
17
17
  :backup_codes, :generate_backup_codes, :trusted_devices,
18
18
  :enable_mfa!, :disable_mfa!
19
19
 
20
- def initialize(request, cookies, opts = {})
20
+ def initialize(request, response, opts = {})
21
21
  # Save a reference in the per-request store so that the request
22
22
  # middleware in request.rb can access it
23
23
  RequestStore.store[:userbin] = self
24
24
 
25
- # By default the session token is persisted in the Rack store, which may
26
- # in turn point to any source. This option give you an option to
27
- # use any store, such as Redis or Memcached to store your Userbin tokens.
28
- if opts[:session_store]
29
- @session_store = opts[:session_store]
30
- else
31
- @session_store = Userbin::SessionStore::Rack.new(request.session)
32
- end
33
-
34
- @trusted_token_store = Userbin::TrustedTokenStore::Rack.new(cookies)
25
+ cookies = Userbin::CookieStore.new(request, response)
26
+ @store = Userbin::TokenStore.new(cookies)
35
27
 
36
28
  @request_context = {
37
29
  ip: request.ip,
@@ -39,58 +31,23 @@ module Userbin
39
31
  }
40
32
  end
41
33
 
42
- def session_token=(value)
43
- if value && value != @session_store.read
44
- @session_store.write(value)
45
- elsif !value
46
- @session_store.destroy
47
- end
48
- end
49
-
50
34
  def session_token
51
- token = @session_store.read
52
- Userbin::SessionToken.new(token) if token
53
- end
54
-
55
- def trusted_device_token=(value)
56
- if value && value != @trusted_token_store.read
57
- @trusted_token_store.write(value)
58
- elsif !value
59
- @trusted_token_store.destroy
60
- end
61
- end
62
-
63
- def trusted_device_token
64
- @trusted_token_store.read
35
+ @store.session_token
65
36
  end
66
37
 
67
- def identify(user_id)
68
- # The user identifier is used in API paths so it needs to be cleaned
69
- user_id = URI.encode(user_id.to_s)
70
-
71
- @session_store.user_id = user_id
72
- @trusted_token_store.user_id = user_id
73
- end
74
-
75
- def authorize
76
- return unless session_token
77
-
78
- if session_token.expired?
79
- Userbin::Monitoring.heartbeat
80
- end
81
- end
82
-
83
- def authorized?
84
- !!session_token
38
+ def session_token=(session_token)
39
+ @store.session_token = session_token
85
40
  end
86
41
 
87
42
  def authorize!
88
- unless session_token
43
+ unless @store.session_token
89
44
  raise Userbin::UserUnauthorizedError,
90
45
  'Need to call login before authorize'
91
46
  end
92
47
 
93
- authorize
48
+ if @store.session_token.expired?
49
+ Userbin::Monitoring.heartbeat
50
+ end
94
51
 
95
52
  if mfa_in_progress?
96
53
  logout
@@ -103,63 +60,66 @@ module Userbin
103
60
  end
104
61
  end
105
62
 
63
+ def authorized?
64
+ !!@store.session_token
65
+ end
66
+
106
67
  def login(user_id, user_attrs = {})
107
68
  # Clear the session token if any
108
- self.session_token = nil
109
-
110
- identify(user_id)
69
+ @store.session_token = nil
111
70
 
112
- user = Userbin::User.new(@session_store.user_id)
71
+ user = Userbin::User.new(user_id.to_s)
113
72
  session = user.sessions.create(
114
- user: user_attrs, trusted_device_token: self.trusted_device_token)
73
+ user: user_attrs, trusted_device_token: @store.trusted_device_token)
115
74
 
116
75
  # Set the session token for use in all subsequent requests
117
- self.session_token = session.token
76
+ @store.session_token = session.token
118
77
 
119
78
  session
120
79
  end
121
80
 
122
- def trust_device(attrs = {})
123
- trusted_device = trusted_devices.create(attrs)
124
-
125
- # Set the session token for use in all subsequent requests
126
- self.trusted_device_token = trusted_device.token
127
- end
128
-
129
- # This method ends the current monitoring session. It should be called
130
- # whenever the user logs out from your system.
131
- #
132
81
  def logout
133
- return unless session_token
82
+ return unless @store.session_token
134
83
 
135
84
  # Destroy the current session specified in the session token
136
85
  begin
137
86
  sessions.destroy('$current')
138
- rescue Userbin::Error # ignored
87
+ rescue Userbin::ApiError # ignored
139
88
  end
140
89
 
141
90
  # Clear the session token
142
- self.session_token = nil
91
+ @store.session_token = nil
92
+ end
93
+
94
+ def trust_device(attrs = {})
95
+ unless @store.session_token
96
+ raise Userbin::UserUnauthorizedError,
97
+ 'Need to call login before trusting device'
98
+ end
99
+ trusted_device = trusted_devices.create(attrs)
100
+
101
+ # Set the session token for use in all subsequent requests
102
+ @store.trusted_device_token = trusted_device.token
143
103
  end
144
104
 
145
105
  def mfa_enabled?
146
- session_token ? session_token.mfa_enabled? : false
106
+ @store.session_token ? @store.session_token.mfa_enabled? : false
147
107
  end
148
108
 
149
109
  def device_trusted?
150
- session_token ? session_token.device_trusted? : false
110
+ @store.session_token ? @store.session_token.device_trusted? : false
151
111
  end
152
112
 
153
113
  def mfa_in_progress?
154
- session_token ? session_token.has_challenge? : false
114
+ @store.session_token ? @store.session_token.mfa_in_progress? : false
155
115
  end
156
116
 
157
117
  def mfa_required?
158
- session_token ? session_token.needs_challenge? : false
118
+ @store.session_token ? @store.session_token.mfa_required? : false
159
119
  end
160
120
 
161
121
  def has_default_pairing?
162
- session_token ? session_token.has_default_pairing? : false
122
+ @store.session_token ? @store.session_token.has_default_pairing? : false
163
123
  end
164
124
  end
165
125
  end
@@ -0,0 +1,11 @@
1
+ module Userbin
2
+ class Account < Model
3
+ def self.fetch
4
+ get('/v1/account')
5
+ end
6
+
7
+ def self.update(settings = {})
8
+ put('/v1/account', settings: settings)
9
+ end
10
+ end
11
+ end
@@ -2,7 +2,7 @@ module Userbin
2
2
  class Context < Model
3
3
  def user_agent
4
4
  if attributes['user_agent']
5
- Userbin::Location.new(attributes['user_agent'])
5
+ Userbin::UserAgent.new(attributes['user_agent'])
6
6
  end
7
7
  end
8
8
 
@@ -4,5 +4,8 @@ module Userbin
4
4
  instance_post :verify
5
5
  instance_post :set_default!
6
6
  belongs_to :user
7
+ has_one :config
7
8
  end
9
+
10
+ class Config < Model; end
8
11
  end
@@ -16,24 +16,24 @@ module Userbin
16
16
  @jwt.expired?
17
17
  end
18
18
 
19
- def needs_challenge?
20
- @jwt.payload['vfy'] > 0
19
+ def device_trusted?
20
+ @jwt.payload['tru'] == 1
21
21
  end
22
22
 
23
- def has_challenge?
24
- @jwt.payload['chg'] == 1
23
+ def has_default_pairing?
24
+ @jwt.payload['dpr'] > 0
25
25
  end
26
26
 
27
27
  def mfa_enabled?
28
28
  @jwt.payload['mfa'] == 1
29
29
  end
30
30
 
31
- def device_trusted?
32
- @jwt.payload['tru'] == 1
31
+ def mfa_in_progress?
32
+ @jwt.payload['chg'] == 1
33
33
  end
34
34
 
35
- def has_default_pairing?
36
- @jwt.payload['dpr'] == 1
35
+ def mfa_required?
36
+ @jwt.payload['vfy'] > 0
37
37
  end
38
38
  end
39
39
  end
@@ -10,7 +10,14 @@ module Userbin
10
10
  end
11
11
 
12
12
  def []=(key, value)
13
- @response.set_cookie key, value
13
+ @request.cookies[key] = value
14
+ if value
15
+ @response.set_cookie(key, value: value,
16
+ expires: Time.now + (365 * 24 * 60 * 60),
17
+ path: '/')
18
+ else
19
+ @response.delete_cookie(key)
20
+ end
14
21
  end
15
22
  end
16
23
  end
@@ -5,8 +5,7 @@ module Padrino
5
5
  module Userbin
6
6
  module Helpers
7
7
  def userbin
8
- store = ::Userbin::CookieStore.new(request, response)
9
- @userbin ||= ::Userbin::Client.new(request, store)
8
+ @userbin ||= ::Userbin::Client.new(request, response)
10
9
  end
11
10
  end
12
11
 
@@ -1,7 +1,7 @@
1
1
  module Userbin
2
2
  module UserbinClient
3
3
  def userbin
4
- @userbin ||= Userbin::Client.new(request, cookies)
4
+ @userbin ||= Userbin::Client.new(request, response)
5
5
  end
6
6
  end
7
7
 
@@ -4,8 +4,7 @@ module Sinatra
4
4
  module Userbin
5
5
  module Helpers
6
6
  def userbin
7
- store = ::Userbin::CookieStore.new(request, response)
8
- @userbin ||= ::Userbin::Client.new(request, store)
7
+ @userbin ||= ::Userbin::Client.new(request, response)
9
8
  end
10
9
  end
11
10
 
@@ -0,0 +1,30 @@
1
+ module Userbin
2
+ class TokenStore
3
+ def initialize(cookies)
4
+ @cookies = cookies
5
+ end
6
+
7
+ def session_token
8
+ token = @cookies['_ubs']
9
+ Userbin::SessionToken.new(token) if token
10
+ end
11
+
12
+ def session_token=(value)
13
+ @cookies['_ubs'] = value
14
+
15
+ if value && value != @cookies['_ubs']
16
+ @cookies['_ubs']
17
+ elsif !value
18
+ @cookies['_ubs'] = nil
19
+ end
20
+ end
21
+
22
+ def trusted_device_token
23
+ @cookies['_ubt']
24
+ end
25
+
26
+ def trusted_device_token=(value)
27
+ @cookies['_ubt'] = value
28
+ end
29
+ end
30
+ end
@@ -1,3 +1,3 @@
1
1
  module Userbin
2
- VERSION = "1.5.0"
2
+ VERSION = "1.6.0"
3
3
  end
@@ -28,16 +28,4 @@ describe 'Userbin::User' do
28
28
  user.save
29
29
  end
30
30
  end
31
-
32
- it 'imports users' do
33
- VCR.use_cassette('user_import') do
34
- users = Userbin::User.import(
35
- users: [
36
- { email: '10@example.com', username: '10' },
37
- { email: '20@example.com', username: '20' }
38
- ]
39
- )
40
- users.count.should == 2
41
- end
42
- end
43
31
  end
@@ -1,9 +1,20 @@
1
1
  require 'rubygems'
2
2
  require 'bundler/setup'
3
3
  require 'rack'
4
- require 'userbin'
5
4
  require 'vcr'
6
5
  require 'webmock/rspec'
6
+ require 'simplecov'
7
+ require 'coveralls'
8
+
9
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
10
+ SimpleCov::Formatter::HTMLFormatter,
11
+ Coveralls::SimpleCov::Formatter
12
+ ]
13
+ SimpleCov.start do
14
+ add_filter 'spec'
15
+ end
16
+
17
+ require 'userbin'
7
18
 
8
19
  VCR.configure do |config|
9
20
  config.cassette_library_dir = 'spec/fixtures/vcr_cassettes'
@@ -1,20 +1,24 @@
1
1
  require 'spec_helper'
2
2
 
3
- class MemoryStore < Userbin::SessionStore
3
+ class MemoryStore < Userbin::TokenStore
4
4
  def initialize
5
5
  @value = nil
6
6
  end
7
7
 
8
- def read
9
- @value
8
+ def session_token
9
+ @value['_ubs']
10
10
  end
11
11
 
12
- def write(value)
13
- @value = value
12
+ def session_token=(value)
13
+ @value['_ubs'] = value
14
14
  end
15
15
 
16
- def destroy
17
- @value = nil
16
+ def trusted_device_token
17
+ @value['_ubt']
18
+ end
19
+
20
+ def trusted_device_token=(value)
21
+ @value['_ubt'] = value
18
22
  end
19
23
  end
20
24
 
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.5.0
4
+ version: 1.6.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-11-07 00:00:00.000000000 Z
11
+ date: 2014-11-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: her
@@ -164,6 +164,20 @@ dependencies:
164
164
  - - ">="
165
165
  - !ruby/object:Gem::Version
166
166
  version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: coveralls
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: 0.7.2
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: 0.7.2
167
181
  description: Secure your application with multi-factor authentication, user activity
168
182
  monitoring, and real-time threat protection.
169
183
  email: johan@userbin.com
@@ -178,6 +192,7 @@ files:
178
192
  - lib/userbin/errors.rb
179
193
  - lib/userbin/ext/her.rb
180
194
  - lib/userbin/jwt.rb
195
+ - lib/userbin/models/account.rb
181
196
  - lib/userbin/models/backup_codes.rb
182
197
  - lib/userbin/models/challenge.rb
183
198
  - lib/userbin/models/context.rb
@@ -195,7 +210,7 @@ files:
195
210
  - lib/userbin/support/padrino.rb
196
211
  - lib/userbin/support/rails.rb
197
212
  - lib/userbin/support/sinatra.rb
198
- - lib/userbin/trusted_token_store.rb
213
+ - lib/userbin/token_store.rb
199
214
  - lib/userbin/utils.rb
200
215
  - lib/userbin/version.rb
201
216
  - spec/fixtures/vcr_cassettes/challenge_create.yml
@@ -1,35 +0,0 @@
1
- module Userbin
2
- class TrustedTokenStore
3
- class Rack < TrustedTokenStore
4
- def initialize(cookies)
5
- @cookies = cookies
6
- end
7
-
8
- def user_id
9
- @cookies['userbin.user_id']
10
- end
11
-
12
- def user_id=(value)
13
- @cookies['userbin.user_id'] = value
14
- end
15
-
16
- def read
17
- @cookies[key]
18
- end
19
-
20
- def write(value)
21
- @cookies[key] = value
22
- end
23
-
24
- def destroy
25
- @cookies.delete(key)
26
- end
27
-
28
- private
29
-
30
- def key
31
- "userbin.trusted_device_token.#{user_id}"
32
- end
33
- end
34
- end
35
- end