uber_login 0.2.0 → 1.0.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,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- MWU0YWFkMDg3NjA3MzFiM2QxN2Y3NmQzODBlOTFjMDIxZGY1YzliYg==
4
+ NjcyNWVjZmViMmQ3ZTY4MDljODFjYjAxZDc2YmY5YmE4ZDEyNjhiZA==
5
5
  data.tar.gz: !binary |-
6
- ZGYxZmFmZjA0ZGEzOGIzY2RmODVmODczOWQ5OTc0MzlmYTM5NGQ4NQ==
6
+ ZDg0ZTA4MzZkODQ4ZThjN2FkOTYyNjllMTk5MDY1ZmUxZDdlN2NmYg==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- Mzc4YWVjYzM0ZmQ5N2YyMDdlOWYxOTMzMjAwMDVlNmIwNzdiODk4YjRmNjQ4
10
- ZDhhY2FmZjg1ZmIwZDA4MDQyNGVjMjExMTE1MGQxMDE3Zjg2ZTQ3NjllMzE4
11
- OGJjMWEwMjhlZjM5ZjUzYWFhNDM1Y2NhOTM3ZDY5NjE4NzQ0ZGY=
9
+ NWFjODkyYjYwN2FlNDFkY2I0NTUwZDZlMmIyMGJhMzRiNTg5MGM1YmY0YzVj
10
+ M2Y0YTBhY2U0NjNkNThiMDg4MDJlYjM4NjAwNWUxNzcxYTc0N2E1MjdmZDZh
11
+ NmQzYWNkN2Q0YzhhODNlYmQ2ZmNmYTNmMzZjZGRlMzdhNmNkMTE=
12
12
  data.tar.gz: !binary |-
13
- Mjc0NGNiMGM4ZmU3ZTEzNjExOWQ2NGU4M2QyODExYTNhNGJkYzEwNWU2N2Yz
14
- ZGZiMWY2ZWVjZTU0MDY5MTRiNzk2YWM0NzEzNTZlMWJiNTdhOWI2YjQ0ZWYx
15
- YTA5YTc2NTFmYmJkZTAyMDJhNDU5ZTc5MDc3NGVlM2UzNTU4MTI=
13
+ MTJmMWI1MDhjODQzMWZlNWEzYjk2YzQxMTYxYzc3MGEzNWMyNDM5ZWU2MTc1
14
+ NDM0ZjQ2OGVhMjgxMzlkN2QyZGJlNThhODNiMDNmOWFjYTI1ZWU5MjVmMWVk
15
+ ZjlmOWI5N2JjMGJmYmY0MjdmNGNiODkzZjc0YWZkMzg2OGZiZTU=
@@ -2,14 +2,37 @@
2
2
  # Use this class in app/config/initializers to change configuration parameters.
3
3
  module UberLogin
4
4
  class Configuration
5
+ # Allow the same user to login on many different devices.
6
+ # This is only effective if strong_sessions is +true+. Otherwise it only affects persistent logins.
7
+ # Defaults to +true+
5
8
  attr_accessor :allow_multiple_login
9
+
10
+ # The validity of a login token (be it a cookie or session token). Tokens whose age is larger than that are
11
+ # considered expired and not valid.
12
+ # Defaults to +nil+ (no expiration)
6
13
  attr_accessor :token_expiration
14
+
15
+ # A token is considered valid only if brought by the same IP address to which it was assigned.
16
+ # This would provide a very effective solution against Cookie sniffing, unless it would affect legitimate users a
17
+ # lot. 99% of ISPs will change user IP on each connecition. Also mobile devices might change IP many times in a
18
+ # hour. Setting this to true may disconnect many mobile users each minute.
19
+ # Only decently usable in a private network where all IPs are static (or if you're really paranoid).
20
+ # Defaults to +false+
7
21
  attr_accessor :tie_tokens_to_ip
8
22
 
23
+ # Non persistent sessions are saved to the database too. On each request the session token is checked against the
24
+ # database just like the cookies one. It won't refresh it, however.
25
+ # This allows you to do nice things, like logging out users, just by removing the token from the database. Or having
26
+ # a full list of open sessions of any kind on any device.
27
+ # Even though this is strongly suggested to be +true+, it might impact performance, issuing a query on almost
28
+ # each page load. Be sure to index :uid and :sequence together on the +login_tokens+ table.
29
+ attr_accessor :strong_sessions
30
+
9
31
  def initialize
10
32
  self.allow_multiple_login = true
11
33
  self.token_expiration = nil
12
34
  self.tie_tokens_to_ip = false
35
+ self.strong_sessions = true
13
36
  end
14
37
  end
15
38
 
@@ -1,66 +1,44 @@
1
+ require 'uber_login/storage'
2
+ require 'uber_login/token_encoder'
3
+ require 'uber_login/token_validator'
4
+
1
5
  ##
2
6
  # This class handles the +:uid+ and +:ulogin+ cookies
3
7
  # It builds and sets the cookies, clears them, checks for their validity.
