uber_login 0.1.1

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 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: