uber_login 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ OGZjMzFmM2JhZGY0ZmJlZGE5MWY5ODc0ZWY4OTE1MDI4MjdkMTc0OA==
5
+ data.tar.gz: !binary |-
6
+ MWQ3YTdhYzI1MzU2YTFlZjQxMjFhYzk4NjI2NTgyZDA5Y2E4MDVkNw==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ MmIyNGE2MzhkYmE3NmI1OTk5MWU0OTM2Y2RmYTI3NWYzNDI2YzUxNDNmNTQy
10
+ MmUxZTdhNjRiMDU0NmJjNDY5NjMxZGZlZWVkNTRmZDQwN2RjNGZmMWE5Mzcz
11
+ YzU2NGRkZGQ0NGI5MWVmNTUzM2RjOGFkNzFiYWQzOGFjNmExYWI=
12
+ data.tar.gz: !binary |-
13
+ MWEyNzkzODc4ZGFiMGE3MDlkYTQyMWUxMDFjNTNhNTYwZWQyNTM0ODY5YzNk
14
+ ZTNhNjkxZDc3YmJmNGI2NTkxZDRmYTk1MTVmNmNlYmMwNzZlNzNmM2E2MjA1
15
+ MmE5MTQ4OWZlZDgxNDk1Njc0NzVmMjJlZTAwN2IwZTBhZTI0OTE=
data/lib/uber_login.rb ADDED
@@ -0,0 +1,135 @@
1
+ require 'uber_login/version'
2
+ require 'uber_login/cookie_manager'
3
+ require 'uber_login/configuration'
4
+ require 'securerandom'
5
+ require 'bcrypt'
6
+ require 'user_agent'
7
+
8
+ module UberLogin
9
+ ##
10
+ # Returns the logged in user.
11
+ # If session[+:uid+] is set it returns that user.
12
+ # If session[+:uid+] is NOT set but cookies[+:uid+] and cookies[+:ulogin+] ARE:
13
+ # * It dissects +:ulogin+ into Sequence and Token
14
+ # * Looks for a LoginToken from UID and Sequence
15
+ # * Test Token against the stored and strong hashed one
16
+ # * If they match, session[+:uid+] is set and it returns the +User+
17
+ # If none of the previous cases, +nil+ is returned.
18
+ # If the cookie did not match, they are cleared from the user browser.
19
+ #
20
+ # All the checks are runt only once and the result is cached
21
+ def current_user
22
+ @current_user ||= current_user_uncached
23
+ end
24
+
25
+ ##
26
+ # Logs in the given +user+
27
+ # If +remember+ is true all the needed cookies are set.
28
+ # session[+:uid+] is set to user.id
29
+ def login(user, remember = false)
30
+ logout_all unless UberLogin.configuration.allow_multiple_login
31
+
32
+ session[:uid] = user.id
33
+ generate_and_set_cookies(user.id) if remember
34
+ end
35
+
36
+ ##
37
+ # Clears session[:uid]
38
+ # If remember cookies were set they're cleared and the token is
39
+ # removed from the database.
40
+ def logout
41
+ session.delete(:uid)
42
+ delete_from_database if cookies[:uid]
43
+ cookie_manager.clear
44
+ end
45
+
46
+ ##
47
+ # Deletes all "remember me" session for this user from whatever device
48
+ # he/she has ever used to login.
49
+ def logout_all
50
+ LoginToken.find_by(uid: session[:uid]).destroy
51
+ session.delete :uid
52
+ cookie_manager.clear
53
+ end
54
+
55
+ private
56
+ def cookie_manager
57
+ @cookie_manager ||= CookieManager.new(cookies, request)
58
+ end
59
+
60
+ # See +current_user+
61
+ def current_user_uncached
62
+ sid = session[:uid]
63
+ sid = login_from_cookies if cookies[:uid] and !sid
64
+ User.find(sid) if sid
65
+ end
66
+
67
+ ##
68
+ # Attempts a login from the +:uid+ and +:ulogin+ cookies.
69
+ def login_from_cookies
70
+ if cookie_manager.valid?
71
+ session[:uid] = cookies[:uid]
72
+ generate_new_token
73
+ session[:uid]
74
+ else
75
+ cookie_manager.clear
76
+ nil
77
+ end
78
+ end
79
+
80
+ ##
81
+ # Deletes the current token from the database and creates a new one in place.
82
+ # Sets the user cookies to match the new values
83
+ def generate_new_token
84
+ delete_from_database
85
+ generate_and_set_cookies(cookies[:uid])
86
+ end
87
+
88
+ ##
89
+ # Creates a pair of cookies.
90
+ # +:uid+ is set to the user id
91
+ # +:ulogin+ is a composite field made of +:sequence+ and +:token+
92
+ #
93
+ # +:sequence+ is used to choose between all possible user login tokens
94
+ # +:token+ is stored +bcrypt+ed in the database and then compared on login
95
+ def generate_and_set_cookies(uid)
96
+ sequence, token = generate_sequence_and_token
97
+ cookie_manager.persistent_login(uid, sequence, token)
98
+ save_to_database
99
+ end
100
+
101
+ ##
102
+ # Creates a LoginToken based on the +uid+, +sequence+ and hashed +token+
103
+ def save_to_database
104
+ token_row = LoginToken.new(
105
+ uid: cookies[:uid],
106
+ sequence: cookie_manager.sequence,
107
+ token: cookie_manager.hashed_token
108
+ )
109
+
110
+ set_user_data token_row
111
+
112
+ token_row.save!
113
+ end
114
+
115
+ ##
116
+ # Removes a LoginToken with +uid+ and +sequence+ taken from the cookies
117
+ def delete_from_database
118
+ sequence = cookie_manager.sequence
119
+ token = LoginToken.find_by(uid: cookies[:uid], sequence: sequence)
120
+ token.destroy
121
+ end
122
+
123
+ def generate_sequence_and_token
124
+ # 9 and 21 are both multiple of 3, so we do not get base64 padding (==)
125
+ [ SecureRandom.base64(9), SecureRandom.base64(21) ]
126
+ end
127
+
128
+ def set_user_data(row)
129
+ user_agent = UserAgent.parse(request.user_agent)
130
+
131
+ row.ip_address = request.remote_ip if row.respond_to? :ip_address=
132
+ row.os = user_agent.os if row.respond_to? :os=
133
+ row.browser = user_agent.browser + ' ' + user_agent.version if row.respond_to? :browser=
134
+ end
135
+ end
@@ -0,0 +1,24 @@
1
+ ##
2
+ # Use this class in app/config/initializers to change configuration parameters.
3
+ module UberLogin
4
+ class Configuration
5
+ attr_accessor :allow_multiple_login
6
+ attr_accessor :token_expiration
7
+ attr_accessor :tie_tokens_to_ip
8
+
9
+ def initialize
10
+ self.allow_multiple_login = true
11
+ self.token_expiration = nil
12
+ self.tie_tokens_to_ip = false
13
+ end
14
+ end
15
+
16
+ def self.configure
17
+ yield(configuration) if block_given?
18
+ end
19
+
20
+ private
21
+ def self.configuration
22
+ @configuration ||= Configuration.new
23
+ end
24
+ end
@@ -0,0 +1,66 @@
1
+ ##
2
+ # This class handles the +:uid+ and +:ulogin+ cookies
3
+ # 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
65
+ end
66
+ end
@@ -0,0 +1,3 @@
1
+ module UberLogin
2
+ VERSION = '0.1.1'
3
+ end
@@ -0,0 +1,115 @@
1
+ require 'spec_helper'
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
21
+
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
26
+
27
+ context 'if IP are tied to Tokens' do
28
+ before { UberLogin.configuration.tie_tokens_to_ip = true }
29
+
30
+ it 'executes ip_equality' do
31
+ expect(cookie_manager).to receive(:ip_equality)
32
+ cookie_manager.valid?
33
+ end
34
+ end
35
+
36
+ it 'does not run expiration by default' do
37
+ expect(cookie_manager).to_not receive(:expiration)
38
+ cookie_manager.valid?
39
+ end
40
+
41
+ context 'if Tokens do expire' do
42
+ before { UberLogin.configuration.token_expiration = 86400 }
43
+
44
+ it 'executes expiration' do
45
+ expect(cookie_manager).to receive(:expiration)
46
+ cookie_manager.valid?
47
+ end
48
+ end
49
+
50
+ context 'all checks return true' do
51
+ it 'returns true' do
52
+ expect(cookie_manager.valid?).to be_true
53
+ end
54
+ end
55
+
56
+ context 'any check fails' do
57
+ before { Array.any_instance.stub(:all?).and_return false }
58
+
59
+ it 'returns false' do
60
+ expect(cookie_manager.valid?).to be_false
61
+ end
62
+ end
63
+ end
64
+
65
+ context 'User id and sequence combination is not found' do
66
+ before { LoginToken.stub(:find_by).and_return nil }
67
+
68
+ it 'returns false' do
69
+ expect(cookie_manager.valid?).to be_false
70
+ end
71
+ end
72
+ end
73
+
74
+ describe '#token_match' do
75
+ before { cookie_manager.stub(:token).and_return 'secret' }
76
+
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
99
+ end
100
+ end
101
+
102
+ describe '#expiration' do
103
+ before { UberLogin.configuration.token_expiration = 86400 }
104
+
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
108
+ end
109
+
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
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,83 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
2
+
3
+ require 'uber_login'
4
+
5
+ class FakeCookieJar < Hash
6
+ def permanent
7
+ self
8
+ end
9
+ end
10
+
11
+ class FakeRequest
12
+ def remote_ip
13
+ '192.168.1.1'
14
+ end
15
+
16
+ def user_agent
17
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1667.0 Safari/537.36"
18
+ end
19
+ end
20
+
21
+ class ApplicationController
22
+ include UberLogin
23
+
24
+ attr_accessor :session
25
+ attr_accessor :cookies
26
+ attr_accessor :request
27
+
28
+ def initialize
29
+ @session = {}
30
+ @cookies = FakeCookieJar.new
31
+ @request = FakeRequest.new
32
+ end
33
+ end
34
+
35
+ # This is required to be an ActiveRecord like class
36
+ # Mongoid and MongoMapper should be just fine and probably others too.
37
+ class LoginToken
38
+ attr_accessor :uid, :sequence, :token
39
+ attr_accessor :ip_address, :os, :browser
40
+
41
+ @@count = 0
42
+
43
+ def initialize(attributes = {})
44
+ attributes.each do |k, v|
45
+ send("#{k}=", v)
46
+ end
47
+ end
48
+
49
+ def save!
50
+ true
51
+ @@count += 1
52
+ end
53
+
54
+ def self.count
55
+ @@count
56
+ end
57
+
58
+ def destroy
59
+ @@count -= 1
60
+ end
61
+
62
+ def self.find_by(hash)
63
+ new
64
+ end
65
+
66
+ def updated_at
67
+ Time.now - 100
68
+ end
69
+ end
70
+
71
+ class User
72
+ attr_accessor :id
73
+
74
+ def initialize(attributes = {})
75
+ attributes.each do |k, v|
76
+ send("#{k}=", v)
77
+ end
78
+ end
79
+
80
+ def self.find(id)
81
+ User.new(id: id)
82
+ end
83
+ end
@@ -0,0 +1,203 @@
1
+ require 'spec_helper'
2
+
3
+ describe UberLogin do
4
+ let(:user) { double(id: 100) }
5
+ let(:controller) { ApplicationController.new }
6
+ let(:session) { controller.session }
7
+ let(:cookies) { controller.cookies }
8
+
9
+ describe '#login' do
10
+ context 'remember is false' do
11
+ it 'sets session[:uid]' do
12
+ controller.login(user)
13
+ expect(session[:uid]).to eq 100
14
+ end
15
+ end
16
+
17
+ context 'remember is true' do
18
+ it 'sets session[:uid]' do
19
+ controller.login(user, true)
20
+ expect(session[:uid]).to eq 100
21
+ end
22
+
23
+ it 'sets the uid cookie' do
24
+ controller.login(user, true)
25
+ expect(cookies[:uid]).to eq 100
26
+ end
27
+
28
+ it 'sets the ulogin cookie' do
29
+ controller.login(user, true)
30
+ expect(cookies[:ulogin]).to match(/[a-z0-9+\/]+:[a-z0-9+\/]+/i)
31
+ end
32
+
33
+ it 'sets both cookies as persistent' do
34
+ expect(cookies).to receive(:permanent).twice.and_return cookies
35
+ controller.login(user, true)
36
+ end
37
+ end
38
+
39
+ context 'only one session is allowed per user' do
40
+ before { UberLogin::Configuration.any_instance.stub(:allow_multiple_login).and_return false }
41
+
42
+ it 'clears all the other tokens' do
43
+ expect_any_instance_of(LoginToken).to receive :destroy
44
+ controller.login(user)
45
+ end
46
+ end
47
+ end
48
+
49
+ describe '#logout' do
50
+ it 'deletes session[:uid]' do
51
+ controller.login(user)
52
+ controller.logout
53
+ expect(session[:uid]).to be_nil
54
+ end
55
+
56
+ context 'persistent login was made' do
57
+ before { controller.login(user, true) }
58
+
59
+ it 'deletes session[:uid]' do
60
+ controller.logout
61
+ expect(session[:uid]).to be_nil
62
+ end
63
+
64
+ it 'deletes login cookies' do
65
+ controller.logout
66
+ expect(cookies[:uid]).to be_nil
67
+ expect(cookies[:ulogin]).to be_nil
68
+ end
69
+
70
+ it 'deletes a LoginToken row' do
71
+ expect {
72
+ controller.logout
73
+ }.to change{ LoginToken.count }.by -1
74
+ end
75
+ end
76
+ end
77
+
78
+ describe '#logout_all' do
79
+ it 'deletes session[:uid]' do
80
+ controller.login(user)
81
+ controller.logout_all
82
+ expect(session[:uid]).to be_nil
83
+ end
84
+
85
+ it 'deletes session[:uid]' do
86
+ controller.logout_all
87
+ expect(session[:uid]).to be_nil
88
+ end
89
+
90
+ it 'deletes login cookies' do
91
+ controller.logout_all
92
+ expect(cookies[:uid]).to be_nil
93
+ expect(cookies[:ulogin]).to be_nil
94
+ end
95
+
96
+ it 'deletes any token associated with the user' do
97
+ expect_any_instance_of(LoginToken).to receive :destroy
98
+ controller.logout_all
99
+ end
100
+ end
101
+
102
+ describe '#save_to_database' do
103
+ before {
104
+ cookies[:uid] = "100"
105
+ cookies[:ulogin] = "dead:beef"
106
+ }
107
+
108
+ it 'saves the triplet to the database' do
109
+ expect_any_instance_of(LoginToken).to receive(:save!)
110
+ controller.send('save_to_database')
111
+ end
112
+ end
113
+
114
+ describe '#set_user_data' do
115
+ let(:row) { LoginToken.new }
116
+
117
+ context 'the token table has an "ip_address" field' do
118
+ it 'sets the field to the client IP' do
119
+ expect(row).to receive(:ip_address=).with('192.168.1.1')
120
+ controller.send('set_user_data', row)
121
+ end
122
+ end
123
+
124
+ context 'the token table has an "os" field' do
125
+ it 'sets the field to the client Operating System' do
126
+ expect(row).to receive(:os=).with('Linux x86_64')
127
+ controller.send('set_user_data', row)
128
+ end
129
+ end
130
+
131
+ context 'the token table has a "browser" field' do
132
+ it 'sets the field to the client Browser and version' do
133
+ expect(row).to receive(:browser=).with('Chrome 32.0.1667.0')
134
+ controller.send('set_user_data', row)
135
+ end
136
+ end
137
+ end
138
+
139
+ describe '#current_user_uncached' do
140
+ context 'session[:uid] is set' do
141
+ before { session[:uid] = 100 }
142
+
143
+ it 'returns an user object with that uid' do
144
+ expect(controller.send(:current_user_uncached).id).to eq 100
145
+ end
146
+ end
147
+
148
+ context 'session[:uid] is nil' do
149
+ before { session[:uid] = nil }
150
+
151
+ context 'cookies[:uid] and cookies[:ulogin] are set' do
152
+ before {
153
+ cookies[:uid] = "100"
154
+ cookies[:ulogin] = "whatever:beef"
155
+ }
156
+
157
+ context 'the cookies are valid' do
158
+ before { CookieManager.any_instance.stub(:valid?).and_return true }
159
+
160
+ it 'returns an user object with that uid' do
161
+ expect(controller.send(:current_user_uncached).id).to eq "100"
162
+ end
163
+
164
+ it 'deletes the token from the database' do
165
+ expect_any_instance_of(LoginToken).to receive(:destroy)
166
+ controller.send(:current_user_uncached)
167
+ end
168
+
169
+ it 'creates a new token for the next login' do
170
+ expect_any_instance_of(LoginToken).to receive(:save!)
171
+ controller.send(:current_user_uncached)
172
+ end
173
+
174
+ it 'refreshes the cookie' do
175
+ controller.send(:current_user_uncached)
176
+ expect(cookies[:uid]).to eq "100"
177
+ expect(cookies[:ulogin]).to_not eq "whatever:beef"
178
+ end
179
+ end
180
+
181
+ context 'the cookies are not valid' do
182
+ before { CookieManager.any_instance.stub(:valid?).and_return false }
183
+
184
+ it 'returns nil' do
185
+ expect(controller.send(:current_user_uncached)).to be_nil
186
+ end
187
+
188
+ it 'clears the cookies for this user' do
189
+ controller.send(:current_user_uncached)
190
+ expect(cookies[:uid]).to be_nil
191
+ expect(cookies[:ulogin]).to be_nil
192
+ end
193
+ end
194
+ end
195
+
196
+ context 'cookies are not set' do
197
+ it 'returns nil' do
198
+ expect(controller.send(:current_user_uncached)).to be_nil
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: uber_login
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Francesco Boffa
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-12-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: useragent
15
+ type: :runtime
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ! '>='
19
+ - !ruby/object:Gem::Version
20
+ version: 0.10.0
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ! '>='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.10.0
27
+ description: Login and logout management with secure "remember me" capabilities
28
+ email: fra.boffa@gmail.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - lib/uber_login.rb
34
+ - lib/uber_login/cookie_manager.rb
35
+ - lib/uber_login/version.rb
36
+ - lib/uber_login/configuration.rb
37
+ - spec/spec_helper.rb
38
+ - spec/cookie_manager_spec.rb
39
+ - spec/uber_login_spec.rb
40
+ homepage: https://github.com/AlfaOmega08/uber_login
41
+ licenses:
42
+ - MIT
43
+ metadata: {}
44
+ post_install_message:
45
+ rdoc_options: []
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ! '>='
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ! '>='
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ requirements: []
59
+ rubyforge_project:
60
+ rubygems_version: 2.1.10
61
+ signing_key:
62
+ specification_version: 4
63
+ summary: Tired of rewriting the login, logout and current_user methods for the millionth
64
+ time? Scared of all the security concerns of writing your own authentication methods?
65
+ This gem will solve all of this problems and still leave you the control over your
66
+ application.
67
+ test_files:
68
+ - spec/spec_helper.rb
69
+ - spec/cookie_manager_spec.rb
70
+ - spec/uber_login_spec.rb
71
+ has_rdoc: