├── .codeclimate.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .ruby-version ├── .travis.yml ├── Appraisals ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── app ├── controllers │ └── authenticate │ │ ├── authenticate_controller.rb │ │ ├── passwords_controller.rb │ │ ├── sessions_controller.rb │ │ └── users_controller.rb ├── mailers │ └── authenticate_mailer.rb └── views │ ├── authenticate_mailer │ ├── change_password.html.erb │ └── change_password.text.erb │ ├── layouts │ └── application.html.erb │ ├── passwords │ ├── edit.html.erb │ └── new.html.erb │ ├── sessions │ └── new.html.erb │ └── users │ └── new.html.erb ├── authenticate.gemspec ├── bin ├── rails └── setup ├── config ├── locales │ └── authenticate.en.yml └── routes.rb ├── gemfiles ├── 4.2.gemfile ├── 5.0.gemfile ├── 5.1.gemfile └── 5.2.gemfile ├── lib ├── authenticate.rb ├── authenticate │ ├── callbacks │ │ ├── authenticatable.rb │ │ ├── brute_force.rb │ │ ├── lifetimed.rb │ │ ├── timeoutable.rb │ │ └── trackable.rb │ ├── configuration.rb │ ├── controller.rb │ ├── crypto │ │ └── bcrypt.rb │ ├── debug.rb │ ├── engine.rb │ ├── lifecycle.rb │ ├── login_status.rb │ ├── model │ │ ├── brute_force.rb │ │ ├── db_password.rb │ │ ├── email.rb │ │ ├── lifetimed.rb │ │ ├── password_reset.rb │ │ ├── timeoutable.rb │ │ ├── trackable.rb │ │ └── username.rb │ ├── modules.rb │ ├── session.rb │ ├── testing │ │ ├── controller_helpers.rb │ │ ├── integration_tests_sign_on.rb │ │ ├── rspec.rb │ │ ├── test_unit.rb │ │ └── view_helpers.rb │ ├── token.rb │ ├── user.rb │ └── version.rb ├── generators │ └── authenticate │ │ ├── controllers │ │ ├── USAGE │ │ └── controllers_generator.rb │ │ ├── helpers.rb │ │ ├── install │ │ ├── USAGE │ │ ├── install_generator.rb │ │ └── templates │ │ │ ├── authenticate.rb │ │ │ ├── db │ │ │ └── migrate │ │ │ │ ├── add_authenticate_brute_force_to_users.rb │ │ │ │ ├── add_authenticate_password_reset_to_users.rb │ │ │ │ ├── add_authenticate_timeoutable_to_users.rb │ │ │ │ ├── add_authenticate_to_users.rb │ │ │ │ └── create_users.rb │ │ │ └── user.rb.erb │ │ ├── routes │ │ ├── USAGE │ │ ├── routes_generator.rb │ │ └── templates │ │ │ └── routes.rb │ │ └── views │ │ ├── USAGE │ │ └── views_generator.rb └── tasks │ └── authenticate_tasks.rake └── spec ├── controllers ├── deprecated_controller_methods_spec.rb └── secured_controller_spec.rb ├── dummy ├── README.rdoc ├── Rakefile ├── app │ ├── assets │ │ ├── images │ │ │ └── .keep │ │ ├── javascripts │ │ │ └── application.js │ │ └── stylesheets │ │ │ └── application.css │ ├── controllers │ │ ├── application_controller.rb │ │ ├── concerns │ │ │ └── .keep │ │ └── welcome_controller.rb │ ├── helpers │ │ └── application_helper.rb │ ├── mailers │ │ └── .keep │ ├── models │ │ ├── .keep │ │ ├── concerns │ │ │ └── .keep │ │ └── user.rb │ └── views │ │ ├── layouts │ │ └── application.html.erb │ │ └── welcome │ │ └── index.html.erb ├── bin │ ├── bundle │ ├── rails │ ├── rake │ └── setup ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── assets.rb │ │ ├── authenticate.rb │ │ ├── backtrace_silencers.rb │ │ ├── cookies_serializer.rb │ │ ├── filter_parameter_logging.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ ├── session_store.rb │ │ └── wrap_parameters.rb │ ├── locales │ │ └── en.yml │ ├── routes.rb │ └── secrets.yml ├── db │ └── schema.rb ├── lib │ └── assets │ │ └── .keep ├── log │ └── .keep └── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ └── favicon.ico ├── factories └── users.rb ├── features ├── brute_force_spec.rb ├── create_user_spec.rb ├── max_session_lifetime_spec.rb ├── new_user_form_spec.rb ├── password_reset_spec.rb ├── password_update_spec.rb ├── sign_in_spec.rb ├── sign_out_spec.rb ├── sign_up_spec.rb └── timeoutable_spec.rb ├── model ├── brute_force_spec.rb ├── configuration_spec.rb ├── db_password_spec.rb ├── email_spec.rb ├── lifetimed_spec.rb ├── modules_spec.rb ├── password_reset_spec.rb ├── session_spec.rb ├── timeoutable_spec.rb ├── token_spec.rb ├── trackable_spec.rb └── user_spec.rb ├── orm └── active_record.rb ├── requests ├── csrf_rotation_spec.rb └── session_key_spec.rb ├── spec_helper.rb └── support ├── controllers └── controller_helpers.rb ├── features └── feature_helpers.rb ├── mailer.rb └── request_helpers.rb /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | brakeman: 4 | enabled: true 5 | bundler-audit: 6 | enabled: true 7 | duplication: 8 | enabled: true 9 | exclude_fingerprints: 10 | - 120bdc2114c46391e7ad73ea4a10b314 11 | - 339cd5bbb9922a29a82aaf5f3d727deb 12 | config: 13 | languages: 14 | - ruby 15 | - javascript 16 | - python 17 | - php 18 | fixme: 19 | enabled: true 20 | rubocop: 21 | enabled: true 22 | ratings: 23 | paths: 24 | - Gemfile.lock 25 | - "**.erb" 26 | - "**.haml" 27 | - "**.rb" 28 | - "**.rhtml" 29 | - "**.slim" 30 | - "**.inc" 31 | - "**.js" 32 | - "**.jsx" 33 | - "**.module" 34 | - "**.php" 35 | - "**.py" 36 | exclude_paths: 37 | - config/ 38 | - spec/ 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | authenticate-*.gem 3 | log/*.log 4 | pkg/ 5 | spec/dummy/db/*.sqlite3 6 | spec/dummy/db/*.sqlite3-journal 7 | spec/dummy/log/*.log 8 | spec/dummy/tmp/ 9 | spec/dummy/log/test.log 10 | spec/dummy/log/development.log 11 | /.idea 12 | Gemfile.lock 13 | *.gemfile.lock 14 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Exclude: 3 | - 'bin/rails' 4 | - 'spec/dummy/**/*' 5 | - 'lib/generators/authenticate/install/templates/db/migrate/**/*' 6 | - 'lib/generators/authenticate/routes/templates/routes.rb' 7 | 8 | Metrics/LineLength: 9 | Max: 120 10 | 11 | Documentation: 12 | Exclude: 13 | - 'lib/generators/**/*' 14 | 15 | Metrics/MethodLength: 16 | Max: 16 17 | 18 | Metrics/ClassLength: 19 | Exclude: 20 | - 'lib/generators/authenticate/install/install_generator.rb' 21 | 22 | Metrics/AbcSize: 23 | Max: 18 24 | 25 | Lint/IneffectiveAccessModifier: 26 | Exclude: 27 | - 'lib/generators/authenticate/**/*' 28 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.4.1 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | cache: bundler 2 | 3 | language: 4 | - ruby 5 | 6 | rvm: 7 | - 2.1.8 8 | - 2.2.4 9 | - 2.3.3 10 | - 2.4.1 11 | 12 | gemfile: 13 | - gemfiles/4.2.gemfile 14 | - gemfiles/5.0.gemfile 15 | - gemfiles/5.1.gemfile 16 | - gemfiles/5.2.gemfile 17 | 18 | 19 | matrix: 20 | exclude: 21 | - rvm: 2.1.8 22 | gemfile: gemfiles/5.0.gemfile 23 | - rvm: 2.1.8 24 | gemfile: gemfiles/5.1.gemfile 25 | - rvm: 2.1.8 26 | gemfile: gemfiles/5.2.gemfile 27 | - rvm: 2.4.1 28 | gemfile: gemfiles/4.2.gemfile 29 | 30 | install: 31 | - "bin/setup" 32 | 33 | sudo: false 34 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | if RUBY_VERSION < "2.4.0" 2 | appraise "4.2" do 3 | gem "rails", "~> 4.2.0" 4 | end 5 | end 6 | 7 | if RUBY_VERSION >= "2.2.2" 8 | appraise "5.0" do 9 | gem "rails", "~> 5.0.0" 10 | end 11 | 12 | appraise "5.1" do 13 | gem "rails", "~> 5.1" 14 | end 15 | 16 | appraise "5.2" do 17 | gem "rails", "~> 5.2" 18 | end 19 | end 20 | 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Authenticate Changelog 2 | 3 | 4 | ## [0.7.3] - June 7, 2018 5 | 6 | ### Support for rails 5.2 7 | - added rails 5.2 support to gemspec 8 | - added rails 5.2 to Appraisals, .travis.yml, gemfiles 9 | - added `sqlite3.represent_boolean_as_integer = true` to dummy application config 10 | - bumped authenticate version 11 | - update request specs, looks for 302 after login 12 | - added build and release tasks to Rakefile 13 | 14 | [0.7.3]: https://github.com/tomichj/authenticate/compare/v0.7.2...v0.7.3 15 | 16 | 17 | ## [0.7.2] - June 22, 2017 18 | 19 | ### API change 20 | - removed new_users_path, sign_up_path remains 21 | 22 | ### New Feature 23 | - added allow_sign_up flag to install generator 24 | 25 | [0.7.2]: https://github.com/tomichj/authenticate/compare/v0.7.1...v0.7.2 26 | 27 | 28 | ## [0.7.1] - June 22, 2017 29 | 30 | ### Fixed 31 | - routes respects config.allow_sign_up? and shuts off /sign_up 32 | - removed spurious output from install generator 33 | 34 | [0.7.1]: https://github.com/tomichj/authenticate/compare/v0.7.0...v0.7.1 35 | 36 | 37 | ## [0.7.0] - May 25, 2017 38 | 39 | ### API Changes 40 | - controller#require_authentication is deprecated, use controller#require_login 41 | - controller#authenticated? is deprecated, use controller#logged_in? 42 | - added controller#logged_out? 43 | `authenticated?` and `required_authentication` will be removed in a future release. 44 | 45 | ### Test support 46 | - Added login_as via middleware for feature/integration/system tests. 47 | - added rspec helpers for view and controller tests 48 | - added test-unit helpers for controller/view tests 49 | 50 | ### Internal changes - will not affect normal apps 51 | - Session#initialize(request, cookies) is now Session#initialize(request) 52 | - Session API changes, #authenticated? renamed #logged_in? 53 | 54 | [0.7.0]: https://github.com/tomichj/authenticate/compare/v0.6.1...v0.7.0 55 | 56 | 57 | ## [0.6.1] - May 16, 2017 58 | 59 | ### Fixed 60 | - install migrations now correctly support rails 4.2.x and rails 5.x. 61 | 62 | [0.6.1]: https://github.com/tomichj/authenticate/compare/v0.6.0...v0.6.1 63 | 64 | 65 | 66 | ## [0.6.0] - May 16, 2017 67 | 68 | ### Security 69 | - Prevent [password reset token leakage] through HTTP referrer across domains. password#edit removes the password 70 | reset token from the url, sets it into the user's session (typically a cookie), and redirects to password#url 71 | without the token in the url. 72 | 73 | - Prevent [session fixation] attacks by rotating CSRF tokens on sign-in by setting 74 | `Authentication.configuration.rotate_csrf_on_sign_in` to `true`. This is recommended for 75 | all applications. The setting defaults to `false` in this release, but will default to `true` 76 | in a future release. 77 | 78 | ### Fixed 79 | - Location to return to after login is now written to session. Was previously written explicitly to a cookie. 80 | - Most controller tests rewritten as feature and request tests. 81 | 82 | [password reset token leakage]: https://security.stackexchange.com/questions/69074/how-to-implement-password-reset-functionality-without-becoming-susceptible-to-cr 83 | [session fixation]: http://guides.rubyonrails.org/security.html#session-fixation 84 | [0.6.0]: https://github.com/tomichj/authenticate/compare/v0.5.0...v0.6.0 85 | 86 | 87 | 88 | ## [0.5.0] - March 26, 2017oh 89 | 90 | ### Support for rails 5.1. 91 | 92 | [0.5.0]: https://github.com/tomichj/authenticate/compare/v0.4.0...v0.5.0 93 | 94 | 95 | 96 | ## [0.4.0] - June 2, 2016 97 | 98 | ### Fixed 99 | - Install generator User: ActiveRecord::Base for Rails 4 apps, ApplicationRecord for rails 5 (issue #2). 100 | 101 | [0.4.0]: https://github.com/tomichj/authenticate/compare/v0.3.3...v0.4.0 102 | 103 | 104 | 105 | ## [0.3.3] - April 29, 2016 106 | 107 | - Password change uses active record's dirty bit to detect that password was updated. 108 | - password_updated attribute removed. 109 | - spec_helper now calls ActiveRecord::Migration.maintain_test_schema! (or check_pending!) to handle dummy test db. 110 | - Added CodeClimate config. 111 | 112 | [0.3.3]: https://github.com/tomichj/authenticate/compare/v0.3.2...v0.3.3 113 | 114 | 115 | 116 | ## [0.3.2] - April 28, 2016 117 | 118 | - Error now raised if User model is missing required attributes. 119 | - All code now conforms to a rubocode profile. 120 | 121 | [0.3.2]: https://github.com/tomichj/authenticate/compare/v0.3.1...v0.3.2 122 | 123 | 124 | 125 | ## [0.3.1] - March 10, 2016 126 | 127 | - User controller now allows arbitrary parameters without having to explicitly declare 128 | them. Still requires email and password. 129 | - Mailer now checks for mail.respond_to?(:deliver_later) rather than rails version, to decide deliver vs deliver_later. 130 | - Removed unused user_id_parameter config method. 131 | 132 | [0.3.1]: https://github.com/tomichj/authenticate/compare/v0.3.0...v0.3.1 133 | 134 | 135 | 136 | ## [0.3.0] - February 24, 2016 137 | 138 | - Moved normalize_email and find_normalized_email methods to base User module. 139 | - Added full suite of controller and feature tests. 140 | 141 | ### Fixes 142 | - failed login count fix was off by one. 143 | - password validation now done only in correct circumstances 144 | 145 | [0.3.0]: https://github.com/tomichj/authenticate/compare/v0.2.2...v0.3.0 146 | 147 | 148 | 149 | ## [0.2.3] - February 13, 2016 150 | 151 | - Small bugfix for :username authentication. 152 | - Improved documentation, started adding wiki pages. 153 | 154 | [0.2.3]: https://github.com/tomichj/authenticate/compare/v0.2.2...v0.2.3 155 | 156 | 157 | 158 | ## [0.2.2] - February 9, 2016 159 | 160 | - Password length range requirements added, defaults to 8..128. 161 | - Generators and app now respect model class more completely, including in routes. 162 | 163 | [0.2.2]: https://github.com/tomichj/authenticate/compare/v0.2.1...v0.2.2 164 | 165 | 166 | 167 | ## [0.2.1] - February 9, 2016 168 | 169 | - Fixed potential password_reset nil pointer. 170 | - Continued adding I18n support. 171 | - Minor documentation improvements. 172 | 173 | [0.2.1]: https://github.com/tomichj/authenticate/compare/v0.2.0...v0.2.1 174 | 175 | 176 | 177 | ## [0.2.0] - February 2, 2016 178 | 179 | - Added app/ including controllers, views, routes, mailers. 180 | 181 | [0.2.0]: https://github.com/tomichj/authenticate/compare/v0.1.0...v0.2.0 182 | 183 | 184 | 185 | ## 0.1.0 - January 23, 2016 186 | 187 | - Initial Release, barely functioning 188 | 189 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | I love pull requests. I'm trying to keep it as easy as possible to contribute changes. There 4 | are just a couple of guidelines to follow to help me stay on top of things. 5 | 6 | 7 | ## Let's talk 8 | 9 | Whether you're fixing a bug or adding a feature, feel free to talk to me first on 10 | [twitter](https://twitter.com/JustinTomich). We can make sure the change isn't already 11 | underway somewhere else. 12 | 13 | 14 | ## Getting started 15 | 16 | * Make sure you have a [GitHub account](https://github.com/signup/free) 17 | * Open a [New Issue](https://github.com/tomichj/authenticate/issues) on github for your change, 18 | assuming one does not already exist. If one already exists, join the conversation. 19 | * Fork the repository on GitHub. 20 | 21 | ## Setup 22 | 23 | Clone the repo: 24 | 25 | `git clone https://github.com//authenticate` 26 | 27 | CD into your clone and run bundler install: 28 | 29 | `cd authenticate && bundle install` 30 | 31 | Make sure the tests pass: 32 | 33 | `rake` 34 | 35 | Make your change. Add tests for your change. Make sure the tests pass: 36 | 37 | `rake` 38 | 39 | I use `rubocop` to maintain ruby coding style. Install and run it like so: 40 | 41 | ```sh 42 | gem install rubocop 43 | rubocop 44 | ``` 45 | 46 | Once you resolve any issues rubocop finds, you're ready to go. Push your fork and 47 | [submit a pull request](https://github.com/tomichj/authenticate/compare/). 48 | 49 | The ball is now in my court. I'll try to comment on your pull request within a couple of business days 50 | (hopefully the same day). 51 | 52 | Things you can do to increase the speed of acceptance: 53 | 54 | * talk to me ahead of time 55 | * write tests 56 | * follow the [ruby style guide](https://github.com/bbatsov/ruby-style-guide) 57 | * write a good [commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 58 | 59 | Thanks very much! 60 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | # gem 'capybara' 6 | 7 | # Declare your gem's dependencies in authenticate.gemspec. 8 | # Bundler will treat runtime dependencies like base dependencies, and 9 | # development dependencies will be added by default to the :development group. 10 | 11 | # Declare any dependencies that are still in development here instead of in 12 | # your gemspec. These might include edge Rails or gems from your path or 13 | # Git. Remember to move these dependencies to your gemspec before releasing 14 | # your gem to rubygems.org. 15 | 16 | # To use a debugger 17 | # gem 'byebug', group: [:development, :test] 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Justin Tomich 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. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Authenticate 2 | 3 | A Rails authentication gem. 4 | 5 | Authenticate is small, simple, but extensible and comprehensive. Authenticate comes out of the box with opinionated 6 | defaults but is open to complete modification. 7 | 8 | Authenticate is inspired by, and draws both concepts and code from: 9 | Devise, Warden, Authlogic, Clearance, Sorcery, and restful_authentication. 10 | 11 | Please use [GitHub Issues] to report bugs. You can contact me directly on twitter 12 | [@JustinTomich](https://twitter.com/justintomich). 13 | 14 | [GitHub Issues]: https://github.com/tomichj/authenticate/issues 15 | 16 | [![Gem Version](https://badge.fury.io/rb/authenticate.svg)](https://badge.fury.io/rb/authenticate) ![Build status](https://travis-ci.org/tomichj/authenticate.svg?branch=master) ![Code Climate](https://codeclimate.com/github/tomichj/authenticate/badges/gpa.svg) 17 | 18 | 19 | ## Philosophy 20 | 21 | * simple - Authenticate's code is straightforward and easy to read. No middleware! 22 | * opinionated - set the "right" defaults, but let you control almost everything if you want 23 | * small footprint - as few public methods and modules as possible. Methods only loaded into your user model if needed. 24 | * configuration driven - almost all configuration is performed in the initializer 25 | 26 | 27 | ### What's different about Authenticate? 28 | 29 | Authenticate provides rails authentication with email & password. Authenticate only works with Rails, and only 30 | with active record; this keeps it simple. There's no middleware, and no compromises or added complexity to 31 | support other ORMs. 32 | 33 | Authenticate uses a modular callback mechanism similar to Warden, but much simpler. A lot of 34 | functionality is provided: there are modules to detect brute force attacks, enforce maximum session 35 | lifetimes, session timeouts, track logins, etc. 36 | 37 | 38 | ## Implementation Overview 39 | 40 | Authenticate: 41 | * loads modules into your user model to provide authentication functionality 42 | * loads `callbacks` that are triggered during authentication and access events. All authentication 43 | decisions are performed in callbacks, e.g. do you have a valid session, has your session timed out, etc. 44 | * loads a module into your controllers (typically `ApplicationController`) to secure controller actions 45 | 46 | The callback architecture is based on the system used by devise and warden, but significantly simplified. 47 | 48 | 49 | ### Session Token 50 | 51 | Authenticate generates a token (called a 'session token') to identify the user from a saved cookie. 52 | When a user authenticates successfully, Authenticate generates and stores a 'session token' for your user in 53 | your database. The session token is also stored in a cookie in the user's browser. 54 | The cookie is then presented upon each subsequent access attempt to your server. 55 | 56 | 57 | ## Install 58 | 59 | To get started, add Authenticate to your `Gemfile` and run `bundle install` to install it: 60 | 61 | ```ruby 62 | gem 'authenticate' 63 | ``` 64 | 65 | Then run the authenticate install generator: 66 | 67 | ```sh 68 | rails generate authenticate:install 69 | ``` 70 | 71 | The generator does the following: 72 | 73 | * Insert `include Authenticate::User` into your `User` model. If you don't have a User model, one is created. 74 | * Insert `include Authenticate::Controller` into your `ApplicationController` 75 | * Add an initializer at `config/initializers/authenticate.rb`. 76 | * Create migrations to create a users table or add columns to your existing table. 77 | 78 | 79 | You'll need to run the migrations that Authenticate just generated: 80 | 81 | ```sh 82 | rake db:migrate 83 | ``` 84 | 85 | Finally, you need to secure any controllers that require authentication by adding 86 | `before_action :require_login`. If your entire app requires authentication, add it to 87 | `ApplicationController`: 88 | 89 | ```ruby 90 | # app/controllers/application_controller.rb 91 | class ApplicationController < ActionController::Base 92 | include Authenticate::Controller 93 | before_action :require_login 94 | protect_from_forgery with: :exception 95 | end 96 | ``` 97 | 98 | 99 | ## Configure 100 | 101 | Override any of these defaults in your application `config/initializers/authenticate.rb`. 102 | 103 | ```ruby 104 | Authenticate.configure do |config| 105 | config.user_model = 'User' 106 | config.cookie_name = 'authenticate_session_token' 107 | config.cookie_expiration = { 1.year.from_now.utc } 108 | config.cookie_domain = nil 109 | config.cookie_path = '/' 110 | config.secure_cookie = false 111 | config.cookie_http_only = false 112 | config.mailer_sender = 'reply@example.com' 113 | config.crypto_provider = Bcrypt 114 | config.timeout_in = nil 115 | config.max_session_lifetime = nil 116 | config.max_consecutive_bad_logins_allowed = nil 117 | config.bad_login_lockout_period = nil 118 | config.password_length = 8..128 119 | config.authentication_strategy = :email 120 | config.redirect_url = '/' 121 | config.allow_sign_up = true 122 | config.routes = true 123 | config.reset_password_within = 2.days 124 | end 125 | ``` 126 | 127 | Configuration parameters are described in detail here: [Configuration](lib/authenticate/configuration.rb) 128 | 129 | 130 | ## Use 131 | 132 | ### Access Control 133 | 134 | Use the `require_login` filter to control access to controller actions. To control access to 135 | all controller actions, add the filter to your `ApplicationController`, e.g.: 136 | 137 | ```ruby 138 | class ApplicationController < ActionController::Base 139 | before_action :require_login 140 | end 141 | ``` 142 | 143 | 144 | ### Authentication 145 | 146 | Authenticate provides a session controller and views to authenticate users with an email and password. 147 | After successful authentication, the user is redirected to the path they attempted to access, 148 | or as specified by the `redirect_url` property in your configuration. This defaults to '/' but can customized: 149 | 150 | ```ruby 151 | Authenticate.configure do |config| 152 | config.redirect_url = '/specials' 153 | end 154 | ``` 155 | 156 | 157 | ### Helpers 158 | 159 | Use `current_user`, `logged_in?`, and `logged_out?` in controllers, views, and helpers. 160 | 161 | Example: 162 | 163 | ```erb 164 | <% if logged_in? %> 165 | <%= current_user.email %> 166 | <%= link_to "Sign out", sign_out_path %> 167 | <% else %> 168 | <%= link_to "Sign in", sign_in_path %> 169 | <% end %> 170 | ``` 171 | 172 | 173 | ### Logout 174 | 175 | Log the user out. The user session_token will be deleted from the database, and the session cookie will 176 | be deleted from the user's browser session. 177 | 178 | ```ruby 179 | # in session controller... 180 | def destroy 181 | logout 182 | redirect_to '/', notice: 'You logged out successfully' 183 | end 184 | ``` 185 | 186 | 187 | ### Password Resets 188 | 189 | Authenticate provides password reset controllers and views. When a user requests a password reset, Authenticate 190 | delivers an email to that user. Change your `mailer_sender`, which is used in the email's "from" header: 191 | 192 | ```ruby 193 | Authenticate.configure do |config| 194 | config.mailer_sender = 'reply@example.com' 195 | end 196 | ``` 197 | 198 | 199 | ## Overriding Authenticate 200 | 201 | ### User Model 202 | 203 | Authenticate assumes your user model is a class named User, but you can 204 | [specify any user model class](https://github.com/tomichj/authenticate/wiki/custom-user-model). 205 | 206 | 207 | ### Username Authentication 208 | 209 | Authenticate uses email and password to login users. You 210 | can also [authenticate with username](https://github.com/tomichj/authenticate/wiki/Authenticate-with-username). 211 | 212 | 213 | ### Routes 214 | 215 | Authenticate adds routes to your application. See [config/routes.rb](/config/routes.rb) for the default routes. 216 | 217 | If you want to control and customize the routes, you can turn off the built-in routes in 218 | the Authenticate configuration with `config.routes = false` and dump a copy of the default routes into your 219 | application for modification. 220 | 221 | To turn off Authenticate's built-in routes: 222 | 223 | ```ruby 224 | # config/initializers/authenticate.rb 225 | Authenticate.configure do |config| 226 | config.routes = false 227 | end 228 | ``` 229 | 230 | You can run a generator to dump a copy of the default routes into your application for modification. The generator 231 | will also switch off the routes as shown immediately above by setting `config.routes = false`. 232 | 233 | ```sh 234 | $ rails generate authenticate:routes 235 | ``` 236 | 237 | 238 | ### Controllers 239 | 240 | If the customization at the views level is not enough, you can customize each controller, and the 241 | authenticate mailer. See [app/controllers](/app/controllers) for the default controllers, and 242 | [app/mailers](/app/mailers) for the default mailer. 243 | 244 | To override an authenticate controller, subclass an authenticate controller and update your routes to point to it. 245 | 246 | For example, to customize `Authenticate::SessionController`: 247 | 248 | * subclass the controller: 249 | 250 | ```ruby 251 | # app/controllers/sessions_controller.rb 252 | class SessionsController < Authenticate::SessionController 253 | # render sign in screen 254 | def new 255 | # ... 256 | end 257 | end 258 | ``` 259 | 260 | * update your routes to use your new controller. 261 | 262 | Start by dumping a copy of authenticate routes to your `config/routes.rb`: 263 | 264 | ```sh 265 | $ rails generate authenticate:routes 266 | ``` 267 | 268 | Now update your routes to point to your new controller: 269 | 270 | ```ruby 271 | # config/routes.rb 272 | resource :sessions, controller: 'sessions', only: [:create, :new, :destroy] 273 | ``` 274 | 275 | You can also use the Authenticate controller generator to copy the default controllers and mailer into 276 | your application: 277 | 278 | ```sh 279 | $ rails generate authenticate:controllers 280 | ``` 281 | 282 | 283 | ### Views 284 | 285 | You can quickly get started with a rails application using the built-in views. See [app/views](/app/views) for 286 | the default views. When you want to customize an Authenticate view, create your own copy of it in your app. 287 | 288 | You can use the Authenticate view generator to copy the default views into your application: 289 | 290 | ```sh 291 | $ rails generate authenticate:views 292 | ``` 293 | 294 | 295 | ### Layout 296 | 297 | Authenticate uses your application's default layout. If you would like to change the layout Authenticate uses when 298 | rendering views, you can either deploy copies of the controllers and customize them, or you can specify 299 | the layout in an initializer. This should be done in a to_prepare callback in `config/application.rb` 300 | because it's executed once in production and before each request in development. 301 | 302 | You can specify the layout per-controller: 303 | 304 | ```ruby 305 | config.to_prepare do 306 | Authenticate::PasswordsController.layout 'my_passwords_layout' 307 | Authenticate::SessionsController.layout 'my_sessions_layout' 308 | Authenticate::UsersController.layout 'my_users_layout' 309 | end 310 | ``` 311 | 312 | 313 | ### Translations 314 | 315 | All flash messages and email lines are stored in i18n translations. You can override them like any other translation. 316 | 317 | See [config/locales/authenticate.en.yml](/config/locales/authenticate.en.yml) for the default messages. 318 | 319 | 320 | ## Extending Authenticate 321 | 322 | Authenticate can be extended via two mechanisms: 323 | 324 | * user modules: add behavior to the user model 325 | * callbacks: add rules or behavior during various authentication events, such as login and subsequent hits 326 | 327 | Most of authenticate's behavior is implemented with a user module and a corresponding callback. User modules add 328 | behavior to the user, and the callback uses the user model data to decide an authentication attempt is valid or 329 | invalid. 330 | 331 | 332 | ### User Modules 333 | 334 | Add behavior to your User model for your callbacks to use. You can include behavior yourself directly 335 | in your User class, but you can also use the Authenticate module loading system. 336 | 337 | To add a custom module for Authenticate to load into your User model, e.g. `MyUserModule`: 338 | 339 | ```ruby 340 | Authenticate.configuration do |config| 341 | config.modules = [MyUserModule] 342 | end 343 | ``` 344 | 345 | 346 | ### Callbacks 347 | 348 | Callbacks can be added to Authenticate. Callbacks available at these points of the authenticate lifecycle: 349 | 350 | - `Authenticate.lifecycle.after_set_user` 351 | Runs with every hit requiring authentication. This includes both the initial authentication process and 352 | subsequent to any controller secured by Authenticate. These callbacks run immediately after the User is determined. 353 | 354 | - `Authenticate.lifecycle.after_authentication` 355 | These callbacks run only during the initial authentication process. 356 | 357 | See [Lifecycle](lib/authenticate/lifecycle.rb) for full details. 358 | 359 | Callbacks must `throw(:failure, message)` to signal an authentication/authorization failure. Callbacks can also perform 360 | other actions on the user or session. Callbacks are invoked with `|user, session, options|`. 361 | 362 | Here's a simple example that counts logins for users. It consists of a module for User implemented as an 363 | `ActiveSupport::Concern`, with a callback that is defined in an `included` block. The module and callback 364 | is added to the User module via the Authenticate configuration. 365 | 366 | ```ruby 367 | # app/models/concerns/login_count.rb 368 | module LoginCount 369 | extend ActiveSupport::Concern 370 | 371 | included do 372 | # Add a callback that is triggered after every authentication 373 | Authenticate.lifecycle.after_authentication name:'login counter' do |user, session, options| 374 | user.count_login if user 375 | end 376 | end 377 | 378 | def count_login 379 | self.login_count ||= 0 380 | self.login_count += 1 381 | end 382 | end 383 | 384 | # config/initializers/authenticate.rb 385 | Authenticate.configuration do |config| 386 | config.modules = [LoginCount] 387 | end 388 | ``` 389 | 390 | More complex callbacks and modules can be implemented in a separate file(s); in that case, 391 | the user module should `require` the callback file to inject it into Authenticate's callback lifecycle. 392 | 393 | 394 | ## Testing 395 | 396 | ### Feature/Integration/System Tests 397 | 398 | Authenticate includes middleware which allows tests to directly sign a test user in, 399 | eliminating the need to visit and submit the sign on form. This can significantly speeds up tests. 400 | Used by integration, system, feature, etc tests. 401 | 402 | Configure your test environment to enable the middleware: 403 | ```ruby 404 | # config/environments/test.rb 405 | MyRailsApp::Application.configure do 406 | # ... 407 | config.middleware.use Authenticate::Testing::IntegrationTestsSignOn 408 | # ... 409 | end 410 | ``` 411 | 412 | Sign a test user in by passing as=USER_ID in a query parameter: 413 | ```ruby 414 | visit root_path(as: user) 415 | ``` 416 | 417 | A feature spec using factory_girl and capybara with the integration sign on middleware might look like this: 418 | ```ruby 419 | require 'spec_helper' 420 | 421 | feature 'dashboard' do 422 | scenario 'logged in user has name on dashboard' do 423 | user = create(:user) 424 | visit dashboard_path(as: user) 425 | expect(page).to have_content user.name 426 | end 427 | end 428 | ``` 429 | 430 | 431 | ### Controller Tests 432 | 433 | To test controller actions protected by authenticate with `before_action :require_login`, you can 434 | use Authenticate's test helpers. 435 | 436 | For `rspec`, add the following to your `spec/spec_helper.rb` or `spec/rails_helper.rb`: 437 | 438 | ```ruby 439 | require 'authenticate/testing/rspec' 440 | ``` 441 | 442 | For `test-unit`, add the following to your `test/test_helper.rb`. 443 | 444 | ```ruby 445 | require 'authenticate/testing/test_unit' 446 | ``` 447 | 448 | This will give you helper methods: 449 | 450 | ```ruby 451 | login_as(user) 452 | logout 453 | ``` 454 | 455 | Once you `login_as(user)`, you will satisfy the `require_login` filter. The other `Authenticate::Controller` 456 | methods will then work: `current_user`, `logged_in?`, `logged_out?` 457 | 458 | A controller spec using `factory_girl` and authenticate's controller helpers might look like this: 459 | ```ruby 460 | require 'spec_helper' 461 | describe DashboardsController do 462 | describe '#show' do 463 | it 'shows view' do 464 | user = create(:user) 465 | login_as(user) 466 | get :show 467 | expect(response).to be_success 468 | expect(response).to render_template 'dashboards/show' 469 | end 470 | end 471 | end 472 | ``` 473 | 474 | Rails 5 built-in test suite's controller tests now extend `ActionDispatch::IntegrationTest`. Use the middleware 475 | `IntegrationTestsSignOn` to support sign on. For example: 476 | ```ruby 477 | require 'test_helper' 478 | class DashboardsControllerTest < ActionDispatch::IntegrationTest 479 | test 'logged in user can GET a dashboard' do 480 | user = create(:user) 481 | get dashboards_show_path(as: user) 482 | assert_response :success 483 | end 484 | end 485 | ``` 486 | 487 | 488 | ### View Tests 489 | 490 | For `rspec`, require `authenticate/testing/rspec` to include view helpers: 491 | 492 | ```ruby 493 | login_as(user) 494 | current_user 495 | logged_in? 496 | logged_out? 497 | ``` 498 | 499 | Once you `login_as(user)`, your view can make use of the other helpers as you'd expect. 500 | 501 | An example view spec using `factory_girl` and authenticate's view helpers: 502 | ```ruby 503 | require 'spec_helper' 504 | describe 'dashboards/show', type: :view do 505 | it 'displays user name' do 506 | user = create(:user) 507 | login_as(user) 508 | render 509 | expect(rendered).to match user.name # view uses `current_user` 510 | end 511 | end 512 | ``` 513 | 514 | 515 | ## Additional Documentation 516 | 517 | Consult the [Authenticate wiki](https://github.com/tomichj/authenticate/wiki/) for additional documentation. 518 | 519 | 520 | ## Versions of Rails Supported 521 | 522 | Authenticate is tested with rails 4.2, 5.0, and 5.1. 523 | 524 | 525 | ## Changelog 526 | 527 | For a summary of changes by version, see the [CHANGELOG.md](/CHANGELOG.md). 528 | 529 | 530 | ## License 531 | 532 | Authenticate is copyright © 2015 Justin Tomich. It is free software, and may be 533 | redistributed under the terms specified in the [`LICENSE`] file. 534 | 535 | [`LICENSE`]: /LICENSE 536 | 537 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'bundler/gem_tasks' 4 | require 'appraisal' 5 | 6 | APP_RAKEFILE = File.expand_path('../spec/dummy/Rakefile', __FILE__) 7 | load 'rails/tasks/engine.rake' 8 | require 'rspec/core/rake_task' 9 | 10 | namespace :dummy do 11 | require_relative "spec/dummy/config/application" 12 | Dummy::Application.load_tasks 13 | end 14 | 15 | RSpec::Core::RakeTask.new(:spec) 16 | 17 | desc 'Run all specs in spec directory (excluding plugin specs)' 18 | task default: :spec 19 | -------------------------------------------------------------------------------- /app/controllers/authenticate/authenticate_controller.rb: -------------------------------------------------------------------------------- 1 | class Authenticate::AuthenticateController < ApplicationController 2 | end 3 | -------------------------------------------------------------------------------- /app/controllers/authenticate/passwords_controller.rb: -------------------------------------------------------------------------------- 1 | # Request password change via an emailed link with a unique token. 2 | # Thanks to devise and Clearance. 3 | class Authenticate::PasswordsController < Authenticate::AuthenticateController 4 | skip_before_action :require_login, only: [:create, :edit, :new, :update], raise: false 5 | skip_before_action :require_authentication, only: [:create, :edit, :new, :update], raise: false 6 | before_action :ensure_existing_user, only: [:edit, :update] 7 | 8 | # Display screen to request a password change email. 9 | # 10 | # GET /users/passwords/new 11 | def new 12 | render template: 'passwords/new' 13 | end 14 | 15 | # Send password change email. 16 | def create 17 | if (user = find_user_for_create) 18 | user.forgot_password! 19 | deliver_email(user) 20 | end 21 | redirect_to sign_in_path, notice: flash_create_description 22 | end 23 | 24 | # Enter a new password. 25 | # 26 | # A get with the token in the url is expected, for example: 27 | # GET /users/passwords/3/edit?token=abcdef 28 | # 29 | # Results in a redirect with the token removed from the url & copied to the session: 30 | # GET /users/passwords/3/edit 31 | # 32 | def edit 33 | @user = find_user_for_edit 34 | 35 | if params[:token] 36 | session[:password_reset_token] = params[:token] 37 | redirect_to url_for 38 | elsif !@user.reset_password_period_valid? 39 | redirect_to sign_in_path, notice: flash_failure_token_expired 40 | else 41 | render template: 'passwords/edit' 42 | end 43 | end 44 | 45 | def update 46 | @user = find_user_for_update 47 | 48 | if !@user.reset_password_period_valid? 49 | redirect_to sign_in_path, notice: flash_failure_token_expired 50 | elsif @user.update_password password_reset_params # password changed, log user back in! 51 | login @user 52 | redirect_to url_after_update, notice: flash_success_password_changed 53 | else 54 | # failed to update password for some reason, perhaps password was too short or otherwise sucked. 55 | flash.now[:notice] = flash_failure_after_update 56 | render template: 'passwords/edit' 57 | end 58 | end 59 | 60 | private 61 | 62 | def deliver_email(user) 63 | mail = ::AuthenticateMailer.change_password(user) 64 | 65 | if mail.respond_to?(:deliver_later) 66 | mail.deliver_later 67 | else 68 | mail.deliver 69 | end 70 | end 71 | 72 | def password_reset_params 73 | params[:password_reset][:password] 74 | end 75 | 76 | def find_user_for_create 77 | Authenticate.configuration.user_model_class.find_by_normalized_email params[:password][:email] 78 | end 79 | 80 | def find_user_for_edit 81 | find_user_by_id_and_password_reset_token 82 | end 83 | 84 | def find_user_for_update 85 | find_user_by_id_and_password_reset_token 86 | end 87 | 88 | def ensure_existing_user 89 | unless find_user_by_id_and_password_reset_token 90 | flash.now[:notice] = flash_failure_when_forbidden 91 | render template: 'passwords/new' 92 | end 93 | end 94 | 95 | def find_user_by_id_and_password_reset_token 96 | token = session[:password_reset_token] || params[:token] 97 | # Authenticate.configuration.user_model_class.where(id: params[:id], password_reset_token: token).first 98 | Authenticate.configuration.user_model_class.find_by_id_and_password_reset_token params[:id], token.to_s 99 | end 100 | 101 | def flash_create_description 102 | translate(:description, 103 | scope: [:Authenticate, :controllers, :passwords], 104 | default: t('passwords.create.description')) 105 | end 106 | 107 | def flash_success_password_changed 108 | translate(:success_password_changed, 109 | scope: [:Authenticate, :controllers, :passwords], 110 | default: t('flashes.success_password_changed')) 111 | end 112 | 113 | def flash_failure_token_expired 114 | translate(:failure_token_expired, 115 | scope: [:Authenticate, :controllers, :passwords], 116 | default: t('flashes.failure_token_expired')) 117 | end 118 | 119 | def flash_failure_when_forbidden 120 | translate(:forbidden, 121 | scope: [:Authenticate, :controllers, :passwords], 122 | default: t('flashes.failure_when_forbidden')) 123 | end 124 | 125 | def flash_failure_after_update 126 | translate(:blank_password, 127 | scope: [:Authenticate, :controllers, :passwords], 128 | default: t('flashes.failure_after_update')) 129 | end 130 | 131 | def url_after_create 132 | sign_in_url 133 | end 134 | 135 | def url_after_update 136 | Authenticate.configuration.redirect_url 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /app/controllers/authenticate/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Allow authenticate users to log in and log out. 3 | # 4 | class Authenticate::SessionsController < Authenticate::AuthenticateController 5 | before_action :redirect_signed_in_users, only: [:new] 6 | skip_before_action :require_login, only: [:create, :new, :destroy], raise: false 7 | skip_before_action :require_authentication, only: [:create, :new, :destroy], raise: false 8 | 9 | # 10 | # Render the login screen. 11 | # 12 | def new 13 | render template: 'sessions/new' 14 | end 15 | 16 | # 17 | # Log the user in, with the provided name and password. 18 | # 19 | def create 20 | @user = authenticate(params) 21 | login(@user) do |status| 22 | if status.success? 23 | redirect_back_or url_after_create 24 | else 25 | flash.now.notice = status.message 26 | render template: 'sessions/new', status: :unauthorized 27 | end 28 | end 29 | end 30 | 31 | # 32 | # Log the user out 33 | # 34 | def destroy 35 | logout 36 | redirect_to url_after_destroy 37 | end 38 | 39 | private 40 | 41 | def redirect_signed_in_users 42 | redirect_to url_for_signed_in_users if logged_in? 43 | end 44 | 45 | def url_after_create 46 | Authenticate.configuration.redirect_url 47 | end 48 | 49 | def url_after_destroy 50 | sign_in_url 51 | end 52 | 53 | def url_for_signed_in_users 54 | url_after_create 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /app/controllers/authenticate/users_controller.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Controller to create new users. 3 | # 4 | class Authenticate::UsersController < Authenticate::AuthenticateController 5 | before_action :redirect_signed_in_users, only: [:create, :new] 6 | skip_before_action :require_login, only: [:create, :new], raise: false 7 | skip_before_action :require_authentication, only: [:create, :new], raise: false 8 | 9 | def new 10 | @user = user_from_params 11 | render template: 'users/new' 12 | end 13 | 14 | def create 15 | @user = user_from_params 16 | 17 | if @user.save 18 | login @user 19 | redirect_back_or url_after_create 20 | else 21 | render template: 'users/new' 22 | end 23 | end 24 | 25 | private 26 | 27 | def redirect_signed_in_users 28 | redirect_to Authenticate.configuration.redirect_url if logged_in? 29 | end 30 | 31 | def url_after_create 32 | Authenticate.configuration.redirect_url 33 | end 34 | 35 | def user_from_params 36 | email = user_params.delete(:email) 37 | password = user_params.delete(:password) 38 | 39 | Authenticate.configuration.user_model_class.new(user_params).tap do |user| 40 | user.email = email 41 | user.password = password 42 | end 43 | end 44 | 45 | def user_params 46 | params[Authenticate.configuration.user_model_param_key] || {} 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /app/mailers/authenticate_mailer.rb: -------------------------------------------------------------------------------- 1 | # Authenticate mailer. 2 | # 3 | # Handles password change requests. 4 | class AuthenticateMailer < ActionMailer::Base 5 | def change_password(user) 6 | @user = user 7 | mail from: Authenticate.configuration.mailer_sender, 8 | to: @user.email, 9 | subject: I18n.t(:change_password, scope: [:authenticate, :models, :authenticate_mailer]) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/views/authenticate_mailer/change_password.html.erb: -------------------------------------------------------------------------------- 1 |