4
- class CookieManager
5
- def initialize(cookies, request)
6
- @cookies = cookies
7
- @request = request
8
- @validity_checks = [ :token_match ]
9
-
10
- @validity_checks << :ip_equality if UberLogin.configuration.tie_tokens_to_ip
11
- @validity_checks << :expiration if UberLogin.configuration.token_expiration
12
- end
13
-
14
- ##
15
- # Clears +:uid+ and +:ulogin+ cookies
16
- def clear
17
- @cookies.delete :uid
18
- @cookies.delete :ulogin
19
- end
20
-
21
- def valid?
22
- token_row = LoginToken.find_by(uid: @cookies[:uid], sequence: sequence)
23
- @validity_checks.all? { |check| send(check, token_row) }
24
- rescue
25
- false
26
- end
27
-
28
- def hashed_token
29
- BCrypt::Password.create(token).to_s
30
- end
31
-
32
- def persistent_login(uid, sequence, token)
33
- @cookies.permanent[:uid] = uid
34
- @cookies.permanent[:ulogin] = ulogin_cookie(sequence, token)
35
- end
36
-
37
- def ulogin_cookie(sequence, token)
38
- sequence + ':' + token
39
- end
40
-
41
- def sequence_and_token
42
- @cookies[:ulogin].split(':')
43
- end
44
-
45
- def sequence
46
- sequence_and_token[0]
47
- end
48
-
49
- def token
50
- sequence_and_token[1]
51
- end
52
-
53
- # Validity checks
54
-
55
- def token_match(row)
56
- BCrypt::Password.new(row.token) == token
57
- end
58
-
59
- def ip_equality(row)
60
- row.ip_address == @request.remote_ip
61
- end
62
-
63
- def expiration(row)
64
- row.updated_at >= Time.now - UberLogin.configuration.token_expiration
8
+ module UberLogin
9
+ class CookieManager
10
+ def initialize(cookies, request)
11
+ @cookies = cookies
12
+ @request = request
13
+ end
14
+
15
+ ##
16
+ # Sets the +:uid+ and +:ulogin+ cookies for next login
17
+ def persistent_login(uid, composite)
18
+ @cookies.permanent[:uid] = uid
19
+ @cookies.permanent[:ulogin] = TokenEncoder.encode_array composite
20
+ end
21
+
22
+ ##
23
+ # Clears +:uid+ and +:ulogin+ cookies
24
+ def clear
25
+ @cookies.delete :uid
26
+ @cookies.delete :ulogin
27
+ end
28
+
29
+ ##
30
+ # Returns true if cookies are considered valid from TokenEncoder validation rules
31
+ def valid?
32
+ token_row = Storage.find_composite(@cookies[:uid], @cookies[:ulogin])
33
+ TokenValidator.new(TokenEncoder.token(@cookies[:ulogin]), @request).valid?(token_row)
34
+ rescue
35
+ false
36
+ end
37
+
38
+ ##
39
+ # Returns true if the +:uid+ and +:ulogin+ cookies are set
40
+ def login_cookies?
41
+ @cookies[:uid] and @cookies[:ulogin]
42
+ end
65
43
  end
66
44
  end
@@ -0,0 +1,38 @@
1
+ require 'uber_login/storage'
2
+ require 'uber_login/token_encoder'
3
+ require 'uber_login/token_validator'
4
+
5
+ ##
6
+ # This class handles the +:uid+ and +:ulogin+ session variables
7
+ # It builds and sets the session variables, clears them, checks for their validity.
8
+ module UberLogin
9
+ class SessionManager
10
+ def initialize(session, request)
11
+ @session = session
12
+ @request = request
13
+ end
14
+
15
+ ##
16
+ # Sets the +:uid+ and +:ulogin+ session variables
17
+ def login(uid, composite)
18
+ @session[:uid] = uid
19
+ @session[:ulogin] = TokenEncoder.encode_array(composite) if UberLogin.configuration.strong_sessions
20
+ end
21
+
22
+ ##
23
+ # Clears +:uid+ and +:ulogin+ session variables
24
+ def clear
25
+ @session.delete :uid
26
+ @session.delete :ulogin
27
+ end
28
+
29
+ ##
30
+ # Returns true if the session is considered valid from TokenEncoder validation rules
31
+ def valid?
32
+ token_row = Storage.find_composite(@session[:uid], @session[:ulogin])
33
+ TokenValidator.new(TokenEncoder.token(@session[:ulogin]), @request).valid?(token_row)
34
+ rescue
35
+ false
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,31 @@
1
+ module UberLogin
2
+ class Storage
3
+ class << self
4
+ def find(uid, sequence)
5
+ LoginToken.find_by(uid: uid, sequence: sequence)
6
+ end
7
+
8
+ def find_composite(uid, composite)
9
+ find(uid, TokenEncoder.sequence(composite))
10
+ rescue # composite might invalid if cookies are tampered
11
+ nil
12
+ end
13
+
14
+ def build(uid, composite)
15
+ LoginToken.new(
16
+ uid: uid,
17
+ sequence: TokenEncoder.sequence(composite),
18
+ token: TokenEncoder.token_hash(composite)
19
+ )
20
+ end
21
+
22
+ def delete_all(uid)
23
+ LoginToken.destroy_all(uid: uid)
24
+ end
25
+
26
+ def delete_all_but(uid, composite)
27
+ # TODO: How to make this ORM agnostic?
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,36 @@
1
+ require 'bcrypt'
2
+
3
+ module UberLogin
4
+ class TokenEncoder
5
+ class << self
6
+ def generate
7
+ # 9 and 21 are both multiple of 3, so we do not get base64 padding (==)
8
+ [ SecureRandom.urlsafe_base64(9), SecureRandom.base64(21) ]
9
+ end
10
+
11
+ def encode(sequence, token)
12
+ encode_array [ sequence, token ]
13
+ end
14
+
15
+ def encode_array(composite_array)
16
+ composite_array.join(':')
17
+ end
18
+
19
+ def decode(composite)
20
+ composite.split(':')
21
+ end
22
+
23
+ def sequence(composite)
24
+ decode(composite)[0]
25
+ end
26
+
27
+ def token(composite)
28
+ decode(composite)[1]
29
+ end
30
+
31
+ def token_hash(composite)
32
+ BCrypt::Password.create(token(composite)).to_s
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,28 @@
1
+ module UberLogin
2
+ class TokenValidator
3
+ def initialize(token, request)
4
+ @token = token
5
+ @request = request
6
+ @validity_checks = [ :token_match ]
7
+ @validity_checks << :ip_equality if UberLogin.configuration.tie_tokens_to_ip
8
+ @validity_checks << :expiration if UberLogin.configuration.token_expiration
9
+ end
10
+
11
+ def valid?(row)
12
+ @validity_checks.all? { |check| send(check, row) }
13
+ end
14
+
15
+ private
16
+ def token_match(row)
17
+ BCrypt::Password.new(row.token) == @token
18
+ end
19
+
20
+ def ip_equality(row)
21
+ row.ip_address == @request.remote_ip
22
+ end
23
+
24
+ def expiration(row)
25
+ row.updated_at >= Time.now - UberLogin.configuration.token_expiration
26
+ end
27
+ end
28
+ end
@@ -1,3 +1,3 @@
1
1
  module UberLogin
