authkit 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/FEATURES.md +73 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +168 -0
  7. data/Rakefile +60 -0
  8. data/authkit.gemspec +27 -0
  9. data/config/database.yml.example +19 -0
  10. data/lib/authkit.rb +5 -0
  11. data/lib/authkit/engine.rb +7 -0
  12. data/lib/authkit/version.rb +3 -0
  13. data/lib/generators/authkit/USAGE +18 -0
  14. data/lib/generators/authkit/install_generator.rb +113 -0
  15. data/lib/generators/authkit/templates/app/controllers/application_controller.rb +94 -0
  16. data/lib/generators/authkit/templates/app/controllers/email_confirmation_controller.rb +25 -0
  17. data/lib/generators/authkit/templates/app/controllers/password_change_controller.rb +29 -0
  18. data/lib/generators/authkit/templates/app/controllers/password_reset_controller.rb +29 -0
  19. data/lib/generators/authkit/templates/app/controllers/sessions_controller.rb +35 -0
  20. data/lib/generators/authkit/templates/app/controllers/users_controller.rb +89 -0
  21. data/lib/generators/authkit/templates/app/models/user.rb +170 -0
  22. data/lib/generators/authkit/templates/app/views/password_change/show.html.erb +16 -0
  23. data/lib/generators/authkit/templates/app/views/password_reset/show.html.erb +12 -0
  24. data/lib/generators/authkit/templates/app/views/sessions/new.html.erb +13 -0
  25. data/lib/generators/authkit/templates/app/views/users/edit.html.erb +58 -0
  26. data/lib/generators/authkit/templates/app/views/users/new.html.erb +58 -0
  27. data/lib/generators/authkit/templates/db/migrate/add_authkit_fields_to_users.rb +110 -0
  28. data/lib/generators/authkit/templates/db/migrate/create_users.rb +17 -0
  29. data/lib/generators/authkit/templates/lib/email_format_validator.rb +11 -0
  30. data/lib/generators/authkit/templates/spec/controllers/application_controller_spec.rb +188 -0
  31. data/lib/generators/authkit/templates/spec/controllers/email_confirmation_controller_spec.rb +80 -0
  32. data/lib/generators/authkit/templates/spec/controllers/password_change_controller_spec.rb +98 -0
  33. data/lib/generators/authkit/templates/spec/controllers/password_reset_controller_spec.rb +87 -0
  34. data/lib/generators/authkit/templates/spec/controllers/sessions_controller_spec.rb +111 -0
  35. data/lib/generators/authkit/templates/spec/controllers/users_controller_spec.rb +195 -0
  36. data/lib/generators/authkit/templates/spec/models/user_spec.rb +268 -0
  37. data/spec/spec_helper.rb +16 -0
  38. metadata +165 -0
