authpwn_rails 0.12.0 → 0.12.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +7 -2
- data/VERSION +1 -1
- data/app/models/credentials/password.rb +16 -8
- data/app/models/credentials/token.rb +8 -0
- data/app/models/tokens/email_verification.rb +3 -0
- data/app/models/tokens/password_reset.rb +5 -2
- data/app/models/tokens/session_uid.rb +54 -0
- data/authpwn_rails.gemspec +8 -2
- data/lib/authpwn_rails.rb +3 -2
- data/lib/authpwn_rails/current_user.rb +1 -10
- data/lib/authpwn_rails/engine.rb +2 -2
- data/lib/authpwn_rails/expires.rb +23 -0
- data/lib/authpwn_rails/generators/all_generator.rb +9 -4
- data/lib/authpwn_rails/generators/templates/credential.rb +1 -1
- data/lib/authpwn_rails/generators/templates/credentials.yml +16 -0
- data/lib/authpwn_rails/generators/templates/initializer.rb +18 -0
- data/lib/authpwn_rails/generators/templates/session/forbidden.html.erb +1 -1
- data/lib/authpwn_rails/generators/templates/session/home.html.erb +1 -1
- data/lib/authpwn_rails/generators/templates/session/new.html.erb +3 -3
- data/lib/authpwn_rails/generators/templates/session/welcome.html.erb +1 -1
- data/lib/authpwn_rails/generators/templates/session_controller.rb +13 -4
- data/lib/authpwn_rails/generators/templates/session_controller_test.rb +12 -2
- data/lib/authpwn_rails/generators/templates/session_mailer.rb +3 -3
- data/lib/authpwn_rails/generators/templates/session_mailer/email_verification_email.html.erb +3 -3
- data/lib/authpwn_rails/generators/templates/session_mailer/reset_password_email.html.erb +3 -3
- data/lib/authpwn_rails/generators/templates/session_mailer_test.rb +4 -4
- data/lib/authpwn_rails/routes.rb +4 -4
- data/lib/authpwn_rails/session.rb +31 -8
- data/lib/authpwn_rails/session_controller.rb +27 -18
- data/lib/authpwn_rails/test_extensions.rb +16 -6
- data/lib/authpwn_rails/user_model.rb +10 -10
- data/test/cookie_controller_test.rb +165 -16
- data/test/credentials/email_verification_token_test.rb +11 -11
- data/test/credentials/password_credential_test.rb +31 -12
- data/test/credentials/session_uid_token_test.rb +98 -0
- data/test/credentials/token_crendential_test.rb +46 -12
- data/test/helpers/db_setup.rb +6 -5
- data/test/helpers/routes.rb +5 -2
- data/test/initializer_test.rb +18 -0
- data/test/session_controller_api_test.rb +127 -53
- data/test/test_extensions_test.rb +41 -0
- data/test/test_helper.rb +3 -0
- data/test/user_test.rb +11 -10
- metadata +9 -3
data/.travis.yml
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.12.
|
1
|
+
0.12.1
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# :namespace
|
2
2
|
module Credentials
|
3
|
-
|
3
|
+
|
4
4
|
# Associates a password with the user account.
|
5
5
|
class Password < ::Credential
|
6
6
|
# Virtual attribute: the user's password.
|
@@ -11,9 +11,16 @@ class Password < ::Credential
|
|
11
11
|
# Virtual attribute: confirmation for the user's password.
|
12
12
|
attr_accessor :password_confirmation
|
13
13
|
|
14
|
-
# A user can have a single password
|
14
|
+
# A user can have a single password.
|
15
15
|
validates :user_id, :uniqueness => true
|
16
16
|
|
17
|
+
# Passwords can expire, if users don't change them often enough.
|
18
|
+
include Authpwn::Expires
|
19
|
+
# Passwords don't expire by default, because it is non-trivial to get e-mail
|
20
|
+
# delivery working in Rails, which is necessary for recovering from expired
|
21
|
+
# passwords.
|
22
|
+
self.expires_after = nil
|
23
|
+
|
17
24
|
# Compares a plain-text password against the password hash in this credential.
|
18
25
|
#
|
19
26
|
# Returns +true+ for a match, +false+ otherwise.
|
@@ -21,16 +28,17 @@ class Password < ::Credential
|
|
21
28
|
return false unless key
|
22
29
|
key == self.class.hash_password(password, key.split('|', 2).first)
|
23
30
|
end
|
24
|
-
|
31
|
+
|
25
32
|
# Compares a plain-text password against the password hash in this credential.
|
26
33
|
#
|
27
34
|
# Returns the authenticated User instance, or a symbol indicating the reason
|
28
35
|
# why the (potentially valid) password was rejected.
|
29
36
|
def authenticate(password)
|
37
|
+
return :expired if expired?
|
30
38
|
return :invalid unless check_password(password)
|
31
39
|
user.auth_bounce_reason(self) || user
|
32
40
|
end
|
33
|
-
|
41
|
+
|
34
42
|
# Password virtual attribute.
|
35
43
|
def password=(new_password)
|
36
44
|
@password = new_password
|
@@ -50,7 +58,7 @@ class Password < ::Credential
|
|
50
58
|
def self.authenticate_email(email, password)
|
51
59
|
user = Credentials::Email.authenticate email
|
52
60
|
return user if user.is_a? Symbol
|
53
|
-
|
61
|
+
|
54
62
|
credential = user.credentials.find { |c| c.kind_of? Credentials::Password }
|
55
63
|
credential ? credential.authenticate(password) : :invalid
|
56
64
|
end
|
@@ -59,14 +67,14 @@ class Password < ::Credential
|
|
59
67
|
def self.hash_password(password, salt)
|
60
68
|
salt + '|' + Digest::SHA2.hexdigest(password + salt)
|
61
69
|
end
|
62
|
-
|
70
|
+
|
63
71
|
# Generates a random salt value.
|
64
72
|
def self.random_salt
|
65
73
|
[(0...12).map { |i| 1 + rand(255) }.pack('C*')].pack('m').strip
|
66
74
|
end
|
67
|
-
|
75
|
+
|
68
76
|
# Forms can only change the plain-text password fields.
|
69
|
-
attr_accessible :password, :password_confirmation
|
77
|
+
attr_accessible :password, :password_confirmation
|
70
78
|
end # class Credentials::Password
|
71
79
|
|
72
80
|
end # namespace Credentials
|
@@ -29,6 +29,10 @@ class Token < ::Credential
|
|
29
29
|
validates :name, :format => /^[A-Za-z0-9\_\-]+$/, :presence => true,
|
30
30
|
:uniqueness => true
|
31
31
|
|
32
|
+
# Tokens can expire. This is a good idea most of the time, because token
|
33
|
+
# codes are supposed to be used quickly.
|
34
|
+
include Authpwn::Expires
|
35
|
+
|
32
36
|
# Authenticates a user using a secret token code.
|
33
37
|
#
|
34
38
|
# The token will be spent on successful authentication. One-time tokens are
|
@@ -67,6 +71,10 @@ class Token < ::Credential
|
|
67
71
|
# Returns the authenticated User instance, or a symbol indicating the reason
|
68
72
|
# why the (potentially valid) token code was rejected.
|
69
73
|
def authenticate
|
74
|
+
if expired?
|
75
|
+
destroy
|
76
|
+
return :invalid
|
77
|
+
end
|
70
78
|
if bounce = user.auth_bounce_reason(self)
|
71
79
|
return bounce
|
72
80
|
end
|
@@ -12,6 +12,9 @@ class EmailVerification < OneTime
|
|
12
12
|
alias_attribute :email, :key
|
13
13
|
validates :email, :presence => true
|
14
14
|
|
15
|
+
# Decent compromise between convenience and security.
|
16
|
+
self.expires_after = 3.days
|
17
|
+
|
15
18
|
# Creates a token with a random code that verifies the given e-mail address.
|
16
19
|
def self.random_for(email_credential)
|
17
20
|
super email_credential.user, email_credential.email, self
|
@@ -1,8 +1,11 @@
|
|
1
1
|
# :namespace
|
2
2
|
module Tokens
|
3
|
-
|
3
|
+
|
4
4
|
# Lets the user to change their password without knowing the old one.
|
5
5
|
class PasswordReset < OneTime
|
6
|
+
# Decent compromise between convenience and security.
|
7
|
+
self.expires_after = 3.days
|
8
|
+
|
6
9
|
# Blanks the user's old password, so the new password form won't ask for it.
|
7
10
|
#
|
8
11
|
# Returns the token instance.
|
@@ -14,7 +17,7 @@ class PasswordReset < OneTime
|
|
14
17
|
super
|
15
18
|
end
|
16
19
|
end
|
17
|
-
|
20
|
+
|
18
21
|
# The credential that is removed by this token.
|
19
22
|
#
|
20
23
|
# This method might return nil if a user initiates password recovery multiple
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# :namespace
|
2
|
+
module Tokens
|
3
|
+
|
4
|
+
class SessionUid < Credentials::Token
|
5
|
+
# The session UID.
|
6
|
+
alias_attribute :suid, :name
|
7
|
+
|
8
|
+
# The IP address and User-Agent string of the browser using this session.
|
9
|
+
store :key, :accessors => [:browser_ip, :browser_ua]
|
10
|
+
|
11
|
+
# The User-Agent header of the browser that received this suid.
|
12
|
+
validates :browser_ua, :presence => true
|
13
|
+
|
14
|
+
# The IP of the computer that received this suid.
|
15
|
+
validates :browser_ip, :presence => true
|
16
|
+
|
17
|
+
# Decent compromise between convenience and security.
|
18
|
+
self.expires_after = 14.days
|
19
|
+
|
20
|
+
# Creates a new session UID token for a user.
|
21
|
+
#
|
22
|
+
# @param [User] user the user authenticated using this session
|
23
|
+
# @param [String] browser_ip the IP of the session
|
24
|
+
# @param [String] browser_ua the User-Agent of the browser used for this
|
25
|
+
# session
|
26
|
+
def self.random_for(user, browser_ip, browser_ua)
|
27
|
+
browser_ua = browser_ua[0, 1536] if browser_ua.length > 1536
|
28
|
+
key = { :browser_ip => browser_ip, :browser_ua => browser_ua }
|
29
|
+
super user, key, self
|
30
|
+
end
|
31
|
+
|
32
|
+
# Refresh precision for the updated_at timestamp, in seconds.
|
33
|
+
#
|
34
|
+
# When a session UID is used to authenticate a user, its updated_at time is
|
35
|
+
# refreshed if it differs from the current time by this much.
|
36
|
+
class_attribute :updates_after, :instance_writer => false
|
37
|
+
self.updates_after = 1.hour
|
38
|
+
|
39
|
+
# Updates the time associated with the session.
|
40
|
+
def spend
|
41
|
+
self.touch if Time.now - updated_at >= updates_after
|
42
|
+
end
|
43
|
+
|
44
|
+
# Garbage-collects database records of expired sessions.
|
45
|
+
#
|
46
|
+
# This method should be called periodically to keep the size of the session
|
47
|
+
# table under control.
|
48
|
+
def self.remove_expired
|
49
|
+
self.where('updated_at < ?', Time.now - expires_after).delete_all
|
50
|
+
self
|
51
|
+
end
|
52
|
+
end # class Tokens::SessionUid
|
53
|
+
|
54
|
+
end # namespace Tokens
|
data/authpwn_rails.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "authpwn_rails"
|
8
|
-
s.version = "0.12.
|
8
|
+
s.version = "0.12.1"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Victor Costan"]
|
12
|
-
s.date = "2012-
|
12
|
+
s.date = "2012-10-05"
|
13
13
|
s.description = "Works with Facebook."
|
14
14
|
s.email = "victor@costan.us"
|
15
15
|
s.extra_rdoc_files = [
|
@@ -34,6 +34,7 @@ Gem::Specification.new do |s|
|
|
34
34
|
"app/models/tokens/email_verification.rb",
|
35
35
|
"app/models/tokens/one_time.rb",
|
36
36
|
"app/models/tokens/password_reset.rb",
|
37
|
+
"app/models/tokens/session_uid.rb",
|
37
38
|
"authpwn_rails.gemspec",
|
38
39
|
"legacy/migrate_011_to_012.rb",
|
39
40
|
"legacy/migrate_09_to_010.rb",
|
@@ -41,12 +42,14 @@ Gem::Specification.new do |s|
|
|
41
42
|
"lib/authpwn_rails/credential_model.rb",
|
42
43
|
"lib/authpwn_rails/current_user.rb",
|
43
44
|
"lib/authpwn_rails/engine.rb",
|
45
|
+
"lib/authpwn_rails/expires.rb",
|
44
46
|
"lib/authpwn_rails/facebook_session.rb",
|
45
47
|
"lib/authpwn_rails/generators/all_generator.rb",
|
46
48
|
"lib/authpwn_rails/generators/templates/001_create_users.rb",
|
47
49
|
"lib/authpwn_rails/generators/templates/003_create_credentials.rb",
|
48
50
|
"lib/authpwn_rails/generators/templates/credential.rb",
|
49
51
|
"lib/authpwn_rails/generators/templates/credentials.yml",
|
52
|
+
"lib/authpwn_rails/generators/templates/initializer.rb",
|
50
53
|
"lib/authpwn_rails/generators/templates/session/forbidden.html.erb",
|
51
54
|
"lib/authpwn_rails/generators/templates/session/home.html.erb",
|
52
55
|
"lib/authpwn_rails/generators/templates/session/new.html.erb",
|
@@ -79,6 +82,7 @@ Gem::Specification.new do |s|
|
|
79
82
|
"test/credentials/one_time_token_credential_test.rb",
|
80
83
|
"test/credentials/password_credential_test.rb",
|
81
84
|
"test/credentials/password_reset_token_test.rb",
|
85
|
+
"test/credentials/session_uid_token_test.rb",
|
82
86
|
"test/credentials/token_crendential_test.rb",
|
83
87
|
"test/facebook_controller_test.rb",
|
84
88
|
"test/fixtures/bare_session/forbidden.html.erb",
|
@@ -94,9 +98,11 @@ Gem::Specification.new do |s|
|
|
94
98
|
"test/helpers/routes.rb",
|
95
99
|
"test/helpers/view_helpers.rb",
|
96
100
|
"test/http_basic_controller_test.rb",
|
101
|
+
"test/initializer_test.rb",
|
97
102
|
"test/routes_test.rb",
|
98
103
|
"test/session_controller_api_test.rb",
|
99
104
|
"test/session_mailer_api_test.rb",
|
105
|
+
"test/test_extensions_test.rb",
|
100
106
|
"test/test_helper.rb",
|
101
107
|
"test/user_extensions/email_field_test.rb",
|
102
108
|
"test/user_extensions/facebook_fields_test.rb",
|
data/lib/authpwn_rails.rb
CHANGED
@@ -3,8 +3,10 @@ require 'active_support'
|
|
3
3
|
# :nodoc: namespace
|
4
4
|
module Authpwn
|
5
5
|
extend ActiveSupport::Autoload
|
6
|
-
|
6
|
+
|
7
7
|
autoload :CredentialModel, 'authpwn_rails/credential_model.rb'
|
8
|
+
autoload :CurrentUser, 'authpwn_rails/current_user.rb'
|
9
|
+
autoload :Expires, 'authpwn_rails/expires.rb'
|
8
10
|
autoload :SessionController, 'authpwn_rails/session_controller.rb'
|
9
11
|
autoload :SessionMailer, 'authpwn_rails/session_mailer.rb'
|
10
12
|
autoload :UserModel, 'authpwn_rails/user_model.rb'
|
@@ -17,7 +19,6 @@ module Authpwn
|
|
17
19
|
end
|
18
20
|
end
|
19
21
|
|
20
|
-
require 'authpwn_rails/current_user.rb'
|
21
22
|
require 'authpwn_rails/facebook_session.rb'
|
22
23
|
require 'authpwn_rails/http_basic.rb'
|
23
24
|
require 'authpwn_rails/routes.rb'
|
@@ -3,16 +3,7 @@ module Authpwn
|
|
3
3
|
|
4
4
|
# The unofficial Rails convention for tracking the authenticated user.
|
5
5
|
module CurrentUser
|
6
|
-
|
7
|
-
|
8
|
-
def current_user=(user)
|
9
|
-
@current_user = user
|
10
|
-
if user
|
11
|
-
session[:user_exuid] = user.to_param
|
12
|
-
else
|
13
|
-
session.delete :user_exuid
|
14
|
-
end
|
15
|
-
end
|
6
|
+
attr_accessor :current_user
|
16
7
|
end # module Authpwn::CurrentUser
|
17
8
|
|
18
9
|
end # namespace Authpwn
|
data/lib/authpwn_rails/engine.rb
CHANGED
@@ -8,11 +8,11 @@ class Engine < Rails::Engine
|
|
8
8
|
generators do
|
9
9
|
require 'authpwn_rails/generators/all_generator.rb'
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
initializer 'authpwn.rspec.extensions' do
|
13
13
|
begin
|
14
14
|
require 'rspec'
|
15
|
-
|
15
|
+
|
16
16
|
RSpec.configure do |c|
|
17
17
|
c.include Authpwn::TestExtensions
|
18
18
|
c.include Authpwn::ControllerTestExtensions
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# :nodoc: namespace
|
2
|
+
module Authpwn
|
3
|
+
|
4
|
+
# Common code for credentials that expire.
|
5
|
+
module Expires
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
# Number of seconds after which a credential becomes unusable.
|
10
|
+
#
|
11
|
+
# Users can reset this timer by updating their credentials, e.g. changing
|
12
|
+
# their password.
|
13
|
+
class_attribute :expires_after, :instance_writer => false
|
14
|
+
end
|
15
|
+
|
16
|
+
# True if this password is too old and should not be used for authentication.
|
17
|
+
def expired?
|
18
|
+
return false unless expires_after
|
19
|
+
updated_at < Time.now - expires_after
|
20
|
+
end
|
21
|
+
end # module Authpwn::Expires
|
22
|
+
|
23
|
+
end # namespace Authpwn
|
@@ -11,7 +11,7 @@ class AllGenerator < Rails::Generators::Base
|
|
11
11
|
File.join('db', 'migrate', '20100725000001_create_users.rb')
|
12
12
|
copy_file 'users.yml', File.join('test', 'fixtures', 'users.yml')
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
15
|
def create_credential_model
|
16
16
|
copy_file 'credential.rb', File.join('app', 'models', 'credential.rb')
|
17
17
|
copy_file '003_create_credentials.rb',
|
@@ -19,17 +19,17 @@ class AllGenerator < Rails::Generators::Base
|
|
19
19
|
copy_file 'credentials.yml',
|
20
20
|
File.join('test', 'fixtures', 'credentials.yml')
|
21
21
|
end
|
22
|
-
|
22
|
+
|
23
23
|
def create_session_controller
|
24
24
|
copy_file 'session_controller.rb',
|
25
|
-
File.join('app', 'controllers', 'session_controller.rb')
|
25
|
+
File.join('app', 'controllers', 'session_controller.rb')
|
26
26
|
copy_file File.join('session_controller_test.rb'),
|
27
27
|
File.join('test', 'functional', 'session_controller_test.rb')
|
28
28
|
|
29
29
|
route "authpwn_session"
|
30
30
|
route "root :to => 'session#show'"
|
31
31
|
end
|
32
|
-
|
32
|
+
|
33
33
|
def create_session_views
|
34
34
|
copy_file File.join('session', 'forbidden.html.erb'),
|
35
35
|
File.join('app', 'views', 'session', 'forbidden.html.erb')
|
@@ -58,6 +58,11 @@ class AllGenerator < Rails::Generators::Base
|
|
58
58
|
File.join('app', 'views', 'session_mailer',
|
59
59
|
'reset_password_email.text.erb')
|
60
60
|
end
|
61
|
+
|
62
|
+
def create_initializer
|
63
|
+
copy_file 'initializer.rb',
|
64
|
+
File.join('config', 'initializers', 'authpwn.rb')
|
65
|
+
end
|
61
66
|
end # class Authpwn::AllGenerator
|
62
67
|
|
63
68
|
end # namespace Authpwn
|
@@ -53,3 +53,19 @@ jane_password_token:
|
|
53
53
|
user: jane
|
54
54
|
type: Tokens::PasswordReset
|
55
55
|
name: nbMLTKN18tYy9plBAbsrwT6zdE2jZqoKPk6Ze4lHMSQ
|
56
|
+
|
57
|
+
john_session_token:
|
58
|
+
user: john
|
59
|
+
type: Tokens::SessionUid
|
60
|
+
name: iyHvfTnYoF1f1jL9Vnb55hnXobf2Ld6HxIW-PXya6dw
|
61
|
+
key: <%= { :browser_ip => '18.241.1.121',
|
62
|
+
:browser_ua => 'Mozilla/5.0 (X11; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1'
|
63
|
+
}.to_yaml.inspect %>
|
64
|
+
|
65
|
+
jane_session_token:
|
66
|
+
user: jane
|
67
|
+
type: Tokens::SessionUid
|
68
|
+
name: sNIfh6UavUSceL0TpubJ-DnZRuxPSTAddoHBb-twEIg
|
69
|
+
key: <%= { :browser_ip => '18.70.0.160',
|
70
|
+
:browser_ua => 'Mozilla/5.0 (X11; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1'
|
71
|
+
}.to_yaml.inspect %>
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Tweak the password expiration interval, or comment out the line to disable
|
2
|
+
# password expiration altogether.
|
3
|
+
#
|
4
|
+
# NOTE: when a user's password expires, he will need to use the password reset
|
5
|
+
# flow, which relies on e-mail delivery. If your application doesn't implement
|
6
|
+
# password reset, or doesn't have working e-mail delivery, disable password
|
7
|
+
# expiration.
|
8
|
+
Credentials::Password.expires_after = 1.year
|
9
|
+
|
10
|
+
# These codes are sent in plaintext in e-mails, be somewhat aggressive.
|
11
|
+
Tokens::EmailVerification.expires_after = 3.days
|
12
|
+
Tokens::PasswordReset.expires_after = 3.days
|
13
|
+
|
14
|
+
# Users are identified by cookies whose codes are looked up in the database.
|
15
|
+
Tokens::SessionUid.expires_after = 14.days
|
16
|
+
# This knob is a compromise between accurate session expiration and write
|
17
|
+
# workload on the database. Keep it below 1% of expires_after.
|
18
|
+
Tokens::SessionUid.updates_after = 1.hour
|
@@ -1,5 +1,5 @@
|
|
1
1
|
<p>
|
2
|
-
This view gets displayed when the user is logged in. Right now,
|
2
|
+
This view gets displayed when the user is logged in. Right now,
|
3
3
|
user <%= current_user.exuid %> is logged in. You should allow the user to
|
4
4
|
<%= link_to 'Log out', session_path, :method => :delete %>.
|
5
5
|
</p>
|