userbin 1.3.1 → 1.4.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: 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