<%= t(".opening") %>

2 | 3 |

4 | <%= link_to t(".link_text", default: "Change my password"), 5 | edit_users_password_url(@user, token: @user.password_reset_token.html_safe) %> 6 |

7 | 8 |

<%= raw t(".closing") %>

9 | -------------------------------------------------------------------------------- /app/views/authenticate_mailer/change_password.text.erb: -------------------------------------------------------------------------------- 1 | <%= t(".opening") %>

2 | 3 | <%= edit_users_password_url(@user, token: @user.password_reset_token.html_safe) %> 4 | 5 | <%= raw t(".closing") %> 6 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= javascript_include_tag 'application' %> 5 | <%= csrf_meta_tag %> 6 | 7 | 8 | 15 | 16 |
17 | <% flash.each do |key, value| -%> 18 |
<%=h value %>
19 | <% end %> 20 |
21 | 22 | <%= yield %> 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/views/passwords/edit.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

<%= t(".title") %>

3 | 4 |

<%= t(".description") %>

5 | 6 | <%= form_for :password_reset, 7 | url: users_password_path(@user, token: @user.password_reset_token), 8 | html: { method: :put } do |form| %> 9 | 10 |
11 | <%= form.label :password %> 12 | <%= form.password_field :password %> 13 |
14 | 15 |
16 | <%= form.submit %> 17 |
18 | 19 | <% end %> 20 |
21 | -------------------------------------------------------------------------------- /app/views/passwords/new.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