2
- VERSION = '0.2.0'
2
+ VERSION = '1.0.0'
3
3
  end
data/lib/uber_login.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'uber_login/version'
2
2
  require 'uber_login/cookie_manager'
3
3
  require 'uber_login/configuration'
4
+ require 'uber_login/session_manager'
4
5
  require 'securerandom'
5
6
  require 'bcrypt'
6
7
  require 'user_agent'
@@ -8,7 +9,10 @@ require 'user_agent'
8
9
  module UberLogin
9
10
  ##
10
11
  # Returns the logged in user.
11
- # If session[+:uid+] is set it returns that user.
12
+ # If session[+:uid+] is set:
13
+ # * if strong sessions are enabled, it checks for session[+:ulogin+] and tests its value against the database
14
+ # * if strong sessions are not enabled, it only returns the corresponding +User+
15
+ #
12
16
  # If session[+:uid+] is NOT set but cookies[+:uid+] and cookies[+:ulogin+] ARE:
13
17
  # * It dissects +:ulogin+ into Sequence and Token
14
18
  # * Looks for a LoginToken from UID and Sequence
@@ -26,24 +30,31 @@ module UberLogin
26
30
  # Logs in the given +user+
27
31
  # If +remember+ is true all the needed cookies are set.
28
32
  # session[+:uid+] is set to user.id
33
+ # If strong sessions are enabled session[+:ulogin+] is set to the same value that cookies[+:ulogin+] would have
29
34
  def login(user, remember = false)
30
35
  logout_all unless UberLogin.configuration.allow_multiple_login
31
36
 
32
- session[:uid] = user.id
33
- generate_and_set_cookies(user.id) if remember
37
+ if strong_sessions or remember
38
+ composite = generate_and_save_token(user.id)
39
+ cookie_manager.persistent_login(user.id, composite) if remember
40
+ else
41
+ composite = nil
42
+ end
43
+
44
+ session_manager.login(user.id, composite)
34
45
  end
35
46
 
36
47
  ##
37
- # Clears session[:uid]
38
- # If remember cookies were set they're cleared and the token is
39
- # removed from the database.
48
+ # If sequence is nil it clears the current session and if remember cookies are in place they're cleared
49
+ # and corresponding token removed from the database.
50
+ # If sequence is not nil it only removes the sequence and token from the database.
40
51
  def logout(sequence = nil)
41
- if sequence
42
- delete_from_database(sequence)
43
- else
44
- session.delete(:uid)
45
- delete_from_database if cookies[:uid]
52
+ if sequence.nil? or sequence == current_sequence
53
+ delete_from_database if cookies[:uid] or strong_sessions
54
+ session_manager.clear
46
55
  cookie_manager.clear
56
+ else
57
+ delete_from_database(sequence)
47
58
  end
48
59
  end
49
60
 
@@ -51,8 +62,8 @@ module UberLogin
51
62
  # Deletes all "remember me" session for this user from whatever device
52
63
  # he/she has ever used to login.
53
64
  def logout_all
54
- LoginToken.find_by(uid: session[:uid]).destroy
55
- session.delete :uid
65
+ Storage.delete_all session[:uid]
66
+ session_manager.clear
56
67
  cookie_manager.clear
57
68
  end
58
69
 
@@ -61,11 +72,19 @@ module UberLogin
61
72
  @cookie_manager ||= CookieManager.new(cookies, request)
62
73
  end
63
74
 
75
+ def session_manager
76
+ @session_manager ||= SessionManager.new(session, request)
77
+ end
78
+
64
79
  # See +current_user+
65
80
  def current_user_uncached
66
- sid = session[:uid]
67
- sid = login_from_cookies if cookies[:uid] and !sid
68
- User.find(sid) if sid
81
+ if session[:uid]
82
+ logout if strong_sessions and !session_manager.valid?
83
+ else
84
+ login_from_cookies if cookie_manager.login_cookies?
85
+ end
86
+
87
+ User.find(session[:uid]) rescue nil
69
88
  end
70
89
 
71
90
  ##
@@ -74,6 +93,7 @@ module UberLogin
74
93
  if cookie_manager.valid?
75
94
  session[:uid] = cookies[:uid]
76
95
  generate_new_token
96
+ session[:ulogin] = cookies[:ulogin]
77
97
  session[:uid]
78
98
  else
79
99
  cookie_manager.clear
@@ -86,7 +106,8 @@ module UberLogin
86
106
  # Sets the user cookies to match the new values
87
107
  def generate_new_token
88
108
  delete_from_database
89
- generate_and_set_cookies(cookies[:uid])
109
+ composite = generate_and_save_token(cookies[:uid])
110
+ cookie_manager.persistent_login(cookies[:uid], composite)
90
111
  end
91
112
 
92
113
  ##
@@ -96,21 +117,16 @@ module UberLogin
96
117
  #
97
118
  # +:sequence+ is used to choose between all possible user login tokens
98
119
  # +:token+ is stored +bcrypt+ed in the database and then compared on login
99
- def generate_and_set_cookies(uid)
100
- sequence, token = generate_sequence_and_token
101
- cookie_manager.persistent_login(uid, sequence, token)
102
- save_to_database
120
+ def generate_and_save_token(uid)
121
+ sequence, token = TokenEncoder.generate
122
+ save_to_database(uid, sequence, token)
123
+ [ sequence, token ]
103
124
  end
