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.
- data/MIT-LICENCE +20 -0
- data/USAGE +22 -0
- data/persistent_cookie_authentication_generator.rb +52 -0
- data/templates/identities.yml +18 -0
- data/templates/identity.rb +4 -0
- data/templates/login_cookie.rb +254 -0
- data/templates/login_cookies.yml +32 -0
- data/templates/migration.rb +42 -0
- data/templates/smtp_tls.rb +67 -0
- data/templates/user.rb +147 -0
- data/templates/user_change_password.rhtml +22 -0
- data/templates/user_controller.rb +302 -0
- data/templates/user_edit.rhtml +15 -0
- data/templates/user_environment.rb +9 -0
- data/templates/user_forgot_password.rhtml +9 -0
- data/templates/user_integration_test.rb +303 -0
- data/templates/user_login.rhtml +23 -0
- data/templates/user_notify.rb +55 -0
- data/templates/user_notify_change_password.rhtml +10 -0
- data/templates/user_notify_forgot_password.rhtml +8 -0
- data/templates/user_notify_signup.rhtml +10 -0
- data/templates/user_show.rhtml +15 -0
- data/templates/user_signup.rhtml +29 -0
- data/templates/user_system.rb +158 -0
- data/templates/user_test.rb +72 -0
- data/templates/users.yml +46 -0
- metadata +78 -0
data/MIT-LICENCE
ADDED
@@ -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,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
|