<%= t(".title") %>

3 | 4 |

<%= t(".description") %>

5 | 6 | <%= form_for :password, url: passwords_path do |form| %> 7 | 8 |
9 | <%= form.label :email %> 10 | <%= form.text_field :email, type: 'email' %> 11 |
12 | 13 |
14 | <%= form.submit %> 15 |
16 | 17 | <% end %> 18 | 19 | 22 | 23 |
24 | -------------------------------------------------------------------------------- /app/views/sessions/new.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

<%= t(".title") %>

3 | 4 | <%= form_for :session, url: session_path do |form| %> 5 | 6 |
7 | <%= form.label :email %> 8 | <%= form.text_field :email, type: 'email' %> 9 |
10 | 11 |
12 | <%= form.label :password %> 13 | <%= form.password_field :password %> 14 |
15 | 16 |
17 | <%= form.submit %> 18 |
19 | 20 | 26 | <% end %> 27 | 28 |
29 | -------------------------------------------------------------------------------- /app/views/users/new.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

<%= t(".title") %>

3 | 4 | <%= form_for @user do |form| %> 5 | 6 | <% if @user.errors.any? %> 7 | 12 | <% end %> 13 | 14 |
15 | <%= form.label :email %> 16 | <%= form.text_field :email, type: 'email' %> 17 |
18 | 19 |
20 | <%= form.label :password %> 21 | <%= form.password_field :password %> 22 |
23 | 24 |
25 | <%= form.submit %> 26 |
27 | 28 | 31 | 32 | <% end %> 33 |
34 | -------------------------------------------------------------------------------- /authenticate.gemspec: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.push File.expand_path('../lib', __FILE__) 2 | 3 | require 'authenticate/version' 4 | require 'date' 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |s| 8 | s.name = 'authenticate' 9 | s.version = Authenticate::VERSION 10 | s.authors = ['Justin Tomich'] 11 | s.email = ['justin@tomich.org'] 12 | s.homepage = 'http://github.com/tomichj/authenticate' 13 | s.summary = 'Authentication for Rails applications' 14 | s.description = 'Authentication for Rails applications' 15 | s.license = 'MIT' 16 | 17 | s.files = `git ls-files`.split("\n") 18 | s.test_files = `git ls-files -- {spec}/*`.split("\n") 19 | 20 | s.require_paths = ['lib'] 21 | s.extra_rdoc_files = %w(LICENSE README.md CHANGELOG.md) 22 | s.rdoc_options = ['--charset=UTF-8'] 23 | 24 | s.add_dependency 'bcrypt' 25 | s.add_dependency 'email_validator', '~> 1.6' 26 | s.add_dependency 'rails', '>= 4.0', '< 5.3' 27 | 28 | s.add_development_dependency 'factory_bot', '~> 4.8.2' 29 | s.add_development_dependency 'rspec-rails', '~> 3.6' 30 | s.add_development_dependency 'pry', '~> 0.10' 31 | s.add_development_dependency 'sqlite3', '~> 1.3' 32 | # s.add_development_dependency 'shoulda-matchers', '~> 2.8' 33 | s.add_development_dependency 'capybara', '~> 2.14' 34 | s.add_development_dependency 'database_cleaner', '~> 1.5' 35 | s.add_development_dependency 'timecop', '~> 0.8' 36 | s.add_development_dependency 'appraisal' 37 | s.add_development_dependency 'rake' 38 | 39 | s.required_ruby_version = Gem::Requirement.new('>= 2.0') 40 | end 41 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application. 3 | 4 | ENGINE_ROOT = File.expand_path('../..', __FILE__) 5 | ENGINE_PATH = File.expand_path('../../lib/authenticate/engine', __FILE__) 6 | 7 | # Set up gems listed in the Gemfile. 8 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 9 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 10 | 11 | require 'rails/all' 12 | require 'rails/engine/commands' 13 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # Install required gems, including Appraisal, which helps us test against 6 | # multiple Rails versions 7 | gem install bundler --conservative 8 | bundle check || bundle install 9 | 10 | if [ -z "$CI" ]; then 11 | bundle exec appraisal install 12 | fi 13 | 14 | # Set up database for the application that Clearance tests against 15 | RAILS_ENV=test bundle exec rake dummy:db:reset 16 | -------------------------------------------------------------------------------- /config/locales/authenticate.en.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en: 3 | authenticate: 4 | models: 5 | authenticate_mailer: 6 | change_password: Change your password 7 | authenticate_mailer: 8 | change_password: 9 | closing: If you didn't request this, ignore this email. Your password has not been changed. 10 | link_text: Change my password 11 | opening: Someone has requested a link to change your password. You can do this through the link below. 12 | flashes: 13 | failure_after_update: Password can't be blank. 14 | failure_when_forbidden: Please double check the URL or try submitting the form again. 15 | failure_when_not_signed_in: Please sign in to continue. 16 | failure_token_expired: Your password change request has expired. Please click 'Forgot Password' and try again. 17 | success_password_changed: Your password has been updated. 18 | helpers: 19 | label: 20 | password: 21 | email: Email address 22 | password_reset: 23 | password: Choose password 24 | submit: 25 | password: 26 | submit: Reset password 27 | password_reset: 28 | submit: Save this password 29 | session: 30 | submit: Sign in 31 | user: 32 | create: Sign up 33 | layouts: 34 | application: 35 | sign_in: Sign in 36 | sign_out: Sign out 37 | passwords: 38 | create: 39 | description: You will receive an email within the next few minutes. It 40 | contains instructions for changing your password. 41 | edit: 42 | description: Your password can be reset. Choose a new password below. 43 | title: Change your password 44 | new: 45 | description: To be emailed a link to reset your password, please enter your email address. 46 | title: Reset your password? 47 | sessions: 48 | form: 49 | forgot_password: Forgot password? 50 | sign_up: Sign up 51 | new: 52 | title: Sign in 53 | users: 54 | new: 55 | sign_in: Sign in 56 | title: Sign up 57 | callbacks: 58 | authenticatable: 59 | failure: Invalid id or password 60 | brute_force: 61 | failure: "Your account is locked, will unlock in %{time_remaining}" 62 | lifetimed: 63 | failure: Your session has reached it's maximum allowed lifetime, you must log in again 64 | timeoutable: 65 | failure: Your session has expired -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | if Authenticate.configuration.routes_enabled? 2 | Rails.application.routes.draw do 3 | resource :session, controller: 'authenticate/sessions', only: [:create, :new, :destroy] 4 | resources :passwords, controller: 'authenticate/passwords', only: [:new, :create] 5 | 6 | user_model = Authenticate.configuration.user_model_route_key 7 | user_actions = Authenticate.configuration.allow_sign_up? ? [:create] : [] 8 | resource user_model, controller: 'authenticate/users', only: user_actions do 9 | resources :passwords, controller: 'authenticate/passwords', only: [:edit, :update] 10 | end 11 | 12 | get '/sign_in', to: 'authenticate/sessions#new', as: 'sign_in' 13 | get '/sign_out', to: 'authenticate/sessions#destroy', as: 'sign_out' 14 | 15 | if Authenticate.configuration.allow_sign_up? 16 | get '/sign_up', to: 'authenticate/users#new', as: 'sign_up' 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /gemfiles/4.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 4.2.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/5.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 5.0.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/5.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 5.1" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/5.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 5.2" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /lib/authenticate.rb: -------------------------------------------------------------------------------- 1 | require 'authenticate/debug' 2 | require 'authenticate/configuration' 3 | require 'authenticate/lifecycle' 4 | require 'authenticate/controller' 5 | require 'authenticate/session' 6 | require 'authenticate/user' 7 | require 'authenticate/engine' 8 | require 'authenticate/modules' 9 | require 'authenticate/testing/integration_tests_sign_on' 10 | 11 | # Top level module of authenticate gem. 12 | module Authenticate 13 | end 14 | -------------------------------------------------------------------------------- /lib/authenticate/callbacks/authenticatable.rb: -------------------------------------------------------------------------------- 1 | # Callback to check that the session has been authenticated. 2 | # 3 | # If user failed to authenticate, toss them out. 4 | Authenticate.lifecycle.after_authentication name: 'authenticatable' do |_user, session, _opts| 5 | throw(:failure, I18n.t('callbacks.authenticatable.failure')) unless session && session.logged_in? 6 | end 7 | -------------------------------------------------------------------------------- /lib/authenticate/callbacks/brute_force.rb: -------------------------------------------------------------------------------- 1 | # Prevents a locked user from logging in, and unlocks users that expired their lock time. 2 | # Runs as a hook after authentication. 3 | Authenticate.lifecycle.prepend_after_authentication name: 'brute force protection' do |user, session, _options| 4 | include ActionView::Helpers::DateHelper 5 | unless session.logged_in? || Authenticate.configuration.max_consecutive_bad_logins_allowed.nil? 6 | user_credentials = User.credentials(session.request.params) 7 | user ||= User.find_by_credentials(user_credentials) 8 | if user && user.respond_to?(:register_failed_login!) 9 | user.register_failed_login! 10 | user.save! 11 | end 12 | end 13 | 14 | # if user is locked, and we allow a lockout period, then unlock the user if they've waited 15 | # longer than the lockout period. 16 | if user && user.respond_to?(:locked?) && user.locked? && !Authenticate.configuration.bad_login_lockout_period.nil? 17 | user.unlock! if user.lock_expires_at <= Time.now.utc 18 | end 19 | 20 | # if the user is still locked, let them know how long they are locked for. 21 | if user && user.respond_to?(:locked?) && user.locked? 22 | remaining = time_ago_in_words(user.lock_expires_at) 23 | throw(:failure, I18n.t('callbacks.brute_force.failure', time_remaining: remaining.to_s)) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/authenticate/callbacks/lifetimed.rb: -------------------------------------------------------------------------------- 1 | # Catch sessions that have been live for too long and kill them, forcing the user to reauthenticate. 2 | Authenticate.lifecycle.after_set_user name: 'lifetimed after set_user', 3 | except: :authentication do |user, _session, _options| 4 | if user && user.respond_to?(:max_session_lifetime_exceeded?) 5 | throw(:failure, I18n.t('callbacks.lifetimed.failure')) if user.max_session_lifetime_exceeded? 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/authenticate/callbacks/timeoutable.rb: -------------------------------------------------------------------------------- 1 | # Update last_access_at on every authentication 2 | Authenticate.lifecycle.after_authentication name: 'timeoutable after authentication' do |user, _session, _options| 3 | if user && user.respond_to?(:last_access_at) 4 | user.last_access_at = Time.now.utc 5 | user.save! 6 | end 7 | end 8 | 9 | # Fail users that have timed out. Otherwise update last_access_at. 10 | Authenticate.lifecycle.after_set_user name: 'timeoutable after set_user', 11 | except: :authentication do |user, _session, _options| 12 | if user && user.respond_to?(:timedout?) 13 | throw(:failure, I18n.t('callbacks.timeoutable.failure')) if user.timedout? 14 | user.last_access_at = Time.now.utc 15 | user.save! 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/authenticate/callbacks/trackable.rb: -------------------------------------------------------------------------------- 1 | # Update all standard tracked stats at each authentication. 2 | Authenticate.lifecycle.after_authentication name: 'trackable' do |user, session, _options| 3 | if user && user.respond_to?(:update_tracked_fields!) 4 | user.update_tracked_fields!(session.request) 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/authenticate/configuration.rb: -------------------------------------------------------------------------------- 1 | # Authenticate 2 | module Authenticate 3 | # 4 | # Configuration for Authenticate. 5 | # 6 | class Configuration 7 | # 8 | # ActiveRecord model class name that represents your user. Specify as a String. 9 | # 10 | # Defaults to '::User'. 11 | # 12 | # To set to a different class: 13 | # 14 | # Authenticate.configure do |config| 15 | # config.user_model = 'BlogUser' 16 | # end 17 | # 18 | # @return [String] 19 | attr_accessor :user_model 20 | 21 | # Name of the session cookie Authenticate will send to client browser. 22 | # 23 | # Defaults to 'authenticate_session_token'. 24 | # 25 | # @return [String] 26 | attr_accessor :cookie_name 27 | 28 | # A lambda called to set the remember token cookie expires attribute. 29 | # 30 | # Defaults to 1 year expiration. 31 | # 32 | # Note this is NOT the authenticate session's max lifetime, but only the cookie's lifetime. 33 | # 34 | # See #max_session_lifetime for more on the session lifetime. 35 | # 36 | # To set cookie expiration yourself: 37 | # 38 | # Authenticate.configure do |config| 39 | # config.cookie_expiration = { 1.month.from_now.utc } 40 | # end 41 | # 42 | # @return [Lambda] 43 | attr_accessor :cookie_expiration 44 | 45 | # The domain to set for the Authenticate session cookie. 46 | # 47 | # Defaults to nil, which will cause the cookie domain to set to the domain of the request. 48 | # 49 | # @return [String] 50 | attr_accessor :cookie_domain 51 | 52 | # Controls which paths the session token cookie is valid for. 53 | # 54 | # Defaults to `"/"` for the entire domain. 55 | # 56 | # For more, see [RFC6265](http://tools.ietf.org/html/rfc6265#section-5.1.4). 57 | # @return [String] 58 | attr_accessor :cookie_path 59 | 60 | # Controls the secure setting on the session cookie. 61 | # 62 | # Defaults to `false`. 63 | # 64 | # When set to 'true', the browser will only send the cookie to the server over HTTPS. 65 | # If set to true over an insecure http (not https) connection, the cookie will not 66 | # be usable and the user will not be successfully authenticated. 67 | # 68 | # You should set this value to true in live environments to prevent session hijacking. 69 | # 70 | # Set to false in development environments. 71 | # 72 | # For more, see [RFC6265](http://tools.ietf.org/html/rfc6265#section-5.2.5). 73 | # @return [Boolean] 74 | attr_accessor :secure_cookie 75 | 76 | # Controls whether the HttpOnly flag should be set on the session cookie. 77 | # If `true`, the cookie will not be made available to JavaScript. 78 | # 79 | # Defaults to `true`. 80 | # 81 | # For more see [RFC6265](http://tools.ietf.org/html/rfc6265#section-5.2.6). 82 | # @return [Boolean] 83 | attr_accessor :cookie_http_only 84 | 85 | # Controls the 'from' address for Authenticate emails. Set this to a value appropriate to your application. 86 | # 87 | # Defaults to reply@example.com. 88 | # 89 | # @return [String] 90 | attr_accessor :mailer_sender 91 | 92 | # Determines what crypto is used when authenticating and setting passwords. 93 | # 94 | # Defaults to {Authenticate::Model::BCrypt}. 95 | # 96 | # At the moment Bcrypt is the only option offered. 97 | # 98 | # Crypto implementations must implement: 99 | # * match?(secret, encrypted) 100 | # * encrypt(secret) 101 | # 102 | # @return [Module #match? #encrypt] 103 | attr_accessor :crypto_provider 104 | 105 | # Invalidate the session after the specified period of idle time. 106 | # If the interval between the current access time and the last access time is greater than timeout_in, 107 | # the session is invalidated. The user will be prompted for authentication again. 108 | # 109 | # Defaults to nil, which is no idle timeout. 110 | # 111 | # Authenticate.configure do |config| 112 | # config.timeout_in = 45.minutes 113 | # end 114 | # 115 | # @return [ActiveSupport::CoreExtensions::Numeric::Time] 116 | attr_accessor :timeout_in 117 | 118 | # Allow a session to 'live' for no more than the given elapsed time, e.g. 8.hours. 119 | # 120 | # Defaults to nil, or no max session time. 121 | # 122 | # If set, a user session will expire once it has been active for max_session_lifetime. 123 | # The user session is invalidated and the next access will will prompt the user for authentication. 124 | # 125 | # Authenticate.configure do |config| 126 | # config.max_session_lifetime = 8.hours 127 | # end 128 | # 129 | # @return [ActiveSupport::CoreExtensions::Numeric::Time] 130 | attr_accessor :max_session_lifetime 131 | 132 | # Number of consecutive bad login attempts allowed. Commonly called "brute force protection". 133 | # The user's consecutive bad logins will be tracked, and if they exceed the allowed maximum, 134 | # the user's account will be locked. The length of the lockout is determined by [#bad_login_lockout_period]. 135 | # 136 | # Default is nil, which disables this feature. 137 | # 138 | # Authenticate.configure do |config| 139 | # config.max_consecutive_bad_logins_allowed = 4 140 | # config.bad_login_lockout_period = 10.minutes 141 | # end 142 | # 143 | # @return [Integer] 144 | attr_accessor :max_consecutive_bad_logins_allowed 145 | 146 | # Time period to lock an account for if the user exceeds max_consecutive_bad_logins_allowed. 147 | # 148 | # If set to nil, account is locked out indefinitely. 149 | # 150 | # @return [ActiveSupport::CoreExtensions::Numeric::Time] 151 | attr_accessor :bad_login_lockout_period 152 | 153 | # Range requirement for password length. 154 | # 155 | # Defaults to `8..128`. 156 | # 157 | # @return [Range] 158 | attr_accessor :password_length 159 | 160 | # Strategy for authentication. 161 | # 162 | # Available strategies: 163 | # * :email - requires user have attribute :email 164 | # * :username - requires user have attribute :username 165 | # 166 | # Defaults to :email. To set to :username: 167 | # 168 | # Configuration.configure do |config| 169 | # config.authentication_strategy = :username 170 | # end 171 | # 172 | # Authenticate is designed to authenticate via :email. Some support for username is included. 173 | # Username still requires an :email attribute on your User model. 174 | # 175 | # Alternatively, you can plug in your own authentication class: 176 | # 177 | # Configuration.configure do |config| 178 | # config.authentication_strategy = MyFunkyAuthClass 179 | # end 180 | # 181 | # @return [Symbol or Class] 182 | attr_accessor :authentication_strategy 183 | 184 | # The default path Authenticate will redirect signed in users to. 185 | # 186 | # Defaults to `"/"`. 187 | # 188 | # This can also be overridden for specific scenarios by overriding controller methods that rely on it. 189 | # @return [String] 190 | attr_accessor :redirect_url 191 | 192 | # Rotate CSRF token on sign in if true. 193 | # 194 | # Defaults to false, but will default to true in 1.0. 195 | # 196 | # @return [Boolean] 197 | attr_accessor :rotate_csrf_on_sign_in 198 | 199 | # Controls whether the "sign up" route, allowing creation of users, is enabled. 200 | # 201 | # Defaults to `true`. 202 | # 203 | # Set to `false` to disable user creation routes. The setting is ignored if routes are disabled. 204 | # 205 | # @return [Boolean] 206 | attr_writer :allow_sign_up 207 | 208 | # Enable or disable Authenticate's built-in routes. 209 | # 210 | # Defaults to 'true'. 211 | # 212 | # If you disable the routes, your application is responsible for all routes. 213 | # 214 | # You can deploy a copy of Authenticate's routes with `rails generate authenticate:routes`, 215 | # which will also set `config.routes = false`. 216 | # 217 | # @return [Boolean] 218 | attr_accessor :routes 219 | 220 | # The time period within which the password must be reset or the token expires. 221 | # If set to nil, the password reset token does not expire. 222 | # 223 | # Defaults to `2.days`. 224 | # 225 | # @return [ActiveSupport::CoreExtensions::Numeric::Time] 226 | attr_accessor :reset_password_within 227 | 228 | # An array of additional modules to load into the User module. 229 | # 230 | # Defaults to an empty array. 231 | # 232 | # @return [Array] 233 | attr_accessor :modules 234 | 235 | # Enable debugging messages. 236 | # @private 237 | # @return [Boolean] 238 | attr_accessor :debug 239 | 240 | def initialize 241 | # Defaults 242 | @debug = false 243 | @cookie_name = 'authenticate_session_token' 244 | @cookie_expiration = -> { 1.year.from_now.utc } 245 | @cookie_domain = nil 246 | @cookie_path = '/' 247 | @secure_cookie = false 248 | @cookie_http_only = true 249 | @mailer_sender = 'reply@example.com' 250 | @redirect_url = '/' 251 | @rotate_csrf_on_sign_in = false 252 | @allow_sign_up = true 253 | @routes = true 254 | @reset_password_within = 2.days 255 | @modules = [] 256 | @user_model = '::User' 257 | @authentication_strategy = :email 258 | @password_length = 8..128 259 | end 260 | 261 | def user_model_class 262 | @user_model_class ||= user_model.constantize 263 | end 264 | 265 | # The routing key for user routes. See `routes.rb`. 266 | # @return [Symbol] 267 | def user_model_route_key 268 | return :users if @user_model == '::User' # avoid nil in generator 269 | user_model_class.model_name.route_key 270 | end 271 | 272 | # The key for accessing user parameters. 273 | # @return [Symbol] 274 | def user_model_param_key 275 | return :user if @user_model == '::User' # avoid nil in generator 276 | user_model_class.model_name.param_key.to_sym 277 | end 278 | 279 | # Actions allowed for :user resources (in routes.rb). 280 | # If sign up is allowed, the [:create] action is allowed, otherwise []. 281 | # @return [Array] 282 | def user_actions 283 | allow_sign_up? ? [:create] : [] 284 | end 285 | 286 | # Is the user sign up route enabled? 287 | # @return [Boolean] 288 | def allow_sign_up? 289 | @allow_sign_up 290 | end 291 | 292 | # @return [Boolean] are Authenticate's built-in routes enabled? 293 | def routes_enabled? 294 | @routes 295 | end 296 | 297 | def rotate_csrf_on_sign_in? 298 | rotate_csrf_on_sign_in 299 | end 300 | 301 | # List of symbols naming modules to load. 302 | def modules 303 | modules = @modules.dup # in case the user pushes any on 304 | modules << @authentication_strategy 305 | modules << :db_password 306 | modules << :password_reset 307 | modules << :trackable # needs configuration 308 | modules << :timeoutable if @timeout_in 309 | modules << :lifetimed if @max_session_lifetime 310 | modules << :brute_force if @max_consecutive_bad_logins_allowed 311 | modules 312 | end 313 | end # end of Configuration class 314 | # 315 | # Access to Authenticate's configuration, e.g.: 316 | # 317 | # puts Authenticate.configuration.redirect_url 318 | # 319 | def self.configuration 320 | @configuration ||= Configuration.new 321 | end 322 | 323 | def self.configuration=(config) 324 | @configuration = config 325 | end 326 | 327 | def self.configure 328 | yield configuration 329 | end 330 | end 331 | -------------------------------------------------------------------------------- /lib/authenticate/controller.rb: -------------------------------------------------------------------------------- 1 | module Authenticate 2 | # 3 | # The authenticate controller methods. 4 | # 5 | # Typically, you include this concern into your ApplicationController. A basic implementation might look like this: 6 | # 7 | # class ApplicationController < ActionController::Base 8 | # include Authenticate::Controller 9 | # before_action :require_login 10 | # protect_from_forgery with: :exception 11 | # end 12 | # 13 | # Methods, generally called from authenticate's app controllers: 14 | # * authenticate(params) - validate a user's identity 15 | # * login(user, &block) - complete login after validating a user's identity, creating an Authenticate session 16 | # * logout - log a user out, invalidating their Authenticate session. 17 | # 18 | # Action/Filter: 19 | # * require_login - restrict access to authenticated users, often from ApplicationController 20 | # 21 | # Helpers, used anywhere: 22 | # * current_user - get the currently logged in user 23 | # * logged_in? - is the user logged in? 24 | # * logged_out? - is the user not logged in? 25 | # 26 | module Controller 27 | extend ActiveSupport::Concern 28 | include Debug 29 | 30 | included do 31 | helper_method :current_user, :logged_in?, :logged_out?, :authenticated? 32 | attr_writer :authenticate_session 33 | end 34 | 35 | # Validate a user's identity with (typically) email/ID & password, and return the User if valid, or nil. 36 | # After calling this, call login(user) to complete the process. 37 | def authenticate(params) 38 | credentials = Authenticate.configuration.user_model_class.credentials(params) 39 | Authenticate.configuration.user_model_class.authenticate(credentials) 40 | end 41 | 42 | # Complete the user's sign in process: after calling authenticate, or after user creates account. 43 | # Runs all valid callbacks and sends the user a session token. 44 | def login(user, &block) 45 | authenticate_session.login user, &block 46 | 47 | if logged_in? && Authenticate.configuration.rotate_csrf_on_sign_in? 48 | session.delete(:_csrf_token) 49 | form_authenticity_token 50 | end 51 | end 52 | 53 | # Log the user out. Typically used in session controller. 54 | # 55 | # class SessionsController < ActionController::Base 56 | # include Authenticate::Controller 57 | # 58 | # def destroy 59 | # logout 60 | # redirect_to '/', notice: 'You logged out successfully' 61 | # end 62 | def logout 63 | authenticate_session.logout 64 | end 65 | 66 | # Use this filter as a before_action to control access to controller actions, 67 | # limiting to logged in users. 68 | # 69 | # Placing in application_controller will control access to all controllers. 70 | # 71 | # Example: 72 | # 73 | # class ApplicationController < ActionController::Base 74 | # before_action :require_login 75 | # 76 | # def index 77 | # # ... 78 | # end 79 | # end 80 | # 81 | def require_login 82 | debug "!!!!!!!!!!!!!!!!!! controller#require_login " # logged_in? #{logged_in?}" 83 | unauthorized unless logged_in? 84 | message = catch(:failure) do 85 | current_user = authenticate_session.current_user 86 | Authenticate.lifecycle.run_callbacks(:after_set_user, current_user, authenticate_session, event: :set_user) 87 | end 88 | unauthorized(message) if message 89 | end 90 | 91 | # Has the user been logged in? Exposed as a helper, can be called from views. 92 | # 93 | # <% if logged_in? %> 94 | # <%= link_to sign_out_path, "Sign out" %> 95 | # <% else %> 96 | # <%= link_to sign_in_path, "Sign in" %> 97 | # <% end %> 98 | # 99 | def logged_in? 100 | debug "!!!!!!!!!!!!!!!!!! controller#logged_in?" 101 | authenticate_session.logged_in? 102 | end 103 | 104 | # Has the user not logged in? Exposed as a helper, can be called from views. 105 | # 106 | # <% if logged_out? %> 107 | # <%= link_to sign_in_path, "Sign in" %> 108 | # <% end %> 109 | # 110 | def logged_out? 111 | !logged_in? 112 | end 113 | 114 | # Get the current user from the current Authenticate session. 115 | # Exposed as a helper , can be called from controllers, views, and other helpers. 116 | # 117 | #