104
125
 
105
126
  ##
106
127
  # Creates a LoginToken based on the +uid+, +sequence+ and hashed +token+
107
- def save_to_database
108
- token_row = LoginToken.new(
109
- uid: cookies[:uid],
110
- sequence: cookie_manager.sequence,
111
- token: cookie_manager.hashed_token
112
- )
113
-
128
+ def save_to_database(uid, sequence, token)
129
+ token_row = Storage.build(uid, TokenEncoder.encode(sequence, token))
114
130
  set_user_data token_row
115
131
 
116
132
  token_row.save!
@@ -119,15 +135,13 @@ module UberLogin
119
135
  ##
120
136
  # Removes a LoginToken with current +uid+ and given +sequence+
121
137
  # If +sequence+ is nil it is taken from the cookies.
138
+ #
139
+ # A token might have already been destroyed from another client with the intent of disconnecting
140
+ # the current session.
122
141
  def delete_from_database(sequence = nil)
123
- sequence = sequence || cookie_manager.sequence
124
- token = LoginToken.find_by(uid: cookies[:uid], sequence: sequence)
125
- token.destroy
126
- end
127
-
128
- def generate_sequence_and_token
129
- # 9 and 21 are both multiple of 3, so we do not get base64 padding (==)
130
- [ SecureRandom.base64(9), SecureRandom.base64(21) ]
142
+ sequence = sequence || current_sequence
143
+ token = Storage.find(cookies[:uid] || session[:uid], sequence)
144
+ token.destroy if token
131
145
  end
132
146
 
133
147
  def set_user_data(row)
@@ -137,4 +151,12 @@ module UberLogin
137
151
  row.os = user_agent.os if row.respond_to? :os=
138
152
  row.browser = user_agent.browser + ' ' + user_agent.version if row.respond_to? :browser=
139
153
  end
154
+
155
+ def strong_sessions
156
+ UberLogin.configuration.strong_sessions
157
+ end
158
+
159
+ def current_sequence
160
+ TokenEncoder.sequence(cookies[:ulogin] || session[:ulogin])
161
+ end
140
162
  end
@@ -1,60 +1,54 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe CookieManager do
4
- let(:cookie_manager) { CookieManager.new({ uid: 100, ulogin: "dead:beef" }, FakeRequest.new) }
5
-
6
- describe '#valid?' do
7
- context 'User id and sequence combination is found' do
8
- let(:fake_token) {
9
- LoginToken.new(
10
- token: BCrypt::Password.create("beef"),
11
- ip_address: '192.168.1.1'
12
- )
13
- }
14
-
15
- before { LoginToken.stub(:find_by).and_return fake_token }
16
-
17
- it 'always executes token_match' do
18
- expect(cookie_manager).to receive(:token_match)
19
- cookie_manager.valid?
20
- end
3
+ describe UberLogin::CookieManager do
4
+ let(:user) { double(id: 100) }
5
+ let(:controller) { ApplicationController.new }
6
+ let(:session) { controller.session }
7
+ let(:cookies) { controller.cookies }
8
+ let(:cookie_manager) { UberLogin::CookieManager.new(cookies, FakeRequest.new) }
9
+
10
+ describe '#persistent_login' do
11
+ it 'sets the :uid cookie' do
12
+ cookie_manager.persistent_login(100, [ 'dead', 'beef' ])
13
+ expect(cookies[:uid]).to eq 100
14
+ end
21
15
 
22
- it 'does not run ip_equality by default' do
23
- expect(cookie_manager).to_not receive(:ip_equality)
24
- cookie_manager.valid?
25
- end
16
+ it 'sets the :ulogin cookie' do
17
+ cookie_manager.persistent_login(100, [ 'dead', 'beef' ])
18
+ expect(cookies[:ulogin]).to eq 'dead:beef'
19
+ end
20
+ end
26
21
 
27
- context 'if IP are tied to Tokens' do
28
- before { UberLogin.configuration.tie_tokens_to_ip = true }
22
+ describe '#clear' do
23
+ before { controller.login(user, true) }
29
24
 
30
- it 'executes ip_equality' do
31
- expect(cookie_manager).to receive(:ip_equality)
32
- cookie_manager.valid?
33
- end
34
- end
25
+ it 'deletes the :uid cookie' do
26
+ cookie_manager.clear
27
+ expect(cookies[:uid]).to be_nil
28
+ end
35
29
 
36
- it 'does not run expiration by default' do
37
- expect(cookie_manager).to_not receive(:expiration)
38
- cookie_manager.valid?
39
- end
30
+ it 'deletes the :ulogin cookie' do
31
+ cookie_manager.clear
32
+ expect(cookies[:ulogin]).to be_nil
33
+ end
34
+ end
40
35
 
41
- context 'if Tokens do expire' do
42
- before { UberLogin.configuration.token_expiration = 86400 }
36
+ describe '#valid?' do
37
+ before { controller.login(user, true) }
43
38
 
44
- it 'executes expiration' do
45
- expect(cookie_manager).to receive(:expiration)
46
- cookie_manager.valid?
47
- end
48
- end
39
+ context 'User id and sequence combination is found' do
40
+ before { UberLogin::Storage.stub(:find_composite).and_return user }
49
41
 
50
42
  context 'all checks return true' do
43
+ before { UberLogin::TokenValidator.any_instance.stub(:valid?).and_return true }
44
+
51
45
  it 'returns true' do
52
46
  expect(cookie_manager.valid?).to be_true
53
47
  end
54
48
  end
55
49
 
56
50
  context 'any check fails' do
57
- before { Array.any_instance.stub(:all?).and_return false }
51
+ before { UberLogin::TokenValidator.any_instance.stub(:valid?).and_return false }
58
52
 
59
53
  it 'returns false' do
60
54
  expect(cookie_manager.valid?).to be_false
@@ -63,7 +57,7 @@ describe CookieManager do
63
57
  end
64
58
 
65
59
  context 'User id and sequence combination is not found' do
66
- before { LoginToken.stub(:find_by).and_return nil }
60
+ before { UberLogin::Storage.stub(:find).and_return nil }
67
61
 
68
62
  it 'returns false' do
69
63
  expect(cookie_manager.valid?).to be_false
@@ -71,45 +65,23 @@ describe CookieManager do
71
65
  end
72
66
  end
73
67
 
74
- describe '#token_match' do
75
- before { cookie_manager.stub(:token).and_return 'secret' }
68
+ describe '#login_cookies?' do
69
+ before { controller.login(user, true) }
76
70
 
77
- it 'returns true if tokens are matched' do
78
- row = double(token: BCrypt::Password.create('secret', cost: 1))
79
- expect(cookie_manager.token_match(row)).to be_true
80
- end
81
-
82
- it 'returns false if tokens are not matched' do
83
- row = double(token: BCrypt::Password.create('s3cr3t', cost: 1))
84
- expect(cookie_manager.token_match(row)).to be_false
85
- end
86
- end
87
-
88
- describe '#ip_equality' do
89
- before { FakeRequest.any_instance.stub(:remote_ip).and_return '10.10.10.10' }
90
-
91
- it 'returns true if IPs are equal' do
92
- row = double(ip_address: '10.10.10.10')
93
- expect(cookie_manager.ip_equality(row)).to be_true
94
- end
95
-
96
- it 'returns false if IPs are different' do
97
- row = double(ip_address: '192.168.1.1')
98
- expect(cookie_manager.ip_equality(row)).to be_false
71
+ context 'both cookies are set' do
72
+ it 'returns true' do
73
+ expect(cookie_manager.login_cookies?).to be_true
74
+ end
99
75
  end
100
- end
101
-
102
- describe '#expiration' do
103
- before { UberLogin.configuration.token_expiration = 86400 }
104
76
 
105
- it 'returns true if less than token_expiration seconds are past' do
106
- row = double(updated_at: Time.now - 100)
107
- expect(cookie_manager.expiration(row)).to be_true
77
+ it 'returns false if uid is missing' do
78
+ cookies.delete :uid
79
+ expect(cookie_manager.login_cookies?).to be_false
108
80
  end
109
81
 
110
- it 'returns false if more than token_expiration seconds are past' do
111
- row = double(updated_at: Time.now - 86401)
112
- expect(cookie_manager.expiration(row)).to be_false
82
+ it 'returns false if ulogin is missing' do
83
+ cookies.delete :ulogin
84
+ expect(cookie_manager.login_cookies?).to be_false
113
85
  end
114
86
  end
115
87
  end
@@ -0,0 +1,80 @@
1
+ require 'spec_helper'
2
+
3
+ describe UberLogin::SessionManager do
4
+ let(:user) { double(id: 100) }
5
+ let(:controller) { ApplicationController.new }
6
+ let(:session) { controller.session }
7
+ let(:cookies) { controller.cookies }
8
+ let(:session_manager) { UberLogin::SessionManager.new(session, FakeRequest.new) }
9
+
10
+ describe '#login' do
11
+ it 'sets the :uid variable' do
12
+ session_manager.login(100, [ 'dead', 'beef' ])
13
+ expect(session[:uid]).to eq 100
14
+ end
15
+
16
+ context 'strong_sessions are not enabled' do
17
+ before { UberLogin.configuration.strong_sessions = false }
18
+
19
+ it 'does not set the :ulogin cookie' do
20
+ session_manager.login(100, [ 'dead', 'beef' ])
21
+ expect(session[:ulogin]).to be_nil
22
+ end
23
+ end
24
+
25
+ context 'strong_sessions are enabled' do
26
+ before { UberLogin.configuration.strong_sessions = true }
27
+
28
+ it 'sets the :ulogin cookie' do
29
+ session_manager.login(100, [ 'dead', 'beef' ])
30
+ expect(session[:ulogin]).to_not be_nil
31
+ end
32
+ end
33
+ end
34
+
35
+ describe '#clear' do
36
+ before { controller.login(user, true) }
37
+
38
+ it 'deletes the :uid variable' do
39
+ session_manager.clear
40
+ expect(session[:uid]).to be_nil
41
+ end
42
+
43
+ it 'deletes the :ulogin variable' do
44
+ session_manager.clear
45
+ expect(session[:ulogin]).to be_nil
46
+ end
47
+ end
48
+
49
+ describe '#valid?' do
50
+ before { controller.login(user, true) }
51
+
52
+ context 'User id and sequence combination is found' do
53
+ before { UberLogin::Storage.stub(:find_composite).and_return user }
54
+
55
+ context 'all checks return true' do
56
+ before { UberLogin::TokenValidator.any_instance.stub(:valid?).and_return true }
57
+
58
+ it 'returns true' do
59
+ expect(session_manager.valid?).to be_true
60
+ end
61
+ end
62
+
63
+ context 'any check fails' do
64
+ before { UberLogin::TokenValidator.any_instance.stub(:valid?).and_return false }
65
+
66
+ it 'returns false' do
67
+ expect(session_manager.valid?).to be_false
68
+ end
69
+ end
70
+ end
71
+
72
+ context 'User id and sequence combination is not found' do
73
+ before { UberLogin::Storage.stub(:find).and_return nil }
74
+
75
+ it 'returns false' do
76
+ expect(session_manager.valid?).to be_false
77
+ end
78
+ end
79
+ end
80
+ end
data/spec/spec_helper.rb CHANGED
@@ -63,6 +63,10 @@ class LoginToken
63
63
  new
64
64
  end
65
65
 
66
+ def self.destroy_all(hash)
67
+
68
+ end
69
+
66
70
  def updated_at
67
71
  Time.now - 100
68
72
  end
@@ -78,6 +82,6 @@ class User
78
82
  end
79
83
 
80
84
  def self.find(id)
81
- User.new(id: id)
85
+ id ? User.new(id: id) : nil
82
86
  end
83
87
  end
File without changes
@@ -0,0 +1,58 @@
1
+ describe UberLogin::TokenEncoder do
2
+ describe '#generate' do
3
+ it 'returns an array of size 2' do
4
+ expect(UberLogin::TokenEncoder.generate.size).to eq 2
5
+ end
6
+ end
7
+
8
+ describe '#encode' do
9
+ it 'retuns a string' do
10
+ expect(UberLogin::TokenEncoder.encode('what', 'ever').class).to eq String
11
+ end
12
+
13
+ it 'returns the two arguments separated by colons' do
14
+ expect(UberLogin::TokenEncoder.encode('what', 'ever')).to eq 'what:ever'
15
+ end
16
+ end
17
+
18
+ describe '#encode_array' do
19
+ it 'retuns a string' do
20
+ expect(UberLogin::TokenEncoder.encode_array([ 'what', 'ever' ]).class).to eq String
21
+ end
22
+
23
+ it 'returns the two arguments separated by colons' do
24
+ expect(UberLogin::TokenEncoder.encode_array([ 'what', 'ever' ])).to eq 'what:ever'
25
+ end
26
+ end
27
+
28
+ describe '#decode' do
29
+ it 'returns an array of size 2' do
30
+ expect(UberLogin::TokenEncoder.decode('dead:beef').size).to eq 2
31
+ end
32
+
33
+ it 'returns the two elements of the token' do
34
+ expect(UberLogin::TokenEncoder.decode('what:ever')).to eq [ 'what', 'ever' ]
35
+ end
36
+ end
37
+
38
+ describe '#sequence' do
39
+ it 'returns the first part of the token' do
40
+ expect(UberLogin::TokenEncoder.sequence('what:ever')).to eq 'what'
41
+ end
42
+ end
43
+
44
+ describe '#token' do
45
+ it 'returns the second part of the token' do
46
+ expect(UberLogin::TokenEncoder.token('what:ever')).to eq 'ever'
47
+ end
48
+ end
49
+
50
+ describe '#token_hash' do
51
+ it 'returns the second part of the token as a hash' do
52
+ token = UberLogin::TokenEncoder.token('what:ever')
53
+ hash = UberLogin::TokenEncoder.token_hash('what:ever')
54
+
55
+ expect(BCrypt::Password.new(hash)).to eq token
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,66 @@
1
+ require 'spec_helper'
2
+
3
+ describe UberLogin::TokenValidator do
4
+ let(:token_validator) { UberLogin::TokenValidator.new('secret', FakeRequest.new) }
5
+ let(:fake_token) {
6
+ LoginToken.new(
7
+ token: BCrypt::Password.create("beef"),
8
+ ip_address: '192.168.1.1'
9
+ )
10
+ }
11
+
12
+ describe '#valid?' do
13
+ it 'always executes token_match' do
14
+ expect(token_validator).to receive(:token_match)
15
+ token_validator.valid?(fake_token)
16
+ end
17
+
18
+ it 'does not run ip_equality by default' do
19
+ expect(token_validator).to_not receive(:ip_equality)
20
+ token_validator.valid?(fake_token)
21
+ end
22
+
23
+ context 'if IP are tied to tokens' do
24
+ before { UberLogin.configuration.tie_tokens_to_ip = true }
25
+ let(:token_validator) { UberLogin::TokenValidator.new('secret', FakeRequest.new) }
26
+
27
+ it 'executes ip_equality' do
28
+ token_validator.stub(:token_match).and_return true
29
+ expect(token_validator).to receive(:ip_equality)
30
+ token_validator.valid?(fake_token)
31
+ end
32
+ end
33
+
34
+ it 'does not run expiration by default' do
35
+ expect(token_validator).to_not receive(:expiration)
36
+ token_validator.valid?(fake_token)
37
+ end
38
+
39
+ context 'if tokens do expire' do
40
+ before { UberLogin.configuration.token_expiration = 86400 }
41
+ let(:token_validator) { UberLogin::TokenValidator.new('secret', FakeRequest.new) }
42
+
43
+ it 'executes expiration' do
44
+ token_validator.stub(:token_match).and_return true
45
+ expect(token_validator).to receive(:expiration)
46
+ token_validator.valid?(fake_token)
47
+ end
48
+ end
49
+
50
+ context 'all checks return true' do
51
+ before { Array.any_instance.stub(:all?).and_return true }
52
+
53
+ it 'returns true' do
54
+ expect(token_validator.valid?(fake_token)).to be_true
55
+ end
56
+ end
57
+
58
+ context 'any check fails' do
59
+ before { Array.any_instance.stub(:all?).and_return false }
60
+
61
+ it 'returns false' do
62
+ expect(token_validator.valid?(fake_token)).to be_false
63
+ end
64
+ end
65
+ end
66
+ end
@@ -12,6 +12,34 @@ describe UberLogin do
12
12
  controller.login(user)
13
13
  expect(session[:uid]).to eq 100
14
14
  end
15
+
16
+ context 'sessions have stored tokens' do
17
+ before { UberLogin.configuration.strong_sessions = true }
18
+
19
+ it 'saves a token to database' do
20
+ expect_any_instance_of(LoginToken).to receive :save!
21
+ controller.login(user)
22
+ end
23
+
24
+ it 'sets session[:ulogin]' do
25
+ controller.login(user)
26
+ expect(session[:ulogin]).to_not be_nil
27
+ end
28
+ end
29
+
30
+ context 'sessions do not have stored tokens' do
31
+ before { UberLogin.configuration.strong_sessions = false }
32
+
33
+ it 'does not save a token to database' do
34
+ expect_any_instance_of(LoginToken).to_not receive :save!
35
+ controller.login(user)
36
+ end
37
+
38
+ it 'does not set session[:ulogin]' do
39
+ controller.login(user)
40
+ expect(session[:ulogin]).to be_nil
41
+ end
42
+ end
15
43
  end
