userbin 1.3.1 → 1.4.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: 01656a6ef1aa891d26aadd81b5c2541f6bdee830
4
- data.tar.gz: 131a697805e6e9a2e289efb08c2a2a60a9dfa6e5
3
+ metadata.gz: 0257d124a0048c84143386d8b283897aea15857b
4
+ data.tar.gz: dc8f8dfb8a4333dba7cd9f1852f54dadbe7a73bf
5
5
  SHA512:
6
- metadata.gz: f46b4ffb2639ba4d842a75b733dd555a46499deeaf40acdbbc1a8ae3b07d1d70132f571eba1b3d75bfa28d1637431718fe395594d3eda29fe3ec9fd2756a4822
7
- data.tar.gz: 3ee6024aae53c0502d9df9d8498873092f6121aa4afb521a2dc6e24567d2fce0e8397719595fbe9a2192878f781b31406301a671d4495d22ff58ae63166e806b
6
+ metadata.gz: 45a474521b983d95e7feb3db328d2253f5e8ba690ac3848b4a78198ff25520f5b5138363ca518c0061a45a290fcab48bdefd1e66972d0911f36a94c70443d753
7
+ data.tar.gz: 811d1c0c867a4f02b4dcceab6c67696fae958d96e130fc7c87b8d337da9727468d42aeda9e67111708aa24935b8f377e782459554033c43eaebaa17249661df5
data/README.md CHANGED
@@ -43,12 +43,12 @@ require 'userbin'
43
43
  Userbin.api_secret = "YOUR_API_SECRET"
44
44
  ```
45
45
 
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.
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. The second argument is a reference to the **cookies** hash, used for storing the trusted device token.
47
47
 
48
48
  ```ruby
49
49
  class ApplicationController < ActionController::Base
50
50
  def userbin
51
- @userbin ||= Userbin::Client.new(request)
51
+ @userbin ||= Userbin::Client.new(request, cookies)
52
52
  end
53
53
  # ...
54
54
  end
@@ -209,7 +209,7 @@ userbin.disable_mfa
209
209
 
210
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
211
 
212
- Capture this error just as with UserUnauthorizedError and redirect the user.
212
+ Capture this error just as with UserUnauthorizedError and redirect the user to a path **not protected** by `authorize!`.
213
213
 
214
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.
215
215
 
@@ -224,6 +224,8 @@ end
224
224
 
225
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.
226
226
 
227
+ When you call `trust_device`, the user will not be challenged for secondary authentication when they log in to your application from that device for a set period of time. You could add this to your form as a checkbox option.
228
+
227
229
  ```ruby
228
230
  class ChallengeController < ApplicationController
229
231
  def show
@@ -236,11 +238,17 @@ class ChallengeController < ApplicationController
236
238
 
237
239
  userbin.challenges.verify(challenge_id, response: code)
238
240
 
241
+ # Avoid verification on next login for better experience
242
+ userbin.trust_device if params[:trust_device]
243
+
239
244
  # Yay, the challenge was verified!
240
245
  redirect_to root_url
241
246
 
242
- rescue Userbin::ForbiddenError
247
+ rescue Userbin::ForbiddenError => e
248
+ sign_out # log out your user locally
249
+
243
250
  flash.notice = 'Wrong code, bye!'
251
+ redirect_to root_path
244
252
  end
245
253
  end
246
254
  ```
data/lib/userbin.rb CHANGED
@@ -13,6 +13,7 @@ require 'userbin/configuration'
13
13
  require 'userbin/client'
14
14
  require 'userbin/errors'
15
15
  require 'userbin/session_store'
16
+ require 'userbin/trusted_token_store'
16
17
  require 'userbin/jwt'
17
18
  require 'userbin/utils'
18
19
  require 'userbin/request'
@@ -30,4 +31,5 @@ require 'userbin/models/monitoring'
30
31
  require 'userbin/models/pairing'
31
32
  require 'userbin/models/backup_codes'
32
33
  require 'userbin/models/session'
34
+ require 'userbin/models/trusted_device'
33
35
  require 'userbin/models/user'
@@ -14,15 +14,16 @@ module Userbin
14
14
  end
15
15
 
16
16
  install_proxy_methods :challenges, :events, :sessions, :pairings,
17
- :backup_codes, :generate_backup_codes, :enable_mfa, :disable_mfa
17
+ :backup_codes, :generate_backup_codes, :trusted_devices,
18
+ :enable_mfa, :disable_mfa
18
19
 
19
- def initialize(request, opts = {})
20
+ def initialize(request, cookies, opts = {})
20
21
  # Save a reference in the per-request store so that the request
21
22
  # middleware in request.rb can access it
22
23
  RequestStore.store[:userbin] = self
23
24
 
24
25
  # By default the session token is persisted in the Rack store, which may
25
- # in turn point to any source. But this option gives you an option to
26
+ # in turn point to any source. This option give you an option to
26
27
  # use any store, such as Redis or Memcached to store your Userbin tokens.
27
28
  if opts[:session_store]
28
29
  @session_store = opts[:session_store]
@@ -30,6 +31,8 @@ module Userbin
30
31
  @session_store = Userbin::SessionStore::Rack.new(request.session)
31
32
  end
32
33
 
34
+ @trusted_token_store = Userbin::TrustedTokenStore::Rack.new(cookies)
35
+
33
36
  @request_context = {
34
37
  ip: request.ip,
35
38
  user_agent: request.user_agent
@@ -49,11 +52,24 @@ module Userbin
49
52
  Userbin::SessionToken.new(token) if token
50
53
  end
51
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
65
+ end
66
+
52
67
  def identify(user_id)
53
68
  # The user identifier is used in API paths so it needs to be cleaned
54
69
  user_id = URI.encode(user_id.to_s)
55
70
 
56
71
  @session_store.user_id = user_id
72
+ @trusted_token_store.user_id = user_id
57
73
  end
58
74
 
59
75
  def authorize
@@ -82,7 +98,9 @@ module Userbin
82
98
  'Logged out due to being unverified'
83
99
  end
84
100
 
85
- raise Userbin::ChallengeRequiredError if mfa_required?
101
+ if mfa_required? && !device_trusted?
102
+ raise Userbin::ChallengeRequiredError
103
+ end
86
104
  end
87
105
 
88
106
  def login(user_id, user_attrs = {})
@@ -92,12 +110,20 @@ module Userbin
92
110
  identify(user_id)
93
111
 
94
112
  session = Userbin::Session.post(
95
- "users/#{@session_store.user_id}/sessions", user: user_attrs)
113
+ "users/#{@session_store.user_id}/sessions", user: user_attrs,
114
+ trusted_device_token: self.trusted_device_token)
96
115
 
97
116
  # Set the session token for use in all subsequent requests
98
117
  self.session_token = session.token
99
118
  end
100
119
 
120
+ def trust_device(attrs = {})
121
+ trusted_device = trusted_devices.create(attrs)
122
+
123
+ # Set the session token for use in all subsequent requests
124
+ self.trusted_device_token = trusted_device.token
125
+ end
126
+
101
127
  # This method ends the current monitoring session. It should be called
102
128
  # whenever the user logs out from your system.
103
129
  #
@@ -118,6 +144,10 @@ module Userbin
118
144
  session_token ? session_token.mfa_enabled? : false
119
145
  end
120
146
 
147
+ def device_trusted?
148
+ session_token ? session_token.device_trusted? : false
149
+ end
150
+
121
151
  def mfa_in_progress?
122
152
  session_token ? session_token.has_challenge? : false
123
153
  end
@@ -0,0 +1,5 @@
1
+ module Userbin
2
+ class TrustedDevice < Model
3
+ collection_path "users/:user_id/trusted_devices"
4
+ end
5
+ end
@@ -10,6 +10,7 @@ module Userbin
10
10
  has_many :events
11
11
  has_many :pairings
12
12
  has_many :sessions
13
+ has_many :trusted_devices
13
14
 
14
15
  def backup_codes(params={})
15
16
  Userbin::BackupCodes.get("/v1/users/#{id}/backup_codes", params)
@@ -35,7 +35,7 @@ module Userbin
35
35
 
36
36
  def call(env)
37
37
  value = Base64.encode64(":#{@api_secret || Userbin.config.api_secret}")
38
- value.gsub!("\n", '')
38
+ value.delete!("\n")
39
39
  env[:request_headers]["Authorization"] = "Basic #{value}"
40
40
  @app.call(env)
41
41
  end
@@ -28,6 +28,10 @@ module Userbin
28
28
  @jwt.payload['mfa'] == 1
29
29
  end
30
30
 
31
+ def device_trusted?
32
+ @jwt.payload['tru'] == 1
33
+ end
34
+
31
35
  def challenge_type
32
36
  @jwt.payload['chg']['typ'].to_sym if has_challenge?
33
37
  end
@@ -0,0 +1,35 @@
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
@@ -1,3 +1,3 @@
1
1
  module Userbin
2
- VERSION = "1.3.1"
2
+ VERSION = "1.4.0"
3
3
  end
data/spec/jwt_spec.rb CHANGED
@@ -13,14 +13,14 @@ describe 'Userbin::JWT' do
13
13
  it 'verifies that JWT has expired' do
14
14
  new_time = Time.utc(2014, 4, 23, 8, 46, 44)
15
15
  Timecop.freeze(new_time) do
16
- Userbin::JWT.new(token).expired?.should be_true
16
+ Userbin::JWT.new(token).should be_expired
17
17
  end
18
18
  end
19
19
 
20
20
  it 'verifies that JWT has not expired' do
21
21
  new_time = Time.utc(2014, 4, 23, 8, 46, 43)
22
22
  Timecop.freeze(new_time) do
23
- Userbin::JWT.new(token).expired?.should be_false
23
+ Userbin::JWT.new(token).should_not be_expired
24
24
  end
25
25
  end
26
26
  end
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.3.1
4
+ version: 1.4.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-10 00:00:00.000000000 Z
11
+ date: 2014-10-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: her
@@ -185,10 +185,12 @@ files:
185
185
  - lib/userbin/models/monitoring.rb
186
186
  - lib/userbin/models/pairing.rb
187
187
  - lib/userbin/models/session.rb
188
+ - lib/userbin/models/trusted_device.rb
188
189
  - lib/userbin/models/user.rb
189
190
  - lib/userbin/request.rb
190
191
  - lib/userbin/session_store.rb
191
192
  - lib/userbin/session_token.rb
193
+ - lib/userbin/trusted_token_store.rb
192
194
  - lib/userbin/utils.rb
193
195
  - lib/userbin/version.rb
194
196
  - spec/fixtures/vcr_cassettes/challenge_create.yml