@@ -0,0 +1,113 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/active_record'
3
+
4
+ module Authkit
5
+ class InstallGenerator < Rails::Generators::Base
6
+ include Rails::Generators::Migration
7
+
8
+ desc "An auth system for your Rails app"
9
+
10
+ def self.source_root
11
+ @source_root ||= File.join(File.dirname(__FILE__), 'templates')
12
+ end
13
+
14
+ def generate_authkit
15
+ generate_migration("create_users")
16
+ generate_migration("add_authkit_fields_to_users")
17
+
18
+ # Ensure the destination structure
19
+ empty_directory "app"
20
+ empty_directory "app/models"
21
+ empty_directory "app/controllers"
22
+ empty_directory "app/views"
23
+ empty_directory "app/views/users"
24
+ empty_directory "app/views/sessions"
25
+ empty_directory "app/views/password_reset"
26
+ empty_directory "app/views/password_change"
27
+ empty_directory "spec"
28
+ empty_directory "spec/models"
29
+ empty_directory "spec/controllers"
30
+ empty_directory "lib"
31
+
32
+ # Fill out some templates (for now, this is just straight copy)
33
+ template "app/models/user.rb", "app/models/user.rb"
34
+ template "app/controllers/users_controller.rb", "app/controllers/users_controller.rb"
35
+ template "app/controllers/sessions_controller.rb", "app/controllers/sessions_controller.rb"
36
+ template "app/controllers/password_reset_controller.rb", "app/controllers/password_reset_controller.rb"
37
+ template "app/controllers/password_change_controller.rb", "app/controllers/password_change_controller.rb"
38
+ template "app/controllers/email_confirmation_controller.rb", "app/controllers/email_confirmation_controller.rb"
39
+
40
+ template "spec/models/user_spec.rb", "spec/models/user_spec.rb"
41
+ template "spec/controllers/application_controller_spec.rb", "spec/controllers/application_controller_spec.rb"
42
+ template "spec/controllers/users_controller_spec.rb", "spec/controllers/users_controller_spec.rb"
43
+ template "spec/controllers/sessions_controller_spec.rb", "spec/controllers/sessions_controller_spec.rb"
44
+ template "spec/controllers/password_reset_controller_spec.rb", "spec/controllers/password_reset_controller_spec.rb"
45
+ template "spec/controllers/password_change_controller_spec.rb", "spec/controllers/password_change_controller_spec.rb"
46
+ template "spec/controllers/email_confirmation_controller_spec.rb", "spec/controllers/email_confirmation_controller_spec.rb"
47
+
48
+ template "lib/email_format_validator.rb", "lib/email_format_validator.rb"
49
+
50
+ # Don't treat these like templates
51
+ copy_file "app/views/users/new.html.erb", "app/views/users/new.html.erb"
52
+ copy_file "app/views/users/edit.html.erb", "app/views/users/edit.html.erb"
53
+ copy_file "app/views/sessions/new.html.erb", "app/views/sessions/new.html.erb"
54
+ copy_file "app/views/password_reset/show.html.erb", "app/views/password_reset/show.html.erb"
55
+ copy_file "app/views/password_change/show.html.erb", "app/views/password_change/show.html.erb"
56
+
57
+ # We don't want to override this file and may have a protected section
58
+ insert_at_end_of_class "app/controllers/application_controller.rb", "app/controllers/application_controller.rb"
59
+
60
+ # Need a temp root
61
+ route "root 'welcome#index'"
62
+
63
+ # Setup the routes
64
+ route "get '/email/confirm/:token', to: 'email_confirmation#show', as: :confirm"
65
+
66
+ route "post '/password/reset', to: 'password_reset#create'"
67
+ route "get '/password/reset', to: 'password_reset#show', as: :password_reset"
68
+ route "post '/password/change/:token', to: 'password_change#create'"
69
+ route "get '/password/change/:token', to: 'password_change#show', as: :password_change"
70
+
71
+ route "get '/signup', to: 'users#new', as: :signup"
72
+ route "get '/logout', to: 'sessions#destroy', as: :logout"
73
+ route "get '/login', to: 'sessions#new', as: :login"
74
+
75
+ route "put '/account', to: 'users#update'"
76
+ route "get '/account', to: 'users#edit', as: :user"
77
+
78
+ route "resources :sessions, only: [:new, :create, :destroy]"
79
+ route "resources :users, only: [:new, :create]"
80
+
81
+ # Support for has_secure_password and has_one_time_password
82
+ gem "active_model_otp"
83
+ gem "bcrypt-ruby", '~> 3.0.0'
84
+
85
+ # RSpec needs to be in the development group to be used in generators
86
+ gem_group :test, :development do
87
+ gem "rspec-rails"
88
+ gem "shoulda-matchers"
89
+ end
90
+ end
91
+
92
+ def self.next_migration_number(dirname)
93
+ ActiveRecord::Generators::Base.next_migration_number(dirname)
94
+ end
95
+
96
+ protected
97
+
98
+ def insert_at_end_of_class(filename, source)
99
+ source = File.expand_path(find_in_source_paths(source.to_s))
100
+ context = instance_eval('binding')
101
+ content = ERB.new(::File.binread(source), nil, '-', '@output_buffer').result(context)
102
+ insert_into_file "app/controllers/application_controller.rb", "#{content}\n", before: /end\n*\z/
103
+ end
104
+
105
+ def generate_migration(filename)
106
+ if self.class.migration_exists?("db/migrate", "#{filename}")
107
+ say_status "skipped", "Migration #{filename}.rb already exists"
108
+ else
109
+ migration_template "db/migrate/#{filename}.rb", "db/migrate/#{filename}.rb"
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,94 @@
1
+
2
+ before_filter :set_time_zone
3
+
4
+ helper_method :logged_in?, :current_user
5
+
6
+ # It is very unlikely that this exception will be created under normal
7
+ # circumstances. Unique validations are handled in Rails, but they are
8
+ # also enforced at the database level to guarantee data integrity. In
9
+ # certain cases (double-clicking a save link, multiple distributed servers)
10
+ # it is possible to get past the Rails validation in which case the
11
+ # database throws an exception.
12
+ rescue_from ActiveRecord::RecordNotUnique, with: :record_not_unique
13
+
14
+ protected
15
+
16
+ def current_user
17
+ return @current_user if defined?(@current_user)
18
+ @current_user ||= User.find_by(session[:user_id]) if session[:user_id]
19
+ @current_user ||= User.user_from_remember_token(cookies.signed[:remember]) unless cookies.signed[:remember].blank?
20
+ session[:user_id] = @current_user.id if @current_user
21
+ session[:time_zone] = @current_user.time_zone if @current_user
22
+ set_time_zone
23
+
24
+ @current_user
25
+ end
26
+
27
+ def allow_tracking?
28
+ "#{request.headers['X-Do-Not-Track']}" != '1' && "#{request.headers['DNT']}" != '1'
29
+ end
30
+
31
+ def logged_in?
32
+ !!current_user
33
+ end
34
+
35
+ def require_login
36
+ deny_user(nil, login_path) unless logged_in?
37
+ end
38
+
39
+ def require_token
40
+ deny_user("Invalid token", root_path) unless @user = User.user_from_token(params[:token])
41
+ end
42
+
43
+ def login(user)
44
+ @current_user = user
45
+ current_user.track_sign_in(request.remote_ip) if allow_tracking?
46
+ current_user.set_token(:remember_token)
47
+ set_remember_cookie
48
+ reset_session
49
+ session[:user_id] = current_user.id
50
+ session[:time_zone] = current_user.time_zone
51
+ set_time_zone
52
+ current_user
53
+ end
54
+
55
+ def logout
56
+ current_user.clear_remember_token if current_user
57
+ cookies.delete(:remember)
58
+ reset_session
59
+ @current_user = nil
60
+ end
61
+
62
+ def set_time_zone
63
+ Time.zone = session[:time_zone] if session[:time_zone].present?
64
+ end
65
+
66
+ def set_remember_cookie
67
+ cookies.permanent.signed[:remember] = {
68
+ value: current_user.remember_token,
69
+ secure: Rails.env.production?
70
+ }
71
+ end
72
+
73
+ def redirect_back_or_default
74
+ redirect_to(session.delete(:return_url) || root_path)
75
+ end
76
+
77
+ def deny_user(message=nil, location=nil)
78
+ location ||= (logged_in? ? root_path : login_path)
79
+
80
+ session[:return_url] = request.fullpath
81
+ respond_to do |format|
82
+ format.json { render(status: 403, nothing: true) }
83
+ format.html do
84
+ flash[:error] = message || "Sorry, you must be logged in to do that"
85
+ redirect_to(location)
86
+ end
87
+ end
88
+
89
+ false
90
+ end
91
+
92
+ def record_not_unique
93
+ respond_with(nil, location: root_path, status: 422)
94
+ end
@@ -0,0 +1,25 @@
1
+ class EmailConfirmationController < ApplicationController
2
+ before_filter :require_token
3
+
4
+ respond_to :html
5
+
6
+ def show
7
+ if @user.email_confirmed
8
+ login(@user)
9
+ flash[:notice] = "Thanks for confirming your email address"
10
+ respond_to do |format|
11
+ format.json { head :no_content }
12
+ format.html { redirect_to root_path }
13
+ end
14
+ else
15
+ respond_to do |format|
16
+ format.json { render json: { status: 'error', errors: @user.errors }.to_json, status: 422 }
17
+ format.html {
18
+ flash[:error] = "Could not confirm email address because it is already in use"
19
+ redirect_to root_path
20
+ }
21
+ end
22
+ end
23
+ end
24
+ end
25
+
@@ -0,0 +1,29 @@
1
+ class PasswordChangeController < ApplicationController
2
+ before_filter :require_token
3
+
4
+ def show
5
+ respond_to do |format|
6
+ format.json { head :no_content }
7
+ format.html
8
+ end
9
+ end
10
+
11
+ def create
12
+ if @user.change_password(params[:password], params[:password_confirmation])
13
+ login(@user)
14
+
15
+ respond_to do |format|
16
+ format.json { head :no_content }
17
+ format.html {
18
+ flash.now[:notice] = "Password updated successfully"
19
+ redirect_to(root_path)
20
+ }
21
+ end
22
+ else
23
+ respond_to do |format|
24
+ format.json { render json: { status: 'error', errors: @user.errors }.to_json, status: 422 }
25
+ format.html { render :show }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ class PasswordResetController < ApplicationController
2
+ def show
3
+ end
4
+
5
+ def create
6
+ username_or_email = "#{params[:email]}".downcase
7
+ user = User.find_by_username_or_email(username_or_email) if username_or_email.present?
8
+
9
+ if user && user.send_reset_password
10
+ logout
11
+
12
+ respond_to do |format|
13
+ format.json { head :no_content }
14
+ format.html {
15
+ flash[:notice] = "We've sent an email which can be used to change your password"
16
+ redirect_to login_path
17
+ }
18
+ end
19
+ else
20
+ respond_to do |format|
21
+ format.json { render json: { errors: ["Invalid user name or email"], status: "error" }, status: 422 }
22
+ format.html {
23
+ flash.now[:error] = "Invalid user name or email"
24
+ render :show
25
+ }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,35 @@
1
+ class SessionsController < ApplicationController
2
+ # Login
3
+ def new
4
+ end
5
+
6
+ def create
7
+ username_or_email = "#{params[:email]}".downcase
8
+ user = User.find_by_username_or_email(username_or_email) if username_or_email.present?
9
+
10
+ if user && user.authenticate(params[:password])
11
+ login(user)
12
+ respond_to do |format|
13
+ format.json { head :no_content }
14
+ format.html { redirect_back_or_default }
15
+ end
16
+ else
17
+ respond_to do |format|
18
+ format.json { render json: { errors: ["Invalid user name or password"], status: "error" }, status: 422 }
19
+ format.html {
20
+ flash.now[:error] = "Invalid user name or password"
21
+ render :new
22
+ }
23
+ end
24
+ end
25
+ end
26
+
27
+ # Logout
28
+ def destroy
29
+ logout
30
+ respond_to do |format|
31
+ format.json { head :no_content }
32
+ format.html { redirect_to root_path }
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,89 @@
1
+ class UsersController < ApplicationController
2
+ before_filter :require_login, only: [:edit, :update]
3
+
4
+ respond_to :html, :json
5
+
6
+ # Signup
7
+ def new
8
+ @user = User.new
9
+ end
10
+
11
+ def create
12
+ @user = User.new(user_create_params)
13
+ if @user.save
14
+ @user.send_confirmation
15
+ login(@user)
16
+ respond_to do |format|
17
+ format.json { head :no_content }
18
+ format.html { redirect_to root_path }
19
+ end
20
+ else
21
+ respond_to do |format|
22
+ format.json { render json: { status: 'error', errors: @user.errors }.to_json, status: 422 }
23
+ format.html { render :new }
24
+ end
25
+ end
26
+ end
27
+
28
+ def edit
29
+ @user = current_user
30
+ end
31
+
32
+ def update
33
+ @user = current_user
34
+
35
+ orig_confirmation_email = @user.confirmation_email
36
+
37
+ if @user.update_attributes(user_update_params)
38
+ # Send a new email confirmation if the user updated their email address
39
+ if @user.confirmation_email.present? &&
40
+ @user.confirmation_email != @user.email &&
41
+ @user.confirmation_email != orig_confirmation_email
42
+ @user.send_confirmation
43
+ end
44
+ respond_to do |format|
45
+ format.json { head :no_content }
46
+ format.html { redirect_to @user }
47
+ end
48
+ else
49
+ respond_to do |format|
50
+ format.json { render json: { status: 'error', errors: @user.errors }.to_json, status: 422 }
51
+ format.html { render :edit }
52
+ end
53
+ end
54
+ end
55
+
56
+ protected
57
+
58
+ # It would be nice to find a strategy to merge these. The only difference is that
59
+ # when signing up you are setting the email, and when changing your settings you
60
+ # are setting the confirmation email.
61
+
62
+ def user_create_params
63
+ params.require(:user).permit(
64
+ :email,
65
+ :username,
66
+ :password,
67
+ :password_confirmation,
68
+ :first_name,
69
+ :last_name,
70
+ :bio,
71
+ :website,
72
+ :phone_number,
73
+ :time_zone)
74
+ end
75
+
76
+ def user_update_params
77
+ params.require(:user).permit(
78
+ :confirmation_email,
79
+ :username,
80
+ :password,
81
+ :password_confirmation,
82
+ :first_name,
83
+ :last_name,
84
+ :bio,
85
+ :website,
86
+ :phone_number,
87
+ :time_zone)
88
+ end
89
+ end
@@ -0,0 +1,170 @@
1
+ require 'email_format_validator'
2
+
3
+ class User < ActiveRecord::Base
4
+ has_secure_password
5
+ has_one_time_password
6
+
7
+ # Uncomment if you are not using strong params (note, that email is only permitted on
8
+ # signup and confirmation_email is only permitted on update):
9
+ #
10
+ # attr_accessible :username,
11
+ # :email,
12
+ # :confirmation_email,
13
+ # :password,
14
+ # :password_confirmation,
15
+ # :time_zone,
16
+ # :first_name,
17
+ # :last_name,
18
+ # :bio,
19
+ # :website,
20
+ # :phone_number
21
+
22
+ before_validation :downcase_email
23
+ before_validation :set_confirmation_email
24
+
25
+ # Whenever the password is set, validate (not only on create)
26
+ validates :password, presence: true, confirmation: true, length: {minimum: 6}, if: :password_set?
27
+ validates :username, presence: true, uniqueness: {case_sensitive: false}
28
+ validates :email, email_format: true, presence: true, uniqueness: true
29
+ validates :confirmation_email, email_format: true, presence: true
30
+
31
+ # Confirm emails check for existing emails for uniqueness as a convenience
32
+ validate :confirmation_email_uniqueness, if: :confirmation_email_set?
33
+
34
+ def self.user_from_token(token)
35
+ verifier = ActiveSupport::MessageVerifier.new(Rails.application.config.secret_token)
36
+ id = verifier.verify(token)
37
+ User.find_by_id(id)
38
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
39
+ nil
40
+ end
41
+
42
+ # The tokens created by this method have unique indexes but they are digests of the
43
+ # id which is unique. Because of this we shouldn't see a conflict. If we do, however
44
+ # we want the ActiveRecord::StatementInvalid or ActiveRecord::RecordNotUnique exeception
45
+ # to bubble up.
46
+ def set_token(field)
47
+ return unless self.persisted?
48
+ verifier = ActiveSupport::MessageVerifier.new(Rails.application.config.secret_token)
49
+ self.send("#{field}_created_at=", Time.now)
50
+ self.send("#{field}=", verifier.generate(self.id))
51
+ self.save
52
+ end
53
+
54
+ # These methods are a little redundant, but give you the opportunity to
55
+ # insert expiry for any of these token based authentication strategies.
56
+ # For example:
57
+ #
58
+ # def self.user_from_remember_token(token)
59
+ # user = user_from_token(token)
60
+ # user = nil if user && user.remember_token_created_at < 30.days.ago
61
+ # user
62
+ # end
63
+ #
64
+ def self.user_from_remember_token(token)
65
+ user_from_token(token)
66
+ end
67
+
68
+ def self.user_from_reset_password_token(token)
69
+ user_from_token(token)
70
+ end
71
+
72
+ def self.user_from_confirmation_token(token)
73
+ user_from_token(token)
74
+ end
75
+
76
+ def self.user_from_unlock_token(token)
77
+ user_from_token(token)
78
+ end
79
+
80
+ def display_name
81
+ [first_name, last_name].compact.join(" ")
82
+ end
83
+
84
+ def track_sign_in(ip)
85
+ self.sign_in_count += 1
86
+ self.last_sign_in_at = self.current_sign_in_at
87
+ self.last_sign_in_ip = self.current_sign_in_ip
88
+ self.current_sign_in_at = Time.now
89
+ self.current_sign_in_ip = ip
90
+ self.save
91
+ end
92
+
93
+ def clear_remember_token
94
+ self.remember_token = nil
95
+ self.remember_token_created_at = nil
96
+ self.save
97
+ end
98
+
99
+ def send_reset_password
100
+ return false unless set_token(:reset_password_token)
101
+
102
+ # TODO: insert your mailer logic here
103
+ true
104
+ end
105
+
106
+ def send_confirmation
107
+ return false unless set_token(:confirmation_token)
108
+
109
+ # TODO: insert your mailer logic here
110
+ true
111
+ end
112
+
113
+ def email_confirmed
114
+ return false if self.confirmation_token.blank? || self.confirmation_email.blank?
115
+
116
+ self.email = self.confirmation_email
117
+
118
+ # Don't nil out the token unless the changes are valid as it may be
119
+ # needed again (when re-rendering the form, for instance)
120
+ if valid?
121
+ self.confirmation_token = nil
122
+ self.confirmation_token_created_at = nil
123
+ end
124
+
125
+ self.save
126
+ end
127
+
128
+ def change_password(password, password_confirmation)
129
+ self.password = password
130
+ self.password_confirmation = password_confirmation
131
+
132
+ # Don't nil out the token unless the changes are valid as it may be
133
+ # needed again (when re-rendering the form, for instance)
134
+ if valid?
135
+ self.reset_password_token = nil
136
+ self.reset_password_token_created_at = nil
137
+ end
138
+
139
+ self.save
140
+ end
141
+
142
+ protected
143
+
144
+ def password_set?
145
+ self.password.present?
146
+ end
147
+
148
+ def downcase_email
149
+ self.email = self.email.downcase if self.email
150
+ end
151
+
152
+ def set_confirmation_email
153
+ self.confirmation_email = self.email if self.confirmation_email.blank?
154
+ end
155
+
156
+ def confirmation_email_set?
157
+ confirmation_email.present? && confirmation_email_changed? && confirmation_email != email
158
+ end
159
+
160
+ # It is possible that a user will change their email, not confirm, and then
161
+ # sign up for the service again using the same email. If they later go to confirm
162
+ # the email change on the first account it will fail because the email will be
163
+ # used by the new signup. Though this is problematic it avoids the larger problem of
164
+ # users blocking new user signups by changing their email address to something they
165
+ # don't control. This check is just for convenience and does not need to
166
+ # guarantee uniqueness.
167
+ def confirmation_email_uniqueness
168
+ errors.add(:confirmation_email, :taken, value: email) if User.where('email = ?', confirmation_email).count > 0
169
+ end
170
+ end