Your email address: <%= current_user.email %>

118 | # 119 | def current_user 120 | authenticate_session.current_user 121 | end 122 | 123 | # Return true if it's an Authenticate controller. Useful if you want to apply a before 124 | # filter to all controllers, except the ones in Authenticate, e.g. 125 | # 126 | # before_action :my_filter, unless: :authenticate_controller? 127 | # 128 | def authenticate_controller? 129 | is_a?(Authenticate::AuthenticateController) 130 | end 131 | 132 | # The old API. DEPRECATED, use #require_login instead. 133 | # 134 | # todo: remove in a future version. 135 | def require_authentication 136 | warn "#{Kernel.caller.first}: [DEPRECATION] " + 137 | "'require_authentication' is deprecated and will be removed in a future release. use 'require_login' instead" 138 | require_login 139 | end 140 | 141 | # The old API. DEPRECATED, use #logged_in? instead. 142 | # 143 | # todo: remove in a future version. 144 | def authenticated? 145 | warn "#{Kernel.caller.first}: [DEPRECATION] " + 146 | "'authenticated?' is deprecated and will be removed in a future release. Use 'logged_in?' instead." 147 | logged_in? 148 | end 149 | 150 | protected 151 | 152 | # User is not authorized, bounce 'em to sign in 153 | def unauthorized(msg = t('flashes.failure_when_not_signed_in')) 154 | authenticate_session.logout 155 | respond_to do |format| 156 | format.any(:js, :json, :xml) { head :unauthorized } 157 | format.any { redirect_unauthorized(msg) } 158 | end 159 | end 160 | 161 | def redirect_unauthorized(flash_message) 162 | store_location! 163 | 164 | if flash_message 165 | flash[:notice] = flash_message # TODO: use locales 166 | end 167 | 168 | if logged_in? 169 | redirect_to url_after_denied_access_when_signed_in 170 | else 171 | redirect_to url_after_denied_access_when_signed_out 172 | end 173 | end 174 | 175 | def redirect_back_or(default) 176 | redirect_to(stored_location || default) 177 | clear_stored_location 178 | end 179 | 180 | # Used as the redirect location when {#unauthorized} is called and there is a 181 | # currently signed in user. 182 | # 183 | # @return [String] 184 | def url_after_denied_access_when_signed_in 185 | Authenticate.configuration.redirect_url 186 | end 187 | 188 | # Used as the redirect location when {#unauthorized} is called and there is 189 | # no currently signed in user. 190 | # 191 | # @return [String] 192 | def url_after_denied_access_when_signed_out 193 | sign_in_url 194 | end 195 | 196 | private 197 | 198 | # Write location to return to in user's session (normally a cookie). 199 | def store_location! 200 | if request.get? 201 | session[:authenticate_return_to] = request.original_fullpath 202 | end 203 | end 204 | 205 | def stored_location 206 | session[:authenticate_return_to] 207 | end 208 | 209 | def clear_stored_location 210 | session[:authenticate_return_to] = nil 211 | end 212 | 213 | def authenticate_session 214 | @authenticate_session ||= Authenticate::Session.new(request) 215 | end 216 | end 217 | end 218 | -------------------------------------------------------------------------------- /lib/authenticate/crypto/bcrypt.rb: -------------------------------------------------------------------------------- 1 | module Authenticate 2 | module Crypto 3 | # 4 | # All crypto providers must implement encrypt(secret) and match?(secret, encrypted) 5 | module BCrypt 6 | require 'bcrypt' 7 | 8 | def encrypt(secret) 9 | ::BCrypt::Password.create secret, cost: cost 10 | end 11 | 12 | def match?(secret, encrypted) 13 | return false unless encrypted.present? 14 | ::BCrypt::Password.new(encrypted) == secret 15 | end 16 | 17 | def cost 18 | @cost ||= ::BCrypt::Engine::DEFAULT_COST 19 | end 20 | 21 | def cost=(val) 22 | if val < ::BCrypt::Engine::MIN_COST 23 | msg = "bcrypt cost cannot be set below the engine's min cost (#{::BCrypt::Engine::MIN_COST})" 24 | raise ArgumentError.new(msg), msg 25 | end 26 | @cost = val 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/authenticate/debug.rb: -------------------------------------------------------------------------------- 1 | module Authenticate 2 | # 3 | # Simple debug output for gem. 4 | # 5 | module Debug 6 | extend ActiveSupport::Concern 7 | 8 | def debug(msg) 9 | if defined?(Rails) && defined?(Rails.logger) && Authenticate.configuration.debug 10 | Rails.logger.info msg.to_s 11 | elsif Authenticate.configuration.debug 12 | puts msg.to_s 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/authenticate/engine.rb: -------------------------------------------------------------------------------- 1 | require 'authenticate' 2 | require 'rails' 3 | 4 | module Authenticate 5 | # 6 | # Authenticate Rails engine. 7 | # Filter password, token, from spewing out. 8 | # 9 | class Engine < ::Rails::Engine 10 | initializer 'authenticate.filter' do |app| 11 | app.config.filter_parameters += [:password, :token] 12 | end 13 | 14 | config.generators do |g| 15 | g.test_framework :rspec 16 | g.fixture_replacement :factory_girl, dir: 'spec/factories' 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/authenticate/lifecycle.rb: -------------------------------------------------------------------------------- 1 | # Authenticate Lifecycle methods within 2 | module Authenticate 3 | # 4 | # Lifecycle stores and runs callbacks for authorization events. 5 | # 6 | # Heavily borrowed from warden (https://github.com/hassox/warden). 7 | # 8 | # = Events: 9 | # * :set_user - called after the user object is loaded, either through id/password or via session token. 10 | # * :authentication - called after the user authenticates with id & password 11 | # 12 | # Callbacks are added via after_set_user or after_authentication. 13 | # 14 | # Callbacks can throw(:failure,message) to signal an authentication/authorization failure, or perform 15 | # actions on the user or session. 16 | # 17 | # = Options 18 | # 19 | # The callback options may optionally specify when to run the callback: 20 | # * only - executes the callback only if it matches the event(s) given 21 | # * except - executes the callback except if it matches the event(s) given 22 | # 23 | # The callback may also specify a 'name' key in options. This is for debugging purposes only. 24 | # 25 | # = Callback block parameters 26 | # 27 | # Callbacks are invoked with the following block parameters: |user, session, opts| 28 | # * user - the user object just loaded 29 | # * session - the Authenticate::Session 30 | # * opts - any options you want passed into the callback 31 | # 32 | # = Example 33 | # 34 | # # A callback to track the users successful logins: 35 | # Authenticate.lifecycle.after_set_user do |user, session, opts| 36 | # user.sign_in_count += 1 37 | # end 38 | # 39 | class Lifecycle 40 | include Debug 41 | 42 | def initialize 43 | @conditions = [:only, :except, :event].freeze 44 | end 45 | 46 | # This callback is triggered after the first time a user is set during per-hit authorization, or during login. 47 | def after_set_user(options = {}, method = :push, &block) 48 | add_callback(after_set_user_callbacks, options, method, &block) 49 | end 50 | 51 | # A callback to run after the user successfully authenticates, during the login process. 52 | # Mechanically identical to [#after_set_user]. 53 | def after_authentication(options = {}, method = :push, &block) 54 | add_callback(after_authentication_callbacks, options, method, &block) 55 | end 56 | 57 | # Run callbacks of the given kind. 58 | # 59 | # * kind - :authenticate or :after_set_user 60 | # * args - user, session, opts hash. Opts is an optional event, e.g. { event: :authentication } 61 | # 62 | # Example: 63 | # Authenticate.lifecycle.run_callbacks(:after_set_user, @current_user, self, { event: :authentication }) 64 | # 65 | def run_callbacks(kind, user, session, *args) # args - |user, session, opts| 66 | # Last callback arg MUST be a Hash 67 | options = args.last 68 | send("#{kind}_callbacks").each do |callback, conditions| # each callback has 'conditions' stored with it 69 | conditions = conditions.dup.delete_if { |key, _val| !@conditions.include? key } 70 | invalid = conditions.find do |key, value| 71 | value.is_a?(Array) ? !value.include?(options[key]) : (value != options[key]) 72 | end 73 | callback.call(user, session, *args) unless invalid 74 | end 75 | nil 76 | end 77 | 78 | def prepend_after_authentication(options = {}, &block) 79 | after_authentication(options, :unshift, &block) 80 | end 81 | 82 | private 83 | 84 | def add_callback(callbacks, options = {}, method = :push, &block) 85 | raise BlockNotGiven unless block_given? 86 | options = process_opts(options) 87 | callbacks.send(method, [block, options]) 88 | end 89 | 90 | # set event: to run callback on based on options 91 | def process_opts(options) 92 | if options.key?(:only) 93 | options[:event] = options.delete(:only) 94 | elsif options.key?(:except) 95 | options[:event] = [:set_user, :authentication] - Array(options.delete(:except)) 96 | end 97 | options 98 | end 99 | 100 | def after_set_user_callbacks 101 | @after_set_user_callbacks ||= [] 102 | end 103 | 104 | def after_authentication_callbacks 105 | @after_authentication_callbacks ||= [] 106 | end 107 | end 108 | 109 | # Invoke lifecycle methods. Example: 110 | # Authenticate.lifecycle.run_callbacks(:after_set_user, current_user, authenticate_session, { event: :set_user }) 111 | # 112 | def self.lifecycle 113 | @lifecycle ||= Lifecycle.new 114 | end 115 | 116 | def self.lifecycle=(lifecycle) 117 | @lifecycle = lifecycle 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/authenticate/login_status.rb: -------------------------------------------------------------------------------- 1 | module Authenticate 2 | # 3 | # Indicate login attempt was successful. Allows caller to supply a block to login() predicated on success? 4 | # 5 | class Success 6 | def success? 7 | true 8 | end 9 | end 10 | 11 | # 12 | # Indicate login attempt was a failure, with a message. 13 | # Allows caller to supply a block to login() predicated on success? 14 | # 15 | class Failure 16 | # The reason the sign in failed. 17 | attr_reader :message 18 | 19 | # @param [String] message The reason the login failed. 20 | def initialize(message) 21 | @message = message 22 | end 23 | 24 | def success? 25 | false 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/authenticate/model/brute_force.rb: -------------------------------------------------------------------------------- 1 | require 'authenticate/callbacks/brute_force' 2 | 3 | module Authenticate 4 | module Model 5 | # 6 | # Protect from brute force attacks. Lock accounts that have too many failed consecutive logins. 7 | # Todo: email user to allow unlocking via a token. 8 | # 9 | # To enable brute force protection, set the config params shown below. Example: 10 | # 11 | # Authenticate.configure do |config| 12 | # config.bad_login_lockout_period = 5.minutes 13 | # config.max_consecutive_bad_logins_allowed = 3 14 | # end 15 | # 16 | # = Columns 17 | # * failed_logins_count - each consecutive failed login increments this counter. Set back to 0 on successful login. 18 | # * lock_expires_at - datetime a locked account will again become available. 19 | # 20 | # = Configuration 21 | # * max_consecutive_bad_logins_allowed - how many failed logins are allowed? 22 | # * bad_login_lockout_period - how long is the user locked out? nil indicates forever. 23 | # 24 | # = Methods 25 | # The following methods are added to your user model: 26 | # * register_failed_login! - increment failed_logins_count, lock account if in violation 27 | # * lock! - lock the account, setting the lock_expires_at attribute 28 | # * unlock! - reset failed_logins_count to 0, lock_expires_at to nil 29 | # * locked? - is the account locked? @return[Boolean] 30 | # * unlocked? - is the account unlocked? @return[Boolean] 31 | # 32 | module BruteForce 33 | extend ActiveSupport::Concern 34 | 35 | def self.required_fields(_klass) 36 | [:failed_logins_count, :lock_expires_at] 37 | end 38 | 39 | def register_failed_login! 40 | self.failed_logins_count ||= 0 41 | self.failed_logins_count += 1 42 | lock! if self.failed_logins_count > max_bad_logins 43 | end 44 | 45 | def lock! 46 | update_attribute(:lock_expires_at, Time.now.utc + lockout_period) 47 | end 48 | 49 | def unlock! 50 | update_attributes(failed_logins_count: 0, lock_expires_at: nil) 51 | end 52 | 53 | def locked? 54 | !unlocked? 55 | end 56 | 57 | def unlocked? 58 | lock_expires_at.nil? 59 | end 60 | 61 | private 62 | 63 | def max_bad_logins 64 | Authenticate.configuration.max_consecutive_bad_logins_allowed 65 | end 66 | 67 | def lockout_period 68 | Authenticate.configuration.bad_login_lockout_period 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/authenticate/model/db_password.rb: -------------------------------------------------------------------------------- 1 | require 'authenticate/crypto/bcrypt' 2 | 3 | module Authenticate 4 | module Model 5 | # 6 | # Encrypts and stores a password in the database to validate the authenticity of a user while logging in. 7 | # 8 | # Authenticate can plug in any crypto provider, but currently only features BCrypt. 9 | # 10 | # A crypto provider must provide: 11 | # * encrypt(secret) - encrypt the secret, @return [String] 12 | # * match?(secret, encrypted) - does the secret match the encrypted? @return [Boolean] 13 | # 14 | # = Columns 15 | # * encrypted_password - the user's password, encrypted 16 | # 17 | # = Methods 18 | # The following methods are added to your user model: 19 | # * password=(new_password) - encrypt and set the user password 20 | # * password_match?(password) - checks to see if the user's password matches the given password 21 | # 22 | # = Validations 23 | # * :password validation, requiring the password is set unless we're skipping due to a password change 24 | # 25 | module DbPassword 26 | extend ActiveSupport::Concern 27 | 28 | def self.required_fields(_klass) 29 | [:encrypted_password] 30 | end 31 | 32 | included do 33 | private_class_method :crypto_provider 34 | include crypto_provider 35 | attr_reader :password 36 | validates :password, 37 | presence: true, 38 | length: { in: password_length }, 39 | unless: :skip_password_validation? 40 | end 41 | 42 | def password_match?(password) 43 | match?(password, encrypted_password) 44 | end 45 | 46 | def password=(new_password) 47 | @password = new_password 48 | self.encrypted_password = encrypt(new_password) unless new_password.nil? 49 | end 50 | 51 | private 52 | 53 | # Class methods for database password management. 54 | module ClassMethods 55 | # We only have one crypto provider at the moment, but look up the provider in the config. 56 | def crypto_provider 57 | Authenticate.configuration.crypto_provider || Authenticate::Crypto::BCrypt 58 | end 59 | 60 | def password_length 61 | Authenticate.configuration.password_length 62 | end 63 | end 64 | 65 | # If we already have an encrypted password and it's not changing, skip the validation. 66 | def skip_password_validation? 67 | encrypted_password.present? && !encrypted_password_changed? 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/authenticate/model/email.rb: -------------------------------------------------------------------------------- 1 | require 'email_validator' 2 | 3 | module Authenticate 4 | module Model 5 | # 6 | # Use :email as the identifier for the user. Email must be unique. 7 | # 8 | # = Columns 9 | # * email - the email address of the user 10 | # 11 | # = Validations 12 | # * :email - require email is set, is a valid format, and is unique 13 | # 14 | # = Callbacks 15 | # 16 | # = Methods 17 | # * normalize_email - normalize the email, removing spaces etc, before saving 18 | # 19 | # = Class Methods 20 | # * credentials(params) - return the credentials required for authorization by email 21 | # * authenticate(credentials) - find user with given email, validate their password, return user if authenticated 22 | # * normalize_email(email) - clean up the given email and return it. 23 | # * find_by_credentials(credentials) - find and return the user with the email address in the credentials 24 | # 25 | module Email 26 | extend ActiveSupport::Concern 27 | 28 | def self.required_fields(_klass) 29 | [:email] 30 | end 31 | 32 | included do 33 | before_validation :normalize_email 34 | validates :email, 35 | email: { strict_mode: true }, 36 | presence: true, 37 | uniqueness: { allow_blank: true } 38 | end 39 | 40 | # Class methods for authenticating using email as the user identifier. 41 | module ClassMethods 42 | # Retrieve credentials from params. 43 | # 44 | # @return [id, pw] 45 | def credentials(params) 46 | return [] if params.nil? || params[:session].nil? 47 | [params[:session][:email], params[:session][:password]] 48 | end 49 | 50 | def authenticate(credentials) 51 | user = find_by_credentials(credentials) 52 | user && user.password_match?(credentials[1]) ? user : nil 53 | end 54 | 55 | def find_by_credentials(credentials) 56 | email = credentials[0] 57 | find_by_normalized_email(email) 58 | end 59 | end 60 | 61 | # Sets the email on this instance to the value returned by class method #normalize_email 62 | # 63 | # @return [String] 64 | def normalize_email 65 | self.email = self.class.normalize_email(email) 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/authenticate/model/lifetimed.rb: -------------------------------------------------------------------------------- 1 | require 'authenticate/callbacks/lifetimed' 2 | 3 | module Authenticate 4 | module Model 5 | # 6 | # Imposes a maximum allowed lifespan on a user's session, after which the session is expired and requires 7 | # re-authentication. 8 | # 9 | # = Configuration 10 | # Set the maximum session lifetime in the initializer, giving a timestamp. 11 | # 12 | # Authenticate.configure do |config| 13 | # config.max_session_lifetime = 8.hours 14 | # end 15 | # 16 | # If the max_session_lifetime configuration parameter is nil, the :lifetimed module is not loaded. 17 | # 18 | # = Columns 19 | # * current_sign_in_at - requires `current_sign_in_at` column. This column is managed by the :trackable plugin. 20 | # 21 | # = Methods 22 | # * max_session_lifetime_exceeded? - true if the user's session has exceeded the max lifetime allowed 23 | # 24 | # 25 | module Lifetimed 26 | extend ActiveSupport::Concern 27 | 28 | def self.required_fields(_klass) 29 | [:current_sign_in_at] 30 | end 31 | 32 | # Has the session reached its maximum allowed lifespan? 33 | def max_session_lifetime_exceeded? 34 | return false if max_session_lifetime.nil? 35 | return false if current_sign_in_at.nil? 36 | current_sign_in_at <= max_session_lifetime.ago 37 | end 38 | 39 | private 40 | 41 | def max_session_lifetime 42 | Authenticate.configuration.max_session_lifetime 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/authenticate/model/password_reset.rb: -------------------------------------------------------------------------------- 1 | module Authenticate 2 | module Model 3 | # Support 'forgot my password' functionality. 4 | # 5 | # = Columns 6 | # * password_reset_token - token required to reset a password 7 | # * password_reset_sent_at - datetime password reset token was emailed to user 8 | # * email - email address of user 9 | # 10 | # = Methods 11 | # * update_password(new_password) - call password setter below, generate a new session token if user.valid?, & save 12 | # * forgot_password! - generate a new password reset token, timestamp, and save 13 | # * reset_password_period_valid? - is the password reset token still usable? 14 | # 15 | module PasswordReset 16 | extend ActiveSupport::Concern 17 | 18 | def self.required_fields(_klass) 19 | [:password_reset_token, :password_reset_sent_at, :email] 20 | end 21 | 22 | # Sets the user's password to the new value. The new password will be encrypted with 23 | # the selected encryption scheme (defaults to Bcrypt). 24 | # 25 | # Updating the user password also generates a new session token. 26 | # 27 | # Validations will be run as part of this update. If the user instance is 28 | # not valid, the password change will not be persisted, and this method will 29 | # return `false`. 30 | # 31 | # @return [Boolean] Was the save successful? 32 | def update_password(new_password) 33 | return false unless reset_password_period_valid? 34 | self.password = new_password 35 | if valid? 36 | clear_reset_password_token 37 | generate_session_token 38 | end 39 | save 40 | end 41 | 42 | # Generates a {#password_reset_token} for the user, which allows them to reset 43 | # their password via an email link. 44 | # 45 | # The user model is saved without validations. Any other changes you made to 46 | # this user instance will also be persisted, without validation. 47 | # It is intended to be called on an instance with no changes (`dirty? == false`). 48 | # 49 | # @return [Boolean] Was the save successful? 50 | def forgot_password! 51 | self.password_reset_token = Authenticate::Token.new 52 | self.password_reset_sent_at = Time.now.utc 53 | save validate: false 54 | end 55 | 56 | # Checks if the reset password token is within the time limit. 57 | # If the application's reset_password_within is nil, then always return true. 58 | # 59 | # Example: 60 | # # reset_password_within = 1.day and reset_password_sent_at = today 61 | # reset_password_period_valid? # returns true 62 | # 63 | def reset_password_period_valid? 64 | reset_within = Authenticate.configuration.reset_password_within 65 | return true if reset_within.nil? 66 | return true if password_reset_sent_at.nil? && password_reset_token.nil? 67 | password_reset_sent_at && password_reset_sent_at.utc >= reset_within.ago.utc 68 | end 69 | 70 | private 71 | 72 | def clear_reset_password_token 73 | self.password_reset_token = nil 74 | self.password_reset_sent_at = nil 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/authenticate/model/timeoutable.rb: -------------------------------------------------------------------------------- 1 | require 'authenticate/callbacks/timeoutable' 2 | 3 | module Authenticate 4 | module Model 5 | # Expire user sessions that have not been accessed within a certain period of time. 6 | # Expired users will be asked for credentials again. 7 | # 8 | # Timeoutable is enabled and configured with the `timeout_in` configuration parameter. 9 | # Example: 10 | # 11 | # Authenticate.configure do |config| 12 | # config.timeout_in = 15.minutes 13 | # end 14 | # 15 | # = Columns 16 | # This module expects and tracks this column on your user model: 17 | # * last_access_at - datetime of the last access by the user 18 | # 19 | # = Configuration 20 | # * timeout_in - maximum idle time allowed before session is invalidated. nil shuts off this feature. 21 | # 22 | # You must specify a non-nil timeout_in in your initializer to enable Timeoutable. 23 | # 24 | # = Methods 25 | # * timedout? - has this user timed out? @return[Boolean] 26 | # * timeout_in - look up timeout period in config, @return [ActiveSupport::CoreExtensions::Numeric::Time] 27 | # 28 | module Timeoutable 29 | extend ActiveSupport::Concern 30 | 31 | def self.required_fields(_klass) 32 | [:last_access_at] 33 | end 34 | 35 | # Checks whether the user session has expired based on configured time. 36 | def timedout? 37 | return false if timeout_in.nil? 38 | return false if last_access_at.nil? 39 | last_access_at <= timeout_in.ago 40 | end 41 | 42 | private 43 | 44 | def timeout_in 45 | Authenticate.configuration.timeout_in 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/authenticate/model/trackable.rb: -------------------------------------------------------------------------------- 1 | require 'authenticate/callbacks/trackable' 2 | 3 | module Authenticate 4 | module Model 5 | # 6 | # Track information about your user sign ins. This module is always enabled. 7 | # 8 | # = Methods 9 | # * update_tracked_fields - update the user's tracked fields based on the request. 10 | # * update_tracked_fields! - update tracked fields and save immediately, bypassing validations 11 | # 12 | # = Columns 13 | # - sign_in_count - increase every time a sign in is successful 14 | # - current_sign_in_at - a timestamp updated at each sign in 15 | # - last_sign_in_at - a timestamp of the previous sign in 16 | # - current_sign_in_ip - the remote ip address of the user at sign in 17 | # - previous_sign_in_ip - the remote ip address of the previous sign in 18 | # 19 | module Trackable 20 | extend ActiveSupport::Concern 21 | 22 | def self.required_fields(_klass) 23 | [:current_sign_in_at, :current_sign_in_ip, :last_sign_in_at, :last_sign_in_ip, :sign_in_count] 24 | end 25 | 26 | def update_tracked_fields(request) 27 | old_current = current_sign_in_at 28 | new_current = Time.now.utc 29 | self.last_sign_in_at = old_current || new_current 30 | self.current_sign_in_at = new_current 31 | 32 | old_current = current_sign_in_ip 33 | new_current = request.remote_ip 34 | self.last_sign_in_ip = old_current || new_current 35 | self.current_sign_in_ip = new_current 36 | 37 | self.sign_in_count ||= 0 38 | self.sign_in_count += 1 39 | end 40 | 41 | def update_tracked_fields!(request) 42 | update_tracked_fields(request) 43 | save(validate: false) 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/authenticate/model/username.rb: -------------------------------------------------------------------------------- 1 | module Authenticate 2 | module Model 3 | # 4 | # Use :username as the identifier for the user. Username must be unique. 5 | # 6 | # = Columns 7 | # * username - the username of your user 8 | # 9 | # = Validations 10 | # * :username requires username is set, ensure it is unique 11 | # 12 | # = class methods 13 | # * credentials(params) - return the credentials required for authorization by username 14 | # * authenticate(credentials) - find user with given username, validate their password, return user if authenticated 15 | # * find_by_credentials(credentials) - find and return the user with the username in the credentials 16 | # 17 | module Username 18 | extend ActiveSupport::Concern 19 | 20 | def self.required_fields(_klass) 21 | [:username, :email] 22 | end 23 | 24 | included do 25 | # before_validation :normalize_username 26 | validates :username, 27 | presence: true, 28 | uniqueness: { allow_blank: true } 29 | end 30 | 31 | # Class methods for managing username-based authentication 32 | module ClassMethods 33 | def credentials(params) 34 | [params[:session][:username], params[:session][:password]] 35 | end 36 | 37 | def authenticate(credentials) 38 | user = find_by_credentials(credentials) 39 | user && user.password_match?(credentials[1]) ? user : nil 40 | end 41 | 42 | def find_by_credentials(credentials) 43 | username = credentials[0] 44 | find_by_username username 45 | end 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/authenticate/modules.rb: -------------------------------------------------------------------------------- 1 | module Authenticate 2 | # 3 | # Modules injects Authenticate modules into the app User model. 4 | # 5 | # Modules are loaded into the user model with class method `load_modules`. Authenticate::User calls 6 | # `load_modules`. Modules are specified in the config as constants; `load_modules` requires them, 7 | # turns them into constants, checks for required fields (see below), and then includes them into the user. 8 | # 9 | # Any module being loaded into User can optionally define a class method `required_fields(klass)` defining 10 | # any required attributes in the User model. For example, the :username module declares: 11 | # 12 | # module Username 13 | # extend ActiveSupport::Concern 14 | # 15 | # def self.required_fields(klass) 16 | # [:username] 17 | # end 18 | # ... 19 | # 20 | # If the model class is missing a required field, Authenticate will fail with a MissingAttribute error. 21 | # The error will declare what required fields are missing. 22 | # 23 | # 24 | module Modules 25 | extend ActiveSupport::Concern 26 | # 27 | # Class methods injected into User model. 28 | # 29 | module ClassMethods 30 | # 31 | # Load all modules declared in Authenticate.configuration.modules. 32 | # Requires them, then loads as a constant, then checks fields, and finally includes. 33 | # 34 | # @raise MissingAttribute if attributes required by Authenticate are missing. 35 | def load_modules 36 | modules_to_include = [] 37 | Authenticate.configuration.modules.each do |mod| 38 | # The built-in modules are referred to by symbol. Additional module classes (constants) can be added 39 | # via Authenticate.configuration.modules. 40 | require "authenticate/model/#{mod}" if mod.is_a?(Symbol) 41 | mod = load_constant(mod) if mod.is_a?(Symbol) 42 | modules_to_include << mod 43 | end 44 | check_fields modules_to_include 45 | modules_to_include.each { |mod| include mod } 46 | end 47 | 48 | private 49 | 50 | def load_constant(module_symbol) 51 | Authenticate::Model.const_get(module_symbol.to_s.classify) 52 | end 53 | 54 | # For each module, look at the fields it requires. Ensure the User 55 | # model including the module has the required fields. 56 | # @raise MissingAttribute if required attributes are missing. 57 | def check_fields(modules) 58 | failed_attributes = [] 59 | instance = new 60 | modules.each do |mod| 61 | if mod.respond_to?(:required_fields) 62 | mod.required_fields(self).each { |field| failed_attributes << field unless instance.respond_to?(field) } 63 | end 64 | end 65 | 66 | if failed_attributes.any? 67 | raise MissingAttribute.new(failed_attributes), 68 | "Required attribute are missing on your user model: #{failed_attributes.join(', ')}" 69 | end 70 | end 71 | end 72 | 73 | # Thrown if required attributes are missing. 74 | class MissingAttribute < StandardError 75 | def initialize(attributes) 76 | @attributes = attributes 77 | end 78 | 79 | def message 80 | "Required attributes are missing on your user model: #{@attributes.join(', ')}" 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/authenticate/session.rb: -------------------------------------------------------------------------------- 1 | require 'authenticate/login_status' 2 | require 'authenticate/debug' 3 | 4 | module Authenticate 5 | # Represents an Authenticate session. 6 | class Session 7 | include Debug 8 | 9 | attr_accessor :request 10 | 11 | # Initialize an Authenticate session. 12 | # 13 | # The presence of a session does NOT mean the user is logged in; call #logged_in? to determine login status. 14 | def initialize(request) 15 | @request = request # trackable module accesses request 16 | @cookies = request.cookie_jar 17 | @session_token = @cookies[cookie_name] 18 | debug 'SESSION initialize: @session_token: ' + @session_token.inspect 19 | end 20 | 21 | # Finish user login process, *after* the user has been authenticated. 22 | # The user is authenticated by Authenticate::Controller#authenticate. 23 | # 24 | # Called when user creates an account or signs back into the app. 25 | # Runs all configured callbacks, checking for login failure. 26 | # 27 | # If login is successful, @current_user is set and a session token is generated 28 | # and returned to the client browser. 29 | # If login fails, the user is NOT logged in. No session token is set, 30 | # and @current_user will not be set. 31 | # 32 | # After callbacks are finished, a {LoginStatus} is yielded to the provided block, 33 | # if one is provided. 34 | # 35 | # @param [User] user login completed for this user 36 | # @yieldparam [Success,Failure] status result of the sign in operation. 37 | # @return [User] 38 | def login(user) 39 | @current_user = user 40 | @current_user.generate_session_token if user.present? 41 | 42 | message = catch(:failure) do 43 | Authenticate.lifecycle.run_callbacks(:after_set_user, @current_user, self, event: :authentication) 44 | Authenticate.lifecycle.run_callbacks(:after_authentication, @current_user, self, event: :authentication) 45 | end 46 | 47 | status = message.present? ? Failure.new(message) : Success.new 48 | if status.success? 49 | @current_user.save 50 | write_cookie if @current_user.session_token 51 | else 52 | @current_user = nil 53 | end 54 | 55 | yield(status) if block_given? 56 | end 57 | 58 | # Get the user represented by this session. 59 | # 60 | # @return [User] 61 | def current_user 62 | debug "session.current_user #{@current_user.inspect}" 63 | @current_user ||= load_user_from_session_token if @session_token.present? 64 | @current_user 65 | end 66 | 67 | # Has this user successfully logged in? 68 | # 69 | # @return [Boolean] 70 | def logged_in? 71 | debug "session.logged_in? #{current_user.present?}" 72 | current_user.present? 73 | end 74 | 75 | # Invalidate the session token, unset the current user and remove the cookie. 76 | # 77 | # @return [void] 78 | def logout 79 | # nuke session_token in db 80 | current_user.reset_session_token! if current_user.present? 81 | 82 | # nuke notion of current_user 83 | @current_user = nil 84 | 85 | # nuke session_token cookie from the client browser 86 | @cookies.delete cookie_name 87 | end 88 | 89 | private 90 | 91 | def write_cookie 92 | cookie_hash = { 93 | path: Authenticate.configuration.cookie_path, 94 | secure: Authenticate.configuration.secure_cookie, 95 | httponly: Authenticate.configuration.cookie_http_only, 96 | value: @current_user.session_token, 97 | expires: Authenticate.configuration.cookie_expiration.call 98 | } 99 | cookie_hash[:domain] = Authenticate.configuration.cookie_domain if Authenticate.configuration.cookie_domain 100 | # Consider adding an option for a signed cookie 101 | @cookies[cookie_name] = cookie_hash 102 | end 103 | 104 | def cookie_name 105 | Authenticate.configuration.cookie_name.freeze.to_sym 106 | end 107 | 108 | def load_user_from_session_token 109 | Authenticate.configuration.user_model_class.where(session_token: @session_token).first 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/authenticate/testing/controller_helpers.rb: -------------------------------------------------------------------------------- 1 | module Authenticate 2 | module Testing 3 | 4 | # Helpers for controller tests/specs. 5 | # 6 | # Example: 7 | # 8 | # describe DashboardsController do 9 | # describe '#show' do 10 | # it 'shows view' do 11 | # user = create(:user) 12 | # login_as(user) 13 | # get :show 14 | # expect(response).to be_success 15 | # end 16 | # end 17 | # end 18 | module ControllerHelpers 19 | def login_as(user) 20 | controller.login(user) 21 | end 22 | 23 | def logout 24 | controller.logout 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/authenticate/testing/integration_tests_sign_on.rb: -------------------------------------------------------------------------------- 1 | module Authenticate 2 | module Testing 3 | 4 | # Middleware which allows tests to bypass your sign on screen. 5 | # Typically used by integration and feature tests, etc. 6 | # Speeds up these tests by eliminating the need to visit and 7 | # submit the signon form repeatedly. 8 | # 9 | # Sign a test user in by passing as=USER_ID in a query parameter. 10 | # If `User#to_param` is overridden you may pass a block to override 11 | # the default user lookup behaviour. 12 | # 13 | # Configure your application's test environment as follows: 14 | # 15 | # # config/environments/test.rb 16 | # MyRailsApp::Application.configure do 17 | # # ... 18 | # config.middleware.use Authenticate::IntegrationTestsSignOn 19 | # # ... 20 | # end 21 | # 22 | # or if `User#to_param` is overridden (to `username` for example): 23 | # 24 | # # config/environments/test.rb 25 | # MyRailsApp::Application.configure do 26 | # # ... 27 | # config.middleware.use Authenticate::IntegrationTestsSignOn do |username| 28 | # User.find_by(username: username) 29 | # end 30 | # # ... 31 | # end 32 | # 33 | # After configuring your app, usage in an integration tests is simple: 34 | # 35 | # user = ... # load user 36 | # visit dashboard_path(as: user) 37 | # 38 | class IntegrationTestsSignOn 39 | def initialize(app, &block) 40 | @app = app 41 | @block = block 42 | end 43 | 44 | def call(env) 45 | do_login(env) 46 | @app.call(env) 47 | end 48 | 49 | private 50 | 51 | def do_login(env) 52 | params = Rack::Utils.parse_query(env['QUERY_STRING']) 53 | user_param = params['as'] 54 | 55 | user = find_user(user_param) if user_param.present? 56 | if user.present? 57 | user.generate_session_token && user.save if user.session_token.nil? 58 | request = Rack::Request.new(env) 59 | request.cookies[Authenticate.configuration.cookie_name] = user.session_token 60 | end 61 | end 62 | 63 | def find_user(user_param) 64 | if @block 65 | @block.call(user_param) 66 | else 67 | Authenticate.configuration.user_model_class.find(user_param) 68 | end 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/authenticate/testing/rspec.rb: -------------------------------------------------------------------------------- 1 | require 'authenticate/testing/controller_helpers' 2 | require 'authenticate/testing/view_helpers' 3 | 4 | RSpec.configure do |config| 5 | config.include Authenticate::Testing::ControllerHelpers, type: :controller 6 | config.include Authenticate::Testing::ViewHelpers, type: :view 7 | config.before(:each, type: :view) do 8 | view.extend Authenticate::Testing::ViewHelpers::CurrentUser 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/authenticate/testing/test_unit.rb: -------------------------------------------------------------------------------- 1 | require 'authenticate/testing/controller_helpers' 2 | 3 | # Support for test unit. 4 | # 5 | # As of Rails 5, controller tests subclass `ActionDispatch::IntegrationTest` and should use 6 | # IntegrationTestsSignOn to bypass the sign on screen. 7 | class ActionController::TestCase 8 | include Authenticate::Testing::ControllerHelpers 9 | end 10 | -------------------------------------------------------------------------------- /lib/authenticate/testing/view_helpers.rb: -------------------------------------------------------------------------------- 1 | module Authenticate 2 | module Testing 3 | 4 | # Helpers for view tests/specs. 5 | # 6 | # Use login_as to log in a user for your test case, which allows 7 | # `current_user`, `logged_in?` and `logged_out?` to work properly in your test. 8 | module ViewHelpers 9 | 10 | # Set the current_user on the view being tested. 11 | def login_as(user) 12 | view.current_user = user 13 | end 14 | 15 | module CurrentUser 16 | attr_accessor :current_user 17 | 18 | def logged_in? 19 | current_user.present? 20 | end 21 | 22 | def logged_out? 23 | !logged_in? 24 | end 25 | end 26 | 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/authenticate/token.rb: -------------------------------------------------------------------------------- 1 | module Authenticate 2 | # 3 | # A secure token, consisting of a big random number. 4 | # 5 | class Token 6 | def self.new 7 | SecureRandom.hex(20).encode('UTF-8') 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/authenticate/user.rb: -------------------------------------------------------------------------------- 1 | require 'authenticate/configuration' 2 | require 'authenticate/token' 3 | require 'authenticate/callbacks/authenticatable' 4 | 5 | module Authenticate 6 | # Required to be included in your configured user class, which is `User` by 7 | # default, but can be changed with {Configuration#user_model=}. 8 | # 9 | # class User 10 | # include Authenticate::User 11 | # # ... 12 | # end 13 | # 14 | # To change the user class from the default User, assign it : 15 | # 16 | # Authenticate.configure do |config| 17 | # config.user_model = 'MyPackage::Gundan' 18 | # end 19 | # 20 | # The fields and methods included by Authenticate::User will depend on what modules you have included in your 21 | # configuration. When your user class is loaded, User will load any modules at that time. If you have another 22 | # initializer that loads User before Authenticate's initializer has run, this may cause interfere with the 23 | # configuration of your user. 24 | # 25 | # Every user will have two methods to manage session tokens: 26 | # - generate_session_token - generates and sets the Authenticate session token 27 | # - reset_session_token! - calls generate_session_token and save! immediately 28 | # 29 | # Every user will have these two class methods to normalize email addresses: 30 | # - normalize_email(email) - normalize the given email address by downcasing, removing spaces. 31 | # - find_by_normalized_email(email) - find a user by his/her normalized email address 32 | module User 33 | extend ActiveSupport::Concern 34 | 35 | included do 36 | include Modules 37 | load_modules 38 | end 39 | 40 | # Generate a new session token for the user, overwriting the existing session token, if any. 41 | # This is not automatically persisted; call {#reset_session_token!} to automatically 42 | # generate and persist the session token update. 43 | def generate_session_token 44 | self.session_token = Authenticate::Token.new 45 | end 46 | 47 | # Generate a new session token and persist the change, ignoring validations. 48 | # This effectively signs out all existing sessions. Called as part of logout. 49 | def reset_session_token! 50 | generate_session_token 51 | save validate: false 52 | end 53 | 54 | # Class methods added to users. 55 | module ClassMethods 56 | def normalize_email(email) 57 | email.to_s.downcase.gsub(/\s+/, '') 58 | end 59 | 60 | # We need to find users by email even if they don't use email to log in 61 | def find_by_normalized_email(email) 62 | find_by_email normalize_email(email) 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/authenticate/version.rb: -------------------------------------------------------------------------------- 1 | module Authenticate 2 | VERSION = '0.7.3'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /lib/generators/authenticate/controllers/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Override the default authentication controllers and mailers. This generator will copy all of the 3 | base authenticate controllers and mailers into your project. 4 | 5 | Examples: 6 | rails generate authenticate:controllers 7 | 8 | View: app/controllers/authenticate/passwords_controller.rb 9 | View: app/controllers/authenticate/sessions_controller.rb 10 | View: app/controllers/authenticate/users_controller.rb 11 | View: app/mailers/authenticate_mailer.rb 12 | 13 | -------------------------------------------------------------------------------- /lib/generators/authenticate/controllers/controllers_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/base' 2 | 3 | # 4 | # deploy view and locale assets 5 | # 6 | module Authenticate 7 | module Generators 8 | class ControllersGenerator < Rails::Generators::Base 9 | source_root File.expand_path('../../../../..', __FILE__) 10 | 11 | def create_controllers 12 | directory 'app/controllers' 13 | end 14 | 15 | def create_mailers 16 | directory 'app/mailers' 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/generators/authenticate/helpers.rb: -------------------------------------------------------------------------------- 1 | module Authenticate 2 | module Generators 3 | module Helpers 4 | private 5 | 6 | # Either return the model passed in a classified form or return the default "User". 7 | def model_class_name 8 | options[:model] ? options[:model].classify : 'User' 9 | end 10 | 11 | def model_path 12 | @model_path ||= File.join('app', 'models', "#{file_path}.rb") 13 | end 14 | 15 | def file_path 16 | model_name.underscore 17 | end 18 | 19 | def namespace 20 | Rails::Generators.namespace if Rails::Generators.respond_to?(:namespace) 21 | end 22 | 23 | def namespaced? 24 | !namespace.nil? 25 | end 26 | 27 | def model_name 28 | if namespaced? 29 | [namespace.to_s] + [model_class_name] 30 | else 31 | [model_class_name] 32 | end.join('::') 33 | end 34 | 35 | def table_name 36 | @table_name ||= begin 37 | base = plural_name 38 | (class_path + [base]).join('_') 39 | end 40 | end 41 | 42 | def class_path 43 | @class_path 44 | end 45 | 46 | def singular_name 47 | @file_name 48 | end 49 | 50 | def plural_name 51 | singular_name.pluralize 52 | end 53 | 54 | def assign_names!(name) #:nodoc: 55 | @class_path = name.include?('/') ? name.split('/') : name.split('::') 56 | @class_path.map!(&:underscore) 57 | @file_name = @class_path.pop 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/generators/authenticate/install/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Explain the generator 3 | 4 | Example: 5 | rails generate authenticate:install 6 | 7 | This will create: 8 | -------------------------------------------------------------------------------- /lib/generators/authenticate/install/install_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/base' 2 | require 'rails/generators/active_record' 3 | require 'generators/authenticate/helpers' 4 | 5 | module Authenticate 6 | module Generators 7 | class InstallGenerator < Rails::Generators::Base 8 | include Rails::Generators::Migration 9 | include Authenticate::Generators::Helpers 10 | 11 | source_root File.expand_path('../templates', __FILE__) 12 | class_option :model, 13 | optional: true, 14 | type: :string, 15 | banner: 'model', 16 | desc: "Specify the model class name if you will use anything other than 'User'" 17 | 18 | class_option :allow_sign_up, 19 | optional: true, 20 | type: :boolean, 21 | banner: 'allow_sign_up', 22 | desc: 'Disable the sign up route' 23 | 24 | def initialize(*) 25 | super 26 | assign_names!(model_class_name) 27 | end 28 | 29 | def verify 30 | if options[:model] && !File.exist?(model_path) 31 | puts "Exiting: the model class you specified, #{options[:model]}, is not found." 32 | exit 1 33 | end 34 | end 35 | 36 | def create_or_inject_into_user_model 37 | if File.exist? model_path 38 | inject_into_class(model_path, model_class_name, " include Authenticate::User\n\n") 39 | else 40 | @model_base_class = model_base_class 41 | template 'user.rb.erb', 'app/models/user.rb' 42 | end 43 | end 44 | 45 | def create_authenticate_user_migration 46 | if users_table_exists? 47 | create_add_columns_migration 48 | else 49 | create_new_users_migration 50 | end 51 | end 52 | 53 | def copy_migration_files 54 | copy_migration 'add_authenticate_brute_force_to_users.rb' 55 | copy_migration 'add_authenticate_timeoutable_to_users.rb' 56 | copy_migration 'add_authenticate_password_reset_to_users.rb' 57 | end 58 | 59 | def inject_into_application_controller 60 | inject_into_class( 61 | 'app/controllers/application_controller.rb', 62 | ApplicationController, 63 | " include Authenticate::Controller\n\n" 64 | ) 65 | end 66 | 67 | def create_initializer 68 | copy_file 'authenticate.rb', 'config/initializers/authenticate.rb' 69 | if options[:model] 70 | inject_into_file( 71 | 'config/initializers/authenticate.rb', 72 | " config.user_model = '#{options[:model]}'\n", 73 | after: "Authenticate.configure do |config|\n" 74 | ) 75 | end 76 | 77 | if options.key? :allow_sign_up 78 | inject_into_file( 79 | 'config/initializers/authenticate.rb', 80 | " config.allow_sign_up = #{options['allow_sign_up']}\n", 81 | after: "Authenticate.configure do |config|\n" 82 | ) 83 | end 84 | end 85 | 86 | private 87 | 88 | def create_new_users_migration 89 | config = { 90 | new_columns: new_columns, 91 | new_indexes: new_indexes 92 | } 93 | copy_migration 'create_users.rb', config 94 | end 95 | 96 | def create_add_columns_migration 97 | if migration_needed? 98 | config = { 99 | new_columns: new_columns, 100 | new_indexes: new_indexes 101 | } 102 | copy_migration('add_authenticate_to_users.rb', config) 103 | end 104 | end 105 | 106 | def copy_migration(migration_name, config = {}) 107 | unless migration_exists?(migration_name) 108 | migration_template( 109 | "db/migrate/#{migration_name}", 110 | "db/migrate/#{migration_name}", 111 | config.merge(migration_version: migration_version) 112 | ) 113 | end 114 | end 115 | 116 | def migration_needed? 117 | new_columns.any? || new_indexes.any? 118 | end 119 | 120 | def new_columns 121 | @new_columns ||= { 122 | email: 't.string :email', 123 | encrypted_password: 't.string :encrypted_password, limit: 128', 124 | session_token: 't.string :session_token, limit: 128', 125 | 126 | # trackable, lifetimed 127 | current_sign_in_at: 't.datetime :current_sign_in_at', 128 | current_sign_in_ip: 't.string :current_sign_in_ip, limit: 128', 129 | last_sign_in_at: 't.datetime :last_sign_in_at', 130 | last_sign_in_ip: 't.string :last_sign_in_ip, limit: 128', 131 | sign_in_count: 't.integer :sign_in_count' 132 | }.reject { |column| existing_users_columns.include?(column.to_s) } 133 | end 134 | 135 | def new_indexes 136 | @new_indexes ||= { 137 | index_users_on_email: "add_index :#{table_name}, :email, unique: true", 138 | index_users_on_session_token: "add_index :#{table_name}, :session_token" 139 | }.reject { |index| existing_users_indexes.include?(index.to_s) } 140 | end 141 | 142 | def migration_exists?(name) 143 | existing_migrations.include?(name) 144 | end 145 | 146 | def existing_migrations 147 | @existing_migrations ||= Dir.glob('db/migrate/*.rb').map do |file| 148 | migration_name_without_timestamp(file) 149 | end 150 | end 151 | 152 | def migration_name_without_timestamp(file) 153 | file.sub(%r{^.*(db/migrate/)(?:\d+_)?}, '') 154 | end 155 | 156 | # def users_table_exists? 157 | # ActiveRecord::Base.connection.table_exists?(table_name) 158 | # end 159 | 160 | def users_table_exists? 161 | # Rails 5 uses 'data sources' 162 | if ActiveRecord::Base.connection.respond_to?(:data_source_exists?) 163 | ActiveRecord::Base.connection.data_source_exists?(table_name) 164 | else 165 | # Rails 4 uses 'tables' 166 | ActiveRecord::Base.connection.table_exists?(table_name) 167 | end 168 | end 169 | 170 | 171 | def existing_users_columns 172 | return [] unless users_table_exists? 173 | ActiveRecord::Base.connection.columns(table_name).map(&:name) 174 | end 175 | 176 | def existing_users_indexes 177 | return [] unless users_table_exists? 178 | ActiveRecord::Base.connection.indexes(table_name).map(&:name) 179 | end 180 | 181 | # for generating a timestamp when using `create_migration` 182 | def self.next_migration_number(dir) 183 | ActiveRecord::Generators::Base.next_migration_number(dir) 184 | end 185 | 186 | def model_base_class 187 | (Rails.version >= '5.0.0') ? 'ApplicationRecord' : 'ActiveRecord::Base' 188 | end 189 | 190 | def migration_version 191 | if Rails.version >= '5.0.0' 192 | "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]" 193 | end 194 | end 195 | end 196 | end 197 | end 198 | -------------------------------------------------------------------------------- /lib/generators/authenticate/install/templates/authenticate.rb: -------------------------------------------------------------------------------- 1 | Authenticate.configure do |config| 2 | config.rotate_csrf_on_sign_in = true 3 | 4 | # config.user_model = 'User' 5 | # config.cookie_name = 'authenticate_session_token' 6 | # config.cookie_expiration = { 1.month.from_now.utc } 7 | # config.cookie_domain = nil 8 | # config.cookie_path = '/' 9 | # config.secure_cookie = false # set to true in production https environments 10 | # config.cookie_http_only = false # set to true if you can 11 | # config.mailer_sender = 'reply@example.com' 12 | # config.crypto_provider = Authenticate::Model::BCrypt 13 | # config.timeout_in = 45.minutes 14 | # config.max_session_lifetime = 8.hours 15 | # config.max_consecutive_bad_logins_allowed = 4 16 | # config.bad_login_lockout_period = 10.minutes 17 | # config.password_length = 8..128 18 | # config.authentication_strategy = :email 19 | # config.redirect_url = '/' 20 | # config.allow_sign_up = true 21 | # config.routes = true 22 | # config.reset_password_within = 2.days 23 | # config.modules = [] 24 | end 25 | -------------------------------------------------------------------------------- /lib/generators/authenticate/install/templates/db/migrate/add_authenticate_brute_force_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddAuthenticateBruteForceToUsers < ActiveRecord::Migration<%= migration_version %> 2 | def change 3 | add_column :<%= table_name %>, :failed_logins_count, :integer, default: 0 4 | add_column :<%= table_name %>, :lock_expires_at, :datetime, default: nil 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/generators/authenticate/install/templates/db/migrate/add_authenticate_password_reset_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddAuthenticatePasswordResetToUsers < ActiveRecord::Migration<%= migration_version %> 2 | def change 3 | add_column :<%= table_name %>, :password_reset_token, :string, default: nil 4 | add_column :<%= table_name %>, :password_reset_sent_at, :datetime, default: nil 5 | add_index :<%= table_name %>, :password_reset_token 6 | end 7 | end 8 | 9 | -------------------------------------------------------------------------------- /lib/generators/authenticate/install/templates/db/migrate/add_authenticate_timeoutable_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddAuthenticateTimeoutableToUsers < ActiveRecord::Migration<%= migration_version %> 2 | def change 3 | add_column :<%= table_name %>, :last_access_at, :datetime, default: nil 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/generators/authenticate/install/templates/db/migrate/add_authenticate_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddAuthenticateToUsers < ActiveRecord::Migration<%= migration_version %> 2 | def self.up 3 | change_table :<%= table_name %> do |t| 4 | <% config[:new_columns].values.each do |column| -%> 5 | <%= column %> 6 | <% end -%> 7 | end 8 | 9 | <% config[:new_indexes].values.each do |index| -%> 10 | <%= index %> 11 | <% end -%> 12 | end 13 | 14 | def self.down 15 | change_table :<%= table_name %> do |t| 16 | <% if config[:new_columns].any? -%> 17 | t.remove <%= new_columns.keys.map { |column| ":#{column}" }.join(", ") %> 18 | <% end -%> 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/generators/authenticate/install/templates/db/migrate/create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration<%= migration_version %> 2 | def change 3 | create_table :<%= table_name %> do |t| 4 | <% config[:new_columns].values.each do |column| -%> 5 | <%= column %> 6 | <% end -%> 7 | end 8 | 9 | <% config[:new_indexes].values.each do |index| -%> 10 | <%= index %> 11 | <% end -%> 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/generators/authenticate/install/templates/user.rb.erb: -------------------------------------------------------------------------------- 1 | class User < <%= @model_base_class %> 2 | include Authenticate::User 3 | end 4 | -------------------------------------------------------------------------------- /lib/generators/authenticate/routes/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Inject the authenticate routes into your `config/routes.rb`. Also turns off authenticate's built-in routes 3 | by adding `config.routes = false` into your `config/initializers/authenticate.rb`. 4 | 5 | Examples: 6 | rails generate authenticate:routes 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/generators/authenticate/routes/routes_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/base' 2 | require 'generators/authenticate/helpers' 3 | 4 | module Authenticate 5 | module Generators 6 | class RoutesGenerator < Rails::Generators::Base 7 | include Authenticate::Generators::Helpers 8 | 9 | source_root File.expand_path('../templates', __FILE__) 10 | 11 | def add_authenticate_routes 12 | route(authenticate_routes) 13 | end 14 | 15 | def disable_authenticate_internal_routes 16 | inject_into_file( 17 | 'config/initializers/authenticate.rb', 18 | " config.routes = false \n", 19 | after: "Authenticate.configure do |config|\n" 20 | ) 21 | end 22 | 23 | private 24 | 25 | def authenticate_routes 26 | @user_model = Authenticate.configuration.user_model_route_key 27 | ERB.new(File.read(routes_file_path)).result(binding) 28 | end 29 | 30 | def routes_file_path 31 | File.expand_path(find_in_source_paths('routes.rb')) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/generators/authenticate/routes/templates/routes.rb: -------------------------------------------------------------------------------- 1 | resource :session, controller: 'authenticate/sessions', only: [:create, :new, :destroy] 2 | resources :passwords, controller: 'authenticate/passwords', only: [:new, :create] 3 | 4 | resource :<%= @user_model %>, controller: 'authenticate/users', only: [:new, :create] do 5 | resources :passwords, controller: 'authenticate/passwords', only: [:edit, :update] 6 | end 7 | 8 | get '/sign_up', to: 'authenticate/users#new', as: 'sign_up' 9 | get '/sign_in', to: 'authenticate/sessions#new', as: 'sign_in' 10 | get '/sign_out', to: 'authenticate/sessions#destroy', as: 'sign_out' 11 | -------------------------------------------------------------------------------- /lib/generators/authenticate/views/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Override the default authenticate views and locale file. This generator will copy all of the 3 | base authenticate views and locale file into your project. 4 | 5 | Examples: 6 | rails generate authenticate:views 7 | 8 | View: app/views/authenticate_mailer/change_password.html.erb 9 | View: app/views/layouts/application.html.erb 10 | View: app/views/passwords/edit.html.erb 11 | View: app/views/passwords/new.html.erb 12 | View: app/views/sessions/new.html.erb 13 | View: app/views/users/new.html.erb 14 | Locale: config/locales/en.yml 15 | -------------------------------------------------------------------------------- /lib/generators/authenticate/views/views_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/base' 2 | 3 | # 4 | # deploy view and locale assets 5 | # 6 | module Authenticate 7 | module Generators 8 | class ViewsGenerator < Rails::Generators::Base 9 | source_root File.expand_path('../../../../..', __FILE__) 10 | 11 | def create_views 12 | directory 'app/views' 13 | end 14 | 15 | def create_locales 16 | directory 'config/locales' 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/tasks/authenticate_tasks.rake: -------------------------------------------------------------------------------- 1 | # desc "Explaining what the task does" 2 | # task :authenticate do 3 | # # Task goes here 4 | # end 5 | -------------------------------------------------------------------------------- /spec/controllers/deprecated_controller_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | # Matcher that asserts user was denied access. 4 | RSpec::Matchers.define :deny_access do 5 | match do |controller| 6 | redirects_to_sign_in?(controller) && sets_flash?(controller) 7 | end 8 | 9 | def redirects_to_sign_in?(controller) 10 | expect(controller).to redirect_to(controller.sign_in_url) 11 | end 12 | 13 | def sets_flash?(controller) 14 | controller.flash[:notice].match(/sign in to continue/) 15 | end 16 | end 17 | 18 | # A dummy 'secured' controller to test 19 | class DeprecatedMethodsController < ActionController::Base 20 | include Authenticate::Controller 21 | before_action :require_authentication, only: :show 22 | 23 | def new 24 | head :ok 25 | end 26 | 27 | def show 28 | head :ok 29 | end 30 | end 31 | 32 | describe DeprecatedMethodsController, type: :controller do 33 | before do 34 | Rails.application.routes.draw do 35 | resource :deprecated_methods, only: [:new, :show] 36 | get '/sign_in' => 'authenticate/sessions#new', as: 'sign_in' 37 | end 38 | end 39 | 40 | after do 41 | Rails.application.reload_routes! 42 | end 43 | 44 | context 'with authenticated user' do 45 | before { sign_in } 46 | 47 | it 'warns but allows access to show' do 48 | expect { do_get :show }.to output(/deprecated/i).to_stderr 49 | expect(subject).to_not deny_access 50 | end 51 | 52 | it 'warns on authenticated?' do 53 | expect { subject.authenticated? }.to output(/deprecated/i).to_stderr 54 | end 55 | 56 | it 'authenticates' do 57 | silence do 58 | expect(subject.authenticated?).to be_truthy 59 | end 60 | end 61 | end 62 | end 63 | 64 | 65 | def silence 66 | # Store the original stderr and stdout in order to restore them later 67 | @original_stderr = $stderr 68 | @original_stdout = $stdout 69 | 70 | # Redirect stderr and stdout 71 | $stderr = $stdout = StringIO.new 72 | 73 | yield 74 | 75 | $stderr = @original_stderr 76 | $stdout = @original_stdout 77 | @original_stderr = nil 78 | @original_stdout = nil 79 | end -------------------------------------------------------------------------------- /spec/controllers/secured_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | # Matcher that asserts user was denied access. 4 | RSpec::Matchers.define :deny_access do 5 | match do |controller| 6 | redirects_to_sign_in?(controller) && sets_flash?(controller) 7 | end 8 | 9 | def redirects_to_sign_in?(controller) 10 | expect(controller).to redirect_to(controller.sign_in_url) 11 | end 12 | 13 | def sets_flash?(controller) 14 | controller.flash[:notice].match(/sign in to continue/) 15 | end 16 | end 17 | 18 | # A dummy 'secured' controller to test 19 | class SecuredAppsController < ActionController::Base 20 | include Authenticate::Controller 21 | before_action :require_login, only: :show 22 | 23 | def new 24 | head :ok 25 | end 26 | 27 | def show 28 | head :ok 29 | end 30 | end 31 | 32 | describe SecuredAppsController, type: :controller do 33 | before do 34 | Rails.application.routes.draw do 35 | resource :secured_app, only: [:new, :show] 36 | get '/sign_in' => 'authenticate/sessions#new', as: 'sign_in' 37 | end 38 | end 39 | 40 | after do 41 | Rails.application.reload_routes! 42 | end 43 | 44 | context 'with authenticated user' do 45 | before { sign_in } 46 | 47 | it 'allows access to new' do 48 | do_get :new 49 | expect(subject).to_not deny_access 50 | end 51 | 52 | it 'allows access to show' do 53 | do_get :show 54 | expect(subject).to_not deny_access 55 | end 56 | end 57 | 58 | context 'with an unauthenticated visitor' do 59 | it 'allows access to new' do 60 | do_get :new 61 | expect(subject).to_not deny_access 62 | end 63 | 64 | it 'denies access to show' do 65 | do_get :show 66 | expect(subject).to deny_access 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/dummy/README.rdoc: -------------------------------------------------------------------------------- 1 | == README 2 | 3 | This README would normally document whatever steps are necessary to get the 4 | application up and running. 5 | 6 | Things you may want to cover: 7 | 8 | * Ruby version 9 | 10 | * System dependencies 11 | 12 | * Configuration 13 | 14 | * Database creation 15 | 16 | * Database initialization 17 | 18 | * How to run the test suite 19 | 20 | * Services (job queues, cache servers, search engines, etc.) 21 | 22 | * Deployment instructions 23 | 24 | * ... 25 | 26 | 27 | Please feel free to use a different markup language if you do not plan to run 28 | rake doc:app. 29 | -------------------------------------------------------------------------------- /spec/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomichj/authenticate/f4cdcbc6e42886394a440182f84ce7fa80cd714a/spec/dummy/app/assets/images/.keep -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require_tree . 14 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | include Authenticate::Controller 3 | # before_action :require_authentication 4 | before_action :require_login 5 | 6 | # Prevent CSRF attacks by raising an exception. 7 | # For APIs, you may want to use :null_session instead. 8 | protect_from_forgery with: :exception 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomichj/authenticate/f4cdcbc6e42886394a440182f84ce7fa80cd714a/spec/dummy/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /spec/dummy/app/controllers/welcome_controller.rb: -------------------------------------------------------------------------------- 1 | class WelcomeController < ApplicationController 2 | def index 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummy/app/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomichj/authenticate/f4cdcbc6e42886394a440182f84ce7fa80cd714a/spec/dummy/app/mailers/.keep -------------------------------------------------------------------------------- /spec/dummy/app/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomichj/authenticate/f4cdcbc6e42886394a440182f84ce7fa80cd714a/spec/dummy/app/models/.keep -------------------------------------------------------------------------------- /spec/dummy/app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomichj/authenticate/f4cdcbc6e42886394a440182f84ce7fa80cd714a/spec/dummy/app/models/concerns/.keep -------------------------------------------------------------------------------- /spec/dummy/app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | include Authenticate::User 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> 6 | <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | 18 | 19 |
20 | <% flash.each do |key, value| -%> 21 |
<%=h value %>
22 | <% end %> 23 |
24 | 25 | <%= yield %> 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /spec/dummy/app/views/welcome/index.html.erb: -------------------------------------------------------------------------------- 1 |

Welcome#index

2 |

Find me in app/views/welcome/index.html.erb

3 | 4 | <%= link_to "Sign out", sign_out_path %> 5 | -------------------------------------------------------------------------------- /spec/dummy/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /spec/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../../config/application', __FILE__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /spec/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /spec/dummy/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | 4 | # path to your application root. 5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 6 | 7 | Dir.chdir APP_ROOT do 8 | # This script is a starting point to setup your application. 9 | # Add necessary setup steps to this file: 10 | 11 | puts "== Installing dependencies ==" 12 | system "gem install bundler --conservative" 13 | system "bundle check || bundle install" 14 | 15 | # puts "\n== Copying sample files ==" 16 | # unless File.exist?("config/database.yml") 17 | # system "cp config/database.yml.sample config/database.yml" 18 | # end 19 | 20 | puts "\n== Preparing database ==" 21 | system "bin/rake db:setup" 22 | 23 | puts "\n== Removing old logs and tempfiles ==" 24 | system "rm -f log/*" 25 | system "rm -rf tmp/cache" 26 | 27 | puts "\n== Restarting application server ==" 28 | system "touch tmp/restart.txt" 29 | end 30 | -------------------------------------------------------------------------------- /spec/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | Bundler.require(*Rails.groups) 6 | require "authenticate" 7 | 8 | module Dummy 9 | class Application < Rails::Application 10 | # Settings in config/environments/* take precedence over those specified here. 11 | # Application configuration should go into files in config/initializers 12 | # -- all .rb files in that directory are automatically loaded. 13 | 14 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 15 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 16 | # config.time_zone = 'Central Time (US & Canada)' 17 | 18 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 19 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 20 | # config.i18n.default_locale = :de 21 | 22 | # Do not swallow errors in after_commit/after_rollback callbacks. 23 | # config.active_record.raise_in_transactional_callbacks = true 24 | 25 | end 26 | end 27 | 28 | Rails.application.routes.default_url_options[:host] = 'localhost:3000' 29 | 30 | if Rails.application.config.active_record.sqlite3.respond_to? :represent_boolean_as_integer 31 | Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true 32 | end 33 | 34 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 5 | $LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__) 6 | -------------------------------------------------------------------------------- /spec/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations. 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | 30 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 31 | # yet still be able to expire them through the digest params. 32 | config.assets.digest = true 33 | 34 | # Adds additional error checking when serving assets at runtime. 35 | # Checks for improperly declared sprockets dependencies. 36 | # Raises helpful error messages. 37 | config.assets.raise_runtime_errors = true 38 | 39 | # Raises error for missing translations 40 | # config.action_view.raise_on_missing_translations = true 41 | end 42 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like 20 | # NGINX, varnish or squid. 21 | # config.action_dispatch.rack_cache = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? 26 | 27 | # Compress JavaScripts and CSS. 28 | config.assets.js_compressor = :uglifier 29 | # config.assets.css_compressor = :sass 30 | 31 | # Do not fallback to assets pipeline if a precompiled asset is missed. 32 | config.assets.compile = false 33 | 34 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 35 | # yet still be able to expire them through the digest params. 36 | config.assets.digest = true 37 | 38 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 39 | 40 | # Specifies the header that your server uses for sending files. 41 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 42 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 43 | 44 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 45 | # config.force_ssl = true 46 | 47 | # Use the lowest log level to ensure availability of diagnostic information 48 | # when problems arise. 49 | config.log_level = :debug 50 | 51 | # Prepend all log lines with the following tags. 52 | # config.log_tags = [ :subdomain, :uuid ] 53 | 54 | # Use a different logger for distributed setups. 55 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 56 | 57 | # Use a different cache store in production. 58 | # config.cache_store = :mem_cache_store 59 | 60 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 61 | # config.action_controller.asset_host = 'http://assets.example.com' 62 | 63 | # Ignore bad email addresses and do not raise email delivery errors. 64 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 65 | # config.action_mailer.raise_delivery_errors = false 66 | 67 | 68 | 69 | 70 | # Tell Action Mailer not to deliver emails to the real world. 71 | # The :test delivery method accumulates sent emails in the 72 | # ActionMailer::Base.deliveries array. 73 | config.action_mailer.delivery_method = :test 74 | 75 | 76 | 77 | 78 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 79 | # the I18n.default_locale when a translation cannot be found). 80 | config.i18n.fallbacks = true 81 | 82 | # Send deprecation notices to registered listeners. 83 | config.active_support.deprecation = :notify 84 | 85 | # Use default logging formatter so that PID and timestamp are not suppressed. 86 | config.log_formatter = ::Logger::Formatter.new 87 | 88 | # Do not dump schema after migrations. 89 | config.active_record.dump_schema_after_migration = false 90 | end 91 | 92 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static file server for tests with Cache-Control for performance. 16 | # config.serve_static_files = true 17 | # config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | config.action_mailer.perform_deliveries = true 34 | 35 | # Randomize the order test cases are executed. 36 | config.active_support.test_order = :random 37 | 38 | # Print deprecation notices to the stderr. 39 | config.active_support.deprecation = :stderr 40 | 41 | # Raises error for missing translations 42 | # config.action_view.raise_on_missing_translations = true 43 | end 44 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile += %w( search.js ) 12 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/authenticate.rb: -------------------------------------------------------------------------------- 1 | Authenticate.configure do |config| 2 | config.timeout_in = 45.minutes 3 | config.max_session_lifetime = 20.minutes 4 | config.max_consecutive_bad_logins_allowed = 2 5 | config.bad_login_lockout_period = 10.minutes 6 | config.reset_password_within = 5.minutes 7 | config.password_length = 8..128 8 | config.debug = true 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json 4 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_dummy_session' 4 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /spec/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | resources :welcome, only: [:index] 3 | root 'welcome#index' 4 | 5 | # The priority is based upon order of creation: first created -> highest priority. 6 | # See how all your routes lay out with "rake routes". 7 | 8 | # You can have the root of your site routed with "root" 9 | # root 'welcome#index' 10 | 11 | # Example of regular route: 12 | # get 'products/:id' => 'catalog#view' 13 | 14 | # Example of named route that can be invoked with purchase_url(id: product.id) 15 | # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase 16 | 17 | # Example resource route (maps HTTP verbs to controller actions automatically): 18 | # resources :products 19 | 20 | # Example resource route with options: 21 | # resources :products do 22 | # member do 23 | # get 'short' 24 | # post 'toggle' 25 | # end 26 | # 27 | # collection do 28 | # get 'sold' 29 | # end 30 | # end 31 | 32 | # Example resource route with sub-resources: 33 | # resources :products do 34 | # resources :comments, :sales 35 | # resource :seller 36 | # end 37 | 38 | # Example resource route with more complex sub-resources: 39 | # resources :products do 40 | # resources :comments 41 | # resources :sales do 42 | # get 'recent', on: :collection 43 | # end 44 | # end 45 | 46 | # Example resource route with concerns: 47 | # concern :toggleable do 48 | # post 'toggle' 49 | # end 50 | # resources :posts, concerns: :toggleable 51 | # resources :photos, concerns: :toggleable 52 | 53 | # Example resource route within a namespace: 54 | # namespace :admin do 55 | # # Directs /admin/products/* to Admin::ProductsController 56 | # # (app/controllers/admin/products_controller.rb) 57 | # resources :products 58 | # end 59 | end 60 | -------------------------------------------------------------------------------- /spec/dummy/config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 8b743fc5fc1621f167b724fc5743a8bc2879babd89d3d378be441e688a3ae4ed3833db2d1d7236a155648b04973ab9513b42ce2bcf7b6107417fc269f7f2a93c 15 | 16 | test: 17 | secret_key_base: cba9ecd3512c1e69ab918b88a57a5ebaaaea6c35ad3347f43cba5c965733138c20b4ef9a33d59747eef26da8f95615ec418b5a12c2c433abd3bbdf4af7e5906c 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /spec/dummy/db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended that you check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(version: 20160130192731) do 15 | 16 | create_table "users", force: :cascade do |t| 17 | t.string "email" 18 | t.string "encrypted_password", limit: 128 19 | t.string "session_token", limit: 128 20 | t.datetime "current_sign_in_at" 21 | t.string "current_sign_in_ip", limit: 128 22 | t.datetime "last_sign_in_at" 23 | t.string "last_sign_in_ip", limit: 128 24 | t.integer "sign_in_count" 25 | t.integer "failed_logins_count", default: 0 26 | t.datetime "lock_expires_at" 27 | t.datetime "last_access_at" 28 | t.string "password_reset_token" 29 | t.datetime "password_reset_sent_at" 30 | end 31 | 32 | add_index "users", ["email"], name: "index_users_on_email", unique: true 33 | add_index "users", ["session_token"], name: "index_users_on_session_token" 34 | 35 | end 36 | -------------------------------------------------------------------------------- /spec/dummy/lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomichj/authenticate/f4cdcbc6e42886394a440182f84ce7fa80cd714a/spec/dummy/lib/assets/.keep -------------------------------------------------------------------------------- /spec/dummy/log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomichj/authenticate/f4cdcbc6e42886394a440182f84ce7fa80cd714a/spec/dummy/log/.keep -------------------------------------------------------------------------------- /spec/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /spec/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /spec/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /spec/dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomichj/authenticate/f4cdcbc6e42886394a440182f84ce7fa80cd714a/spec/dummy/public/favicon.ico -------------------------------------------------------------------------------- /spec/factories/users.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | sequence :email do |n| 3 | "user#{n}@example.com" 4 | end 5 | 6 | factory :user do 7 | email 8 | password 'password' 9 | 10 | trait :without_email do 11 | email nil 12 | end 13 | 14 | trait :without_password do 15 | password nil 16 | encrypted_password nil 17 | end 18 | 19 | trait :with_session_token do 20 | session_token 'this_is_a_big_fake_long_token' 21 | end 22 | 23 | trait :with_password_reset_token_and_timestamp do 24 | password_reset_token Authenticate::Token.new 25 | password_reset_sent_at 10.seconds.ago 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/features/brute_force_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | feature 'visitor has consecutive bad logins' do 4 | before do 5 | @user = create(:user) 6 | Authenticate.configuration.max_consecutive_bad_logins_allowed = 2 7 | end 8 | 9 | scenario 'less than max bad logins does not lock account' do 10 | sign_in_with @user.email, 'badpassword' 11 | sign_in_with @user.email, 'badpassword' 12 | sign_in_with @user.email, @user.password 13 | 14 | expect_user_to_be_signed_in 15 | end 16 | 17 | scenario 'exceeds max bad logins and locks account' do 18 | sign_in_with @user.email, 'badpassword' 19 | sign_in_with @user.email, 'badpassword' 20 | sign_in_with @user.email, 'badpassword' 21 | 22 | expect_locked_account 23 | expect_lockout_time_to_be_displayed 24 | expect_user_to_be_signed_out 25 | end 26 | 27 | scenario 'user locks account, waits for lock to expire, logs in successfully' do 28 | sign_in_with @user.email, 'badpassword' 29 | sign_in_with @user.email, 'badpassword' 30 | sign_in_with @user.email, 'badpassword' 31 | 32 | Timecop.travel 50.minutes do 33 | sign_in_with @user.email, @user.password 34 | expect_user_to_be_signed_in 35 | end 36 | end 37 | end 38 | 39 | def expect_locked_account 40 | expect(page).to have_content 'Your account is locked' 41 | end 42 | 43 | def expect_lockout_time_to_be_displayed 44 | expect(page).to have_content '10 minutes' 45 | end 46 | -------------------------------------------------------------------------------- /spec/features/create_user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | feature 'create a user with valid attributes' do 4 | 5 | # this doesn't belong as a feature test but it will catch regressions. 6 | # consider moving to a request spec or... something. 7 | scenario 'increases number of users' do 8 | expect { create_user_with_valid_params }.to change { User.count }.by(1) 9 | end 10 | 11 | scenario 'signs in the user after creation' do 12 | create_user_with_valid_params 13 | expect_user_to_be_signed_in 14 | end 15 | 16 | scenario 'redirects to redirect_url' do 17 | create_user_with_valid_params 18 | expect_path_is_redirect_url 19 | end 20 | end 21 | 22 | feature 'visit a protected url, then create user' do 23 | scenario 'redirects to the protected url after user is created' do 24 | visit '/welcome' 25 | create_user_with_valid_params 26 | expect(current_path).to eq '/welcome' 27 | end 28 | end 29 | 30 | feature 'create user after signed in' do 31 | scenario 'cannot get to new user page' do 32 | user = create(:user, email: 'test.user@example.com') 33 | sign_in_with user.email, user.password 34 | visit sign_up_path 35 | expect_path_is_redirect_url 36 | end 37 | end 38 | 39 | def create_user_with_valid_params(user_attrs = attributes_for(:user)) 40 | visit sign_up_path 41 | fill_in 'user_email', with: user_attrs[:email] 42 | fill_in 'user_password', with: user_attrs[:password] 43 | click_button 'Sign up' 44 | end 45 | -------------------------------------------------------------------------------- /spec/features/max_session_lifetime_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | feature 'visitor has consecutive bad logins' do 4 | before(:each) do 5 | @user = create(:user) 6 | end 7 | 8 | scenario 'visitor logs in and subsequent click within lifetime' do 9 | sign_in_with @user.email, @user.password 10 | expect_user_to_be_signed_in 11 | 12 | Timecop.travel 1.minutes do 13 | visit root_path 14 | expect_user_to_be_signed_in 15 | end 16 | end 17 | 18 | scenario 'visitor logs in and lets session live too long' do 19 | sign_in_with @user.email, @user.password 20 | expect_user_to_be_signed_in 21 | 22 | Timecop.travel 2.days do 23 | visit root_path 24 | expect(current_path).to eq sign_in_path 25 | expect_user_to_be_signed_out 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/features/new_user_form_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | feature 'visitor at sign up form, not signed in' do 4 | scenario 'visit with no arguments' do 5 | visit sign_up_path 6 | expect(page).to have_current_path sign_up_path 7 | within 'h2' do 8 | expect(page).to have_content /Sign up/i 9 | end 10 | end 11 | 12 | scenario 'defaults email to value provided in query string' do 13 | visit sign_up_path(user: { email: 'dude@example.com' }) 14 | expect(page).to have_selector 'input[value="dude@example.com"]' 15 | end 16 | end 17 | 18 | feature 'visitor at new user form, already signed in' do 19 | scenario 'redirects user to redirect_url' do 20 | user = create(:user, email: 'test.user@example.com') 21 | sign_in_with 'Test.USER@example.com', user.password 22 | visit sign_up_path 23 | expect_path_is_redirect_url 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/features/password_reset_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | feature 'visitor requests password reset' do 4 | before(:each) do 5 | ActionMailer::Base.deliveries.clear 6 | end 7 | 8 | scenario 'navigates to Forgot Password page' do 9 | visit sign_in_path 10 | click_link 'Forgot Password' 11 | expect(current_path).to eq new_password_path 12 | expect(page).to have_content I18n.t("passwords.new.description") 13 | end 14 | 15 | scenario 'uses valid email' do 16 | user = create(:user) 17 | request_password_reset_for user.email 18 | 19 | expect_password_change_request_success_message 20 | expect_user_to_have_password_reset_attributes user 21 | expect_password_reset_email_for user 22 | end 23 | 24 | scenario 'with an unknown email' do 25 | request_password_reset_for 'fake.email@example.com' 26 | 27 | expect_password_change_request_success_message 28 | expect_mailer_to_have_no_deliveries 29 | expect(current_path).to eq sign_in_path 30 | end 31 | 32 | scenario 'with invalid email' do 33 | request_password_reset_for 'not_an_email_address' 34 | 35 | expect_password_change_request_success_message 36 | expect_mailer_to_have_no_deliveries 37 | end 38 | end 39 | 40 | def request_password_reset_for(email) 41 | visit new_password_path 42 | fill_in 'password_email', with: email 43 | click_button 'Reset password' 44 | end 45 | 46 | def expect_password_change_request_success_message 47 | expect(page).to have_content I18n.t('passwords.create.description') 48 | end 49 | 50 | def expect_user_to_have_password_reset_attributes(user) 51 | user.reload 52 | expect(user.password_reset_token).not_to be_blank 53 | expect(user.password_reset_sent_at).not_to be_blank 54 | end 55 | 56 | def expect_password_reset_email_for(user) 57 | expect(ActionMailer::Base.deliveries).not_to be_empty 58 | ActionMailer::Base.deliveries.any? do |email| 59 | email.to == [user.email] && 60 | email.html_part.body =~ /#{user.password_reset_token}/ && 61 | email.text_part.body =~ /#{user.password_reset_token}/ 62 | end 63 | end 64 | 65 | def expect_mailer_to_have_no_deliveries 66 | expect(ActionMailer::Base.deliveries).to be_empty 67 | end 68 | 69 | feature 'visitor sets new password' do 70 | scenario 'requests password change' do 71 | user = given_user_with_password_reset_token 72 | visit_password_update_page_for user 73 | request_password_change 74 | expect_password_is_changed_for user 75 | expect_redirect_to_root 76 | end 77 | 78 | scenario 'attempts password change with fake password reset token' do 79 | user = given_user_with_fake_password_reset_token 80 | visit_password_update_page_for user 81 | expect_failure_flash 82 | end 83 | end 84 | 85 | def given_user_with_fake_password_reset_token 86 | user = create :user 87 | user.password_reset_token = 'big_fake_token' 88 | user 89 | end 90 | 91 | def given_user_with_password_reset_token 92 | create :user, :with_password_reset_token_and_timestamp 93 | end 94 | 95 | def visit_password_update_page_for(user) 96 | visit edit_users_password_path(user.id, token: user.password_reset_token) 97 | end 98 | 99 | def request_password_change 100 | fill_in 'password_reset_password', with: 'new_dumb_password' 101 | click_button 'Save this password' 102 | end 103 | 104 | def expect_password_is_changed_for(user) 105 | old_encrypted_password = user.encrypted_password 106 | expect(user.reload.encrypted_password).to_not eq old_encrypted_password 107 | end 108 | 109 | def expect_redirect_to_root 110 | expect(current_path).to eq Authenticate.configuration.redirect_url 111 | end 112 | 113 | def expect_failure_flash 114 | expect(page).to have_content 'Please double check the URL or try submitting the form again.' 115 | end 116 | -------------------------------------------------------------------------------- /spec/features/password_update_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | 4 | feature 'visit password edit screen' do 5 | scenario 'with valid token in url, redirects to the edit page with the token removed from the url' do 6 | user = create(:user, :with_password_reset_token_and_timestamp) 7 | visit_password_reset_page_for(user) 8 | expect(current_path).to eq edit_users_password_path(user) 9 | expect(current_path).to_not have_content('token') 10 | end 11 | 12 | scenario 'with an invalid token in url, failure and prompt to request a password reset' do 13 | user = create(:user, :with_password_reset_token_and_timestamp) 14 | visit_password_reset_page_for(user, 'this is an invalid token') 15 | expect_forbidden_failure 16 | end 17 | 18 | scenario 'with a valid token, but an expired timestamp' do 19 | user = create(:user, :with_password_reset_token_and_timestamp, password_reset_sent_at: 20.years.ago) 20 | visit_password_reset_page_for(user) 21 | expect_token_expired_failure 22 | end 23 | 24 | scenario 'with a nil token' do 25 | user = create(:user) 26 | visit_password_reset_page_for(user, token: nil) 27 | expect_forbidden_failure 28 | end 29 | end 30 | 31 | 32 | feature 'visitor updates password' do 33 | before(:each) do 34 | @user = create(:user, :with_password_reset_token_and_timestamp) 35 | end 36 | 37 | scenario 'with valid password, signs in user' do 38 | update_password @user, 'newpassword' 39 | expect_user_to_be_signed_in 40 | end 41 | 42 | scenario 'with a valid password, password is updated' do 43 | old_pw = @user.encrypted_password 44 | update_password @user, 'newpassword' 45 | expect_password_was_updated(old_pw) 46 | end 47 | 48 | scenario 'password change signs in user' do 49 | update_password @user, 'newpassword' 50 | sign_out 51 | sign_in_with @user.email, 'newpassword' 52 | expect_user_to_be_signed_in 53 | end 54 | 55 | scenario 'signs in, redirects user' do 56 | update_password @user, 'newpassword' 57 | expect_path_is_redirect_url 58 | end 59 | end 60 | 61 | feature 'visitor updates password with invalid password' do 62 | before(:each) do 63 | @user = create(:user, :with_password_reset_token_and_timestamp) 64 | end 65 | 66 | scenario 'with a blank password, signs out user' do 67 | update_password @user, '' 68 | expect_invalid_password 69 | expect_user_to_be_signed_out 70 | end 71 | 72 | scenario 'with a short password, flashes invalid password' do 73 | update_password @user, 'short' 74 | expect_invalid_password 75 | expect_user_to_be_signed_out 76 | end 77 | end 78 | 79 | 80 | def update_password(user, password) 81 | visit_password_reset_page_for user 82 | fill_in 'password_reset_password', with: password 83 | click_button 'Save this password' 84 | end 85 | 86 | def visit_password_reset_page_for(user, token = user.password_reset_token) 87 | visit edit_users_password_path(id: user, token: token) 88 | end 89 | 90 | def expect_invalid_password 91 | expect(page).to have_content I18n.t('flashes.failure_after_update') 92 | end 93 | 94 | def expect_forbidden_failure 95 | expect(page).to have_content I18n.t('passwords.new.description') 96 | expect(page).to have_content I18n.t('flashes.failure_when_forbidden') 97 | end 98 | 99 | def expect_token_expired_failure 100 | expect(page).to have_content 'Sign in' 101 | expect(page).to have_content I18n.t('flashes.failure_token_expired') 102 | end 103 | 104 | # def expect_path_is_redirect_url 105 | # expect(current_path).to eq(Authenticate.configuration.redirect_url) 106 | # end 107 | 108 | def expect_password_was_updated(old_password) 109 | expect(@user.reload.encrypted_password).not_to eq old_password 110 | end 111 | -------------------------------------------------------------------------------- /spec/features/sign_in_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | feature 'visitor signs in' do 4 | scenario 'with valid email and password' do 5 | user = create(:user) 6 | sign_in_with user.email, user.password 7 | expect_user_to_be_signed_in 8 | end 9 | 10 | scenario 'with valid mixed-case email and password' do 11 | user = create(:user, email: 'test.user@example.com') 12 | sign_in_with 'Test.USER@example.com', user.password 13 | expect_user_to_be_signed_in 14 | end 15 | 16 | scenario 'with invalid password' do 17 | user = create(:user) 18 | sign_in_with user.email, 'invalid password' 19 | expect_page_to_display_sign_in_error 20 | expect_user_to_be_signed_out 21 | end 22 | 23 | scenario 'with invalid email' do 24 | sign_in_with 'unknown@example.com', 'password' 25 | expect_page_to_display_sign_in_error 26 | expect_user_to_be_signed_out 27 | end 28 | end 29 | 30 | feature 'visitor goes to sign in page' do 31 | scenario 'signed out user is not redirected' do 32 | visit sign_in_path 33 | expect_sign_in_page 34 | end 35 | 36 | scenario 'signed in user is redirected' do 37 | user = create(:user) 38 | sign_in_with user.email, user.password 39 | visit sign_in_path 40 | expect_path_is_redirect_url 41 | expect_user_to_be_signed_in 42 | end 43 | end 44 | 45 | feature 'user is not signed in' do 46 | scenario 'redirected to sign in' do 47 | visit welcome_index_path 48 | expect_sign_in_page 49 | end 50 | end 51 | 52 | -------------------------------------------------------------------------------- /spec/features/sign_out_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | feature 'visitor signs out' do 4 | before do 5 | @user = create(:user) 6 | end 7 | 8 | scenario 'sign in and sign out' do 9 | sign_in_with(@user.email, @user.password) 10 | sign_out 11 | expect_user_to_be_signed_out 12 | end 13 | 14 | scenario 'sign out again' do 15 | sign_in_with(@user.email, @user.password) 16 | visit sign_out_path 17 | visit sign_out_path 18 | expect_user_to_be_signed_out 19 | end 20 | 21 | scenario 'redirects to sign in' do 22 | sign_in_with(@user.email, @user.password) 23 | visit sign_out_path 24 | expect_sign_in_path 25 | end 26 | end 27 | 28 | 29 | def expect_sign_in_path 30 | expect(current_path).to eq sign_in_path 31 | end 32 | -------------------------------------------------------------------------------- /spec/features/sign_up_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | feature 'visitor signs up' do 4 | scenario 'navigates to sign up page' do 5 | visit sign_in_path 6 | click_link 'Sign Up' 7 | expect_sign_up_page 8 | end 9 | 10 | scenario 'signs up with valid email and password' do 11 | sign_up_with 'valid@example.com', 'password' 12 | expect_user_to_be_signed_in 13 | end 14 | 15 | scenario 'signs up with invalid email' do 16 | sign_up_with 'bad_email', 'password' 17 | expect_user_to_be_signed_out 18 | end 19 | 20 | scenario 'signs up with invalid short password' do 21 | sign_up_with 'bad_email', '111' 22 | expect_user_to_be_signed_out 23 | end 24 | 25 | scenario 'signs up with blank password' do 26 | sign_up_with 'bad_email', '' 27 | expect_user_to_be_signed_out 28 | end 29 | end 30 | 31 | def expect_sign_up_page 32 | expect(current_path).to eq sign_up_path 33 | end 34 | 35 | def sign_up_with(email, password) 36 | visit sign_up_path 37 | fill_in 'user_email', with: email 38 | fill_in 'user_password', with: password 39 | click_button 'Sign up' 40 | end 41 | -------------------------------------------------------------------------------- /spec/features/timeoutable_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | feature 'visitor session time' do 4 | before do 5 | @user = create(:user) 6 | Authenticate.configuration.timeout_in = 10.minutes 7 | end 8 | 9 | scenario 'visitor logs in, subsequent click within timeout' do 10 | sign_in_with @user.email, @user.password 11 | expect_user_to_be_signed_in 12 | 13 | Timecop.travel 5.minutes do 14 | visit root_path 15 | expect_user_to_be_signed_in 16 | end 17 | end 18 | 19 | scenario 'visitor logs in, subsequent click after session times out' do 20 | sign_in_with @user.email, @user.password 21 | expect_user_to_be_signed_in 22 | 23 | Timecop.travel 11.minutes do 24 | visit root_path 25 | expect(current_path).to eq sign_in_path 26 | expect_user_to_be_signed_out 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/model/brute_force_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'authenticate/model/brute_force' 3 | 4 | describe Authenticate::Model::BruteForce do 5 | before(:each) do 6 | @user = create(:user) 7 | end 8 | 9 | it 'responds to locked?' do 10 | expect(@user).to respond_to :locked? 11 | end 12 | 13 | it 'knows when it is locked' do 14 | expect(@user.locked?).to be_falsey 15 | @user.lock! 16 | expect(@user.locked?).to be_truthy 17 | end 18 | 19 | context '#register_failed_login!' do 20 | it 'locks when failed login count reaches max' do 21 | @user.register_failed_login! 22 | @user.register_failed_login! 23 | @user.register_failed_login! 24 | expect(@user.locked?).to be_truthy 25 | end 26 | 27 | it 'sets lockout period' do 28 | @user.register_failed_login! 29 | @user.register_failed_login! 30 | @user.register_failed_login! 31 | expect(@user.lock_expires_at).to_not be_nil 32 | end 33 | end 34 | 35 | context '#lock!' do 36 | it 'before lock, locked_expires_at is nil' do 37 | expect(@user.lock_expires_at).to be_nil 38 | end 39 | 40 | it 'sets locked_expires_at' do 41 | @user.lock! 42 | expect(@user.lock_expires_at).to_not be_nil 43 | expect(@user.lock_expires_at).to be_utc 44 | end 45 | end 46 | 47 | context '#unlock!' do 48 | before(:each) do 49 | @user.lock! 50 | @user.unlock! 51 | end 52 | it 'zeros failed_logins_count' do 53 | expect(@user.failed_logins_count).to be(0) 54 | end 55 | it 'nils lock_expires_at' do 56 | expect(@user.lock_expires_at).to be_nil 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/model/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'authenticate/configuration' 3 | 4 | describe Authenticate::Configuration do 5 | context 'user model' do 6 | module Gug 7 | # Faux user model 8 | class Profile 9 | extend ActiveModel::Naming 10 | end 11 | end 12 | 13 | before(:each) do 14 | @conf = Authenticate::Configuration.new 15 | @conf.user_model = 'Gug::Profile' 16 | end 17 | 18 | it 'gets a class for a user model' do 19 | expect(@conf.user_model_class).to be(Gug::Profile) 20 | end 21 | 22 | it 'get a route key for a user model' do 23 | expect(@conf.user_model_route_key).to eq('gug_profiles') 24 | end 25 | 26 | it 'get a param key for a user model' do 27 | expect(@conf.user_model_param_key).to eq(:gug_profile) 28 | end 29 | 30 | describe '#authentication_strategy' do 31 | context 'with no strategy set' do 32 | it 'defaults to email' do 33 | expect(@conf.authentication_strategy).to eq :email 34 | end 35 | it 'includes email in modules' do 36 | expect(@conf.modules).to include :email 37 | end 38 | it 'does not include username in modules' do 39 | expect(@conf.modules).to_not include :username 40 | end 41 | end 42 | 43 | context 'with strategy set to username' do 44 | before do 45 | @conf.authentication_strategy = :username 46 | end 47 | it 'includes username in modules' do 48 | expect(@conf.modules).to include :username 49 | end 50 | it 'does not include email in modules' do 51 | expect(@conf.modules).to_not include :email 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/model/db_password_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'authenticate/model/db_password' 3 | 4 | describe Authenticate::Model::DbPassword do 5 | describe 'Passwords' do 6 | context '#password_match?' do 7 | subject { create(:user, password: 'password') } 8 | 9 | it 'matches a password' do 10 | expect(subject.password_match?('password')).to be_truthy 11 | end 12 | 13 | it 'fails to match a bad password' do 14 | expect(subject.password_match?('bad password')).to be_falsey 15 | end 16 | 17 | it 'saves passwords' do 18 | subject.password = 'new_password' 19 | subject.save! 20 | 21 | user = User.find(subject.id) 22 | expect(user.password_match?('new_password')).to be_truthy 23 | end 24 | end 25 | 26 | describe 'Validations' do 27 | context 'on a new user' do 28 | it 'should not be valid without a password' do 29 | user = build(:user, :without_password) 30 | expect(user).to_not be_valid 31 | end 32 | 33 | it 'should be not be valid with a short password' do 34 | user = build(:user) 35 | user.password = 'short' 36 | expect(user).to_not be_valid 37 | end 38 | 39 | it 'is valid with a long password' do 40 | user = build(:user) 41 | user.password = 'thisisalongpassword' 42 | expect(user).to be_valid 43 | end 44 | end 45 | 46 | context 'on an existing user' do 47 | subject { create(:user, password: 'password') } 48 | 49 | it { is_expected.to be_valid } 50 | 51 | it 'should not be valid with an empty password' do 52 | subject.password = '' 53 | expect(subject).to_not be_valid 54 | end 55 | 56 | it 'should be valid with a new (valid) password' do 57 | subject.password = 'new password' 58 | expect(subject).to be_valid 59 | end 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/model/email_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'authenticate/model/email' 3 | 4 | describe Authenticate::Model::Email do 5 | it 'validates email' do 6 | user = build(:user, :without_email) 7 | user.save 8 | expect(user.errors.count).to be(2) 9 | expect(user.errors.messages[:email]).to include('is invalid') 10 | expect(user.errors.messages[:email]).to include("can't be blank") 11 | end 12 | 13 | it 'extracts credentials from params' do 14 | params = { session: { email: 'foo', password: 'bar' } } 15 | expect(User.credentials(params)).to match_array(%w(foo bar)) 16 | end 17 | 18 | it 'authenticates from credentials' do 19 | user = create(:user) 20 | expect(User.authenticate([user.email, user.password])).to eq(user) 21 | end 22 | 23 | it 'validates unique email address' do 24 | original = build(:user, email: 'email@email.com') 25 | dupe_email = build(:user, email: 'email@email.com') 26 | 27 | original.save 28 | dupe_email.save 29 | 30 | expect(dupe_email.errors.count).to be(1) 31 | expect(dupe_email.errors.messages[:email]).to include('has already been taken') 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/model/lifetimed_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'authenticate/model/lifetimed' 3 | 4 | describe Authenticate::Model::Lifetimed do 5 | context '#max_session_lifetime_exceeded?' do 6 | it 'passes fresh sessions' do 7 | Timecop.freeze do 8 | user = create(:user, current_sign_in_at: 1.minute.ago.utc) 9 | expect(user).to_not be_max_session_lifetime_exceeded 10 | end 11 | end 12 | 13 | it 'detects timed out sessions' do 14 | Timecop.freeze do 15 | user = create(:user, current_sign_in_at: 5.hours.ago.utc) 16 | expect(user).to be_max_session_lifetime_exceeded 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/model/modules_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Authenticate::Modules do 4 | # dummy user model to test .check_fields 5 | class UserProfile 6 | extend ActiveModel::Model 7 | include Authenticate::Modules 8 | end 9 | 10 | describe '.check_fields' do 11 | context 'user model with missing fields' do 12 | it 'fails when required_fields are not present' do 13 | expect { UserProfile.load_modules }.to raise_error(Authenticate::Modules::MissingAttribute) 14 | end 15 | end 16 | context 'user model with all fields' do 17 | it 'fleshed out user model is fine' do 18 | expect { User.load_modules }.to_not raise_error 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/model/password_reset_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'authenticate/model/password_reset' 3 | 4 | describe Authenticate::Model::PasswordReset do 5 | context 'forgot_password!' do 6 | subject { create(:user) } 7 | before { subject.forgot_password! } 8 | 9 | it 'generates a password reset token' do 10 | expect(subject.password_reset_token).to_not be_nil 11 | end 12 | 13 | it 'sets password reset sent at' do 14 | expect(subject.password_reset_sent_at).to_not be_nil 15 | end 16 | end 17 | 18 | context '#reset_password_period_valid?' do 19 | subject { create(:user) } 20 | 21 | it 'always true if reset_password_within config param is nil' do 22 | within = Authenticate.configuration.reset_password_within 23 | subject.password_reset_sent_at = 10.days.ago 24 | Authenticate.configuration.reset_password_within = nil 25 | expect(subject.reset_password_period_valid?).to be_truthy 26 | Authenticate.configuration.reset_password_within = within 27 | end 28 | 29 | it 'false if time exceeded' do 30 | subject.password_reset_sent_at = 10.minutes.ago 31 | expect(subject.reset_password_period_valid?).to be_falsey 32 | end 33 | 34 | it 'true if time within limit' do 35 | subject.password_reset_sent_at = 1.minutes.ago 36 | expect(subject.reset_password_period_valid?).to be_truthy 37 | end 38 | end 39 | 40 | context '#update_password' do 41 | subject { create(:user) } 42 | 43 | context 'within time limit' do 44 | before(:each) { subject.password_reset_sent_at = 1.minutes.ago } 45 | 46 | it 'allows password update within time limit' do 47 | expect(subject.update_password('password2')).to be_truthy 48 | end 49 | 50 | it 'clears password reset token' do 51 | subject.update_password 'password2' 52 | expect(subject.password_reset_token).to be_nil 53 | end 54 | 55 | it 'generates a new session token' do 56 | token = subject.session_token 57 | subject.update_password 'password2' 58 | expect(subject.session_token).to_not eq(token) 59 | end 60 | end 61 | 62 | context 'after time limit' do 63 | it 'stops password update' do 64 | subject.password_reset_sent_at = 6.minutes.ago 65 | expect(subject.update_password('password2')).to be_falsey 66 | end 67 | end 68 | 69 | context 'password_reset_sent_at is nil' do 70 | it 'stops password update' do 71 | subject.password_reset_sent_at = nil 72 | subject.password_reset_token = 'notNilResetToken' 73 | expect(subject.update_password('password2')).to be_falsey 74 | end 75 | end 76 | end 77 | end 78 | 79 | -------------------------------------------------------------------------------- /spec/model/session_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Authenticate::Session do 4 | describe 'session token' do 5 | it 'finds a user from session token' do 6 | user = create(:user, :with_session_token) 7 | request = mock_request cookies: session_cookie_for(user) 8 | session = Authenticate::Session.new(request) 9 | expect(session.current_user).to eq user 10 | end 11 | it 'nil user without a session token' do 12 | request = mock_request 13 | session = Authenticate::Session.new(request) 14 | expect(session.current_user).to be_nil 15 | end 16 | it 'returns nil with a bogus session token' do 17 | request = mock_request cookies: { Authenticate.configuration.cookie_name.freeze.to_sym => 'some made up value' } 18 | session = Authenticate::Session.new(request) 19 | expect(session.current_user).to be_nil 20 | end 21 | end 22 | 23 | describe '#login' do 24 | it 'sets current_user' do 25 | user = create(:user) 26 | session = Authenticate::Session.new(mock_request) 27 | session.login(user) 28 | expect(session.current_user).to eq user 29 | end 30 | context 'with a block' do 31 | it 'passes the success status to the block when login succeeds' do 32 | user = create(:user) 33 | session = Authenticate::Session.new(mock_request) 34 | session.login(user) do |status| 35 | expect(status.success?).to eq true 36 | end 37 | end 38 | it 'passes the failure status to the block when login fails' do 39 | session = Authenticate::Session.new(mock_request) 40 | session.login nil do |status| 41 | expect(status.success?).to eq false 42 | end 43 | end 44 | end 45 | context 'with nil argument' do 46 | it 'assigned current_user to nil' do 47 | session = Authenticate::Session.new(mock_request) 48 | session.login nil 49 | expect(session.current_user).to be_nil 50 | end 51 | end 52 | context 'modules' do 53 | it 'runs the callbacks' do 54 | user = create(:user, :with_session_token, sign_in_count: 0) 55 | request = mock_request cookies: { authenticate_session_token: user.session_token } 56 | session = Authenticate::Session.new(request) 57 | expect { session.login(user) }. to change { user.sign_in_count }.by(1) 58 | end 59 | it 'fails login if a callback fails' do 60 | session = Authenticate::Session.new(mock_request) 61 | session.login nil do |status| 62 | expect(status.success?).to eq false 63 | expect(status.message).to eq I18n.t('callbacks.authenticatable.failure') 64 | end 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/model/timeoutable_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'authenticate/model/timeoutable' 3 | 4 | describe Authenticate::Model::Timeoutable do 5 | subject { create(:user) } 6 | 7 | it 'does not timeout while last_access_at is valid' do 8 | Timecop.freeze do 9 | subject.last_access_at = 1.minutes.ago 10 | expect(subject.timedout?).to be_falsey 11 | end 12 | end 13 | 14 | it 'does timeout when last_access_at is stale' do 15 | Timecop.freeze do 16 | subject.last_access_at = 1.days.ago 17 | expect(subject.timedout?).to be_truthy 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/model/token_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Authenticate::Token do 4 | it 'is a random hex string' do 5 | token = 'my_token' 6 | allow(SecureRandom).to receive(:hex).with(20).and_return(token) 7 | expect(Authenticate::Token.new).to eq token 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/model/trackable_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'authenticate/model/trackable' 3 | 4 | describe Authenticate::Model::Trackable do 5 | subject { create(:user) } 6 | context '#last_sign_in_at' do 7 | it 'sets to old current_sign_in_at if it is not nil' do 8 | old_sign_in = 2.days.ago.utc 9 | subject.current_sign_in_at = old_sign_in 10 | subject.update_tracked_fields mock_request 11 | expect(subject.last_sign_in_at).to eq(old_sign_in) 12 | end 13 | 14 | it 'sets to current time if old current_sign_in_at is nil' do 15 | subject.current_sign_in_at = nil 16 | subject.update_tracked_fields mock_request 17 | expect(subject.last_sign_in_at).to be_within(5.seconds).of(Time.now.utc) 18 | end 19 | end 20 | 21 | context '#last_sign_in_ip' do 22 | it 'sets to old current_sign_in_ip if it is not nil' do 23 | old_ip = '222.222.222.222' 24 | subject.current_sign_in_ip = old_ip 25 | subject.update_tracked_fields mock_request 26 | expect(subject.last_sign_in_ip).to eq(old_ip) 27 | end 28 | 29 | it 'sets to current ip if old current_sign_in_ip is nil' do 30 | subject.current_sign_in_ip = nil 31 | subject.update_tracked_fields mock_request 32 | expect(subject.last_sign_in_ip).to_not be_nil 33 | end 34 | end 35 | 36 | it 'sets current_sign_in_at to now' do 37 | subject.current_sign_in_at = nil 38 | subject.update_tracked_fields mock_request 39 | expect(subject.current_sign_in_at).to be_within(5.seconds).of(Time.now.utc) 40 | end 41 | 42 | context '#sign_in_count' do 43 | it 'initializes a nil count' do 44 | subject.sign_in_count = nil 45 | subject.update_tracked_fields mock_request 46 | expect(subject.sign_in_count).to eq(1) 47 | end 48 | it 'increments existing count' do 49 | subject.sign_in_count = 4 50 | subject.update_tracked_fields mock_request 51 | expect(subject.sign_in_count).to eq(5) 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/model/user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Authenticate::User do 4 | context 'session tokens' do 5 | it 'generates a new session token' do 6 | user = create(:user, :with_session_token) 7 | old_token = user.session_token 8 | user.generate_session_token 9 | expect(user.session_token).to_not eq old_token 10 | end 11 | 12 | it 'saves user when reset_session_token! called' do 13 | user = create(:user, :with_session_token) 14 | old_token = user.session_token 15 | user.reset_session_token! 16 | new_user = User.find(user.id) 17 | expect(new_user.session_token).to_not eq old_token 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/orm/active_record.rb: -------------------------------------------------------------------------------- 1 | require 'authenticate' 2 | 3 | ActiveRecord::Migration.verbose = false 4 | # ActiveRecord::Base.logger = Logger.new(nil) 5 | # ActiveRecord::Base.include_root_in_json = true 6 | 7 | def setup_orm 8 | ActiveRecord::Migrator.migrate(migrations_path) 9 | end 10 | 11 | def teardown_orm 12 | ActiveRecord::Migrator.rollback(migrations_path) 13 | end 14 | 15 | def migrations_path 16 | Rails.root.join('db', 'migrate', 'core') 17 | end 18 | -------------------------------------------------------------------------------- /spec/requests/csrf_rotation_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'CSRF rotation' do 4 | around do |example| 5 | ActionController::Base.allow_forgery_protection = true 6 | example.run 7 | ActionController::Base.allow_forgery_protection = false 8 | end 9 | 10 | context 'Authenticate configuration is set to rotate CSRF token on sign in' do 11 | describe 'sign in' do 12 | before do 13 | @user = create(:user, password: 'password') 14 | end 15 | it 'rotates the CSRF token' do 16 | Authenticate.configure { |config| config.rotate_csrf_on_sign_in = true } 17 | 18 | # go to sign in screen, generating csrf 19 | get sign_in_path 20 | original_token = csrf_token 21 | 22 | # post a login 23 | do_post session_path, params: { **session_params } 24 | 25 | # expect that we now have a new csrf token 26 | expect(response).to have_http_status(302) 27 | expect(csrf_token).not_to eq original_token 28 | expect(csrf_token).to be_present 29 | end 30 | end 31 | end 32 | 33 | def csrf_token 34 | session[:_csrf_token] 35 | end 36 | 37 | def session_params 38 | { session: { email: @user.email, password: @user.password }, authenticity_token: csrf_token } 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/requests/session_key_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'session key assignment' do 4 | context 'user signs in' do 5 | before do 6 | @user = create(:user) 7 | do_post session_path, params: { session: { email: @user.email, password: @user.password } } 8 | end 9 | 10 | it 'redirects after login' do 11 | expect(response).to have_http_status(302) 12 | end 13 | 14 | it 'sets user session token' do 15 | @user.reload 16 | expect(@user.session_token).to_not be_nil 17 | end 18 | 19 | it 'sets session token in cookie' do 20 | expect(cookies['authenticate_session_token']).to_not be_nil 21 | end 22 | 23 | it 'sets current_user' do 24 | expect(controller.current_user).to eq(@user) 25 | end 26 | 27 | context 'user signs out' do 28 | it 'rotates user session token' do 29 | old_session = @user.session_token 30 | do_get sign_out_path 31 | @user.reload 32 | expect(@user.session_token).to_not eq old_session 33 | end 34 | 35 | it 'removes session cookie' do 36 | do_get sign_out_path 37 | expect(cookies['authenticate_session_token']).to eq '' 38 | end 39 | 40 | it 'sets current_user to nil' do 41 | do_get sign_out_path 42 | expect(controller.current_user).to be_nil 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 2 | ENV['RAILS_ENV'] ||= 'test' 3 | 4 | require File.expand_path('../dummy/config/environment.rb', __FILE__) 5 | 6 | # nasty hacky catch of environment data wiped out by tests run in rails 4 via appraisal 7 | if ActiveRecord::VERSION::STRING >= '5.0' 8 | system('bin/rails dummy:db:environment:set RAILS_ENV=test') 9 | end 10 | 11 | require 'rspec/rails' 12 | require 'capybara/rails' 13 | require 'capybara/rspec' 14 | require 'database_cleaner' 15 | require 'factory_bot' 16 | require 'timecop' 17 | 18 | Dir[File.join(File.dirname(__FILE__), 'support/**/*.rb')].each { |f| require f } 19 | 20 | Rails.backtrace_cleaner.remove_silencers! 21 | DatabaseCleaner.strategy = :truncation 22 | 23 | # Load factory bot factories. 24 | Dir[File.join(File.dirname(__FILE__), 'factories/**/*.rb')].each { |f| require f } 25 | 26 | # Build test database in spec/dummy/db. There's probably a better way to do this. 27 | if defined?(ActiveRecord::Migration.maintain_test_schema!) 28 | ActiveRecord::Migration.maintain_test_schema! # rails 4.1+ 29 | else 30 | ActiveRecord::Migration.check_pending! # rails 4.0 31 | end 32 | 33 | if ActiveRecord::VERSION::STRING >= '4.2' && ActiveRecord::VERSION::STRING < '5.0' 34 | ActiveRecord::Base.raise_in_transactional_callbacks = true 35 | end 36 | 37 | RSpec.configure do |config| 38 | config.include FactoryBot::Syntax::Methods 39 | config.infer_spec_type_from_file_location! 40 | config.order = :random 41 | config.use_transactional_fixtures = true 42 | 43 | config.expect_with :rspec do |expectations| 44 | expectations.syntax = :expect 45 | end 46 | 47 | config.mock_with :rspec do |mocks| 48 | mocks.syntax = :expect 49 | end 50 | 51 | config.after(:each, type: :feature) do 52 | DatabaseCleaner.clean # Truncate the database 53 | Capybara.reset_sessions! # Forget the (simulated) browser state 54 | Capybara.use_default_driver # Revert Capybara.current_driver to Capybara.default_driver 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/support/controllers/controller_helpers.rb: -------------------------------------------------------------------------------- 1 | require 'authenticate/controller' 2 | 3 | module Controllers 4 | # Helpers for controller tests 5 | module ControllerHelpers 6 | def sign_in 7 | user = create(:user) 8 | sign_in_as user 9 | end 10 | 11 | def sign_in_as(user) 12 | controller.login user 13 | end 14 | 15 | def sign_out 16 | controller.logout 17 | end 18 | end 19 | end 20 | 21 | RSpec.configure do |config| 22 | config.include Controllers::ControllerHelpers, type: :controller 23 | end 24 | -------------------------------------------------------------------------------- /spec/support/features/feature_helpers.rb: -------------------------------------------------------------------------------- 1 | module Features 2 | # Helpers for feature tests 3 | module FeatureHelpers 4 | def sign_in_with(email, password) 5 | visit sign_in_path 6 | fill_in 'session_email', with: email 7 | fill_in 'session_password', with: password 8 | click_button 'Sign in' 9 | end 10 | 11 | def sign_out 12 | within '#header' do 13 | click_link I18n.t('layouts.application.sign_out') 14 | end 15 | end 16 | 17 | def expect_user_to_be_signed_in 18 | visit root_path 19 | expect(page).to have_link 'Sign out' 20 | end 21 | 22 | def expect_page_to_display_sign_in_error 23 | expect(page).to have_content I18n.t('callbacks.authenticatable.failure') 24 | end 25 | 26 | def expect_user_to_be_signed_out 27 | expect(page).to have_content 'Sign in' 28 | end 29 | 30 | def expect_path_is_redirect_url 31 | expect(current_path).to eq(Authenticate.configuration.redirect_url) 32 | end 33 | 34 | def expect_sign_in_page 35 | expect(current_path).to eq sign_in_path 36 | end 37 | end 38 | end 39 | 40 | RSpec.configure do |config| 41 | config.include Features::FeatureHelpers, type: :feature 42 | end 43 | -------------------------------------------------------------------------------- /spec/support/mailer.rb: -------------------------------------------------------------------------------- 1 | # Hacky monkey patch, do later deliveries right now, so 2 | class ActionMailer::MessageDelivery 3 | def deliver_later 4 | deliver_now 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/support/request_helpers.rb: -------------------------------------------------------------------------------- 1 | module RequestHelpers 2 | 3 | # 4 | # Dumb glue methods, to deal with rails 4 vs rails 5 get/post methods. 5 | # 6 | def do_post(path, *args) 7 | if Rails::VERSION::MAJOR >= 5 8 | post path, *args 9 | else 10 | post path, *(args.collect{|i| i.values}.flatten) 11 | end 12 | end 13 | 14 | def do_get(path, *args) 15 | if Rails::VERSION::MAJOR >= 5 16 | get path, *args 17 | else 18 | get path, *(args.collect{|i| i.values}.flatten) 19 | end 20 | end 21 | 22 | # def do_put(path, *args) 23 | # if Rails::VERSION::MAJOR >= 5 24 | # put path, *args 25 | # else 26 | # put path, *(args.collect{|i| i.values}.flatten) 27 | # end 28 | # end 29 | 30 | 31 | def mock_request(params: {}, cookies: {}) 32 | req = double('request') 33 | allow(req).to receive(:params).and_return(params) 34 | allow(req).to receive(:remote_ip).and_return('111.111.111.111') 35 | allow(req).to receive(:cookie_jar).and_return(cookies) 36 | req 37 | end 38 | 39 | def session_cookie_for(user) 40 | { Authenticate.configuration.cookie_name.freeze.to_sym => user.session_token } 41 | end 42 | end 43 | 44 | RSpec.configure do |config| 45 | config.include RequestHelpers 46 | end 47 | --------------------------------------------------------------------------------