16
44
 
17
45
  context 'remember is true' do
@@ -27,7 +55,7 @@ describe UberLogin do
27
55
 
28
56
  it 'sets the ulogin cookie' do
29
57
  controller.login(user, true)
30
- expect(cookies[:ulogin]).to match(/[a-z0-9+\/]+:[a-z0-9+\/]+/i)
58
+ expect(cookies[:ulogin]).to match(/[a-z0-9_\-]+:[a-z0-9+\/]+/i)
31
59
  end
32
60
 
33
61
  it 'sets both cookies as persistent' do
@@ -37,37 +65,40 @@ describe UberLogin do
37
65
  end
38
66
 
39
67
  context 'only one session is allowed per user' do
40
- before { UberLogin::Configuration.any_instance.stub(:allow_multiple_login).and_return false }
68
+ before { UberLogin.configuration.allow_multiple_login = false }
41
69
 
42
70
  it 'clears all the other tokens' do
43
- expect_any_instance_of(LoginToken).to receive :destroy
71
+ expect(LoginToken).to receive :destroy_all
44
72
  controller.login(user)
45
73
  end
46
74
  end
47
75
  end
48
76
 
49
77
  describe '#logout' do
78
+ before { controller.login(user, true) }
79
+
50
80
  context 'sequence is nil' do
51
81
  it 'deletes session[:uid]' do
52
- controller.login(user)
53
82
  controller.logout
54
83
  expect(session[:uid]).to be_nil
55
84
  end
56
85
 
57
- context 'persistent login was made' do
58
- before { controller.login(user, true) }
86
+ it 'deletes session[:ulogin]' do
87
+ controller.logout
88
+ expect(session[:ulogin]).to be_nil
89
+ end
59
90
 
60
- it 'deletes session[:uid]' do
61
- controller.logout
62
- expect(session[:uid]).to be_nil
63
- end
91
+ it 'deletes cookies[:uid]' do
92
+ controller.logout
93
+ expect(cookies[:uid]).to be_nil
94
+ end
64
95
 
65
- it 'deletes login cookies' do
66
- controller.logout
67
- expect(cookies[:uid]).to be_nil
68
- expect(cookies[:ulogin]).to be_nil
69
- end
96
+ it 'deletes cookies[:ulogin]' do
97
+ controller.logout
98
+ expect(cookies[:ulogin]).to be_nil
99
+ end
70
100
 
101
+ context 'persistent login was made' do
71
102
  it 'deletes a LoginToken row' do
72
103
  expect {
73
104
  controller.logout
@@ -76,9 +107,37 @@ describe UberLogin do
76
107
  end
77
108
  end
78
109
 
79
- context 'sequence is not nil' do
80
- before { controller.login(user, true) }
110
+ context 'sequence is equal to current user sequence' do
111
+ it 'deletes session[:uid]' do
112
+ controller.logout(UberLogin::TokenEncoder.sequence(cookies[:ulogin]))
113
+ expect(session[:uid]).to be_nil
114
+ end
115
+
116
+ it 'deletes session[:ulogin]' do
117
+ controller.logout(UberLogin::TokenEncoder.sequence(cookies[:ulogin]))
118
+ expect(session[:ulogin]).to be_nil
119
+ end
120
+
121
+ it 'deletes cookies[:uid]' do
122
+ controller.logout(UberLogin::TokenEncoder.sequence(cookies[:ulogin]))
123
+ expect(cookies[:uid]).to be_nil
124
+ end
81
125
 
126
+ it 'deletes cookies[:ulogin]' do
127
+ controller.logout(UberLogin::TokenEncoder.sequence(cookies[:ulogin]))
128
+ expect(cookies[:ulogin]).to be_nil
129
+ end
130
+
131
+ context 'persistent login was made' do
132
+ it 'deletes a LoginToken row' do
133
+ expect {
134
+ controller.logout(UberLogin::TokenEncoder.sequence(cookies[:ulogin]))
135
+ }.to change{ LoginToken.count }.by -1
136
+ end
137
+ end
138
+ end
139
+
140
+ context 'sequence is not nil' do
82
141
  it 'does not clear session[:uid]' do
83
142
  controller.logout('sequence')
84
143
  expect(session[:uid]).to_not be_nil
@@ -103,72 +162,42 @@ describe UberLogin do
103
162
  end
104
163
 
105
164
  describe '#logout_all' do
165
+ before { controller.login(user, true) }
166
+
106
167
  it 'deletes session[:uid]' do
107
- controller.login(user)
108
168
  controller.logout_all
109
169
  expect(session[:uid]).to be_nil
110
170
  end
111
171
 
112
- it 'deletes session[:uid]' do
172
+ it 'deletes session[:ulogin]' do
113
173
  controller.logout_all
114
- expect(session[:uid]).to be_nil
174
+ expect(session[:ulogin]).to be_nil
115
175
  end
116
176
 
117
- it 'deletes login cookies' do
177
+ it 'deletes cookies[:uid]' do
118
178
  controller.logout_all
119
179
  expect(cookies[:uid]).to be_nil
120
- expect(cookies[:ulogin]).to be_nil
121
180
  end
122
181
 
123
- it 'deletes any token associated with the user' do
124
- expect_any_instance_of(LoginToken).to receive :destroy
182
+ it 'deletes cookies[:ulogin]' do
125
183
  controller.logout_all
184
+ expect(cookies[:ulogin]).to be_nil
126
185
  end
127
- end
128
-
129
- describe '#save_to_database' do
130
- before {
131
- cookies[:uid] = "100"
132
- cookies[:ulogin] = "dead:beef"
133
- }
134
-
135
- it 'saves the triplet to the database' do
136
- expect_any_instance_of(LoginToken).to receive(:save!)
137
- controller.send('save_to_database')
138
- end
139
- end
140
-
141
- describe '#set_user_data' do
142
- let(:row) { LoginToken.new }
143
-
144
- context 'the token table has an "ip_address" field' do
145
- it 'sets the field to the client IP' do
146
- expect(row).to receive(:ip_address=).with('192.168.1.1')
147
- controller.send('set_user_data', row)
148
- end
149
- end
150
-
151
- context 'the token table has an "os" field' do
152
- it 'sets the field to the client Operating System' do
153
- expect(row).to receive(:os=).with('Linux x86_64')
154
- controller.send('set_user_data', row)
155
- end
156
- end
157
-
158
- context 'the token table has a "browser" field' do
159
- it 'sets the field to the client Browser and version' do
160
- expect(row).to receive(:browser=).with('Chrome 32.0.1667.0')
161
- controller.send('set_user_data', row)
162
- end
186
+ it 'deletes any token associated with the user' do
187
+ expect(LoginToken).to receive :destroy_all
188
+ controller.logout_all
163
189
  end
164
190
  end
165
191
 
166
- describe '#current_user_uncached' do
192
+ describe '#current_user' do
167
193
  context 'session[:uid] is set' do
168
- before { session[:uid] = 100 }
194
+ before {
195
+ session[:uid] = 100
196
+ session[:ulogin] = 'dead:beef'
197
+ }
169
198
 
170
199
  it 'returns an user object with that uid' do
171
- expect(controller.send(:current_user_uncached).id).to eq 100
200
+ expect(controller.current_user.id).to eq 100
172
201
  end
173
202
  end
174
203
 
@@ -182,38 +211,38 @@ describe UberLogin do
182
211
  }
183
212
 
184
213
  context 'the cookies are valid' do
185
- before { CookieManager.any_instance.stub(:valid?).and_return true }
214
+ before { UberLogin::CookieManager.any_instance.stub(:valid?).and_return true }
186
215
 
187
216
  it 'returns an user object with that uid' do
188
- expect(controller.send(:current_user_uncached).id).to eq "100"
217
+ expect(controller.current_user.id).to eq "100"
189
218
  end
190
219
 
191
220
  it 'deletes the token from the database' do
192
221
  expect_any_instance_of(LoginToken).to receive(:destroy)
193
- controller.send(:current_user_uncached)
222
+ controller.current_user
194
223
  end
195
224
 
196
225
  it 'creates a new token for the next login' do
197
226
  expect_any_instance_of(LoginToken).to receive(:save!)
198
- controller.send(:current_user_uncached)
227
+ controller.current_user
199
228
  end
200
229
 
201
230
  it 'refreshes the cookie' do
202
- controller.send(:current_user_uncached)
231
+ controller.current_user
203
232
  expect(cookies[:uid]).to eq "100"
204
233
  expect(cookies[:ulogin]).to_not eq "whatever:beef"
205
234
  end
206
235
  end
207
236
 
208
237
  context 'the cookies are not valid' do
209
- before { CookieManager.any_instance.stub(:valid?).and_return false }
238
+ before { UberLogin::CookieManager.any_instance.stub(:valid?).and_return false }
210
239
 
211
240
  it 'returns nil' do
212
- expect(controller.send(:current_user_uncached)).to be_nil
241
+ expect(controller.current_user).to be_nil
213
242
  end
214
243
 
215
244
  it 'clears the cookies for this user' do
216
- controller.send(:current_user_uncached)
245
+ controller.current_user
217
246
  expect(cookies[:uid]).to be_nil
218
247
  expect(cookies[:ulogin]).to be_nil
219
248
  end
@@ -222,7 +251,7 @@ describe UberLogin do
222
251
 
223
252
  context 'cookies are not set' do
224
253
  it 'returns nil' do
225
- expect(controller.send(:current_user_uncached)).to be_nil
254
+ expect(controller.current_user).to be_nil
226
255
  end
227
256
  end
228
257
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: uber_login
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Francesco Boffa
@@ -32,11 +32,19 @@ extra_rdoc_files: []
32
32
  files:
33
33
  - lib/uber_login.rb
34
34
  - lib/uber_login/cookie_manager.rb
35
+ - lib/uber_login/token_validator.rb
36
+ - lib/uber_login/token_encoder.rb
37
+ - lib/uber_login/storage.rb
35
38
  - lib/uber_login/version.rb
39
+ - lib/uber_login/session_manager.rb
36
40
  - lib/uber_login/configuration.rb
41
+ - spec/token_encoder_spec.rb
42
+ - spec/storage_spec.rb
37
43
  - spec/spec_helper.rb
38
44
  - spec/cookie_manager_spec.rb
39
45
  - spec/uber_login_spec.rb
46
+ - spec/session_manager_spec.rb
47
+ - spec/token_validator_spec.rb
40
48
  homepage: https://github.com/AlfaOmega08/uber_login
41
49
  licenses:
42
50
  - MIT
@@ -65,7 +73,11 @@ summary: Tired of rewriting the login, logout and current_user methods for the m
65
73
  This gem will solve all of this problems and still leave you the control over your
66
74
  application.
67
75
  test_files:
76
+ - spec/token_encoder_spec.rb
77
+ - spec/storage_spec.rb
68
78
  - spec/spec_helper.rb
69
79
  - spec/cookie_manager_spec.rb
70
80
  - spec/uber_login_spec.rb
81
+ - spec/session_manager_spec.rb
82
+ - spec/token_validator_spec.rb
71
83
  has_rdoc: