persistent_cookie_authentication_generator 0.0.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.
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Wong Liang Zan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/USAGE ADDED
@@ -0,0 +1,22 @@
1
+ NAME
2
+ persistent cookie authentication - rails authentication system with persistent cookie management
3
+
4
+ SYNOPSIS
5
+ ruby script/generate persistent_cookie_authentication
6
+
7
+ DESCRIPTION
8
+ This generator creates an authentication system with persistent cookie management
9
+
10
+ Feature include
11
+ - a model which uses SHA1 encryption and salted hashes for passwords
12
+ - a controller with signup, login, welcome and logoff actions
13
+ - gmail smtp server integration
14
+ - account creation that requires account verification from the registered email address) and supports forgotten and changed passwords
15
+ - a mixin which lets you easily add advanced authentication features to your abstract base controller
16
+ - extensive unit and functional test cases to make sure nothing breaks.
17
+ - token based authentication
18
+ - persistent cookie management that allows anonymous users to be authenticated via cookies
19
+
20
+ ACKNOWLEDGEMENTS
21
+
22
+ The code is heavily modified from salted_hash_login generator.
@@ -0,0 +1,52 @@
1
+ class PersistentCookieAuthenticationGenerator < Rails::Generator::Base
2
+ def manifest
3
+ record do |m|
4
+
5
+ #controller
6
+ m.file "user_controller.rb", File.join("app", "controllers", "user_controller.rb")
7
+
8
+ #models
9
+ m.file "identity.rb", File.join("app", "models", "identity.rb")
10
+ m.file "login_cookie.rb", File.join("app", "models", "login_cookie.rb")
11
+ m.file "user.rb", File.join("app", "models", "user.rb")
12
+ m.file "user_notify.rb", File.join("app", "models", "user_notify.rb")
13
+
14
+ #user view
15
+ m.directory File.join("app", "views", "user")
16
+ m.file "user_change_password.rhtml", File.join("app", "views", "user", "change_password.rhtml")
17
+ m.file "user_edit.rhtml", File.join("app", "views", "user", "edit.rhtml")
18
+ m.file "user_forgot_password.rhtml", File.join("app", "views", "user", "forgot_password.rhtml")
19
+ m.file "user_login.rhtml", File.join("app", "views", "user", "login.rhtml")
20
+ m.file "user_show.rhtml", File.join("app", "views", "user", "show.rhtml")
21
+ m.file "user_signup.rhtml", File.join("app", "views", "user", "signup.rhtml")
22
+
23
+ #user notify view
24
+ m.directory File.join("app", "views", "user_notify")
25
+ m.file "user_notify_change_password.rhtml", File.join("app", "views", "user_notify", "change_password.rhtml")
26
+ m.file "user_notify_forgot_password.rhtml", File.join("app", "views", "user_notify", "forgot_password.rhtml")
27
+ m.file "user_notify_signup.rhtml", File.join("app", "views", "user_notify", "signup.rhtml")
28
+
29
+ #migration
30
+ m.directory File.join("db", "migrate")
31
+ m.migration_template("migration.rb", File.join("db", "migrate"), {:migration_file_name => "create_authentication"})
32
+
33
+ #mixins + external libs
34
+ m.file "smtp_tls.rb", File.join("lib", "smtp_tls.rb")
35
+ m.file "user_system.rb", File.join("lib", "user_system.rb")
36
+
37
+ #environment configurations
38
+ m.file "user_environment.rb", File.join("config", "environments", "user_environment.rb")
39
+
40
+ #tests
41
+ m.file "user_integration_test.rb", File.join("test", "integration", "user_integration_test.rb")
42
+ m.file "user_test.rb", File.join("test", "unit", "user_test.rb")
43
+
44
+ #test fixtures
45
+ m.file "identities.yml", File.join("test", "fixtures", "identities.yml")
46
+ m.file "login_cookies.yml", File.join("test", "fixtures", "login_cookies.yml")
47
+ m.file "users.yml", File.join("test", "fixtures", "users.yml")
48
+
49
+ end
50
+ end
51
+ end
52
+
@@ -0,0 +1,18 @@
1
+ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
2
+ identityOne:
3
+ id: 1
4
+
5
+ identityTwo:
6
+ id: 2
7
+
8
+ identityThree:
9
+ id: 3
10
+
11
+ identityFour:
12
+ id: 4
13
+
14
+ identityFive:
15
+ id: 5
16
+
17
+ identitySix:
18
+ id: 6
@@ -0,0 +1,4 @@
1
+ class Identity < ActiveRecord::Base
2
+ has_one :user
3
+ has_many :login_cookies
4
+ end
@@ -0,0 +1,254 @@
1
+ class LoginCookie < ActiveRecord::Base
2
+ belongs_to :identity
3
+
4
+ validates_presence_of :login, :series, :token
5
+ validates_uniqueness_of :token
6
+
7
+ #separator for the cookie
8
+ @@separator = "@"
9
+ @@cookieSegmentLength = 10
10
+
11
+
12
+
13
+ # exchanges the cookie or set a new cookie before entering a protected region
14
+ def self.requireCookie(cookieValue)
15
+ if LoginCookie.authenticate(cookieValue)
16
+ newCookie = LoginCookie.regenerateCookie(cookieValue)
17
+ return newCookie
18
+ else
19
+ return LoginCookie.generateCookie
20
+ end
21
+ end
22
+
23
+
24
+
25
+ # verify the identity of a cookie
26
+ def self.verifyIdentity(cookieValue, identityID)
27
+ #defensive programming
28
+ if cookieValue == nil || identityID == nil
29
+ return false
30
+ end
31
+
32
+ loginValue = LoginCookie.getCookieLogin(cookieValue)
33
+
34
+ #cookie might belong to a user
35
+ user = User.find(:first, :conditions => ["login = ?", loginValue])
36
+ return identityID == user.identity_id if user != nil
37
+
38
+ #cookie might belong to anonymous user
39
+ appCookie = LoginCookie.find(:first, :conditions => ["login = ?", loginValue])
40
+ return identityID == appCookie.identity_id if appCookie != nil
41
+
42
+ return false
43
+ end
44
+
45
+
46
+
47
+ # setting the number of default credits
48
+ def self.regenerateCookie(cookieValue)
49
+ #defensive programming
50
+ if cookieValue == nil
51
+ raise "null inputs"
52
+ end
53
+
54
+ cookieFound = LoginCookie.getCookieRef(cookieValue)
55
+ raise "cookie not found" if cookieFound == nil
56
+
57
+ #delete the prev entry, write the new entry in
58
+ newCookie = self.addNewCookieEntry(cookieFound.login,
59
+ cookieFound.series,
60
+ self.generate_random_string(@@cookieSegmentLength),
61
+ cookieFound.identity_id)
62
+ cookieFound.destroy
63
+
64
+ return newCookie.login + @@separator + newCookie.series + @@separator + newCookie.token
65
+ end
66
+
67
+
68
+
69
+ # gets the identity of a user
70
+ def self.getIdentity(cookieValue)
71
+ #defensive programming
72
+ if cookieValue == nil
73
+ return 0
74
+ end
75
+
76
+ return self.getIdentityFromLogin(LoginCookie.getCookieLogin(cookieValue))
77
+ end
78
+
79
+
80
+
81
+ # generates a new cookie
82
+ def self.generateCookie(login = nil, series = nil)
83
+ cookieLogin = login ||= self.generate_random_string(@@cookieSegmentLength)
84
+ cookieSeries = series ||= self.generate_random_string(@@cookieSegmentLength)
85
+ cookieToken = self.generate_random_string(@@cookieSegmentLength)
86
+
87
+ #adds it to the db
88
+ self.addNewCookieEntry(cookieLogin, cookieSeries, cookieToken, self.getIdentityFromLogin(login))
89
+
90
+ return cookieLogin + @@separator + cookieSeries + @@separator + cookieToken
91
+ end
92
+
93
+
94
+
95
+ # verify if the cookie belongs to an anonymous user
96
+ def self.isAnonymousIdentity(cookieValue)
97
+ #defensive programming
98
+ if cookieValue == nil
99
+ return false
100
+ end
101
+
102
+ loginValue = LoginCookie.getCookieLogin(cookieValue)
103
+
104
+ #Indication 1: cookie must not have a login that belongs to a user
105
+ user = User.find(:first, :conditions => ["login = ?", loginValue])
106
+
107
+ #Indication 2: the cookie identity must not have other users
108
+ cookieIdentityID = self.getIdentityFromLogin(loginValue)
109
+ cookieIdentity = Identity.find(cookieIdentityID)
110
+
111
+ return user == nil && cookieIdentity.user == nil
112
+ end
113
+
114
+
115
+
116
+ # verify if the cookie is anonymous
117
+ def self.isAnonymous(cookieValue)
118
+ #defensive programming
119
+ if cookieValue == nil
120
+ return false
121
+ end
122
+
123
+ loginValue = LoginCookie.getCookieLogin(cookieValue)
124
+
125
+ #cookie might belong to a user
126
+ user = User.find(:first, :conditions => ["login = ?", loginValue])
127
+ return user == nil
128
+ end
129
+
130
+
131
+
132
+ # extracts the login from the cookie
133
+ def self.getCookieLogin(cookieValue)
134
+ #defensive programming
135
+ if cookieValue == nil
136
+ raise "null inputs"
137
+ end
138
+
139
+ cookieMatch = /([\d\w]+)@([\d\w]+)@([\d\w]+)/.match(cookieValue)
140
+ return cookieMatch[1]
141
+ end
142
+
143
+
144
+
145
+ # associating the cookie with the identity
146
+ def self.getCookieRef(cookieValue)
147
+ #defensive programming
148
+ if cookieValue == nil
149
+ raise "null inputs"
150
+ end
151
+
152
+ cookieMatch = /([\d\w]+)@([\d\w]+)@([\d\w]+)/.match(cookieValue)
153
+ return find(:first, :conditions => ["login = ? AND series = ? AND token = ?", cookieMatch[1], cookieMatch[2], cookieMatch[3]])
154
+ end
155
+
156
+
157
+
158
+ private
159
+
160
+
161
+
162
+ # check whether the cookie exists
163
+ def self.authenticate(cookieValue)
164
+ if cookieValue == nil
165
+ return false
166
+ end
167
+
168
+ cookieMatch = /([\d\w]+)@([\d\w]+)@([\d\w]+)/.match(cookieValue)
169
+ cookieFound = LoginCookie.getCookieRef(cookieValue)
170
+
171
+ if cookieFound != nil
172
+ return true
173
+ else
174
+ self.verifyCookieInjection(cookieMatch[1], cookieMatch[2])
175
+ return false
176
+ end
177
+ end
178
+
179
+
180
+
181
+ # gets the identity of a user
182
+ def self.getIdentityFromLogin(loginValue)
183
+ #defensive programming
184
+ if loginValue == nil
185
+ raise "null inputs"
186
+ end
187
+
188
+ #cookie might belong to a user
189
+ user = User.find(:first, :conditions => ["login = ?", loginValue])
190
+ return user.identity_id if user != nil
191
+
192
+ #cookie might belong to anonymous user
193
+ appCookie = LoginCookie.find(:first, :conditions => ["login = ?", loginValue])
194
+ return appCookie.identity_id if appCookie != nil
195
+
196
+ #if its a new user
197
+ identity = Identity.new
198
+ identity.save
199
+ return identity.id
200
+ end
201
+
202
+
203
+
204
+ #generates a random string
205
+ def self.generate_random_string(length)
206
+ chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
207
+ newRandomStr = ""
208
+ 1.upto(length) { |i| newRandomStr << chars[rand(chars.size-1)] }
209
+ return newRandomStr
210
+ end
211
+
212
+
213
+
214
+ #adds a new cookie to the db
215
+ def self.addNewCookieEntry(cookieLogin, cookieSeries, cookieToken, cookieIdentityID)
216
+ newLoginCookie = LoginCookie.new(:login => cookieLogin,
217
+ :series => cookieSeries,
218
+ :token => cookieToken,
219
+ :identity_id => cookieIdentityID)
220
+ newLoginCookie.save
221
+ return newLoginCookie
222
+ end
223
+
224
+
225
+
226
+ #deletes all the cookies with the same login and series
227
+ def self.deleteAllCookiesOf(cookieLogin, cookieSeries)
228
+ #defensive programming
229
+ if cookieLogin == nil or cookieSeries == nil
230
+ raise "null inputs"
231
+ end
232
+
233
+ cookiesFound = find(:all, :conditions => ["login = ? AND series = ?", cookieLogin, cookieSeries])
234
+ cookiesFound.each { |cookie| cookie.destroy }
235
+ end
236
+
237
+
238
+
239
+ #verify if there's any cookie tampering
240
+ def self.verifyCookieInjection(cookieLogin, cookieSeries)
241
+ #defensive programming
242
+ if cookieLogin == nil or cookieSeries == nil
243
+ raise "null inputs"
244
+ end
245
+
246
+ cookiesFound = find(:all, :conditions => ["login = ? AND series = ?", cookieLogin, cookieSeries])
247
+
248
+ if cookiesFound != nil
249
+ #SQL injection discovered, up to you to do something about it
250
+ self.deleteAllCookiesOf(cookieLogin, cookieSeries)
251
+ end
252
+ end
253
+
254
+ end
@@ -0,0 +1,32 @@
1
+ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
2
+ #bob has 1 known cookie + 1 anonymous
3
+ #existing bob has 1 known cookie
4
+ #1 anonymous user cookie
5
+ cookieOne:
6
+ id: 1
7
+ login: bob
8
+ series: seriesbob
9
+ token: tokenbob
10
+ identity_id: 1
11
+
12
+ cookieTwo:
13
+ id: 2
14
+ login: anonymous1
15
+ series: seriesanonymous1
16
+ token: tokenanonymous1
17
+ identity_id: 1
18
+
19
+ cookieThree:
20
+ id: 3
21
+ login: existingbob
22
+ series: seriesexistingbob
23
+ token: tokenexistingbob
24
+ identity_id: 2
25
+
26
+ cookieFour:
27
+ id: 4
28
+ login: anonymous2
29
+ series: seriesanonymous2
30
+ token: tokenanonymous2
31
+ identity_id: 6
32
+
@@ -0,0 +1,42 @@
1
+ class CreateAuthentication < ActiveRecord::Migration
2
+
3
+ def self.up
4
+ create_table "identities", :force => true do |t|
5
+ t.datetime "created_at"
6
+ end
7
+
8
+
9
+ create_table "users", :force => true do |t|
10
+ t.string "login", :limit => 80, :default => "", :null => false
11
+ t.string "salted_password", :limit => 40, :default => "", :null => false
12
+ t.string "email", :limit => 60, :default => "", :null => false
13
+ t.string "salt", :limit => 40, :default => "", :null => false
14
+ t.integer "verified", :default => 0
15
+ t.string "security_token", :limit => 40
16
+ t.datetime "created_at"
17
+ t.datetime "updated_at"
18
+ t.integer "identity_id", :null => false
19
+ end
20
+
21
+
22
+
23
+ create_table "login_cookies", :force => true do |t|
24
+ t.string "login", :limit => 80, :null => false
25
+ t.string "series", :limit => 40, :null => false
26
+ t.string "token", :limit => 40, :null => false
27
+ t.datetime "created_at"
28
+ t.integer "identity_id", :null => false
29
+ end
30
+
31
+ end
32
+
33
+
34
+ def self.down
35
+ drop_table :identities
36
+ drop_table :users
37
+ drop_table :login_cookies
38
+ end
39
+
40
+
41
+
42
+ end