userbin 1.5.0 → 1.6.0

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